//! Install modpacks from different sources. use crate::data::{Loader, ManagedPackage, ManagedVersion, PackageData}; use crate::prelude::ClusterPath; use crate::proxy::send::{init_ingress, send_ingress}; use crate::proxy::IngressId; use crate::store::{ClusterStage, PackageSide}; use crate::utils::http::{fetch, fetch_advanced, fetch_json, write_icon}; use crate::utils::io; use crate::{IngressType, InnerPathLinux, State}; use reqwest::Method; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PackFormat { pub game: String, pub format_version: i32, pub version_id: String, pub name: String, pub summary: Option, pub files: Vec, pub dependencies: HashMap, } #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PackFile { pub path: InnerPathLinux, pub hashes: HashMap, pub env: Option>, pub downloads: Vec, pub file_size: u32, } #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(rename_all = "camelCase", from = "String")] pub enum PackFileHash { Sha1, Sha512, Unknown(String), } impl From for PackFileHash { fn from(s: String) -> Self { return match s.as_str() { "sha1" => PackFileHash::Sha1, "sha512" => PackFileHash::Sha512, _ => PackFileHash::Unknown(s), }; } } #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(rename_all = "camelCase")] pub enum EnvType { Client, Server, } #[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)] pub enum PackDependency { #[serde(rename = "forge")] Forge, #[serde(rename = "neoforge")] #[serde(alias = "neo-forge")] NeoForge, #[serde(rename = "fabric-loader")] FabricLoader, #[serde(rename = "quilt-loader")] QuiltLoader, #[serde(rename = "minecraft")] Minecraft, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", tag = "type")] pub enum CreatePackLocation { /// Create a pack from a Modrinth modpack. FromModrinth { package_id: String, version_id: String, title: String, icon_url: Option, }, /// Create a pack from a file (importing, importing an mrpack or zip-style modpack) FromFile { path: PathBuf }, } /// Modpack associated [`Cluster`] wrapper. #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreatePackCluster { /// The name of the cluster, and relative path. pub name: String, /// The game version (MC) of the cluster. pub mc_version: String, /// The mod [`Loader`] to use (if applicable). pub mod_loader: Loader, /// The mod [`Loader`] version to use, set to `latest` or `stable` or the ID of the loader. /// The default value is `latest` for the latest version of the mod loader. pub loader_version: Option, /// The icon of the cluster. pub icon: Option, /// The URL of an icon for a cluster. (ONLY USE THIS FOR A TEMPORARY CLUSTER) pub icon_url: Option, /// The linked package data (mainly used for modpacks and updating files). pub package_data: Option, /// Whether or not the cluster should skip being installed. pub skip: Option, /// Whether or not the cluster should skip file watching. pub skip_watch: Option, } impl Default for CreatePackCluster { fn default() -> Self { CreatePackCluster { name: "Untitled".to_string(), mc_version: "1.20.4".to_string(), mod_loader: Loader::Vanilla, loader_version: None, icon: None, icon_url: None, package_data: None, skip: Some(true), skip_watch: Some(false), } } } #[derive(Clone)] pub struct CreatePack { pub file: bytes::Bytes, pub description: CreatePackDescription, } #[derive(Clone, Debug)] pub struct CreatePackDescription { pub icon: Option, pub override_title: Option, pub package_id: Option, pub version_id: Option, pub existing_ingress: Option, pub cluster_path: ClusterPath, } pub fn get_cluster_from_pack(location: CreatePackLocation) -> CreatePackCluster { match location { CreatePackLocation::FromModrinth { package_id, version_id, title, icon_url, } => CreatePackCluster { name: title, icon_url, package_data: Some(PackageData { package_id: Some(package_id), version_id: Some(version_id), locked: Some(true), }), ..Default::default() }, CreatePackLocation::FromFile { path } => { let file_name = path .file_stem() .unwrap_or_default() .to_string_lossy() .to_string(); CreatePackCluster { name: file_name, ..Default::default() } } } } #[tracing::instrument] #[onelauncher_macros::memory] pub async fn generate_pack_from_version_id( package_id: String, version_id: String, title: String, icon_url: Option, cluster_path: ClusterPath, existing_ingress: Option, ) -> crate::Result { let state = State::get().await?; let ingress = if let Some(ing) = existing_ingress { send_ingress(&ing, 0.0, Some("downloading pack file")).await?; ing } else { init_ingress( IngressType::DownloadPack { cluster_path: cluster_path.full_path().await?, package_name: title, icon: icon_url, package_version: version_id.clone(), }, 100.0, "downloading pack file", ) .await? }; send_ingress(&ingress, 0.0, Some("fetching version")).await?; let version: ManagedVersion = fetch_json( Method::GET, &format!( "{}version/{}", crate::constants::MODRINTH_API_URL, version_id ), None, None, &state.fetch_semaphore, ) .await?; send_ingress(&ingress, 10.0, None).await?; let (url, hash) = if let Some(file) = version.files.iter().find(|x| x.primary) { Some((file.url.clone(), file.hashes.get("sha1"))) } else { version .files .first() .map(|file| (file.url.clone(), file.hashes.get("sha1"))) } .ok_or_else(|| anyhow::anyhow!("specified version has no files"))?; let file = fetch_advanced( Method::GET, &url, hash.map(|x| &**x), None, None, Some((&ingress, 70.0)), &state.fetch_semaphore, ) .await?; send_ingress(&ingress, 0.0, Some("fetching project metadata")).await?; let pkg: ManagedPackage = fetch_json( Method::GET, &format!( "{}project/{}", crate::constants::MODRINTH_API_URL, version.package_id ), None, None, &state.fetch_semaphore, ) .await?; send_ingress(&ingress, 10.0, Some("Retrieving icon")).await?; let icon = if let Some(icon_url) = pkg.icon_url { let state = State::get().await?; let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?; let filename = icon_url.rsplit('/').next(); if let Some(filename) = filename { Some( write_icon( filename, &state.directories.caches_dir().await, icon_bytes, &state.io_semaphore, ) .await?, ) } else { None } } else { None }; send_ingress(&ingress, 10.0, None).await?; Ok(CreatePack { file, description: CreatePackDescription { icon, override_title: None, package_id: Some(package_id), version_id: Some(version_id), existing_ingress: Some(ingress), cluster_path, }, }) } #[tracing::instrument] #[onelauncher_macros::memory] pub async fn generate_pack_from_file( path: PathBuf, cluster_path: ClusterPath, ) -> crate::Result { let file = io::read(&path).await?; Ok(CreatePack { file: bytes::Bytes::from(file), description: CreatePackDescription { icon: None, override_title: None, package_id: None, version_id: None, existing_ingress: None, cluster_path, }, }) } /// Sets generated cluster attributes to the pack attributes (using `cluster::edit`). /// This includes the pack name, icon, game version, loader version, and loader. #[onelauncher_macros::memory] pub async fn set_cluster_information( cluster_path: ClusterPath, description: &CreatePackDescription, backup_name: &str, deps: &HashMap, ignore_lock: bool, ) -> crate::Result<()> { let mut game_version: Option<&String> = None; let mut mod_loader = None; let mut loader_version = None; for (key, value) in deps { match key { PackDependency::Forge => { mod_loader = Some(Loader::Forge); loader_version = Some(value); } PackDependency::NeoForge => { mod_loader = Some(Loader::NeoForge); loader_version = Some(value); } PackDependency::FabricLoader => { mod_loader = Some(Loader::Fabric); loader_version = Some(value); } PackDependency::QuiltLoader => { mod_loader = Some(Loader::Quilt); loader_version = Some(value); } PackDependency::Minecraft => game_version = Some(value), } } let game_version = if let Some(game_version) = game_version { game_version } else { return Err(anyhow::anyhow!("pack did not specify Minecraft version").into()); }; let mod_loader = mod_loader.unwrap_or(Loader::Vanilla); let loader_version = if mod_loader != Loader::Vanilla { crate::cluster::create::get_loader_version( game_version.clone(), mod_loader, loader_version.cloned(), ) .await? } else { None }; crate::api::cluster::edit(&cluster_path, |cl| { cl.meta.name = description .override_title .clone() .unwrap_or_else(|| backup_name.to_string()); cl.stage = ClusterStage::PackDownloading; let package_id = description.package_id.clone(); let version_id = description.version_id.clone(); cl.meta.package_data = if package_id.is_some() && version_id.is_some() { Some(PackageData { package_id, version_id, locked: if !ignore_lock { Some(true) } else { cl.meta.package_data.as_ref().and_then(|x| x.locked) }, }) } else { None }; cl.meta.icon.clone_from(&description.icon); cl.meta.mc_version.clone_from(game_version); cl.meta.loader_version.clone_from(&loader_version); cl.meta.loader = mod_loader; async { Ok(()) } }) .await?; Ok(()) }