nitro_core 0.32.0

Core functionality for launching Minecraft
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
use std::io::{Cursor, Read};
use std::path::PathBuf;

use anyhow::{Context, bail};
use nitro_shared::minecraft::VersionManifest;
use nitro_shared::output::{MessageContents, NitroOutput};
use nitro_shared::util::DeserListOrSingle;
use nitro_shared::{UpdateDepth, translate};
use reqwest::Client;
use serde::Deserialize;
use zip::ZipArchive;

use crate::io::files::create_leading_dirs;
use crate::io::files::paths::Paths;
use crate::io::java::JavaMajorVersion;
use crate::io::json_from_file;
use crate::io::update::UpdateManager;
use crate::net::download::ProgressiveDownload;

/// The client metadata, which is used for information about
/// how to set up and launch the client for every version of the game.
#[derive(Deserialize, Debug, Clone)]
pub struct ClientMeta {
	/// Arguments for the client. Can have a different field name and format
	/// depending on how new the file is in the manifest
	#[serde(alias = "minecraftArguments")]
	pub arguments: args::Arguments,
	/// Info about the assets index
	#[serde(rename = "assetIndex")]
	#[serde(default)]
	pub asset_index: Option<AssetIndexInfo>,
	/// Assets version
	#[serde(rename = "assets")]
	#[serde(default)]
	pub assets_version: Option<String>,
	/// File downloads
	#[serde(default)]
	pub downloads: Option<Downloads>,
	/// Java information
	#[serde(rename = "javaVersion")]
	#[serde(default)]
	pub java_info: Option<JavaInfo>,
	/// Libraries to download for the client
	pub libraries: Vec<libraries::Library>,
	/// Java main class for the client
	#[serde(rename = "mainClass")]
	pub main_class: String,
	/// Logging information
	#[serde(default)]
	pub logging: Option<LogInfo>,
}

/// Information in the meta about the assets index
#[derive(Deserialize, Debug, Clone)]
pub struct AssetIndexInfo {
	/// The URL to the assets index for this version
	pub url: String,
}

/// Download information for different files
#[derive(Deserialize, Debug, Clone)]
pub struct Downloads {
	/// Download info for the client.jar
	pub client: DownloadInfo,
	/// Download info for the server.jar
	pub server: DownloadInfo,
}

/// Information for the downloading of a specific file
#[derive(Deserialize, Debug, Clone)]
pub struct DownloadInfo {
	/// The URL to the file
	pub url: String,
}

/// Information about Java for this version
#[derive(Deserialize, Debug, Clone)]
pub struct JavaInfo {
	/// The Java major version to use
	#[serde(rename = "majorVersion")]
	pub major_version: JavaMajorVersion,
}

/// Information about logging for this version
#[derive(Deserialize, Debug, Clone)]
pub struct LogInfo {
	/// Client logging
	pub client: ClientLogInfo,
}

/// Information about logging for the client
#[derive(Deserialize, Debug, Clone)]
pub struct ClientLogInfo {
	/// The JVM argument to use for specifying the path of the log.
	/// It contains a token '${file}' that should be replaced with the path
	/// to the file.
	pub argument: String,
	/// Download for the logging configuration file
	pub file: DownloadInfo,
}

/// Game arguments in the client meta
pub mod args {
	use super::*;
	/// The old and new formats for the game argument list
	#[derive(Deserialize, Debug, Clone)]
	#[serde(untagged)]
	pub enum Arguments {
		/// The new format with both JVM and game args
		New(NewArguments),
		/// The old format with just a list of game args in a string,
		/// separated by spaces
		Old(String),
	}

	/// Arguments for the game from the client meta, in the new format
	#[derive(Deserialize, Debug, Clone)]
	pub struct NewArguments {
		/// Arguments for the JVM
		pub jvm: Vec<ArgumentItem>,
		/// Arguments for the game
		pub game: Vec<ArgumentItem>,
	}

	/// A new argument item
	#[derive(Deserialize, Debug, Clone)]
	#[serde(untagged)]
	pub enum ArgumentItem {
		/// A simple string argument
		Simple(String),
		/// An argument or set of arguments with a condition
		Conditional(ConditionalArguments),
	}

	/// Complex arguments with conditions
	#[derive(Deserialize, Debug, Clone)]
	pub struct ConditionalArguments {
		/// Rules to check for the arguments to be applied
		pub rules: Vec<conditions::Rule>,
		/// The argument(s) to apply if the conditions succeed
		pub value: DeserListOrSingle<String>,
	}
}

/// Deserialization for libraries
pub mod libraries {
	use std::collections::HashMap;

	use super::*;

	/// A library to install
	#[derive(Deserialize, Debug, Clone)]
	pub struct Library {
		/// Downloads for this library
		#[serde(default)]
		pub downloads: Downloads,
		/// Maven name of this library
		pub name: String,
		/// Natives classifiers
		#[serde(default)]
		pub natives: HashMap<String, String>,
		/// Rules to check for this library to be downloaded
		#[serde(default)]
		pub rules: Vec<conditions::Rule>,
		/// Rules for extraction
		#[serde(default)]
		pub extract: ExtractionRules,
		/// Another library download URL if downloads is not present
		#[serde(default)]
		pub url: Option<String>,
	}

	/// Downloads for a library
	#[derive(Deserialize, Debug, Default, Clone)]
	pub struct Downloads {
		/// Artifact for the main library file
		pub artifact: Option<Artifact>,
		/// Optional artifacts for native libraries, to be extracted.
		/// Referred to by their native classifiers.
		#[serde(rename = "classifiers")]
		#[serde(default)]
		pub native_classifiers: HashMap<String, Artifact>,
	}

	/// A single download artifact
	#[derive(Deserialize, Debug, Clone)]
	pub struct Artifact {
		/// Path to store the artifact in
		pub path: String,
		/// URL to download the artifact from
		pub url: String,
	}

	/// Extraction rules for a library
	#[derive(Deserialize, Debug, Clone, Default)]
	#[serde(default)]
	pub struct ExtractionRules {
		/// Files to exclude from the extraction
		pub exclude: Vec<String>,
	}
}

/// Facilities for conditions in the meta
pub mod conditions {
	use std::fmt::Display;

	use super::*;

	/// A rule condition
	#[derive(Deserialize, Debug, Clone)]
	pub struct Rule {
		/// Action for inverting the rule
		pub action: RuleAction,
		/// Features to check for the condition
		#[serde(default)]
		pub features: RuleFeatures,
		/// OS properties to check for the condition
		#[serde(default)]
		pub os: OSConditions,
	}

	/// Used in argument rules to invert a condition
	#[derive(Deserialize, Debug, Clone)]
	#[serde(rename_all = "snake_case")]
	pub enum RuleAction {
		/// Allow the arguments if the conditions are met
		Allow,
		/// Remove the arguments if the conditions are met
		Disallow,
	}

	impl RuleAction {
		/// Check if this rule is allowed
		pub fn is_allowed(&self) -> bool {
			matches!(&self, Self::Allow)
		}

		/// Check if the allowance of this rule matches a condition.
		/// If the rule is allowed, but the condition fails, then the return is false.
		/// If the rule is not allowed, but the condition succeeds, then the return is also false.
		pub fn is_allowed_with_condition(&self, condition: bool) -> bool {
			self.is_allowed() == condition
		}
	}

	/// Features that can be checked for a conditional argument rule
	#[derive(Deserialize, Debug, Default, Clone)]
	pub struct RuleFeatures {
		/// Feature for if the user is a demo user. Should be checked if present
		pub is_demo_user: Option<bool>,
		/// Feature for if a custom window resolution is set. Should be checked if present
		pub has_custom_resolution: Option<bool>,
		/// Feature for if QuickPlay is enabled. Should be checked if present
		#[serde(alias = "has_quick_plays_support")]
		pub has_quick_play_support: Option<bool>,
		/// Feature for if QuickPlay singleplayer is enabled. Should be checked if present
		pub is_quick_play_singleplayer: Option<bool>,
		/// Feature for if QuickPlay multiplayer is enabled. Should be checked if present
		pub is_quick_play_multiplayer: Option<bool>,
		/// Feature for if QuickPlay Realms is enabled. Should be checked if present
		pub is_quick_play_realms: Option<bool>,
	}

	/// Operating-system related conditions for argument rules
	#[derive(Deserialize, Debug, Default, Clone)]
	pub struct OSConditions {
		/// Condition for the type of OS. Should be checked if present
		pub name: Option<OSName>,
		/// Condition for the target architecture. Should be checked if present
		pub arch: Option<OSArch>,
	}

	/// Operating systems for OS conditions
	#[derive(Deserialize, Debug, Clone)]
	#[serde(rename_all = "snake_case")]
	pub enum OSName {
		/// Windows operating system
		Windows,
		/// Windows on ARM
		#[serde(rename = "windows-arm64")]
		WindowsArm64,
		/// MacOS operating system
		#[serde(alias = "osx")]
		#[serde(rename = "macos")]
		MacOS,
		/// MacOS on ARM
		#[serde(rename = "osx-arm64")]
		MacOSArm64,
		/// Linux operating system
		Linux,
		/// Linux on ARM
		#[serde(rename = "linux-arm64")]
		LinuxArm64,
		/// Unknown variant
		#[serde(untagged)]
		Other(String),
	}

	impl Display for OSName {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			write!(
				f,
				"{}",
				match self {
					Self::Windows => "windows",
					Self::WindowsArm64 => "windows_arm64",
					Self::MacOS => "macos",
					Self::MacOSArm64 => "macos_arm64",
					Self::Linux => "linux",
					Self::LinuxArm64 => "linux_arm64",
					Self::Other(other) => &other,
				}
			)
		}
	}

	/// Architecture for OS conditions
	#[derive(Deserialize, Debug, Clone)]
	#[serde(rename_all = "snake_case")]
	pub enum OSArch {
		/// x86 architecture
		X86,
		/// x86_64 architecture
		X86_64,
		/// ARM architecture
		#[serde(alias = "aarch64")]
		Arm,
	}

	impl Display for OSArch {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			write!(
				f,
				"{}",
				match self {
					Self::X86 => "x86",
					Self::X86_64 => "x86_64",
					Self::Arm => "arm",
				}
			)
		}
	}
}

/// Gets the specific client info JSON file for a Minecraft version
pub async fn get(
	version: &str,
	version_manifest: &VersionManifest,
	paths: &Paths,
	manager: &UpdateManager,
	client: &Client,
	o: &mut impl NitroOutput,
) -> anyhow::Result<ClientMeta> {
	let version_string = version.to_owned();

	let entry = version_manifest
		.versions
		.iter()
		.find(|x| x.id == version_string);
	let Some(entry) = entry else {
		bail!("Minecraft version does not exist or was not found in the manifest");
	};

	let path = get_path(&version_string, paths);
	create_leading_dirs(&path)?;

	let meta = if manager.update_depth < UpdateDepth::Full && path.exists() {
		json_from_file(path).context("Failed to read client meta contents from file")?
	} else {
		let mut download = ProgressiveDownload::bytes(&entry.url, client).await?;

		while !download.is_finished() {
			download.poll_download().await?;
			o.display(MessageContents::Associated(
				Box::new(download.get_progress()),
				Box::new(MessageContents::Simple(translate!(
					o,
					DownloadingClientMeta
				))),
			));
		}
		let mut bytes = download.finish();

		// Unzip if we need to
		if entry.is_zipped {
			let mut zip =
				ZipArchive::new(Cursor::new(&bytes)).context("Failed to open zip archive")?;
			if !zip.is_empty() {
				let mut out = None;
				for i in 0..zip.len() {
					let mut file = zip.by_index(i).expect("Index should exist");
					if file.is_file() {
						let mut buf = Vec::with_capacity(
							file.size().try_into().expect("Stop using 32 pointer width"),
						);
						file.read_to_end(&mut buf)
							.context("Failed to read zip file")?;
						out = Some(buf);
					}
				}
				if let Some(out) = out {
					bytes = out;
				} else {
					bail!("No files found for use in zip file");
				}
			} else {
				bail!("Zipped client meta has no files inside")
			}
		}

		std::fs::write(path, &bytes).context("Failed to write client meta to a file")?;

		// Parsing must come second since simd json overwrites the slice

		simd_json::from_slice(&mut bytes).context("Failed to parse client meta")?
	};

	Ok(meta)
}

/// Gets the path to the client meta for a given version
pub fn get_path(version: &str, paths: &Paths) -> PathBuf {
	paths
		.internal
		.join("versions")
		.join(format!("{version}.json"))
}