use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize, de}; use crate::{ State, install::{InstallPhaseDetails, InstallProgressReporter}, pack::{ import::{self, finish_import}, install_from::{self, CreatePackDescription, PackDependency}, }, util::io, }; // instance.cfg // https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/MinecraftInstance.cpp #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] #[serde(untagged)] enum MMCInstanceEnum { General(MMCInstanceGeneral), Instance(MMCInstance), } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] struct MMCInstanceGeneral { pub general: MMCInstance, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] pub struct MMCInstance { pub java_path: Option, pub jvm_args: Option, #[serde(default)] #[serde(deserialize_with = "deserialize_optional_bool")] pub managed_pack: Option, #[serde(rename = "ManagedPackID")] pub managed_pack_id: Option, pub managed_pack_type: Option, #[serde(rename = "ManagedPackVersionID")] pub managed_pack_version_id: Option, pub managed_pack_version_name: Option, #[serde(rename = "iconKey")] pub icon_key: Option, #[serde(rename = "name")] pub name: Option, } // serde_ini reads 'true' and 'false' as strings, so we need to convert them to booleans fn deserialize_optional_bool<'de, D>( deserializer: D, ) -> Result, D::Error> where D: de::Deserializer<'de>, { let s = Option::::deserialize(deserializer)?; match s { Some(string) => match string.as_str() { "true" => Ok(Some(true)), "false" => Ok(Some(false)), _ => Err(de::Error::custom("expected 'true' or 'false'")), }, None => Ok(None), } } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "lowercase")] pub enum MMCManagedPackType { Modrinth, Flame, ATLauncher, #[serde(other)] Unknown, } // mmc-pack.json #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MMCPack { components: Vec, format_version: u32, } // https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/Component.h #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MMCComponent { pub uid: String, #[serde(default)] pub version: Option, #[serde(default)] pub dependency_only: bool, #[serde(default)] pub important: bool, #[serde(default)] pub disabled: bool, pub cached_name: Option, pub cached_version: Option, #[serde(default)] pub cached_requires: Vec, #[serde(default)] pub cached_conflicts: Vec, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MMCComponentRequirement { pub uid: String, pub equals_version: Option, pub suggests: Option, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] #[serde(untagged)] enum MMCLauncherEnum { General(MMCLauncherGeneral), Instance(MMCLauncher), } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] struct MMCLauncherGeneral { pub general: MMCLauncher, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] pub struct MMCLauncher { instance_dir: String, } // Checks if if its a folder, and the folder contains instance.cfg and mmc-pack.json, and they both parse #[tracing::instrument] pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool { let instance_cfg = instance_folder.join("instance.cfg"); let mmc_pack = instance_folder.join("mmc-pack.json"); let Ok((mmc_pack, _)) = io::read_any_encoding_to_string(&mmc_pack).await else { return false; }; load_instance_cfg(&instance_cfg).await.is_ok() && serde_json::from_str::(&mmc_pack).is_ok() } #[tracing::instrument] pub async fn get_instances_subpath(config: PathBuf) -> Option { let launcher = io::read_any_encoding_to_string(&config).await.ok()?.0; let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?; match launcher { MMCLauncherEnum::General(p) => Some(p.general.instance_dir), MMCLauncherEnum::Instance(p) => Some(p.instance_dir), } } // Loading the INI (instance.cfg) file async fn load_instance_cfg(file_path: &Path) -> crate::Result { match serde_ini::from_str::( &io::read_any_encoding_to_string(file_path).await?.0, )? { MMCInstanceEnum::General(instance_cfg) => Ok(instance_cfg.general), MMCInstanceEnum::Instance(instance_cfg) => Ok(instance_cfg), } } // #[tracing::instrument] pub(crate) async fn import_mmc_instance_dir( mmc_instance_path: PathBuf, icons_dir: Option, instance_id: &str, reporter: InstallProgressReporter, details: InstallPhaseDetails, symlink: bool, ) -> crate::Result<()> { let mmc_pack = serde_json::from_str::( &io::read_any_encoding_to_string( &mmc_instance_path.join("mmc-pack.json"), ) .await? .0, )?; let instance_cfg = load_instance_cfg(&mmc_instance_path.join("instance.cfg")).await?; // Re-cache icon let icon = if let Some(icon_key) = instance_cfg.icon_key { let mut icon = None; for icon_dir in icons_dir.iter().chain(std::iter::once(&mmc_instance_path)) { let icon_path = icon_dir.join(&icon_key); icon = import::recache_icon(icon_path).await?; if icon.is_none() { let icon_path = icon_dir.join(format!("{icon_key}.png")); icon = import::recache_icon(icon_path).await?; } if icon.is_some() { break; } } icon } else { None }; // Create description from instance.cfg let mut description = CreatePackDescription { icon, override_title: instance_cfg.name, project_id: None, version_id: None, instance_id: instance_id.to_string(), source_filename: None, }; let mut minecraft_folder = mmc_instance_path.join("minecraft"); if !minecraft_folder.is_dir() { minecraft_folder = mmc_instance_path.join(".minecraft"); if !minecraft_folder.is_dir() { return Err(crate::ErrorKind::InputError( "Instance is missing Minecraft directory".to_string(), ) .into()); } } // Managed pack if instance_cfg.managed_pack.unwrap_or(false) { match instance_cfg.managed_pack_type { Some(MMCManagedPackType::Modrinth) => { description.project_id = instance_cfg.managed_pack_id; description.version_id = instance_cfg.managed_pack_version_id; // Modrinth Managed Pack // Kept separate as we may in the future want to add special handling for modrinth managed packs import_mmc_unmanaged(instance_id, minecraft_folder, "Imported Modrinth Modpack".to_string(), description, mmc_pack, reporter, details, symlink).await?; } Some(MMCManagedPackType::Flame | MMCManagedPackType::ATLauncher) => { // For flame/atlauncher managed packs // Treat as unmanaged, but with 'minecraft' folder instead of '.minecraft' import_mmc_unmanaged(instance_id, minecraft_folder, "Imported Modpack".to_string(), description, mmc_pack, reporter, details, symlink).await?; }, Some(_) => { // For managed packs that aren't modrinth, flame, atlauncher // Treat as unmanaged import_mmc_unmanaged(instance_id, minecraft_folder, "ImportedModpack".to_string(), description, mmc_pack, reporter, details, symlink).await?; }, _ => return Err(crate::ErrorKind::InputError("Instance is managed, but managed pack type not specified in instance.cfg".to_string()).into()) } } else { // Directly import unmanaged pack import_mmc_unmanaged( instance_id, minecraft_folder, "Imported Modpack".to_string(), description, mmc_pack, reporter, details, symlink, ) .await?; } Ok(()) } async fn import_mmc_unmanaged( instance_id: &str, minecraft_folder: PathBuf, backup_name: String, description: CreatePackDescription, mmc_pack: MMCPack, reporter: InstallProgressReporter, details: InstallPhaseDetails, symlink: bool, ) -> crate::Result<()> { let dependencies = mmc_dependencies(&mmc_pack)?; install_from::set_instance_information( instance_id.to_string(), &description, &backup_name, None, &dependencies, false, ) .await?; // Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc) let state = State::get().await?; finish_import( instance_id, Some(minecraft_folder), None, false, &state.io_semaphore, reporter, details, symlink, ) .await?; Ok(()) } fn mmc_dependencies( mmc_pack: &MMCPack, ) -> crate::Result> { let mut dependencies = std::collections::HashMap::new(); let has_legacy_fabric = mmc_pack.components.iter().any(|component| { component .uid .to_ascii_lowercase() .starts_with("net.legacyfabric") }); let has_cleanroom = mmc_pack.components.iter().any(|component| { component .uid .to_ascii_lowercase() .starts_with("com.cleanroommc") }); for component in &mmc_pack.components { let uid = component.uid.to_ascii_lowercase(); if uid.contains("labymod") { return Err(crate::ErrorKind::InputError( "Unsupported loader LabyMod: Axolotl does not install, update, or repair LabyMod instances" .to_string(), ) .into()); } let dependency = if uid.starts_with("net.fabricmc.fabric-loader") { Some(if has_legacy_fabric { PackDependency::LegacyFabric } else { PackDependency::FabricLoader }) } else if uid.starts_with("net.legacyfabric") { Some(PackDependency::LegacyFabric) } else if uid.starts_with("net.minecraftforge") { (!has_cleanroom).then_some(PackDependency::Forge) } else if uid.starts_with("net.neoforged") { Some(PackDependency::NeoForge) } else if uid.starts_with("org.quiltmc.quilt-loader") { Some(PackDependency::QuiltLoader) } else if uid.starts_with("com.cleanroommc") { Some(PackDependency::Cleanroom) } else if uid.contains("liteloader") { Some(PackDependency::LiteLoader) } else if uid.contains("optifabric") { Some(PackDependency::OptiFabric) } else if uid.contains("optifine") { Some(PackDependency::OptiFine) } else if uid.starts_with("net.minecraft") { Some(PackDependency::Minecraft) } else { None }; if let Some(dependency) = dependency { let version = component .version .clone() .filter(|v| !v.is_empty()) .ok_or_else(|| { crate::ErrorKind::InputError(format!( "MultiMC component {} is missing its version", component.uid )) })?; dependencies.insert(dependency, version); } } Ok(dependencies) } #[cfg(test)] mod tests { use super::*; fn component(uid: &str, version: &str) -> MMCComponent { MMCComponent { uid: uid.to_string(), version: Some(version.to_string()), dependency_only: false, important: true, disabled: false, cached_name: None, cached_version: None, cached_requires: Vec::new(), cached_conflicts: Vec::new(), } } #[test] fn dependencies_preserve_legacy_fabric_and_optifine_components() { let pack = MMCPack { format_version: 1, components: vec![ component("net.minecraft", "1.8.9"), component("net.legacyfabric.intermediary", "1.8.9"), component("net.fabricmc.fabric-loader", "0.13.1.4"), component("optifine.OptiFine", "1.8.9_HD_U_M6_pre2"), component("optifabric.OptiFabric", "1.13.16"), ], }; let dependencies = mmc_dependencies(&pack).unwrap(); assert_eq!( dependencies.get(&PackDependency::LegacyFabric), Some(&"0.13.1.4".to_string()) ); assert_eq!( dependencies.get(&PackDependency::OptiFine), Some(&"1.8.9_HD_U_M6_pre2".to_string()) ); assert_eq!( dependencies.get(&PackDependency::OptiFabric), Some(&"1.13.16".to_string()) ); } #[test] fn dependencies_reject_labymod_components() { let pack = MMCPack { format_version: 1, components: vec![component("net.labymod.LabyMod", "4.4.20")], }; assert!(mmc_dependencies(&pack).is_err()); } }