Skip to main content

nitro_mods/
forge.rs

1use std::{fmt::Display, ops::DerefMut, path::Path, process::Command};
2
3use anyhow::{Context, bail};
4use nitro_core::{
5	io::{
6		files::create_leading_dirs,
7		java::classpath::{CLASSPATH_SEP, Classpath},
8		json_from_file,
9	},
10	net::game_files::{
11		client_meta::{
12			ClientMeta,
13			args::{ArgumentItem, Arguments},
14		},
15		libraries::get_classpath,
16	},
17};
18use nitro_net::neoforge;
19use nitro_shared::{
20	Side, UpdateDepth, no_window,
21	output::{MessageContents, NitroOutput},
22	versions::VersionInfo,
23};
24use reqwest::Client;
25
26/// Mode we are in (Forge / NeoForge)
27/// This way we don't have to duplicate a lot of functions since these both
28/// have very similar download steps
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum Mode {
31	/// NeoForge loader
32	NeoForge,
33}
34
35impl Mode {
36	/// Convert to a lowercase string
37	pub fn to_str(&self) -> &'static str {
38		match self {
39			Self::NeoForge => "neoforge",
40		}
41	}
42}
43
44impl Display for Mode {
45	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46		write!(
47			f,
48			"{}",
49			match self {
50				Self::NeoForge => "NeoForge",
51			}
52		)
53	}
54}
55
56/// Installs Forge or NeoForge from a version. Returns the classpath, main class, and JVM and game args
57pub async fn install(
58	client: &Client,
59	internal_dir: &Path,
60	update_depth: UpdateDepth,
61	version_info: &VersionInfo,
62	side: Side,
63	mode: Mode,
64	forge_version: &str,
65	jvm_path: &Path,
66	o: &mut impl NitroOutput,
67) -> anyhow::Result<ForgeInstallResult> {
68	let forge_dir = internal_dir.join("forge").join(mode.to_str());
69
70	let installer_file_name = format!("{}-{forge_version}-installer.jar", mode.to_str());
71	let installer_path = forge_dir.join(&installer_file_name);
72	create_leading_dirs(&installer_path)?;
73
74	if !installer_path.exists() || update_depth == UpdateDepth::Force {
75		let mut process = o.get_process();
76		process.display(MessageContents::StartProcess(format!(
77			"Downloading {mode} installer"
78		)));
79
80		match mode {
81			Mode::NeoForge => neoforge::download_installer(forge_version, &installer_path, client)
82				.await
83				.context("Failed to download installer")?,
84		}
85		process.display(MessageContents::Success(format!(
86			"{mode} installer downloaded"
87		)));
88	}
89
90	if side == Side::Client {
91		create_mojang_launcher_jsons(internal_dir)
92			.context("Failed to create Microsoft launcher JSONs")?;
93	}
94
95	let client_meta_path = internal_dir
96		.join("versions")
97		.join(format!("{}-{forge_version}", mode.to_str()))
98		.join(format!("{}-{forge_version}.json", mode.to_str()));
99
100	let forge_lib_dir = internal_dir
101		.join("libraries")
102		.join(format!("net/neoforged/neoforge/{forge_version}"));
103
104	let server_jar_path = match mode {
105		Mode::NeoForge => forge_lib_dir.join(format!("neoforge-{forge_version}-server.jar")),
106	};
107
108	let already_installed = match side {
109		Side::Client => client_meta_path.exists(),
110		Side::Server => server_jar_path.exists(),
111	};
112	let already_installed = already_installed && update_depth != UpdateDepth::Force;
113
114	let mut process = o.get_process();
115	process.display(MessageContents::StartProcess(format!(
116		"Checking {mode} version info"
117	)));
118
119	// Run the installer if not not installed
120	if !already_installed {
121		process.display(MessageContents::StartProcess(format!(
122			"Running {mode} installer"
123		)));
124
125		let result = run_installer(
126			&installer_path,
127			side,
128			internal_dir,
129			&forge_dir,
130			jvm_path,
131			process.deref_mut(),
132		);
133		if let Err(e) = result {
134			let log_file_name = installer_file_name + ".log";
135			if let Ok(log_text) = std::fs::read_to_string(forge_dir.join(log_file_name)) {
136				process.display(MessageContents::Error(format!(
137					"Installer log:\n{log_text}"
138				)));
139			}
140			bail!("Failed to run installer: {e}");
141		}
142
143		process.display(MessageContents::Success(format!("{mode} installer ran")));
144	}
145
146	// Get data based on the side
147
148	match side {
149		Side::Client => {
150			let client_meta: ClientMeta = json_from_file(&client_meta_path)
151				.context("Failed to read version JSON for Forge")?;
152
153			let Arguments::New(args) = client_meta.arguments else {
154				bail!("Arguments in incorrect format");
155			};
156
157			let libraries_dir = internal_dir.join("libraries");
158
159			let jvm_args = args
160				.jvm
161				.into_iter()
162				.filter_map(|x| {
163					if let ArgumentItem::Simple(arg) = x {
164						Some(process_arg(&arg, &libraries_dir, &version_info.version))
165					} else {
166						None
167					}
168				})
169				.collect();
170
171			let game_args = args
172				.game
173				.into_iter()
174				.filter_map(|x| {
175					if let ArgumentItem::Simple(arg) = x {
176						Some(process_arg(&arg, &libraries_dir, &version_info.version))
177					} else {
178						None
179					}
180				})
181				.collect();
182
183			let classpath = get_classpath(&client_meta.libraries, internal_dir)
184				.context("Failed to get classpath")?;
185
186			process.display(MessageContents::Success(format!("{mode} installed")));
187
188			Ok(ForgeInstallResult {
189				classpath,
190				main_class: client_meta.main_class,
191				jvm_args,
192				game_args,
193				exclude_game_jar: true,
194				exclude_default_args: false,
195			})
196		}
197		Side::Server => {
198			// Read arguments from the server args file
199			let args_file_path = if cfg!(target_os = "windows") {
200				forge_lib_dir.join("win_args.txt")
201			} else {
202				forge_lib_dir.join("unix_args.txt")
203			};
204			let args = std::fs::read_to_string(&args_file_path)
205				.context("Failed to read server args file")?;
206			let libraries_dir_str =
207				format!("{}/", internal_dir.join("libraries").to_string_lossy());
208			// There's lots of arguments on the same lines
209			let mut args: Vec<_> = args
210				.lines()
211				.flat_map(|x| x.split(' '))
212				.map(|x| {
213					if x == "-DlibraryDirectory=libraries" {
214						format!(
215							"-DlibraryDirectory={}",
216							internal_dir.join("libraries").display()
217						)
218					} else {
219						x.replace("libraries/", &libraries_dir_str)
220					}
221				})
222				.collect();
223
224			args.push("nogui".into());
225
226			Ok(ForgeInstallResult {
227				classpath: Classpath::new(),
228				main_class: String::new(),
229				jvm_args: args,
230				game_args: Vec::new(),
231				exclude_game_jar: false,
232				exclude_default_args: true,
233			})
234		}
235	}
236}
237
238/// Result from the Forge install() function, to be added to the defaults from the client meta.
239pub struct ForgeInstallResult {
240	/// Java classpath
241	pub classpath: Classpath,
242	/// Java main class
243	pub main_class: String,
244	/// Args for the JVM
245	pub jvm_args: Vec<String>,
246	/// Args for the game
247	pub game_args: Vec<String>,
248	/// Whether to skip adding the game JAR to the final classpath
249	pub exclude_game_jar: bool,
250	/// Whether to exclude all of the generated / default arguments to the JVM and game
251	pub exclude_default_args: bool,
252}
253
254/// Runs the installer at the given path
255fn run_installer(
256	path: &Path,
257	side: Side,
258	install_dir: &Path,
259	forge_dir: &Path,
260	jvm_path: &Path,
261	o: &mut impl NitroOutput,
262) -> anyhow::Result<()> {
263	let mut command = Command::new(jvm_path);
264
265	command.arg("-jar");
266	command.arg(path);
267
268	match side {
269		Side::Client => command.arg("--installClient"),
270		Side::Server => command.arg("--installServer"),
271	};
272
273	command.arg(install_dir);
274
275	// This is where the log file will end up
276	command.current_dir(forge_dir);
277
278	no_window!(command);
279
280	o.debug(MessageContents::Simple(format!(
281		"Forge Installer Command:\n{command:?}"
282	)));
283
284	let exit = command.spawn()?.wait()?;
285	if !exit.success() {
286		bail!("Installer returned non-zero status: {exit}")
287	}
288
289	Ok(())
290}
291
292/// Creates JSON files required for the installer to work
293fn create_mojang_launcher_jsons(dir: &Path) -> anyhow::Result<()> {
294	std::fs::write(dir.join("launcher_profiles.json"), "{}")?;
295	std::fs::write(dir.join("launcher_profiles_microsoft_store.json"), "{}")?;
296	Ok(())
297}
298
299/// Processes an argument from the version JSON to replace tokens
300fn process_arg(arg: &str, libraries_dir: &Path, version_name: &str) -> String {
301	let arg = arg.replace("${classpath_separator}", &format!("{CLASSPATH_SEP}"));
302	let arg = arg.replace("${library_directory}", &libraries_dir.to_string_lossy());
303
304	arg.replace("${version_name}", version_name)
305}