This commit is contained in:
Leon Wilzer 2024-03-20 20:28:45 +01:00
parent 9c5ece87e7
commit 5344fde4a5
Signed by: leon
GPG Key ID: 02C1E8FC4D87721C
4 changed files with 630 additions and 0 deletions

26
Cargo.toml Normal file
View File

@ -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

343
src/curseforge.rs Normal file
View File

@ -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<Semaphore> = Lazy::new(|| Semaphore::new(4));
static CLIENT: Lazy<Client> = Lazy::new(CurseForgePack::new_client);
#[derive(Debug)]
#[derive(Serialize,Deserialize)]
#[derive(Getters)]
#[getset(get = "pub")]
pub struct CurseForgePack
{
author: String,
files: Vec<File>,
#[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<CFModLoader>,
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<Self, Self::Error> {
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::<LinkedHashMap<String, ModLoader>>();
// 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::<FuturesUnordered<_>>().await.collect::<LinkedHashMap<_,_>>().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<File>
{
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::<Value>(&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::<Value>(&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::<Value>(&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::<Value>(&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!")
}
}

30
src/main.rs Normal file
View File

@ -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();
}

231
src/modpack.rs Normal file
View File

@ -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<String, ModLoader>,
pack: LinkedHashMap<String, Mod>,
}
impl Modpack
{
pub fn new(info: ModpackInfo, loader: LinkedHashMap<String, ModLoader>, pack: LinkedHashMap<String, Mod>) -> 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<String>,
author: impl Into<String>,
modpack_version: impl Into<String>,
minecraft_version: impl Into<String>,
descriptipn: impl Into<String>,
) -> 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<String>, 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<PathBuf>,
relations: Vec<Relation>,
}
impl Mod
{
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
optional: bool,
overrule: Option<PathBuf>,
relations: Vec<Relation>,
) -> 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<String>, 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<usize> for RelationType
{
type Error = ();
fn try_from(value: usize) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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(()),
}
}
}