use anyhow::{Result, anyhow, bail}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::{collections::BTreeSet, path::{Path, PathBuf}}; use tokio::fs; use enumflags2::{bitflags, make_bitflags, BitFlags}; use crate::{model::{local::{self, PackageManifest}, misc::Game, rainy}, util}; // {namespace}-{name} #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)] pub struct PkgKey(pub String); // {namespace}-{name}-{version} #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)] pub struct PkgKeyVersion(String); #[derive(Copy, Clone, Display, Debug, Serialize, Deserialize, Default)] pub enum PackageSource { #[default] Rainy, Local(Game) } #[derive(Clone, Default, Serialize, Deserialize, Debug)] #[allow(dead_code)] pub struct Package { pub namespace: String, pub name: String, pub description: String, pub loc: Option, pub rmt: Option, pub source: PackageSource, } #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub enum Status { Unchecked, Unsupported, OK(BitFlags, DLLs), } #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct DLLs { pub game: Option, pub amd: Option } #[bitflags] #[repr(u16)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Feature { Mod, Aime, AMNet, Mu3Hook, Mu3IO, ChusanHook, ChuniIO, Mempatcher, GameDLL, AmdDLL } #[derive(Clone, Serialize, Deserialize, Debug)] #[allow(dead_code)] pub struct Local { pub version: String, pub path: PathBuf, pub dependencies: BTreeSet, pub status: Status, pub icon: String, } #[derive(Clone, Serialize, Deserialize, Debug)] #[allow(dead_code)] pub struct Remote { pub version: String, pub package_url: String, pub download_url: String, pub icon: String, pub deprecated: bool, pub nsfw: bool, pub categories: Vec, pub dependencies: BTreeSet, pub file_size: i64, } impl PkgKey { pub fn split(&self) -> Result<(String, String)> { let (namespace, name) = self.0 .split_at(self.0.find("-").ok_or_else(|| anyhow!("Invalid package key"))?); Ok((namespace.to_owned(), name[1..].to_owned())) // cut the hyphen } } impl Package { pub fn from_rainy(mut p: rainy::V1Package) -> Option { if p.versions.len() == 0 { return None; } let v = p.versions.swap_remove(0); Some(Package { namespace: p.owner, name: v.name, description: v.description, loc: None, rmt: Some(Remote { package_url: p.package_url, download_url: v.download_url, icon: v.icon, deprecated: p.is_deprecated, nsfw: p.has_nsfw_content, version: v.version_number, categories: p.categories, dependencies: Self::sanitize_deps(v.dependencies), file_size: v.file_size }), source: PackageSource::Rainy, }) } pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result { let str = fs::read_to_string(dir.join("manifest.json")).await?; let mft: local::PackageManifest = serde_json::from_str(&str)?; let icon = dir.join("icon.png") .as_os_str() .to_str() .unwrap() .to_owned(); let status = Self::parse_status(&mft, &dir); let dependencies = Self::sanitize_deps(mft.dependencies); Ok(Package { namespace: Self::dir_to_namespace(&dir)?, name: mft.name.clone(), description: mft.description.clone(), loc: Some(Local { version: mft.version_number, path: dir.to_owned(), icon, status, dependencies }), rmt: None, source }) } pub fn key(&self) -> PkgKey { PkgKey(format!("{}-{}", self.namespace, self.name)) } pub fn path(&self) -> PathBuf { match self.source { PackageSource::Rainy => util::pkg_dir().join(self.key().0), PackageSource::Local(game) => util::pkg_dir() .parent() .unwrap() .join(format!("pkg-{game}")) .join(&self.name), } } pub fn _dir_to_key(dir: &Path) -> Result { let (key, _) = Self::parse_dir_name(dir)?; Ok(key) } pub fn dir_to_namespace(dir: &Path) -> Result { let (_, n) = Self::parse_dir_name(dir)?; Ok(n) } fn manifest(dir: &Path) -> Result { serde_json::from_reader(std::fs::File::open(dir.join("manifest.json"))?) .map_err(|err| anyhow!("Invalid manifest: {}", err)) } fn parse_dir_name(dir: &Path) -> Result<(String, String)> { let mft = Self::manifest(dir)?; let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$")?; let dir_name = dir.file_name() .to_owned() .ok_or_else(|| anyhow!("Invalid directory name"))? .to_str() .ok_or_else(|| anyhow!("Illegal directory name"))?; let namespace; if let Some(caps) = regex.captures(dir_name) { let name_match = caps.get(2) .ok_or_else(|| anyhow!("Invalid directory name"))?; if name_match.as_str() != mft.name { bail!("Invalid manifest or directory name"); } namespace = caps.get(1) .ok_or_else(|| anyhow!("Invalid directory name?"))? .as_str() .to_owned(); Ok((format!("{}-{}", namespace, mft.name), namespace)) } else { bail!("Error reading {}: invalid directory name", dir_name); } } fn sanitize_deps(src: BTreeSet) -> BTreeSet { let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)-[0-9\.]+$") .expect("Invalid regex"); let mut res = BTreeSet::::new(); for dep in src { let caps = regex.captures(&dep.0) .expect("Invalid dependency"); res.insert(PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))); } res } fn parse_status(mft: &PackageManifest, dir: impl AsRef) -> Status { if mft.installers.len() == 0 { if dir.as_ref().join("post_load.ps1").exists() { return Status::Unsupported; } if dir.as_ref().join("app").join("data").exists() { return Status::Unsupported; } return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None }); } else { let mut flags = BitFlags::default(); let mut game_dll = None; let mut amd_dll = None; for installer in &mft.installers { if let Some(serde_json::Value::String(id)) = installer.get("identifier") { if id == "rainycolor" { flags |= Feature::Mod; } else if id == "segatools" { if let Some(serde_json::Value::String(module)) = installer.get("module") { flags |= Self::parse_segatools_module(&module); } if let Some(serde_json::Value::Array(arr)) = installer.get("module") { for elem in arr { if let serde_json::Value::String(module) = elem { flags |= Self::parse_segatools_module(module); } } } } else if id == "native_mod" { if let Some(serde_json::Value::String(path)) = installer.get("dll-game") { flags |= Feature::GameDLL; flags |= Feature::Mod; game_dll = Some(path.to_owned()); } if let Some(serde_json::Value::String(path)) = installer.get("dll_game") { flags |= Feature::GameDLL; flags |= Feature::Mod; game_dll = Some(path.to_owned()); } if let Some(serde_json::Value::String(path)) = installer.get("dll-amdaemon") { flags |= Feature::AmdDLL; flags |= Feature::Mod; amd_dll = Some(path.to_owned()); } } else { return Status::Unsupported; } } } log::debug!("{} parse result: {:?} {:?} {:?}", mft.name, flags, game_dll, amd_dll); Status::OK(flags, DLLs { game: game_dll, amd: amd_dll }) } } fn parse_segatools_module(module: &str) -> BitFlags { match module { "mu3hook" => make_bitflags!(Feature::Mu3Hook), "chusanhook" => make_bitflags!(Feature::ChusanHook), "amnet" => make_bitflags!(Feature::{AMNet | Aime}), "aimeio" => make_bitflags!(Feature::Aime), "mu3io" => make_bitflags!(Feature::Mu3IO), "chuniio" => make_bitflags!(Feature::ChuniIO), _ => BitFlags::default() } } }