1use serde::{Deserialize, Serialize};
2use std::{
3 collections::HashMap,
4 path::{Path, PathBuf},
5};
6use tokio::fs;
7
8use crate::{
9 download::{download_batch, download_file, DownloadTask},
10 error::{HexoError, Result},
11 install::vanilla::{InstanceConfig, LibEntry, LoaderType},
12};
13
14const FORGE_META_URL: &str =
15 "https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json";
16const FORGE_MAVEN_BASE: &str =
17 "https://maven.minecraftforge.net/net/minecraftforge/forge";
18
19#[derive(Debug, Deserialize, Serialize, Clone)]
20pub struct ForgeInstallProfile {
21 pub libraries: Vec<ForgeLibrary>,
22 pub processors: Vec<ForgeProcessor>,
23 pub data: HashMap<String, ForgeDataEntry>,
24}
25
26#[derive(Debug, Deserialize, Serialize, Clone)]
27pub struct ForgeLibrary {
28 pub name: String,
29 pub downloads: ForgeLibraryDownloads,
30}
31
32#[derive(Debug, Deserialize, Serialize, Clone)]
33pub struct ForgeLibraryDownloads {
34 pub artifact: ForgeArtifact,
35}
36
37#[derive(Debug, Deserialize, Serialize, Clone)]
38pub struct ForgeArtifact {
39 pub path: String,
40 pub sha1: String,
41 pub url: String,
42}
43
44#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct ForgeProcessor {
46 pub jar: String,
47 pub classpath: Vec<String>,
48 pub args: Vec<String>,
49 pub sides: Option<Vec<String>>,
50}
51
52#[derive(Debug, Deserialize, Serialize, Clone)]
53pub struct ForgeDataEntry {
54 pub client: String,
55 pub server: String,
56}
57
58#[derive(Debug, Deserialize, Serialize, Clone)]
59pub struct ForgeVersionJson {
60 #[serde(rename = "mainClass")]
61 pub main_class: String,
62 pub libraries: Vec<ForgeLibrary>,
63 pub arguments: ForgeVersionArgs,
64}
65
66#[derive(Debug, Deserialize, Serialize, Clone)]
67pub struct ForgeVersionArgs {
68 pub jvm: Option<Vec<String>>,
69 pub game: Option<Vec<String>>,
70}
71
72#[derive(Debug, Deserialize)]
75pub struct LegacyInstallProfile {
76 pub install: LegacyInstall,
77 #[serde(rename = "versionInfo")]
78 pub version_info: LegacyVersionInfo,
79}
80
81#[derive(Debug, Deserialize)]
82pub struct LegacyInstall {
83 pub path: String,
85 #[serde(rename = "filePath")]
87 pub file_path: String,
88}
89
90#[derive(Debug, Deserialize)]
91pub struct LegacyVersionInfo {
92 #[serde(rename = "mainClass")]
93 pub main_class: String,
94 #[serde(rename = "minecraftArguments")]
95 pub minecraft_arguments: String,
96 pub libraries: Vec<LegacyLibrary>,
97}
98
99#[derive(Debug, Deserialize)]
100pub struct LegacyLibrary {
101 pub name: String,
102 #[serde(default)]
104 pub url: Option<String>,
105 #[serde(default)]
107 pub clientreq: Option<bool>,
108}
109
110pub async fn get_forge_versions(mc_version: &str) -> Result<Vec<String>> {
112 let client = reqwest::Client::new();
113 let data: HashMap<String, Vec<String>> =
114 client.get(FORGE_META_URL).send().await?.json().await?;
115
116 Ok(data.get(mc_version).cloned().unwrap_or_default())
117}
118
119pub async fn install_forge(
121 mc_version: &str,
122 forge_version: Option<&str>,
123 instance_name: &str,
124 java_path: &Path,
125 base_dir: &Path,
126) -> Result<()> {
127 let versions = get_forge_versions(mc_version).await?;
128 let forge_ver = if let Some(v) = forge_version {
129 v.to_string()
130 } else {
131 versions
133 .into_iter()
134 .last()
135 .ok_or_else(|| HexoError::VersionNotFound(format!("Forge for {}", mc_version)))?
136 };
137
138 let installer_url = format!(
140 "{}/{}/forge-{}-installer.jar",
141 FORGE_MAVEN_BASE, forge_ver, forge_ver
142 );
143
144 install_forge_like(
145 mc_version,
146 &forge_ver,
147 &installer_url,
148 instance_name,
149 java_path,
150 base_dir,
151 LoaderType::Forge,
152 )
153 .await
154}
155
156
157pub async fn install_forge_like(
158 mc_version: &str,
159 loader_version: &str,
160 installer_url: &str,
161 instance_name: &str,
162 java_path: &Path,
163 base_dir: &Path,
164 loader_type: LoaderType,
165) -> Result<()> {
166 let instance_dir = base_dir.join("instance").join(instance_name);
167 let lib_dir = base_dir.join("libraries");
168 let temp_dir = base_dir.join("temp").join(loader_version);
169 fs::create_dir_all(&temp_dir).await?;
170
171 let installer_path = temp_dir.join("forge-installer.jar");
172 download_file(&DownloadTask::new(installer_url, &installer_path)).await?;
173
174 let profile_value: serde_json::Value =
176 read_json_from_zip(&installer_path, "install_profile.json").await?;
177 if profile_value.get("versionInfo").is_some() {
178 let legacy: LegacyInstallProfile = serde_json::from_value(profile_value)?;
179 let result =
180 install_forge_legacy(&installer_path, &legacy, instance_name, base_dir).await;
181 let _ = fs::remove_dir_all(&temp_dir).await;
182 return result;
183 }
184 let install_profile: ForgeInstallProfile = serde_json::from_value(profile_value)?;
185
186 let mut lib_tasks: Vec<DownloadTask> = Vec::new();
187 for lib in &install_profile.libraries {
188 let art = &lib.downloads.artifact;
189 if art.url.is_empty() {
190 continue;
191 }
192 let path = lib_dir.join(&art.path);
193 lib_tasks.push(DownloadTask::new(&art.url, &path).with_sha1(art.sha1.clone()));
194 }
195 download_batch(lib_tasks, 8, |_, _| {}).await?;
196
197 let lzma_path = temp_dir.join("client.lzma");
198 extract_file_from_zip(&installer_path, "data/client.lzma", &lzma_path).await?;
199
200 let mc_jar = instance_dir.join(format!("{}.jar", mc_version));
201 run_processors(
202 &install_profile.processors,
203 &install_profile.data,
204 &installer_path,
205 &lzma_path,
206 &mc_jar,
207 &lib_dir,
208 java_path,
209 &temp_dir,
210 )
211 .await?;
212
213 let version_json: ForgeVersionJson =
214 read_json_from_zip(&installer_path, "version.json").await?;
215
216 let mut new_lib_entries: Vec<LibEntry> = Vec::new();
217 let mut ver_lib_tasks: Vec<DownloadTask> = Vec::new();
218 for lib in &version_json.libraries {
219 let art = &lib.downloads.artifact;
220 if art.url.is_empty() {
221 continue;
222 }
223 let path = lib_dir.join(&art.path);
224 ver_lib_tasks.push(DownloadTask::new(&art.url, &path).with_sha1(art.sha1.clone()));
225 new_lib_entries.push(LibEntry {
226 name: lib.name.clone(),
227 path: path.clone(),
228 sha1: art.sha1.clone(),
229 });
230 }
231 download_batch(ver_lib_tasks, 8, |_, _| {}).await?;
232
233 let mut config = InstanceConfig::load(&instance_dir).await?;
234
235 if let Some(jvm_args) = &version_json.arguments.jvm {
237 let cp_pos = config
238 .start_args
239 .iter()
240 .position(|a| a == "-cp")
241 .unwrap_or(0);
242 for (i, arg) in jvm_args.iter().enumerate() {
243 config.start_args.insert(cp_pos + i, arg.clone());
244 }
245 }
246
247 if let Some(game_args) = &version_json.arguments.game {
249 if let Some(mc_pos) = config.start_args.iter().position(|a| a == "${mainClass}") {
250 for (i, arg) in game_args.iter().enumerate() {
251 config.start_args.insert(mc_pos + 1 + i, arg.clone());
252 }
253 }
254 }
255
256 config.main_class = version_json.main_class.clone();
257 config.loader_type = loader_type;
258
259 new_lib_entries.extend(config.lib_list.drain(..));
261 config.lib_list = new_lib_entries;
262
263 config.save(&instance_dir).await?;
264
265 fs::remove_dir_all(&temp_dir).await?;
266
267 Ok(())
268}
269
270async fn install_forge_legacy(
274 installer_path: &Path,
275 profile: &LegacyInstallProfile,
276 instance_name: &str,
277 base_dir: &Path,
278) -> Result<()> {
279 let instance_dir = base_dir.join("instance").join(instance_name);
280 let lib_dir = base_dir.join("libraries");
281
282 let universal_dest = resolve_maven_path(&lib_dir, &profile.install.path);
284 if let Some(parent) = universal_dest.parent() {
285 fs::create_dir_all(parent).await?;
286 }
287 extract_file_from_zip(installer_path, &profile.install.file_path, &universal_dest).await?;
288
289 let mut lib_tasks: Vec<DownloadTask> = Vec::new();
290 let mut new_lib_entries: Vec<LibEntry> = Vec::new();
291 for lib in &profile.version_info.libraries {
292 if lib.clientreq == Some(false) {
293 continue;
294 }
295 let path = resolve_maven_path(&lib_dir, &lib.name);
296 new_lib_entries.push(LibEntry {
297 name: lib.name.clone(),
298 path: path.clone(),
299 sha1: String::new(),
300 });
301
302 if lib.name == profile.install.path {
304 continue;
305 }
306
307 let base = lib
308 .url
309 .clone()
310 .unwrap_or_else(|| "https://libraries.minecraft.net/".to_string());
311 let sep = if base.ends_with('/') { "" } else { "/" };
312 let url = format!("{}{}{}", base, sep, maven_relative_path(&lib.name));
313 lib_tasks.push(DownloadTask::new(url, &path));
314 }
315 download_batch(lib_tasks, 8, |_, _| {}).await?;
316
317 let mut config = InstanceConfig::load(&instance_dir).await?;
318 config.main_class = profile.version_info.main_class.clone();
319 config.loader_type = LoaderType::Forge;
320 config.start_args = profile
322 .version_info
323 .minecraft_arguments
324 .split_whitespace()
325 .map(|s| s.to_string())
326 .collect();
327 new_lib_entries.extend(config.lib_list.drain(..));
329 config.lib_list = new_lib_entries;
330 config.save(&instance_dir).await?;
331
332 Ok(())
333}
334
335#[allow(clippy::too_many_arguments)]
336async fn run_processors(
337 processors: &[ForgeProcessor],
338 data: &HashMap<String, ForgeDataEntry>,
339 installer_path: &Path,
340 lzma_path: &Path,
341 mc_jar: &Path,
342 lib_dir: &Path,
343 java_path: &Path,
344 temp_dir: &Path,
345) -> Result<()> {
346 let mut rargs: HashMap<String, String> = HashMap::new();
347
348 for (key, entry) in data {
349 let value = &entry.client;
350 let resolved = if value.starts_with('[') && value.ends_with(']') {
351 path_str(&resolve_maven_path(lib_dir, &value[1..value.len() - 1]))
352 } else {
353 value.clone()
354 };
355 rargs.insert(format!("{{{}}}", key), resolved);
356 }
357
358 rargs.insert("{INSTALLER}".to_string(), path_str(installer_path));
361 rargs.insert("{ROOT}".to_string(), path_str(temp_dir));
362 rargs.insert("{SIDE}".to_string(), "client".to_string());
363 rargs.insert("{MINECRAFT_JAR}".to_string(), path_str(mc_jar));
364 rargs.insert("{BINPATCH}".to_string(), path_str(lzma_path));
365
366 for proc in processors {
367 if let Some(sides) = &proc.sides {
368 if !sides.contains(&"client".to_string()) {
369 continue;
370 }
371 }
372
373 let proc_jar = resolve_maven_path_from_name(lib_dir, &proc.jar)?;
374 let main_class = get_jar_main_class(&proc_jar)?;
375
376 let mut classpath = proc
377 .classpath
378 .iter()
379 .map(|n| resolve_maven_path_from_name(lib_dir, n).map(|p| path_str(&p)))
380 .collect::<Result<Vec<_>>>()?;
381 classpath.push(path_str(&proc_jar));
382
383 let cp_sep = classpath_separator();
384 let cp = classpath.join(cp_sep);
385
386 let resolved_args: Vec<String> = proc
387 .args
388 .iter()
389 .map(|arg| {
390 if arg.starts_with('[') && arg.ends_with(']') {
391 let name = &arg[1..arg.len() - 1];
392 path_str(&resolve_maven_path(lib_dir, name))
393 } else {
394 let mut s = arg.clone();
395 for (k, v) in &rargs {
396 s = s.replace(k.as_str(), v.as_str());
397 }
398 s
399 }
400 })
401 .collect();
402
403 let status = std::process::Command::new(java_path)
404 .args(["-cp", &cp, &main_class])
405 .args(&resolved_args)
406 .status()
407 .map_err(|e| HexoError::ProcessorFailed(e.to_string()))?;
408
409 if !status.success() {
410 return Err(HexoError::ProcessorFailed(format!(
411 "processor {} 退出碼非零",
412 proc.jar
413 )));
414 }
415 }
416
417 Ok(())
418}
419
420async fn read_json_from_zip<T: serde::de::DeserializeOwned + Send + 'static>(
421 zip_path: &Path,
422 entry_name: &str,
423) -> Result<T> {
424 let zip_path = zip_path.to_owned();
425 let entry_name = entry_name.to_owned();
426 tokio::task::spawn_blocking(move || {
427 let file = std::fs::File::open(&zip_path)?;
428 let mut archive = zip::ZipArchive::new(file)?;
429 let entry = archive.by_name(&entry_name)?;
430 let value: T = serde_json::from_reader(entry)?;
431 Ok(value)
432 })
433 .await
434 .map_err(|e| HexoError::Other(e.to_string()))?
435}
436
437async fn extract_file_from_zip(
438 zip_path: &Path,
439 entry_name: &str,
440 out_path: &Path,
441) -> Result<()> {
442 let zip_path = zip_path.to_owned();
443 let entry_name = entry_name.to_owned();
444 let out_path = out_path.to_owned();
445 tokio::task::spawn_blocking(move || {
446 let file = std::fs::File::open(&zip_path)?;
447 let mut archive = zip::ZipArchive::new(file)?;
448 let mut entry = archive.by_name(&entry_name)?;
449 let mut out = std::fs::File::create(&out_path)?;
450 std::io::copy(&mut entry, &mut out)?;
451 Ok(())
452 })
453 .await
454 .map_err(|e| HexoError::Other(e.to_string()))?
455}
456
457fn resolve_maven_path(lib_dir: &Path, name: &str) -> PathBuf {
459 let (base, ext) = if let Some((b, e)) = name.split_once('@') {
460 (b, e)
461 } else {
462 (name, "jar")
463 };
464
465 let parts: Vec<&str> = base.split(':').collect();
466 let group = parts[0].replace('.', "/");
467 let artifact = parts[1];
468 let version = parts[2];
469 let classifier = parts.get(3).copied().unwrap_or("");
470
471 let file_name = if classifier.is_empty() {
472 format!("{}-{}.{}", artifact, version, ext)
473 } else {
474 format!("{}-{}-{}.{}", artifact, version, classifier, ext)
475 };
476
477 lib_dir.join(group).join(artifact).join(version).join(file_name)
478}
479
480fn maven_relative_path(name: &str) -> String {
482 let (base, ext) = if let Some((b, e)) = name.split_once('@') {
483 (b, e)
484 } else {
485 (name, "jar")
486 };
487
488 let parts: Vec<&str> = base.split(':').collect();
489 let group = parts[0].replace('.', "/");
490 let artifact = parts[1];
491 let version = parts[2];
492 let classifier = parts.get(3).copied().unwrap_or("");
493
494 let file_name = if classifier.is_empty() {
495 format!("{}-{}.{}", artifact, version, ext)
496 } else {
497 format!("{}-{}-{}.{}", artifact, version, classifier, ext)
498 };
499
500 format!("{}/{}/{}/{}", group, artifact, version, file_name)
501}
502
503fn resolve_maven_path_from_name(lib_dir: &Path, name: &str) -> Result<PathBuf> {
504 let path = resolve_maven_path(lib_dir, name);
505 if !path.exists() {
506 return Err(HexoError::Other(format!(
507 "jar 不存在: {}",
508 path.display()
509 )));
510 }
511 Ok(path)
512}
513
514fn get_jar_main_class(jar_path: &Path) -> Result<String> {
515 let file = std::fs::File::open(jar_path)?;
516 let mut archive =
517 zip::ZipArchive::new(file).map_err(|e| HexoError::Other(e.to_string()))?;
518 let manifest = archive
519 .by_name("META-INF/MANIFEST.MF")
520 .map_err(|e| HexoError::Other(e.to_string()))?;
521 use std::io::Read;
522 let mut content = String::new();
523 let mut r = manifest;
524 r.read_to_string(&mut content)?;
525 for line in content.lines() {
526 if line.starts_with("Main-Class:") {
527 return Ok(line["Main-Class:".len()..].trim().to_string());
528 }
529 }
530 Err(HexoError::Other(format!(
531 "找不到 Main-Class in {}",
532 jar_path.display()
533 )))
534}
535
536fn path_str(p: &Path) -> String {
537 p.to_string_lossy().replace('\\', "/")
538}
539
540fn classpath_separator() -> &'static str {
541 if cfg!(target_os = "windows") { ";" } else { ":" }
542}