mcvm_core/launch/client/
args.rs

1use mcvm_shared::output::{MCVMOutput, MessageContents, MessageLevel};
2use mcvm_shared::util::{ARCH_STRING, OS_STRING};
3use mcvm_shared::versions::VersionPattern;
4
5use crate::instance::{InstanceKind, WindowResolution};
6use crate::launch::{LaunchParameters, QuickPlayType};
7
8use crate::io::files::paths::Paths;
9use crate::net::game_files::assets::get_virtual_dir_path;
10use crate::net::game_files::client_meta::args::ArgumentItem;
11use crate::user::UserKind;
12
13/// Process an argument for the client from the client meta
14pub(crate) fn process_arg(arg: &ArgumentItem, params: &LaunchParameters) -> Vec<String> {
15	let mut out = Vec::new();
16	let InstanceKind::Client { window } = &params.side else {
17		panic!("Instance is not a client")
18	};
19	match arg {
20		ArgumentItem::Simple(arg) => {
21			let arg = process_simple_arg(arg, params);
22			if let Some(arg) = arg {
23				out.push(arg);
24			}
25		}
26		ArgumentItem::Conditional(arg) => {
27			for rule in &arg.rules {
28				let allowed = rule.action.is_allowed();
29
30				if let Some(os_name) = &rule.os.name {
31					if allowed != (OS_STRING == os_name.to_string()) {
32						return vec![];
33					}
34				}
35				if let Some(os_arch) = &rule.os.arch {
36					if allowed != (ARCH_STRING == os_arch.to_string()) {
37						return vec![];
38					}
39				}
40
41				if let Some(has_custom_resolution) = &rule.features.has_custom_resolution {
42					if *has_custom_resolution && window.resolution.is_none() {
43						return vec![];
44					}
45				}
46				if let Some(is_demo_user) = &rule.features.is_demo_user {
47					if *is_demo_user {
48						let use_demo = match params.users.get_chosen_user() {
49							Some(user) => matches!(user.kind, UserKind::Demo),
50							None => false,
51						};
52						if !use_demo {
53							return vec![];
54						}
55					}
56				}
57				if let Some(quick_play_support) = &rule.features.has_quick_play_support {
58					if *quick_play_support {
59						let uses_quick_play =
60							!matches!(params.launch_config.quick_play, QuickPlayType::None);
61						if !uses_quick_play {
62							return vec![];
63						}
64					}
65				}
66				if let Some(quick_play_singleplayer) = &rule.features.is_quick_play_singleplayer {
67					if *quick_play_singleplayer {
68						let uses_quick_play =
69							!matches!(params.launch_config.quick_play, QuickPlayType::World { .. });
70						if !uses_quick_play {
71							return vec![];
72						}
73					}
74				}
75				if let Some(quick_play_multiplayer) = &rule.features.is_quick_play_multiplayer {
76					if *quick_play_multiplayer {
77						let uses_quick_play = !matches!(
78							params.launch_config.quick_play,
79							QuickPlayType::Server { .. }
80						);
81						if !uses_quick_play {
82							return vec![];
83						}
84					}
85				}
86				if let Some(quick_play_realms) = &rule.features.is_quick_play_realms {
87					if *quick_play_realms {
88						let uses_quick_play =
89							!matches!(params.launch_config.quick_play, QuickPlayType::Realm { .. });
90						if !uses_quick_play {
91							return vec![];
92						}
93					}
94				}
95			}
96
97			for arg in arg.value.iter() {
98				out.extend(process_simple_arg(arg, params));
99			}
100		}
101	};
102
103	out
104}
105
106/// Process a simple string argument
107pub(crate) fn process_simple_arg(arg: &str, params: &LaunchParameters) -> Option<String> {
108	replace_arg_placeholders(arg, params)
109}
110
111/// Get the string for a placeholder token in an argument
112macro_rules! placeholder {
113	($name:expr) => {
114		concat!("${", $name, "}")
115	};
116}
117
118/// Replace placeholders in a string argument from the client meta
119pub(crate) fn replace_arg_placeholders(arg: &str, params: &LaunchParameters) -> Option<String> {
120	// Branding properties
121	let mut out = arg.replace(
122		placeholder!("launcher_name"),
123		&params.branding.launcher_name,
124	);
125	out = out.replace(
126		placeholder!("launcher_version"),
127		&params.branding.launcher_version,
128	);
129
130	// Game files information
131	out = out.replace(placeholder!("classpath"), &params.classpath.get_str());
132	out = out.replace(
133		placeholder!("natives_directory"),
134		&params
135			.paths
136			.internal
137			.join("versions")
138			.join(params.version.to_string())
139			.join("natives")
140			.to_string_lossy()
141			.to_string(),
142	);
143	out = out.replace(placeholder!("version_name"), params.version);
144	out = out.replace(placeholder!("version_type"), "mcvm");
145	out = out.replace(
146		placeholder!("game_directory"),
147		&params.launch_dir.to_string_lossy().to_string(),
148	);
149	out = out.replace(
150		placeholder!("assets_root"),
151		&params.paths.assets.to_string_lossy().to_string(),
152	);
153	out = out.replace(placeholder!("assets_index_name"), params.version);
154	out = out.replace(
155		placeholder!("game_assets"),
156		&get_virtual_dir_path(params.paths)
157			.to_string_lossy()
158			.to_string(),
159	);
160
161	out = out.replace(placeholder!("clientid"), "mcvm");
162	// Apparently this is used for Twitch on older versions
163	out = out.replace(placeholder!("user_properties"), "\"\"");
164
165	// Window resolution
166	let InstanceKind::Client { window } = &params.side else {
167		panic!("Instance is not a client")
168	};
169	if let Some(WindowResolution { width, height }) = window.resolution {
170		out = out.replace(placeholder!("resolution_width"), &width.to_string());
171		out = out.replace(placeholder!("resolution_height"), &height.to_string());
172	}
173
174	// QuickPlayType
175	out = out.replace(placeholder!("quickPlayPath"), "quickPlay/log.json");
176	out = out.replace(
177		placeholder!("quickPlaySingleplayer"),
178		if let QuickPlayType::World { world } = &params.launch_config.quick_play {
179			world
180		} else {
181			""
182		},
183	);
184	out = out.replace(
185		placeholder!("quickPlayMultiplayer"),
186		&if let QuickPlayType::Server { server, port } = &params.launch_config.quick_play {
187			if let Some(port) = port {
188				format!("{server}:{port}")
189			} else {
190				server.clone()
191			}
192		} else {
193			String::new()
194		},
195	);
196	out = out.replace(
197		placeholder!("quickPlayRealms"),
198		if let QuickPlayType::Realm { realm } = &params.launch_config.quick_play {
199			realm
200		} else {
201			""
202		},
203	);
204
205	// User
206	match params.users.get_chosen_user() {
207		Some(user) => {
208			// User type
209			let user_type = match user.get_kind() {
210				UserKind::Microsoft { .. } => "msa",
211				_ => "msa",
212			};
213			out = out.replace(placeholder!("user_type"), user_type);
214
215			if let Some(username) = user.get_name() {
216				out = out.replace(placeholder!("auth_player_name"), username);
217			}
218			if let Some(uuid) = user.get_uuid() {
219				out = out.replace(placeholder!("auth_uuid"), uuid);
220			}
221			if let Some(access_token) = user.get_access_token() {
222				out = out.replace(placeholder!("auth_access_token"), &access_token.0);
223			}
224			if let UserKind::Microsoft {
225				xbox_uid: Some(xbox_uid),
226			} = &user.kind
227			{
228				out = out.replace(placeholder!("auth_xuid"), xbox_uid);
229			}
230
231			// Blank any args we don't replace since the game will complain if we don't
232			if out.contains(placeholder!("auth_player_name"))
233				|| out.contains(placeholder!("auth_access_token"))
234				|| out.contains(placeholder!("auth_uuid"))
235				|| out.contains(placeholder!("auth_xuid"))
236			{
237				return Some(String::new());
238			}
239		}
240		None => {
241			if out.contains(placeholder!("auth_player_name")) {
242				return Some("UnknownUser".into());
243			}
244			if out.contains(placeholder!("auth_access_token"))
245				|| out.contains(placeholder!("auth_uuid"))
246			{
247				return Some(String::new());
248			}
249		}
250	}
251
252	Some(out)
253}
254
255/// Create the additional game arguments for Quick Play
256pub fn create_quick_play_args(
257	quick_play: &QuickPlayType,
258	version: &str,
259	version_list: &[String],
260	o: &mut impl MCVMOutput,
261) -> Vec<String> {
262	let mut out = Vec::new();
263
264	match quick_play {
265		QuickPlayType::World { .. }
266		| QuickPlayType::Realm { .. }
267		| QuickPlayType::Server { .. } => {
268			let before_23w14a =
269				VersionPattern::Before("23w13a".into()).matches_single(version, version_list);
270			match quick_play {
271				QuickPlayType::None => {}
272				QuickPlayType::World { .. } => {
273					if before_23w14a {
274						o.display(
275							MessageContents::Warning(
276								"World Quick Play has no effect before 23w14a (1.20)".into(),
277							),
278							MessageLevel::Important,
279						);
280					}
281				}
282				QuickPlayType::Realm { .. } => {
283					if before_23w14a {
284						o.display(
285							MessageContents::Warning(
286								"Realm Quick Play has no effect before 23w14a (1.20)".into(),
287							),
288							MessageLevel::Important,
289						);
290					}
291				}
292				QuickPlayType::Server { server, port } => {
293					if before_23w14a {
294						out.push("--server".into());
295						out.push(server.clone());
296						if let Some(port) = port {
297							out.push("--port".into());
298							out.push(port.to_string());
299						}
300					}
301				}
302			}
303		}
304		_ => {}
305	}
306
307	out
308}
309
310/// Fill the logging path argument with the correct path
311pub fn fill_logging_path_arg(arg: String, version: &str, paths: &Paths) -> Option<String> {
312	let path = crate::net::game_files::log_config::get_path(version, paths);
313	Some(arg.replace(placeholder!("path"), path.to_str()?))
314}