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 ModpackInfo {
109 name: self.name.clone(),
110 version: self.version.clone(),
111 mc_version: self.minecraft.version.clone(),
112 loader,
113 loader_version,
114 }.validated()
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 install_cfpack_files(pack_path, instance_name, base_dir, curseforge, progress).await
139}
140
141pub async fn install_cfpack_files(
145 pack_path: &Path,
146 instance_name: &str,
147 base_dir: &Path,
148 curseforge: &CurseForgeClient,
149 progress: ProgressFn,
150) -> Result<ModpackInstallResult> {
151 let manifest = read_cf_manifest(pack_path).await?;
152 let info = manifest.info()?;
153
154 let game_dir = instance_game_dir(base_dir, instance_name);
155 tokio::fs::create_dir_all(&game_dir).await?;
156
157 progress(0, 0, "Extracting overrides");
158 extract_zip_dir(pack_path, &manifest.overrides, &game_dir).await?;
159
160 let wanted: Vec<&CfManifestFile> = manifest.files.iter().filter(|f| f.required).collect();
161 if wanted.is_empty() {
162 return Ok(ModpackInstallResult { info, manual_downloads: Vec::new(), skipped: Vec::new() });
163 }
164
165 progress(0, 0, "Resolving CurseForge files");
166 let file_ids: Vec<u64> = wanted.iter().map(|f| f.file_id).collect();
167 let project_ids: Vec<u64> = wanted.iter().map(|f| f.project_id).collect();
168 let files = curseforge.get_files(&file_ids).await?;
169 let projects: HashMap<u64, _> = curseforge
170 .get_mods(&project_ids)
171 .await?
172 .into_iter()
173 .map(|m| (m.id, m))
174 .collect();
175
176 let mut tasks = Vec::new();
177 let mut manual_downloads = Vec::new();
178 for file in files {
179 let project = projects.get(&file.mod_id);
180 let subdir = match project.and_then(|p| p.class_id) {
181 Some(CLASS_RESOURCE_PACKS) => "resourcepacks",
182 Some(CLASS_SHADERS) => "shaderpacks",
183 _ => "mods",
184 };
185 let dest = safe_join(&game_dir.join(subdir), &file.file_name)?;
186
187 match &file.download_url {
188 Some(url) => {
189 let mut task = DownloadTask::new(url, dest);
190 if let Some(sha1) = file.sha1() {
191 task = task.with_sha1(sha1);
192 }
193 tasks.push(task);
194 }
195 None => manual_downloads.push(ManualDownload {
196 name: project.map_or_else(|| file.display_name.clone(), |p| p.name.clone()),
197 file_name: file.file_name.clone(),
198 website: project
199 .and_then(|p| p.links.as_ref())
200 .and_then(|l| l.website_url.as_ref())
201 .map(|u| format!("{}/files/{}", u, file.id)),
202 dest,
203 }),
204 }
205 }
206
207 let p = progress.clone();
208 download_batch(tasks, 8, move |d, t| p(d, t, "Downloading modpack files")).await?;
209
210 Ok(ModpackInstallResult { info, manual_downloads, skipped: Vec::new() })
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn parse_manifest() {
219 let json = r#"{
220 "minecraft": {
221 "version": "1.20.1",
222 "modLoaders": [{ "id": "forge-47.2.0", "primary": true }]
223 },
224 "manifestType": "minecraftModpack",
225 "manifestVersion": 1,
226 "name": "Test",
227 "version": "1.0",
228 "author": "someone",
229 "files": [
230 { "projectID": 1, "fileID": 2, "required": true },
231 { "projectID": 3, "fileID": 4, "required": false }
232 ],
233 "overrides": "overrides"
234 }"#;
235 let manifest: CfManifest = serde_json::from_str(json).unwrap();
236 let info = manifest.info().unwrap();
237 assert_eq!(info.mc_version, "1.20.1");
238 assert_eq!(info.loader, LoaderType::Forge);
239 assert_eq!(info.loader_version.as_deref(), Some("47.2.0"));
240 assert_eq!(manifest.files.iter().filter(|f| f.required).count(), 1);
241 }
242}