291 lines
9.6 KiB
Rust
291 lines
9.6 KiB
Rust
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<Local>,
|
|
pub rmt: Option<Remote>,
|
|
pub source: PackageSource,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
|
pub enum Status {
|
|
Unchecked,
|
|
Unsupported,
|
|
OK(BitFlags<Feature>, DLLs),
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
|
pub struct DLLs {
|
|
pub game: Option<String>,
|
|
pub amd: Option<String>
|
|
}
|
|
|
|
#[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<PkgKey>,
|
|
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<String>,
|
|
pub dependencies: BTreeSet<PkgKey>,
|
|
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<Package> {
|
|
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<Package> {
|
|
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<String> {
|
|
let (key, _) = Self::parse_dir_name(dir)?;
|
|
Ok(key)
|
|
}
|
|
|
|
pub fn dir_to_namespace(dir: &Path) -> Result<String> {
|
|
let (_, n) = Self::parse_dir_name(dir)?;
|
|
Ok(n)
|
|
}
|
|
|
|
fn manifest(dir: &Path) -> Result<local::PackageManifest> {
|
|
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<PkgKeyVersion>) -> BTreeSet<PkgKey> {
|
|
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)-[0-9\.]+$")
|
|
.expect("Invalid regex");
|
|
let mut res = BTreeSet::<PkgKey>::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<Path>) -> 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<Feature, u16> {
|
|
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()
|
|
}
|
|
}
|
|
} |