From 5344fde4a56016151bb4ab3e7ec6e9c2cefb24e7 Mon Sep 17 00:00:00 2001 From: Leon Wilzer Date: Wed, 20 Mar 2024 20:28:45 +0100 Subject: [PATCH] fml --- Cargo.toml | 26 ++++ src/curseforge.rs | 343 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 30 ++++ src/modpack.rs | 231 +++++++++++++++++++++++++++++++ 4 files changed, 630 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/curseforge.rs create mode 100644 src/main.rs create mode 100644 src/modpack.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..43a6950 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "umm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde_json = { version = "1.0.114", features = ["preserve_order"] } +serde = { version = "1.0.197", features = ["derive"] } +getset = "0.1.2" +futures = "0.3.30" +reqwest = { version = "0.11.26", features = ["blocking"] } +clap = { version = "4.5.2", features = ["cargo"] } +tokio = { version = "1.36.0", features = ["rt-multi-thread", "rt", "macros", "time"] } +toml = { version = "0.8.11", features = ["parse"] } +hashlink = { version = "0.9.0", features = ["serde", "serde_impl"] } +colored = "2.1.0" +env_logger = "0.11.3" +once_cell = "1.19.0" + +[profile.release] +debug = 1 + +[profile.dev] +debug = 1 diff --git a/src/curseforge.rs b/src/curseforge.rs new file mode 100644 index 0000000..3df07aa --- /dev/null +++ b/src/curseforge.rs @@ -0,0 +1,343 @@ +#![allow(non_snake_case)] + +use std::env; +use std::fmt::Debug; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use clap::{crate_name, crate_version}; +use colored::Colorize; +use futures::{stream, StreamExt}; +use futures::executor::block_on; +use getset::{CopyGetters, Getters}; +use hashlink::LinkedHashMap; +use once_cell::sync::Lazy; +use reqwest::Client; +use reqwest::header::{HeaderMap, HeaderValue}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tokio::sync::Semaphore; + +use crate::modpack::{Mod, ModLoader, ModLoaderType, Modpack, ModpackInfo, Relation, RelationType}; + +const MANIFEST_TYPE: &str = "minecraftModpack"; +const MANIFEST_VERSION: usize = 1; +const MINECRAFT_GAME_ID: usize = 432; +const CURSEFORGE_API_URL: &str = "https://api.curseforge.com"; +// const CURSEFORGE_API_URL: &str = "http://localhost:6666"; +const HTTP_TIMEOUT: Duration = Duration::from_secs(10); +const CONCURRENT_REQUESTS: usize = 1; +static semaphore: Lazy = Lazy::new(|| Semaphore::new(4)); + +static CLIENT: Lazy = Lazy::new(CurseForgePack::new_client); + +#[derive(Debug)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct CurseForgePack +{ + author: String, + files: Vec, + #[allow(non_snake_case)] + manifestType: String, + #[allow(non_snake_case)] + manifestVersion: usize, + minecraft: Minecraft, + name: String, + overrides: String, + version: String, +} + +#[derive(Debug)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct File +{ + #[allow(non_snake_case)] + fileID: usize, + #[allow(non_snake_case)] + projectID: usize, + required: bool, +} + +impl From<&File> for Mod +{ + fn from(value: &File) -> Self { + block_on(CurseForgePack::lookup_file(value)) + } +} + +#[derive(Debug)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct Minecraft +{ + #[allow(non_snake_case)] + modLoaders: Vec, + version: String, +} + +#[derive(Debug)] +#[derive(Serialize, Deserialize)] +#[derive(CopyGetters, Getters)] +pub struct CFModLoader +{ + #[getset(get = "pub")] + id: String, + #[getset(get_copy = "pub")] + primary: bool, +} + +impl From<&ModLoader> for CFModLoader +{ + fn from(value: &ModLoader) -> Self { + Self + { + id: format!("{}-{}", value.loader_type().to_string().to_lowercase(), value.version()), + primary: value.primary(), + } + } +} + +impl From<&CFModLoader> for ModLoader +{ + fn from(value: &CFModLoader) -> Self { + let mut id_iter = value.id.split('-'); + Self::new(ModLoaderType::from(&CFModLoaderType::try_from(id_iter.next().unwrap()).unwrap()), id_iter.next().unwrap(), value.primary) + } +} + +#[derive(Debug)] +#[derive(Copy, Clone)] +#[derive(Serialize,Deserialize)] +pub enum CFModLoaderType +{ + Any = 0, + Forge = 1, + Cauldron = 2, + LiteLoader = 3, + Fabric = 4, + Quilt = 5, + NeoForge = 6, +} + +impl TryFrom<&str> for CFModLoaderType +{ + type Error = (); + + fn try_from(value: &str) -> Result { + return match value.to_lowercase().as_str() + { + "any" => Ok(CFModLoaderType::Any), + "forge" => Ok(CFModLoaderType::Forge), + "cauldron" => Ok(CFModLoaderType::Cauldron), + "liteloader" => Ok(CFModLoaderType::LiteLoader), + "fabric" => Ok(CFModLoaderType::Fabric), + "quilt" => Ok(CFModLoaderType::Quilt), + "neoforge" => Ok(CFModLoaderType::NeoForge), + _ => Err(()), + }; + } +} + +impl From<&ModLoaderType> for CFModLoaderType +{ + fn from(value: &ModLoaderType) -> Self { + match value + { + ModLoaderType::Any => CFModLoaderType::Any, + ModLoaderType::Forge => CFModLoaderType::Forge, + ModLoaderType::Cauldron => CFModLoaderType::Cauldron, + ModLoaderType::LiteLoader => CFModLoaderType::LiteLoader, + ModLoaderType::Fabric => CFModLoaderType::Fabric, + ModLoaderType::Quilt => CFModLoaderType::Quilt, + ModLoaderType::NeoForge => CFModLoaderType::NeoForge, + } + } +} + + +impl From<&CFModLoaderType> for ModLoaderType +{ + fn from(value: &CFModLoaderType) -> ModLoaderType { + match value + { + CFModLoaderType::Any => ModLoaderType::Any, + CFModLoaderType::Forge => ModLoaderType::Forge, + CFModLoaderType::Cauldron => ModLoaderType::Cauldron, + CFModLoaderType::LiteLoader => ModLoaderType::LiteLoader, + CFModLoaderType::Fabric => ModLoaderType::Fabric, + CFModLoaderType::Quilt => ModLoaderType::Quilt, + CFModLoaderType::NeoForge => ModLoaderType::NeoForge, + } + } +} + + +impl From<&Modpack> for CurseForgePack +{ + fn from(modpack: &Modpack) -> Self { + let mc = Minecraft { + modLoaders: modpack.loader().values().map(CFModLoader::from).collect(), + version: modpack.info().minecraft_version().clone(), + }; + + Self { + author: modpack.info().author().clone(), + files: block_on(Self::lookup_all_mods(modpack)), + manifestType: MANIFEST_TYPE.to_string(), + manifestVersion: MANIFEST_VERSION, + minecraft: mc, + name: modpack.info().title().clone(), + overrides: String::from("overrides"), + version: modpack.info().modpack_version().clone() + } + } +} + +impl From<&CurseForgePack> for Modpack +{ + fn from(value: &CurseForgePack) -> Self { + let i: AtomicUsize = AtomicUsize::new(0); + let info = ModpackInfo::new(value.name(), value.author(), value.version(), value.minecraft.version(), ""); + let loader = value.minecraft().modLoaders().iter().map(|value| + ( + value.id.split('-').next().unwrap().to_owned(), + ModLoader::from(value) + ) + ).collect::>(); + + // let pack = block_on( + // async + // { + // stream::iter(value.files().iter()).map(|value| async { let modification = CurseForgePack::lookup_file(value).await; + // println!("i: {}", i.fetch_add(1, Ordering::Relaxed)); + // (modification.name().clone(), modification) + // }).collect::>().await.collect::>().await + // }); + + let pack = block_on( + async + { + stream::iter(value.files().iter()).map(|value| async { + let modification = CurseForgePack::lookup_file(value).await; + println!("i: {}", i.fetch_add(1, Ordering::Relaxed)); + (modification.name().clone(), modification) + }).buffer_unordered(CONCURRENT_REQUESTS).collect().await + + }); + Self::new(info, loader, pack) + } +} + +impl CurseForgePack +{ + async fn lookup_all_mods(modpack: &Modpack) -> Vec + { + stream::iter(modpack.pack()).map(|(_, modification)| Self::lookup_mod(modification, modpack)).buffer_unordered(CONCURRENT_REQUESTS).collect().await + } + async fn lookup_mod(modification: &Mod, modpack: &Modpack) -> File + { + println!("Looking up: {}", modification.name().bright_yellow()); + let modloader = modpack.primary_loader().map_or_else(|| CFModLoaderType::Any, |value| CFModLoaderType::from(value.loader_type())); + + let project_txt = Self::client().await.get(format!("{CURSEFORGE_API_URL}/v1/mods/search?gameId={MINECRAFT_GAME_ID}&classId=6&slug={}", modification.name())).send().await.unwrap().text().await.unwrap(); + let project_json = serde_json::from_str::(&project_txt).unwrap(); + + let project_id = project_json["data"][0]["id"].as_u64().expect("Project ID is not an integer number!") as usize; + + let files_txt = Self::client().await.get(format!("{CURSEFORGE_API_URL}/v1/mods/{project_id}/files?gameVersion={}&modLoaderType={}", modpack.info().minecraft_version(), modloader as u8)).send().await.unwrap().text().await.unwrap(); + let files_json = serde_json::from_str::(&files_txt).unwrap(); + + + let file_id = match modification.version().as_str() + { + "*" => files_json["data"].get(0), + "/" => { + println!("Warning: The following mod has a placeholder version specifier: {}", modification.name().bright_yellow()); + files_json["data"].get(0) + } + _ => files_json["data"].as_array().unwrap().iter().find(| x| + (*x)["displayName"].as_str().unwrap().contains(modification.version())) + }; + + #[allow(non_snake_case)] + let fileID = match file_id + { + Some(v) => v["id"].as_u64().unwrap() as usize, + None => panic!("Could not lookup file for {modification:?}"), + }; + + println!("Looked up mod: {}", modification.name().green()); + + File + { + fileID, + projectID: project_id, + required: !*modification.optional(), + } + } + + async fn lookup_file(file: &File) -> Mod + { + let project_txt = Self::client().await.get(format!("{CURSEFORGE_API_URL}/v1/mods/{}", file.projectID)).send().await.unwrap().text().await.unwrap(); + let project_json = serde_json::from_str::(&project_txt).unwrap(); + + println!("ProjectID: {}", file.projectID); + println!("FileID: {}", file.fileID); + let file_txt = Self::client().await.get(format!("{CURSEFORGE_API_URL}/v1/mods/{}/files/{}", file.projectID, file.fileID)).send().await.unwrap(); + println!("FileToJSON"); + let file_json = serde_json::from_str::(&file_txt.text().await.unwrap()).unwrap(); + + println!("Relations"); + let relations = match file_json["dependencies"].as_array() + { + Some(a) => { stream::iter(a.iter()) + .map(|dep| async { + let mod_txt = Self::client().await.get(format!("{CURSEFORGE_API_URL}/v1/mods/{}", dep["modId"])).send().await.unwrap().text().await.unwrap(); + let mod_json: Value = serde_json::from_str(mod_txt.as_str()).unwrap(); + Relation::new(mod_json["data"]["slug"].as_str().unwrap().to_owned(), RelationType::try_from(dep["relationType"].as_u64().unwrap() as usize).unwrap()) + } + ).buffer_unordered(CONCURRENT_REQUESTS).collect().await}, + None => Vec::new(), + }; + + + println!("Mod"); + let modification = Mod::new( + project_json["data"]["name"].as_str().unwrap().to_string(), + "/".to_string(), // CurseForge does not store mod version number... + !file.required, + None, + relations, + ); + println!("Looked up: {}", modification.name().green()); + // sleep(Duration::from_millis(250)).await; + modification + } + + async fn client() -> Client + { + let permit = semaphore.acquire().await.unwrap(); + CLIENT.clone() + } + + fn new_client() -> Client + { + let mut headers = HeaderMap::new(); + let api_key = env::var("CURSEFORGE_API_KEY").expect("You must supply a Curseforge API key trough setting the CURSEFORGE_API_KEY environment variable!"); + headers.insert("x-api-key", HeaderValue::from_str(api_key.as_str()).unwrap()); + + Client::builder() + .default_headers(headers) + .user_agent(format!("{}/{}",crate_name!(),crate_version!())) + .timeout(HTTP_TIMEOUT) + .connect_timeout(HTTP_TIMEOUT) + .http2_keep_alive_timeout(HTTP_TIMEOUT) + .pool_idle_timeout(HTTP_TIMEOUT) + .build().expect("Could not build Curseforge Client!") + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a2478ea --- /dev/null +++ b/src/main.rs @@ -0,0 +1,30 @@ +use std::fs::{File, read_to_string}; +use crate::curseforge::CurseForgePack; +use crate::modpack::Modpack; +use std::env; + +pub mod modpack; +pub mod curseforge; + +#[tokio::main] +async fn main() { + env::set_var("RUST_LOG", "debug"); + env_logger::init(); + + // let pack_str = read_to_string("pack.toml").expect("Could not read pack.toml to String"); + // let pack: Modpack = toml::from_str(&pack_str).expect("Could not parse pack.toml"); + // let cf: CurseForgePack = (&pack).into(); + // let manifest = File::create("manifest.json").unwrap(); + // serde_json::to_writer(manifest, &cf).unwrap() + + let all_manifest = read_to_string("mods/manifest.json").unwrap(); + let cfp: CurseForgePack = serde_json::from_str(&all_manifest).unwrap(); + println!("Create UMM"); + let umm: Modpack = Modpack::from(&cfp); + let mod_pack= File::create("mods/pack.toml").unwrap(); + serde_json::to_writer(mod_pack, &umm).unwrap(); + println!("Convert to CFP"); + let cfp2 = CurseForgePack::from(&umm); + let cfp2_file = File::create("manifest_2.json").unwrap(); + serde_json::to_writer(cfp2_file, &cfp2).unwrap(); +} \ No newline at end of file diff --git a/src/modpack.rs b/src/modpack.rs new file mode 100644 index 0000000..6805aad --- /dev/null +++ b/src/modpack.rs @@ -0,0 +1,231 @@ +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; +use getset::{CopyGetters, Getters}; +use hashlink::LinkedHashMap; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct Modpack +{ + info: ModpackInfo, + loader: LinkedHashMap, + pack: LinkedHashMap, +} + +impl Modpack +{ + pub fn new(info: ModpackInfo, loader: LinkedHashMap, pack: LinkedHashMap) -> Self + { + Self + { + info, + loader, + pack, + } + } + pub fn primary_loader(&self) -> Option<&ModLoader> + { + self.loader().values().find(|loader| + { + loader.primary() + } + ) + } +} + +#[derive(Clone)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct ModpackInfo +{ + title: String, + author: String, + modpack_version: String, + minecraft_version: String, + description: String, +} + +impl ModpackInfo +{ + pub fn new( + title: impl Into, + author: impl Into, + modpack_version: impl Into, + minecraft_version: impl Into, + descriptipn: impl Into, + ) -> Self + { + Self + { + title: title.into(), + author: author.into(), + modpack_version: modpack_version.into(), + minecraft_version: minecraft_version.into(), + description: descriptipn.into(), + } + } +} + +#[derive(Clone)] +#[derive(Serialize,Deserialize)] +#[derive(CopyGetters, Getters)] +pub struct ModLoader +{ + #[getset(get = "pub")] + loader_type: ModLoaderType, + #[getset(get = "pub")] + version: String, + #[getset(get_copy = "pub")] + primary: bool, +} + +impl ModLoader +{ + pub fn new(loader_type: ModLoaderType, version: impl Into, primary: bool) -> Self + { + Self + { + loader_type, + version: version.into(), + primary, + } + } +} + +#[derive(Debug)] +#[derive(Copy, Clone)] +#[derive(Serialize,Deserialize)] +pub enum ModLoaderType +{ + Any = 0, + Forge = 1, + Cauldron = 2, + LiteLoader = 3, + Fabric = 4, + Quilt = 5, + NeoForge = 6, +} + +impl Display for ModLoaderType +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self + { + Self::Any => write!(f, "Any"), + Self::Forge => write!(f, "Forge"), + Self::Cauldron => write!(f, "Cauldron"), + Self::LiteLoader => write!(f, "LiteLoader"), + Self::Fabric => write!(f, "Fabric"), + Self::Quilt => write!(f, "Quilt"), + Self::NeoForge => write!(f, "NeoForge"), + } + } +} + +#[derive(Clone, Debug)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct Mod +{ + name: String, + version: String, + optional: bool, + overrule: Option, + relations: Vec, +} + +impl Mod +{ + pub fn new( + name: impl Into, + version: impl Into, + optional: bool, + overrule: Option, + relations: Vec, + ) -> Self + { + Self + { + name: name.into(), + version: version.into(), + optional, + overrule, + relations, + } + } +} + +#[derive(Clone, Debug)] +#[derive(Serialize,Deserialize)] +#[derive(Getters)] +#[getset(get = "pub")] +pub struct Relation +{ + mod_slug: String, + relation_type: RelationType, +} + +impl Relation +{ + pub fn new(mod_slug: impl Into, relation_type: RelationType) -> Self + { + Self + { + mod_slug: mod_slug.into(), + relation_type, + } + } +} + +#[derive(Clone, Debug)] +#[derive(Serialize,Deserialize)] +pub enum RelationType +{ + EmbeddedLibrary = 1, + OptionalDependency = 2, + RequiredDependency = 3, + Tool = 4, + Incompatible = 5, + Include = 6, +} + +impl TryFrom for RelationType +{ + type Error = (); + + fn try_from(value: usize) -> Result { + match value + { + 1 => Ok(Self::EmbeddedLibrary), + 2 => Ok(Self::OptionalDependency), + 3 => Ok(Self::RequiredDependency), + 4 => Ok(Self::Tool), + 5 => Ok(Self::Incompatible), + 6 => Ok(Self::Include), + _ => Err(()), + } + } +} + +impl TryFrom<&str> for RelationType +{ + type Error = (); + + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() + { + "embeddedlibrary" => Ok(Self::EmbeddedLibrary), + "optionaldependency" => Ok(Self::OptionalDependency), + "requireddependency" => Ok(Self::RequiredDependency), + "tool" => Ok(Self::Tool), + "incompatible" => Ok(Self::Incompatible), + "include" => Ok(Self::Include), + _ => Err(()), + } + } +} \ No newline at end of file