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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum Mode {
31 NeoForge,
33}
34
35impl Mode {
36 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
56pub 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 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 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 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 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
238pub struct ForgeInstallResult {
240 pub classpath: Classpath,
242 pub main_class: String,
244 pub jvm_args: Vec<String>,
246 pub game_args: Vec<String>,
248 pub exclude_game_jar: bool,
250 pub exclude_default_args: bool,
252}
253
254fn 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 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
292fn 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
299fn 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}