use crate::minecraft::{ Argument, ArgumentType, JavaVersion, Library, VersionInfo, VersionType, }; use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::{HashMap, HashSet}; /// The latest version of the format the fabric model structs deserialize to pub const CURRENT_FABRIC_FORMAT_VERSION: usize = 1; /// The latest version of the format the fabric model structs deserialize to pub const CURRENT_FORGE_FORMAT_VERSION: usize = 3; /// The latest version of the format the quilt model structs deserialize to pub const CURRENT_QUILT_FORMAT_VERSION: usize = 2; /// The latest version of the format the neoforge model structs deserialize to pub const CURRENT_NEOFORGE_FORMAT_VERSION: usize = 1; /// Current OptiFine manifest cache format. pub const CURRENT_OPTIFINE_FORMAT_VERSION: usize = 1; /// Current Cleanroom manifest cache format. pub const CURRENT_CLEANROOM_FORMAT_VERSION: usize = 1; /// Current LiteLoader manifest cache format. pub const CURRENT_LITELOADER_FORMAT_VERSION: usize = 2; /// Current Legacy Fabric manifest cache format. pub const CURRENT_LEGACY_FABRIC_FORMAT_VERSION: usize = 1; /// Current Babric manifest cache format. pub const CURRENT_BABRIC_FORMAT_VERSION: usize = 2; /// Metadata for locating and caching a loader manifest. #[derive(Debug, Clone)] pub struct LoaderManifestMetadata { /// The canonical loader name used in launcher-meta paths. pub loader: String, /// The latest manifest format version for this loader. pub format_version: usize, /// The cache key that includes the loader format version. pub cache_key: String, /// Optional Minecraft version for game-scoped loader metadata. pub game_version: Option, /// The fallback launcher-meta path to the manifest. pub path: String, } /// Returns metadata for the latest manifest format for the provided loader. pub fn loader_manifest_metadata(loader: &str) -> LoaderManifestMetadata { let format_version = current_loader_manifest_format_version(loader); let cache_key = format!("{loader}-v{format_version}"); let path = fallback_loader_manifest_path(loader); LoaderManifestMetadata { loader: loader.to_string(), format_version, cache_key, game_version: None, path, } } /// Returns metadata for a loader manifest scoped to one Minecraft version. pub fn loader_manifest_metadata_for_game( loader: &str, game_version: &str, ) -> LoaderManifestMetadata { let mut metadata = loader_manifest_metadata(loader); metadata.cache_key = format!("{}:{game_version}", metadata.cache_key); metadata.game_version = Some(game_version.to_string()); metadata } /// Returns loader manifest metadata from a versioned cache key. pub fn loader_manifest_metadata_from_cache_key( cache_key: &str, ) -> LoaderManifestMetadata { let (manifest_key, game_version) = cache_key .split_once(':') .map_or((cache_key, None), |(key, game_version)| { (key, Some(game_version.to_string())) }); if let Some((loader, format_version)) = manifest_key .rsplit_once("-v") .and_then(|(loader, version)| { version .parse::() .ok() .map(|version| (loader, version)) }) { let path = fallback_loader_manifest_path(loader); LoaderManifestMetadata { loader: loader.to_string(), format_version, cache_key: cache_key.to_string(), game_version, path, } } else { loader_manifest_metadata(cache_key) } } fn current_loader_manifest_format_version(loader: &str) -> usize { match loader { "fabric" => CURRENT_FABRIC_FORMAT_VERSION, "forge" => CURRENT_FORGE_FORMAT_VERSION, "quilt" => CURRENT_QUILT_FORMAT_VERSION, "neo" => CURRENT_NEOFORGE_FORMAT_VERSION, "optifine" => CURRENT_OPTIFINE_FORMAT_VERSION, "cleanroom" => CURRENT_CLEANROOM_FORMAT_VERSION, "lite_loader" => CURRENT_LITELOADER_FORMAT_VERSION, "legacy_fabric" => CURRENT_LEGACY_FABRIC_FORMAT_VERSION, "babric" => CURRENT_BABRIC_FORMAT_VERSION, _ => 0, } } fn fallback_loader_manifest_path(loader: &str) -> String { let format_version = match loader { "quilt" => 1, _ => 0, }; format!("{loader}/v{format_version}/manifest.json") } /// The dummy replace string library names, inheritsFrom, and version names should be replaced with pub const DUMMY_REPLACE_STRING: &str = "${modrinth.gameVersion}"; /// Returns whether a Minecraft version uses the unobfuscated distribution. pub fn uses_unobfuscated_minecraft(game_version: &str) -> bool { if let Some((year, snapshot)) = game_version.split_once('w') { let snapshot = snapshot.as_bytes(); return year.parse::().is_ok_and(|year| year >= 26) && snapshot.len() >= 3 && snapshot[..2].iter().all(u8::is_ascii_digit) && snapshot[2..].iter().all(u8::is_ascii_alphabetic); } let mut components = game_version.split('.'); let Some(major) = components .next() .and_then(|value| value.parse::().ok()) else { return false; }; let Some(minor) = components .next() .and_then(|value| value.parse::().ok()) else { return false; }; if components.any(|value| value.parse::().is_err()) { return false; } major > 26 || (major == 26 && minor >= 1) } /// Removes loader libraries that are incompatible with the selected game version. /// /// For Cleanroom the vanilla LWJGL 2 family is dropped: the /// `org.lwjgl.lwjgl:lwjgl` binding (shares the `org.lwjgl` package with the /// Cleanroom LWJGL 3 line), the natives-only carrier /// `org.lwjgl.lwjgl:lwjgl-platform` (whose root-level extracted `lwjgl.dll` / /// `liblwjgl.so` / `liblwjgl.dylib` would be what `System.loadLibrary("lwjgl")` /// resolves first in the natives directory, while the Cleanroom LWJGL 3 /// natives are nested), and `org.lwjgl.lwjgl:lwjgl_util` at exactly /// `2.9.4-nightly-20150209` (LWJGL 2 utility classes the LWJGL 3 port does /// not provide; the version condition mirrors HMCL's `DefaultLauncher` /// Cleanroom handling, which removes every classpath entry containing /// `2.9.4-nightly-20150209`). HMCL itself never classpaths native jars and /// its first-wins native extraction over self-contained official Cleanroom /// manifests keeps the old LWJGL 2 native out of the natives directory, so /// dropping the carrier is an Axolotl-side strengthening for merged /// direct links, not an HMCL library rule. The vanilla JNA platform and the /// Mojang ICU bundle are removed by the pre-existing Axolotl Cleanroom rule /// (not HMCL logic): with Cleanroom's newer JNA line on the classpath the /// 1.12.2-era `net.java.dev.jna:platform` and `com.ibm.icu:icu4j-core-mojang` /// carry conflicting runtime-class versions. The Cleanroom LWJGL 3 line /// (`org.lwjgl:lwjgl:3.4.1-unsafe`), `lwjglxx`, and the vanilla JNA core are /// untouched. pub fn normalize_loader_libraries( loader: &str, game_version: &str, libraries: &mut Vec, ) -> Vec { let remove_fabric_intermediary = loader == "fabric" && uses_unobfuscated_minecraft(game_version); let remove_cleanroom_conflicts = loader == "cleanroom"; if !remove_fabric_intermediary && !remove_cleanroom_conflicts { return Vec::new(); } let mut removed = Vec::new(); libraries.retain(|library| { let mut coordinates = library.name.split(':'); let group = coordinates.next(); let artifact = coordinates.next(); let version = coordinates.next(); let has_version = version.is_some(); let is_intermediary = remove_fabric_intermediary && group == Some("net.fabricmc") && artifact == Some("intermediary") && has_version; let conflicts_with_cleanroom = remove_cleanroom_conflicts && has_version && (matches!( (group, artifact), (Some("org.lwjgl.lwjgl"), Some("lwjgl")) | (Some("org.lwjgl.lwjgl"), Some("lwjgl-platform")) | (Some("net.java.dev.jna"), Some("platform")) | (Some("com.ibm.icu"), Some("icu4j-core-mojang")) ) || (group == Some("org.lwjgl.lwjgl") && artifact == Some("lwjgl_util") && version == Some("2.9.4-nightly-20150209"))); if is_intermediary || conflicts_with_cleanroom { removed.push(library.name.clone()); } !is_intermediary && !conflicts_with_cleanroom }); removed } /// A data variable entry that depends on the side of the installation #[derive(Serialize, Deserialize, Debug)] pub struct SidedDataEntry { /// The value on the client pub client: String, /// The value on the server pub server: String, } fn deserialize_date<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; serde_json::from_str::>(&format!("\"{s}\"")) .or_else(|_| { NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f") .map(|date| date.and_utc()) }) .map_err(serde::de::Error::custom) } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] /// A partial version returned by fabric meta pub struct PartialVersionInfo { /// The version ID of the version pub id: String, /// The version ID this partial version inherits from pub inherits_from: String, /// The time that the version was released #[serde(deserialize_with = "deserialize_date")] pub release_time: DateTime, /// The latest time a file in this version was updated #[serde(deserialize_with = "deserialize_date")] pub time: DateTime, #[serde(skip_serializing_if = "Option::is_none")] /// The classpath to the main class to launch the game pub main_class: Option, #[serde(skip_serializing_if = "Option::is_none")] /// (Legacy) Arguments passed to the game pub minecraft_arguments: Option, #[serde(skip_serializing_if = "Option::is_none")] /// Arguments passed to the game or JVM pub arguments: Option>>, /// Libraries that the version depends on pub libraries: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] /// Java runtime required by the loader profile, when it overrides Minecraft. pub java_version: Option, #[serde(rename = "type")] /// The type of version pub type_: VersionType, #[serde(skip_serializing_if = "Option::is_none")] /// (Forge-only) pub data: Option>, #[serde(skip_serializing_if = "Option::is_none")] /// (Forge-only) The list of processors to run after downloading the files pub processors: Option>, } /// A processor to be ran after downloading the files #[derive(Serialize, Deserialize, Debug)] pub struct Processor { /// Maven coordinates for the JAR library of this processor. pub jar: String, /// Maven coordinates for all the libraries that must be included in classpath when running this processor. pub classpath: Vec, /// Arguments for this processor. pub args: Vec, #[serde(skip_serializing_if = "Option::is_none")] /// Represents a map of outputs. Keys and values can be data values pub outputs: Option>, #[serde(skip_serializing_if = "Option::is_none")] /// Which sides this processor shall be ran on. /// Valid values: client, server, extract pub sides: Option>, } /// Merges a partial version into a complete one pub fn merge_partial_version( partial: PartialVersionInfo, merge: VersionInfo, ) -> VersionInfo { let merge_id = merge.id.clone(); let mut libraries = vec![]; // We skip duplicate libraries that exist already in the partial version for mut lib in merge.libraries { let lib_artifact = lib.name.rsplit_once(':').map(|x| x.0); if let Some(lib_artifact) = lib_artifact { if !partial.libraries.iter().any(|x| { let target_artifact = x.name.rsplit_once(':').map(|x| x.0); target_artifact == Some(lib_artifact) && x.include_in_classpath }) { libraries.push(lib); } else { lib.include_in_classpath = false; } } else { libraries.push(lib); } } let arguments = if let Some(partial_args) = partial.arguments { if let Some(merge_args) = merge.arguments { let mut new_map = HashMap::new(); fn argument_key(argument: &Argument) -> String { serde_json::to_string(argument) .unwrap_or_else(|_| format!("{argument:?}")) } fn add_keys( new_map: &mut HashMap>, args: HashMap>, ) { for (type_, arguments) in args { let existing = new_map.entry(type_).or_default(); let max_overlap = existing.len().min(arguments.len()); let overlap = (1..=max_overlap) .rev() .find(|&length| { existing[existing.len() - length..] .iter() .map(argument_key) .eq(arguments[..length] .iter() .map(argument_key)) }) .unwrap_or(0); existing.extend(arguments.into_iter().skip(overlap)); } } add_keys(&mut new_map, merge_args); add_keys(&mut new_map, partial_args); Some(new_map) } else { Some(partial_args) } } else { merge.arguments }; let mut libraries = libraries .into_iter() .chain(partial.libraries) .map(|mut library| { library.name = library.name.replace(DUMMY_REPLACE_STRING, &merge_id); library }) .collect::>(); // Loader profiles may repeat a vanilla coordinate with a reduced // description (Forge 1.16.x is a notable example: its duplicate LWJGL // entry omits `natives` and `downloads.classifiers`). Merge metadata from // duplicates before dropping them so native archives remain discoverable. let mut merged_libraries = Vec::with_capacity(libraries.len()); for library in libraries { if let Some(existing) = merged_libraries .iter_mut() .find(|existing: &&mut Library| existing.name == library.name) { existing.include_in_classpath |= library.include_in_classpath; if existing.natives.is_none() { existing.natives = library.natives.clone(); } if let Some(downloads) = &library.downloads { let existing_downloads = existing.downloads.get_or_insert_with(|| downloads.clone()); if existing_downloads.artifact.is_none() { existing_downloads.artifact = downloads.artifact.clone(); } if existing_downloads.classifiers.is_none() { existing_downloads.classifiers = downloads.classifiers.clone(); } } if existing.extract.is_none() { existing.extract = library.extract.clone(); } } else { merged_libraries.push(library); } } libraries = merged_libraries; VersionInfo { arguments, asset_index: merge.asset_index, assets: merge.assets, downloads: merge.downloads, id: partial.id.replace(DUMMY_REPLACE_STRING, &merge_id), java_version: partial.java_version.or(merge.java_version), libraries, logging: merge.logging, main_class: if let Some(main_class) = partial.main_class { main_class } else { merge.main_class }, minecraft_arguments: partial .minecraft_arguments .or(merge.minecraft_arguments), minimum_launcher_version: merge.minimum_launcher_version, release_time: partial.release_time, time: partial.time, type_: partial.type_, data: partial.data, processors: partial.processors, } } #[cfg(test)] mod merge_tests { use super::*; use chrono::Utc; use std::collections::HashMap; fn library(name: &str) -> Library { Library { downloads: None, extract: None, name: name.to_string(), url: None, natives: None, rules: None, checksums: None, include_in_classpath: true, downloadable: true, } } fn version_info() -> VersionInfo { VersionInfo { arguments: Some(HashMap::from([( ArgumentType::Game, vec![Argument::Normal("--demo".to_string())], )])), asset_index: serde_json::from_value(serde_json::json!({ "id": "legacy", "sha1": "test", "size": 0, "totalSize": 0, "url": "https://example.com/index.json" })) .unwrap(), assets: "legacy".to_string(), downloads: HashMap::new(), id: "1.12.2-forge".to_string(), java_version: None, libraries: vec![ library("example:shared:1.0"), library("example:base:1.0"), ], logging: None, main_class: "example.Main".to_string(), minecraft_arguments: Some( "--username ${auth_player_name} --gameDir ${game_directory}" .to_string(), ), minimum_launcher_version: 0, release_time: Utc::now(), time: Utc::now(), type_: VersionType::Release, data: None, processors: None, } } #[test] fn merge_partial_version_keeps_loader_order_and_removes_exact_duplicates() { let now = Utc::now(); let partial = PartialVersionInfo { id: "1.12.2-liteloader".to_string(), inherits_from: "1.12.2".to_string(), release_time: now, time: now, main_class: Some("net.minecraft.launchwrapper.Launch".to_string()), minecraft_arguments: None, arguments: Some(HashMap::from([( ArgumentType::Game, vec![ Argument::Normal("--demo".to_string()), Argument::Normal("--tweakClass".to_string()), Argument::Normal("example.Tweaker".to_string()), ], )])), libraries: vec![ library("example:shared:2.0"), library("example:lite:1.0"), ], java_version: None, type_: VersionType::Release, data: None, processors: None, }; let merged = merge_partial_version(partial, version_info()); assert_eq!( merged .libraries .iter() .map(|library| library.name.as_str()) .collect::>(), ["example:base:1.0", "example:shared:2.0", "example:lite:1.0"] ); assert_eq!(merged.main_class, "net.minecraft.launchwrapper.Launch"); assert_eq!( merged.minecraft_arguments.as_deref(), Some("--username ${auth_player_name} --gameDir ${game_directory}") ); assert_eq!( merged.arguments.unwrap()[&ArgumentType::Game] .iter() .filter_map(|argument| match argument { Argument::Normal(value) => Some(value.as_str()), Argument::Ruled { .. } => None, }) .collect::>(), ["--demo", "--tweakClass", "example.Tweaker"] ); } #[test] fn merge_partial_version_preserves_repeated_jvm_options() { let now = Utc::now(); let mut minecraft = version_info(); minecraft.arguments = Some(HashMap::from([( ArgumentType::Jvm, vec![Argument::Normal("-cp".to_string())], )])); let partial = PartialVersionInfo { id: "1.20.1-forge".to_string(), inherits_from: "1.20.1".to_string(), release_time: now, time: now, main_class: Some( "cpw.mods.bootstraplauncher.BootstrapLauncher".to_string(), ), minecraft_arguments: None, arguments: Some(HashMap::from([( ArgumentType::Jvm, vec![ Argument::Normal("--add-opens".to_string()), Argument::Normal( "java.base/java.util.jar=cpw.mods.securejarhandler" .to_string(), ), Argument::Normal("--add-opens".to_string()), Argument::Normal( "java.base/java.lang.invoke=cpw.mods.securejarhandler" .to_string(), ), ], )])), libraries: Vec::new(), java_version: None, type_: VersionType::Release, data: None, processors: None, }; let merged = merge_partial_version(partial, minecraft); let arguments = &merged.arguments.unwrap()[&ArgumentType::Jvm]; assert_eq!( arguments .iter() .filter(|argument| { matches!(argument, Argument::Normal(value) if value == "--add-opens") }) .count(), 2 ); } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] /// A manifest containing information about a mod loader's versions pub struct Manifest { /// The game versions the mod loader supports pub game_versions: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] /// Groups of game versions that share compatible loader version profiles pub version_groups: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] /// A game version of Minecraft pub struct Version { /// The minecraft version ID pub id: String, /// Whether the release is stable or not pub stable: bool, #[serde(skip_serializing_if = "Option::is_none")] /// The loader profile group for this Minecraft version pub version_group: Option, /// A map that contains loader versions for the game version pub loaders: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] /// A group of Minecraft versions that share loader version profiles pub struct VersionGroup { /// The version group ID pub id: String, /// The loader versions for this version group pub loaders: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] /// A version of a Minecraft mod loader pub struct LoaderVersion { /// The version ID of the loader pub id: String, /// The URL of the version's manifest pub url: String, /// Whether the loader is stable or not pub stable: bool, /// How the version profile at `url` should be resolved. #[serde(default, skip_serializing_if = "is_json_profile_source")] pub profile_source: LoaderProfileSource, /// JSON profile used if the official source cannot be resolved. #[serde(default, skip_serializing_if = "Option::is_none")] pub fallback_url: Option, } /// Source format used to resolve a loader version profile. #[derive( Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, )] #[serde(rename_all = "snake_case")] pub enum LoaderProfileSource { /// A partial Minecraft version JSON document. #[default] Json, /// An official Forge-compatible installer JAR. Installer, /// A profile synthesized from the LiteLoader versions manifest. LiteLoader, /// A profile synthesized from Babric's Glass Maven metadata and legacy profile. Babric, } fn is_json_profile_source(source: &LoaderProfileSource) -> bool { *source == LoaderProfileSource::Json } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn library(name: &str) -> Library { Library { downloads: None, extract: None, name: name.to_string(), url: None, natives: None, rules: None, checksums: None, include_in_classpath: true, downloadable: true, } } #[test] fn loader_manifest_metadata_uses_independent_format_versions() { let fabric = loader_manifest_metadata("fabric"); assert_eq!(fabric.format_version, 1); assert_eq!(fabric.cache_key, "fabric-v1"); assert_eq!(fabric.game_version, None); assert_eq!(fabric.path, "fabric/v0/manifest.json"); assert_eq!(loader_manifest_metadata("quilt").format_version, 2); assert_eq!(loader_manifest_metadata("forge").format_version, 3); assert_eq!(loader_manifest_metadata("neo").format_version, 1); assert_eq!(loader_manifest_metadata("babric").format_version, 2); assert_eq!(loader_manifest_metadata("babric").cache_key, "babric-v2"); assert_eq!( loader_manifest_metadata("quilt").path, "quilt/v1/manifest.json" ); let forge_1201 = loader_manifest_metadata_for_game("forge", "1.20.1"); let forge_262 = loader_manifest_metadata_for_game("forge", "26.2"); assert_eq!(forge_1201.cache_key, "forge-v3:1.20.1"); assert_eq!(forge_262.cache_key, "forge-v3:26.2"); assert_ne!(forge_1201.cache_key, forge_262.cache_key); assert_eq!(forge_1201.game_version.as_deref(), Some("1.20.1")); assert_eq!( loader_manifest_metadata_from_cache_key(&forge_1201.cache_key) .game_version .as_deref(), Some("1.20.1") ); } #[test] fn unobfuscated_version_boundary_is_conservative() { for version in ["26.1", "26.2", "26.1.1", "27.0", "26w14a"] { assert!(uses_unobfuscated_minecraft(version), "{version}"); } for version in [ "26.0", "26", "1.21", "1.21.11", "25w46a", "26w非", "invalid", ] { assert!(!uses_unobfuscated_minecraft(version), "{version}"); } } #[test] fn normalization_only_removes_exact_fabric_intermediary_coordinates() { let retained = [ "net.fabricmc:fabric-loader:0.19.3", "net.fabricmc:tiny-mappings-parser:0.3.0", "example:intermediary-helper:1.0", "example:contains-intermediary:1.0", ]; let mut libraries = std::iter::once(library("net.fabricmc:intermediary:26.2")) .chain(retained.iter().map(|name| library(name))) .collect(); assert_eq!( normalize_loader_libraries("fabric", "26.2", &mut libraries), vec!["net.fabricmc:intermediary:26.2"] ); assert_eq!( libraries .iter() .map(|library| library.name.as_str()) .collect::>(), retained ); assert!( normalize_loader_libraries("fabric", "26.2", &mut libraries) .is_empty() ); let paths = libraries .iter() .map(|library| { crate::get_path_from_artifact(&library.name).unwrap() }) .collect::>(); assert!(!paths.iter().any(|path| { path == "net/fabricmc/intermediary/26.2/intermediary-26.2.jar" })); assert!(paths.iter().any(|path| { path == "net/fabricmc/fabric-loader/0.19.3/fabric-loader-0.19.3.jar" })); } #[test] fn normalization_removes_intermediary_at_26_1_boundary() { let mut libraries = vec![library("net.fabricmc:intermediary:26.1")]; assert_eq!( normalize_loader_libraries("fabric", "26.1", &mut libraries), vec!["net.fabricmc:intermediary:26.1"] ); assert!(libraries.is_empty()); } #[test] fn normalization_preserves_legacy_fabric_and_other_loaders() { for (loader, version) in [ ("fabric", "1.21"), ("quilt", "26.2"), ("vanilla", "26.2"), ("forge", "26.2"), ("neo", "26.2"), ] { let mut libraries = vec![library("net.fabricmc:intermediary:26.2")]; assert!( normalize_loader_libraries(loader, version, &mut libraries) .is_empty() ); assert_eq!(libraries.len(), 1); } } #[test] fn cleanroom_normalization_removes_lwjgl2_family_and_legacy_mojang_conflicts() { // The vanilla LWJGL 2 family (binding, util at 2.9.4-nightly-20150209, // natives carrier at any version) plus the pre-existing Axolotl // Cleanroom rule targets (vanilla JNA platform, Mojang ICU) are // removed. The Cleanroom LWJGL 3 line, lwjglxx, the version-scoped // lwjgl_util at other versions, and the Cleanroom JNA core stay. let retained = [ "org.lwjgl:lwjgl:3.4.1-unsafe", "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", "net.java.dev.jna:jna:5.19.1", "net.java.dev.jna:jna-platform:5.19.1", "com.ibm.icu:icu4j:78.3", "com.cleanroommc:lwjglxx:1.1.22", ]; let removed = [ "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "net.java.dev.jna:platform:3.4.0", "com.ibm.icu:icu4j-core-mojang:51.2", "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", ]; let mut libraries = removed .iter() .chain(retained.iter()) .map(|name| library(name)) .collect(); assert_eq!( normalize_loader_libraries("cleanroom", "1.12.2", &mut libraries), removed ); assert_eq!( libraries .iter() .map(|library| library.name.as_str()) .collect::>(), retained ); } #[test] fn cleanroom_normalization_matches_lwjgl2_coordinates_by_version_scope() { // lwjgl and lwjgl-platform keep the pre-existing artifact match (any // version); lwjgl_util only matches at 2.9.4-nightly-20150209, // mirroring HMCL's classpath filter for Cleanroom. The LWJGL 3 line // and lwjglxx never match. let mut libraries = vec![ library("org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209"), library("org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209"), library("org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209"), library("org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822"), library("org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822"), library("org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822"), library("org.lwjgl:lwjgl:3.4.1-unsafe"), library("org.lwjgl:lwjgl-platform:9.9.9"), library("com.cleanroommc:lwjglxx:1.1.22"), ]; assert_eq!( normalize_loader_libraries("cleanroom", "1.12.2", &mut libraries), [ "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", ] ); assert_eq!( libraries .iter() .map(|library| library.name.as_str()) .collect::>(), [ "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", "org.lwjgl:lwjgl:3.4.1-unsafe", "org.lwjgl:lwjgl-platform:9.9.9", "com.cleanroommc:lwjglxx:1.1.22", ] ); } #[test] fn cleanroom_runtime_conflicts_remain_for_other_loaders() { for loader in ["vanilla", "forge", "fabric", "quilt", "neo"] { let mut libraries = vec![ library("org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209"), library( "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", ), library("net.java.dev.jna:platform:3.4.0"), library("com.ibm.icu:icu4j-core-mojang:51.2"), ]; assert!( normalize_loader_libraries(loader, "1.12.2", &mut libraries) .is_empty() ); assert_eq!(libraries.len(), 4); } } fn merged_fabric_version(game_version: &str) -> VersionInfo { let partial: PartialVersionInfo = serde_json::from_value(json!({ "id": "fabric-loader-0.19.3-${modrinth.gameVersion}", "inheritsFrom": "${modrinth.gameVersion}", "releaseTime": "2026-07-22T00:00:00Z", "time": "2026-07-22T00:00:00Z", "mainClass": "net.fabricmc.loader.impl.launch.knot.KnotClient", "libraries": [ { "name": "net.fabricmc:intermediary:${modrinth.gameVersion}" }, { "name": "net.fabricmc:fabric-loader:0.19.3" }, { "name": "org.ow2.asm:asm:9.9" } ], "type": "release" })) .unwrap(); let minecraft: VersionInfo = serde_json::from_value(json!({ "arguments": {}, "assetIndex": { "id": game_version, "sha1": "asset-sha1", "size": 1, "totalSize": 1, "url": "https://example.com/assets.json" }, "assets": game_version, "downloads": {}, "id": game_version, "libraries": [], "mainClass": "net.minecraft.client.main.Main", "minimumLauncherVersion": 0, "releaseTime": "2026-07-22T00:00:00Z", "time": "2026-07-22T00:00:00Z", "type": "release" })) .unwrap(); merge_partial_version(partial, minecraft) } #[test] fn merged_fabric_v0_profile_is_normalized_before_consumers() { let mut modern = merged_fabric_version("26.2"); assert!(modern .libraries .iter() .any(|library| library.name == "net.fabricmc:intermediary:26.2")); assert_eq!( normalize_loader_libraries("fabric", "26.2", &mut modern.libraries), vec!["net.fabricmc:intermediary:26.2"] ); let serialized = serde_json::to_string(&modern).unwrap(); assert!(!serialized.contains("net.fabricmc:intermediary")); assert!(serialized.contains("net.fabricmc:fabric-loader:0.19.3")); assert!(serialized.contains("org.ow2.asm:asm:9.9")); let mut legacy = merged_fabric_version("1.21"); assert!( normalize_loader_libraries("fabric", "1.21", &mut legacy.libraries) .is_empty() ); assert!(legacy .libraries .iter() .any(|library| library.name == "net.fabricmc:intermediary:1.21")); } }