1use std::collections::HashMap;
8use std::path::Path;
9
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 download::{download_batch, DownloadTask},
14 error::{HexoError, Result},
15 install::{loader::ProgressFn, vanilla::LoaderType},
16 modpack::{
17 extract_zip_dir, install_pack_loader, instance_game_dir, read_zip_json, safe_join,
18 ManualDownload, ModpackInfo, ModpackInstallResult,
19 },
20 mods::curseforge::CurseForgeClient,
21};
22
23pub const MANIFEST_FILE: &str = "manifest.json";
24
25const CLASS_RESOURCE_PACKS: u32 = 12;
27const CLASS_SHADERS: u32 = 6552;
29
30#[derive(Debug, Deserialize, Serialize, Clone)]
31#[serde(rename_all = "camelCase")]
32pub struct CfManifest {
33 pub minecraft: CfManifestMinecraft,
34 #[serde(default)]
35 pub manifest_type: String,
36 #[serde(default)]
37 pub name: String,
38 #[serde(default)]
39 pub version: Option<String>,
40 #[serde(default)]
41 pub author: Option<String>,
42 #[serde(default)]
43 pub files: Vec<CfManifestFile>,
44 #[serde(default = "default_overrides")]
45 pub overrides: String,
46}
47
48fn default_overrides() -> String {
49 "overrides".into()
50}
51
52#[derive(Debug, Deserialize, Serialize, Clone)]
53#[serde(rename_all = "camelCase")]
54pub struct CfManifestMinecraft {
55 pub version: String,
56 #[serde(default)]
57 pub mod_loaders: Vec<CfManifestLoader>,
58}
59
60#[derive(Debug, Deserialize, Serialize, Clone)]
61pub struct CfManifestLoader {
62 pub id: String,
64 #[serde(default)]
65 pub primary: bool,
66}
67
68#[derive(Debug, Deserialize, Serialize, Clone)]
69pub struct CfManifestFile {
70 #[serde(rename = "projectID")]
71 pub project_id: u64,
72 #[serde(rename = "fileID")]
73 pub file_id: u64,
74 #[serde(default = "default_true")]
75 pub required: bool,
76}
77
78fn default_true() -> bool {
79 true
80}
81
82impl CfManifest {
83 pub fn info(&self) -> Result<ModpackInfo> {
84 let loader = self
85 .minecraft
86 .mod_loaders
87 .iter()
88 .find(|l| l.primary)
89 .or_else(|| self.minecraft.mod_loaders.first());
90
91 let (loader, loader_version) = match loader {
92 None => (LoaderType::Vanilla, None),
93 Some(l) => {
94 let (kind, ver) = l
95 .id
96 .split_once('-')
97 .ok_or_else(|| HexoError::UnsupportedLoader(l.id.clone()))?;
98 let kind = match kind {
99 "forge" => LoaderType::Forge,
100 "neoforge" => LoaderType::NeoForge,
101 "fabric" => LoaderType::Fabric,
102 _ => return Err(HexoError::UnsupportedLoader(l.id.clone())),
103 };
104 (kind, Some(ver.to_string()))
105 }
106 };
107
108 Ok(ModpackInfo {
109 name: self.name.clone(),
110 version: self.version.clone(),
111 mc_version: self.minecraft.version.clone(),
112 loader,
113 loader_version,
114 })
115 }
116}
117
118pub async fn read_cf_manifest(pack_path: &Path) -> Result<CfManifest> {
120 read_zip_json(pack_path, MANIFEST_FILE).await
121}
122
123pub async fn install_cfpack(
126 pack_path: &Path,
127 instance_name: &str,
128 base_dir: &Path,
129 java_path: Option<&Path>,
130 curseforge: &CurseForgeClient,
131 progress: ProgressFn,
132) -> Result<ModpackInstallResult> {
133 let manifest = read_cf_manifest(pack_path).await?;
134 let info = manifest.info()?;
135
136 install_pack_loader(&info, instance_name, base_dir, java_path, progress.clone()).await?;
137
138 let game_dir = instance_game_dir(base_dir, instance_name);
139
140 progress(0, 0, "Extracting overrides");
141 extract_zip_dir(pack_path, &manifest.overrides, &game_dir).await?;
142
143 let wanted: Vec<&CfManifestFile> = manifest.files.iter().filter(|f| f.required).collect();
144 if wanted.is_empty() {
145 return Ok(ModpackInstallResult { info, manual_downloads: Vec::new(), skipped: Vec::new() });
146 }
147
148 progress(0, 0, "Resolving CurseForge files");
149 let file_ids: Vec<u64> = wanted.iter().map(|f| f.file_id).collect();
150 let project_ids: Vec<u64> = wanted.iter().map(|f| f.project_id).collect();
151 let files = curseforge.get_files(&file_ids).await?;
152 let projects: HashMap<u64, _> = curseforge
153 .get_mods(&project_ids)
154 .await?
155 .into_iter()
156 .map(|m| (m.id, m))
157 .collect();
158
159 let mut tasks = Vec::new();
160 let mut manual_downloads = Vec::new();
161 for file in files {
162 let project = projects.get(&file.mod_id);
163 let subdir = match project.and_then(|p| p.class_id) {
164 Some(CLASS_RESOURCE_PACKS) => "resourcepacks",
165 Some(CLASS_SHADERS) => "shaderpacks",
166 _ => "mods",
167 };
168 let dest = safe_join(&game_dir.join(subdir), &file.file_name)?;
169
170 match &file.download_url {
171 Some(url) => {
172 let mut task = DownloadTask::new(url, dest);
173 if let Some(sha1) = file.sha1() {
174 task = task.with_sha1(sha1);
175 }
176 tasks.push(task);
177 }
178 None => manual_downloads.push(ManualDownload {
179 name: project.map_or_else(|| file.display_name.clone(), |p| p.name.clone()),
180 file_name: file.file_name.clone(),
181 website: project
182 .and_then(|p| p.links.as_ref())
183 .and_then(|l| l.website_url.as_ref())
184 .map(|u| format!("{}/files/{}", u, file.id)),
185 dest,
186 }),
187 }
188 }
189
190 let p = progress.clone();
191 download_batch(tasks, 8, move |d, t| p(d, t, "Downloading modpack files")).await?;
192
193 Ok(ModpackInstallResult { info, manual_downloads, skipped: Vec::new() })
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn parse_manifest() {
202 let json = r#"{
203 "minecraft": {
204 "version": "1.20.1",
205 "modLoaders": [{ "id": "forge-47.2.0", "primary": true }]
206 },
207 "manifestType": "minecraftModpack",
208 "manifestVersion": 1,
209 "name": "Test",
210 "version": "1.0",
211 "author": "someone",
212 "files": [
213 { "projectID": 1, "fileID": 2, "required": true },
214 { "projectID": 3, "fileID": 4, "required": false }
215 ],
216 "overrides": "overrides"
217 }"#;
218 let manifest: CfManifest = serde_json::from_str(json).unwrap();
219 let info = manifest.info().unwrap();
220 assert_eq!(info.mc_version, "1.20.1");
221 assert_eq!(info.loader, LoaderType::Forge);
222 assert_eq!(info.loader_version.as_deref(), Some("47.2.0"));
223 assert_eq!(manifest.files.iter().filter(|f| f.required).count(), 1);
224 }
225}