1use lighty_auth::{AuthProvider, UserProfile};
5use lighty_loaders::types::version_metadata::Version;
6use lighty_loaders::types::VersionInfo;
7use std::borrow::Cow;
8use std::collections::{HashMap, HashSet};
9
10pub const KEY_AUTH_PLAYER_NAME: &str = "auth_player_name";
15pub const KEY_AUTH_UUID: &str = "auth_uuid";
17pub const KEY_AUTH_ACCESS_TOKEN: &str = "auth_access_token";
19pub const KEY_AUTH_XUID: &str = "auth_xuid";
21pub const KEY_CLIENT_ID: &str = "clientid";
23pub const KEY_USER_TYPE: &str = "user_type";
25pub const KEY_USER_PROPERTIES: &str = "user_properties";
27pub const KEY_VERSION_NAME: &str = "version_name";
29pub const KEY_VERSION_TYPE: &str = "version_type";
31pub const KEY_GAME_DIRECTORY: &str = "game_directory";
33pub const KEY_ASSETS_ROOT: &str = "assets_root";
35pub const KEY_NATIVES_DIRECTORY: &str = "natives_directory";
37pub const KEY_LIBRARY_DIRECTORY: &str = "library_directory";
39pub const KEY_ASSETS_INDEX_NAME: &str = "assets_index_name";
41pub const KEY_LAUNCHER_NAME: &str = "launcher_name";
43pub const KEY_LAUNCHER_VERSION: &str = "launcher_version";
45pub const KEY_CLASSPATH: &str = "classpath";
47pub const KEY_CLASSPATH_SEPARATOR: &str = "classpath_separator";
49
50const DEFAULT_ACCESS_TOKEN: &str = "0";
52const DEFAULT_XUID: &str = "0";
53const DEFAULT_USER_TYPE: &str = "legacy";
54const DEFAULT_USER_PROPERTIES: &str = "{}";
55const DEFAULT_VERSION_TYPE: &str = "release";
56const CP_FLAG: &str = "-cp";
57
58fn user_type_for(provider: &AuthProvider) -> &'static str {
66 match provider {
67 AuthProvider::Microsoft { .. } => "msa",
68 AuthProvider::Azuriom { .. } => "mojang",
69 AuthProvider::Offline | AuthProvider::Custom { .. } => "legacy",
70 }
71}
72
73pub trait Arguments {
80 fn build_arguments(
89 &self,
90 builder: &Version,
91 profile: Option<&UserProfile>,
92 arg_overrides: &HashMap<String, String>,
93 arg_removals: &HashSet<String>,
94 jvm_overrides: &HashMap<String, String>,
95 jvm_removals: &HashSet<String>,
96 raw_args: &[String],
97 ) -> Vec<String>;
98}
99
100impl<T: VersionInfo> Arguments for T {
101 fn build_arguments(
102 &self,
103 builder: &Version,
104 profile: Option<&UserProfile>,
105 arg_overrides: &HashMap<String, String>,
106 arg_removals: &HashSet<String>,
107 jvm_overrides: &HashMap<String, String>,
108 jvm_removals: &HashSet<String>,
109 raw_args: &[String],
110 ) -> Vec<String> {
111 let mut variables = create_variable_map(self, builder, profile);
112
113 for (key, value) in arg_overrides {
114 variables.insert(key.clone(), value.clone());
115 }
116
117 variables.insert(
121 KEY_GAME_DIRECTORY.into(),
122 self.runtime_dir().display().to_string(),
123 );
124
125 let game_args = replace_variables_in_vec(&builder.arguments.game, &variables);
126
127 let mut jvm_args = builder.arguments.jvm
128 .as_ref()
129 .map(|jvm| replace_variables_in_vec(jvm, &variables))
130 .unwrap_or_else(|| build_default_jvm_args(&variables));
131
132 #[cfg(target_os = "macos")]
134 if !jvm_args.iter().any(|arg| arg == "-XstartOnFirstThread") {
135 jvm_args.insert(0, "-XstartOnFirstThread".to_string());
136 }
137
138 if !jvm_args.iter().any(|arg| arg.starts_with("-Djava.library.path=")) {
140 let natives_dir = variables.get(KEY_NATIVES_DIRECTORY).cloned().unwrap_or_default();
141 jvm_args.insert(0, format!("-Djava.library.path={}", natives_dir));
142 }
143
144 if !jvm_args.iter().any(|arg| arg.starts_with("-Dminecraft.launcher.brand=")) {
145 let launcher_name = variables.get(KEY_LAUNCHER_NAME).cloned().unwrap_or_default();
146 jvm_args.insert(0, format!("-Dminecraft.launcher.brand={}", launcher_name));
147 }
148
149 if !jvm_args.iter().any(|arg| arg.starts_with("-Dminecraft.launcher.version=")) {
150 let launcher_version = variables.get(KEY_LAUNCHER_VERSION).cloned().unwrap_or_default();
151 jvm_args.insert(0, format!("-Dminecraft.launcher.version={}", launcher_version));
152 }
153
154 let module_path_opt = jvm_args
156 .iter()
157 .position(|arg| arg == "-p")
158 .and_then(|p_idx| jvm_args.get(p_idx + 1).cloned());
159
160 if let Some(cp_idx) = jvm_args.iter().position(|arg| arg == CP_FLAG) {
161 if let Some(ref module_path) = module_path_opt {
162 lighty_core::trace_debug!("Module-path detected: {}", module_path);
163 if let Some(existing_cp) = jvm_args.get(cp_idx + 1) {
164 let filtered_classpath =
165 filter_classpath_from_modulepath(existing_cp, module_path);
166 lighty_core::trace_debug!("Classpath filtered for module-path");
167
168 jvm_args[cp_idx + 1] = filtered_classpath;
169 } else {
170 lighty_core::trace_warn!("-cp found but no value after it");
171 }
172 } else {
173 lighty_core::trace_debug!("No module-path, keeping existing classpath unchanged");
174 }
175 } else {
176 let classpath = variables.get(KEY_CLASSPATH).cloned().unwrap_or_default();
177
178 if let Some(ref module_path) = module_path_opt {
179 lighty_core::trace_debug!("Module-path detected: {}", module_path);
180 let filtered_classpath = filter_classpath_from_modulepath(&classpath, module_path);
181 lighty_core::trace_debug!("Classpath filtered for module-path");
182
183 jvm_args.push(CP_FLAG.into());
184 jvm_args.push(filtered_classpath);
185 } else {
186 jvm_args.push(CP_FLAG.into());
187 jvm_args.push(classpath);
188 }
189 }
190
191 apply_jvm_overrides(&mut jvm_args, jvm_overrides);
192 apply_jvm_removals(&mut jvm_args, jvm_removals);
193 let game_args = apply_arg_removals(game_args, arg_removals);
194
195 let mut full_args = jvm_args;
196 full_args.push(builder.main_class.main_class.clone());
197 full_args.extend(game_args);
198
199 full_args.extend_from_slice(raw_args);
200
201 lighty_core::trace_debug!(args = ?full_args, "Launch arguments built");
202
203 full_args
204 }
205}
206
207fn create_variable_map<T: VersionInfo>(
214 version: &T,
215 builder: &Version,
216 profile: Option<&UserProfile>,
217) -> HashMap<String, String> {
218 let mut map = HashMap::new();
219
220 #[cfg(target_os = "windows")]
221 let classpath_separator = ";";
222 #[cfg(not(target_os = "windows"))]
223 let classpath_separator = ":";
224
225 let username = profile.map(|p| p.username.as_str()).unwrap_or("");
228 let uuid = profile.map(|p| p.uuid.as_str()).unwrap_or("");
229
230 let token_secret: Option<lighty_auth::SecretString> = profile.and_then(|p| {
234 #[cfg(feature = "keyring")]
235 if let Some(h) = &p.token_handle {
236 return h.read().ok();
237 }
238 p.access_token.clone()
239 });
240 let access_token = token_secret
241 .as_ref()
242 .map(|s| lighty_auth::ExposeSecret::expose_secret(s))
243 .unwrap_or(DEFAULT_ACCESS_TOKEN);
244 let xuid = profile
245 .and_then(|p| p.xuid.as_deref())
246 .unwrap_or(DEFAULT_XUID);
247 let user_type = profile
248 .map(|p| user_type_for(&p.provider))
249 .unwrap_or(DEFAULT_USER_TYPE);
250
251 map.insert(KEY_AUTH_PLAYER_NAME.into(), username.into());
252 map.insert(KEY_AUTH_UUID.into(), uuid.into());
253 map.insert(KEY_AUTH_ACCESS_TOKEN.into(), access_token.into());
254 map.insert(KEY_AUTH_XUID.into(), xuid.into());
255 map.insert(KEY_CLIENT_ID.into(), lighty_core::AppState::client_id().to_string());
256 map.insert(KEY_USER_TYPE.into(), user_type.into());
257 map.insert(KEY_USER_PROPERTIES.into(), DEFAULT_USER_PROPERTIES.into());
258
259 map.insert(KEY_VERSION_NAME.into(), version.name().into());
260 map.insert(KEY_VERSION_TYPE.into(), DEFAULT_VERSION_TYPE.into());
261
262 map.insert(KEY_GAME_DIRECTORY.into(), version.runtime_dir().display().to_string());
265 map.insert(KEY_ASSETS_ROOT.into(), version.game_dirs().join("assets").display().to_string());
266 map.insert(KEY_NATIVES_DIRECTORY.into(), version.game_dirs().join("natives").display().to_string());
267 map.insert(KEY_LIBRARY_DIRECTORY.into(), version.game_dirs().join("libraries").display().to_string());
268
269 let assets_index_name = builder.assets_index
270 .as_ref()
271 .map(|idx| idx.id.clone())
272 .unwrap_or_else(|| version.minecraft_version().into());
273 map.insert(KEY_ASSETS_INDEX_NAME.into(), assets_index_name);
274
275 map.insert(KEY_LAUNCHER_NAME.into(), lighty_core::AppState::name().to_string());
276 map.insert(KEY_LAUNCHER_VERSION.into(), lighty_core::AppState::app_version().to_string());
277
278 let classpath = build_classpath(version, &builder.libraries);
279 map.insert(KEY_CLASSPATH.into(), classpath);
280 map.insert(KEY_CLASSPATH_SEPARATOR.into(), classpath_separator.to_string());
281
282 map
283}
284
285fn build_classpath<T: VersionInfo>(version: &T, libraries: &[lighty_loaders::types::version_metadata::Library]) -> String {
287 #[cfg(target_os = "windows")]
288 let separator = ";";
289 #[cfg(not(target_os = "windows"))]
290 let separator = ":";
291
292 let lib_dir = version.game_dirs().join("libraries");
293
294 let mut classpath_entries: Vec<String> = libraries
295 .iter()
296 .filter_map(|lib| {
297 lib.path.as_ref().map(|path| {
298 lib_dir.join(path).display().to_string()
299 })
300 })
301 .collect();
302
303 classpath_entries.push(
304 version.game_dirs().join(format!("{}.jar", version.name())).display().to_string()
305 );
306
307 classpath_entries.join(separator)
308}
309
310fn build_default_jvm_args(variables: &HashMap<String, String>) -> Vec<String> {
312 let natives_dir = variables.get(KEY_NATIVES_DIRECTORY).cloned().unwrap_or_default();
313 let launcher_name = variables.get(KEY_LAUNCHER_NAME).cloned().unwrap_or_default();
314 let launcher_version = variables.get(KEY_LAUNCHER_VERSION).cloned().unwrap_or_default();
315 let classpath = variables.get(KEY_CLASSPATH).cloned().unwrap_or_default();
316
317 vec![
318 "-Xms1024M".into(),
319 "-Xmx2048M".into(),
320 format!("-Djava.library.path={}", natives_dir),
321 format!("-Dminecraft.launcher.brand={}", launcher_name),
322 format!("-Dminecraft.launcher.version={}", launcher_version),
323 CP_FLAG.into(),
324 classpath,
325 ]
326}
327
328
329fn replace_variables_in_vec(args: &[String], variables: &HashMap<String, String>) -> Vec<String> {
331 args.iter()
332 .map(|arg| replace_variables_cow(arg, variables).into_owned())
333 .collect()
334}
335
336fn replace_variables_cow<'a>(
338 input: &'a str,
339 variables: &HashMap<String, String>
340) -> Cow<'a, str> {
341 if !input.contains("${") {
342 return Cow::Borrowed(input);
343 }
344
345 let mut result = String::with_capacity(input.len() + 128);
346 let mut last_end = 0;
347
348 for (start, _) in input.match_indices("${") {
349 if let Some(end_offset) = input[start..].find('}') {
350 let end = start + end_offset;
351 let key = &input[start + 2..end];
352
353 result.push_str(&input[last_end..start]);
354
355 if let Some(value) = variables.get(key) {
356 result.push_str(value);
357 } else {
358 result.push_str(&input[start..=end]);
359 }
360
361 last_end = end + 1;
362 }
363 }
364
365 result.push_str(&input[last_end..]);
366 Cow::Owned(result)
367}
368
369fn apply_jvm_overrides(jvm_args: &mut Vec<String>, jvm_overrides: &HashMap<String, String>) {
376 for (key, value) in jvm_overrides {
377 let formatted_option = format_jvm_option(key, value);
378
379 let key_prefix = format!("-{}", key.split('=').next().unwrap_or(key));
380 if let Some(pos) = jvm_args.iter().position(|arg| arg.starts_with(&key_prefix)) {
381 jvm_args[pos] = formatted_option;
382 } else {
383 if let Some(cp_pos) = jvm_args.iter().position(|arg| arg == CP_FLAG) {
385 jvm_args.insert(cp_pos, formatted_option);
386 } else {
387 jvm_args.push(formatted_option);
388 }
389 }
390 }
391}
392
393fn format_jvm_option(key: &str, value: &str) -> String {
401 if value.is_empty() {
402 format!("-{}", key)
403 } else if key.starts_with('X') && !key.contains(':') && !key.contains('=') {
404 format!("-{}{}", key, value)
406 } else {
407 format!("-{}={}", key, value)
409 }
410}
411
412fn apply_jvm_removals(jvm_args: &mut Vec<String>, jvm_removals: &HashSet<String>) {
414 jvm_args.retain(|arg| {
415 let arg_key = if let Some(stripped) = arg.strip_prefix('-') {
416 stripped.split('=').next().unwrap_or(stripped)
417 .split(|c: char| c.is_numeric()).next().unwrap_or(stripped)
418 } else {
419 return true;
420 };
421
422 !jvm_removals.contains(arg_key)
423 });
424}
425
426fn apply_arg_removals(game_args: Vec<String>, arg_removals: &HashSet<String>) -> Vec<String> {
428 game_args.into_iter()
429 .filter(|arg| {
430 !arg_removals.iter().any(|removal| {
431 arg == removal || arg.starts_with(&format!("--{}", removal))
432 })
433 })
434 .collect()
435}
436
437fn filter_classpath_from_modulepath(classpath: &str, module_path: &str) -> String {
440 let separator = get_path_separator();
441
442 let module_artifacts: std::collections::HashSet<String> = module_path
444 .split(separator)
445 .filter_map(|path| {
446 std::path::Path::new(path)
447 .file_name()
448 .and_then(|f| f.to_str())
449 .and_then(|filename| {
450 if let Some(stem) = filename.strip_suffix(".jar") {
451 let mut base_name = stem;
452 if let Some(pos) = stem.rfind(|c: char| c.is_ascii_digit()) {
453 if let Some(dash_pos) = stem[..pos].rfind('-') {
454 base_name = &stem[..dash_pos];
455 }
456 }
457 Some(base_name.to_string())
458 } else {
459 None
460 }
461 })
462 })
463 .collect();
464
465 lighty_core::trace_debug!("Module artifacts to exclude: {:?}", module_artifacts);
466
467 classpath
468 .split(separator)
469 .filter(|path| {
470 if let Some(filename) = std::path::Path::new(path)
471 .file_name()
472 .and_then(|f| f.to_str())
473 {
474 if let Some(stem) = filename.strip_suffix(".jar") {
475 let base_name = if let Some(pos) = stem.rfind(|c: char| c.is_ascii_digit()) {
476 if let Some(dash_pos) = stem[..pos].rfind('-') {
477 &stem[..dash_pos]
478 } else {
479 stem
480 }
481 } else {
482 stem
483 };
484 !module_artifacts.contains(base_name)
485 } else {
486 true
487 }
488 } else {
489 true
490 }
491 })
492 .collect::<Vec<&str>>()
493 .join(separator)
494}
495
496fn get_path_separator() -> &'static str {
497 #[cfg(target_os = "windows")] {
498 ";"
499 }
500 #[cfg(not(target_os = "windows"))] {
501 ":"
502 }
503}