1use lighty_loaders::types::version_metadata::Version;
5use lighty_loaders::types::VersionInfo;
6use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8
9pub const KEY_AUTH_PLAYER_NAME: &str = "auth_player_name";
11pub const KEY_AUTH_UUID: &str = "auth_uuid";
12pub const KEY_AUTH_ACCESS_TOKEN: &str = "auth_access_token";
13pub const KEY_AUTH_XUID: &str = "auth_xuid";
14pub const KEY_CLIENT_ID: &str = "clientid";
15pub const KEY_USER_TYPE: &str = "user_type";
16pub const KEY_USER_PROPERTIES: &str = "user_properties";
17pub const KEY_VERSION_NAME: &str = "version_name";
18pub const KEY_VERSION_TYPE: &str = "version_type";
19pub const KEY_GAME_DIRECTORY: &str = "game_directory";
20pub const KEY_ASSETS_ROOT: &str = "assets_root";
21pub const KEY_NATIVES_DIRECTORY: &str = "natives_directory";
22pub const KEY_LIBRARY_DIRECTORY: &str = "library_directory";
23pub const KEY_ASSETS_INDEX_NAME: &str = "assets_index_name";
24pub const KEY_LAUNCHER_NAME: &str = "launcher_name";
25pub const KEY_LAUNCHER_VERSION: &str = "launcher_version";
26pub const KEY_CLASSPATH: &str = "classpath";
27pub const KEY_CLASSPATH_SEPARATOR: &str = "classpath_separator";
28
29const DEFAULT_ACCESS_TOKEN: &str = "0";
31const DEFAULT_XUID: &str = "0";
32const DEFAULT_CLIENT_ID: &str = "{client-id}";
33const DEFAULT_USER_TYPE: &str = "legacy";
34const DEFAULT_USER_PROPERTIES: &str = "{}";
35const DEFAULT_VERSION_TYPE: &str = "release";
36const LAUNCHER_NAME: &str = "LightyLauncher";
37const LAUNCHER_VERSION: &str = "1.0.0";
38const CP_FLAG: &str = "-cp";
39
40pub trait Arguments {
41 fn build_arguments(
42 &self,
43 builder: &Version,
44 username: &str,
45 uuid: &str,
46 arg_overrides: &HashMap<String, String>,
47 arg_removals: &HashSet<String>,
48 jvm_overrides: &HashMap<String, String>,
49 jvm_removals: &HashSet<String>,
50 raw_args: &[String],
51 ) -> Vec<String>;
52}
53
54impl<T: VersionInfo> Arguments for T {
55 fn build_arguments(
56 &self,
57 builder: &Version,
58 username: &str,
59 uuid: &str,
60 arg_overrides: &HashMap<String, String>,
61 arg_removals: &HashSet<String>,
62 jvm_overrides: &HashMap<String, String>,
63 jvm_removals: &HashSet<String>,
64 raw_args: &[String],
65 ) -> Vec<String> {
66 let mut variables = create_variable_map(self, builder, username, uuid);
68
69 for (key, value) in arg_overrides {
71 variables.insert(key.clone(), value.clone());
72 }
73
74 let game_args = replace_variables_in_vec(&builder.arguments.game, &variables);
76
77 let mut jvm_args = builder.arguments.jvm
78 .as_ref()
79 .map(|jvm| replace_variables_in_vec(jvm, &variables))
80 .unwrap_or_else(|| build_default_jvm_args(&variables));
81
82 if !jvm_args.iter().any(|arg| arg.starts_with("-Djava.library.path=")) {
86 let natives_dir = variables.get(KEY_NATIVES_DIRECTORY).cloned().unwrap_or_default();
87 jvm_args.insert(0, format!("-Djava.library.path={}", natives_dir));
88 }
89
90 if !jvm_args.iter().any(|arg| arg.starts_with("-Dminecraft.launcher.brand=")) {
92 let launcher_name = variables.get(KEY_LAUNCHER_NAME).cloned().unwrap_or_default();
93 jvm_args.insert(0, format!("-Dminecraft.launcher.brand={}", launcher_name));
94 }
95
96 if !jvm_args.iter().any(|arg| arg.starts_with("-Dminecraft.launcher.version=")) {
97 let launcher_version = variables.get(KEY_LAUNCHER_VERSION).cloned().unwrap_or_default();
98 jvm_args.insert(0, format!("-Dminecraft.launcher.version={}", launcher_version));
99 }
100
101 if !jvm_args.contains(&CP_FLAG.to_string()) {
103 let classpath = variables.get(KEY_CLASSPATH).cloned().unwrap_or_default();
104 jvm_args.push(CP_FLAG.into());
105 jvm_args.push(classpath);
106 }
107
108 apply_jvm_overrides(&mut jvm_args, jvm_overrides);
110
111 apply_jvm_removals(&mut jvm_args, jvm_removals);
113
114 let game_args = apply_arg_removals(game_args, arg_removals);
116
117 let mut full_args = jvm_args;
119 full_args.push(builder.main_class.main_class.clone());
120 full_args.extend(game_args);
121
122 full_args.extend_from_slice(raw_args);
124
125 lighty_core::trace_debug!(args = ?full_args, "Launch arguments built");
126
127 full_args
128 }
129}
130
131fn create_variable_map<T: VersionInfo>(
133 version: &T,
134 builder: &Version,
135 username: &str,
136 uuid: &str,
137) -> HashMap<String, String> {
138 let mut map = HashMap::new();
139
140 #[cfg(target_os = "windows")]
141 let classpath_separator = ";";
142 #[cfg(not(target_os = "windows"))]
143 let classpath_separator = ":";
144
145 map.insert(KEY_AUTH_PLAYER_NAME.into(), username.into());
147 map.insert(KEY_AUTH_UUID.into(), uuid.into());
148 map.insert(KEY_AUTH_ACCESS_TOKEN.into(), DEFAULT_ACCESS_TOKEN.into());
149 map.insert(KEY_AUTH_XUID.into(), DEFAULT_XUID.into());
150 map.insert(KEY_CLIENT_ID.into(), DEFAULT_CLIENT_ID.into());
151 map.insert(KEY_USER_TYPE.into(), DEFAULT_USER_TYPE.into());
152 map.insert(KEY_USER_PROPERTIES.into(), DEFAULT_USER_PROPERTIES.into());
153
154 map.insert(KEY_VERSION_NAME.into(), version.name().into());
156 map.insert(KEY_VERSION_TYPE.into(), DEFAULT_VERSION_TYPE.into());
157
158 map.insert(KEY_GAME_DIRECTORY.into(), version.game_dirs().join("runtime").display().to_string());
160 map.insert(KEY_ASSETS_ROOT.into(), version.game_dirs().join("assets").display().to_string());
161 map.insert(KEY_NATIVES_DIRECTORY.into(), version.game_dirs().join("natives").display().to_string());
162 map.insert(KEY_LIBRARY_DIRECTORY.into(), version.game_dirs().join("libraries").display().to_string());
163
164 let assets_index_name = builder.assets_index
166 .as_ref()
167 .map(|idx| idx.id.clone())
168 .unwrap_or_else(|| version.minecraft_version().into());
169 map.insert(KEY_ASSETS_INDEX_NAME.into(), assets_index_name);
170
171 map.insert(KEY_LAUNCHER_NAME.into(), LAUNCHER_NAME.into());
173 map.insert(KEY_LAUNCHER_VERSION.into(), LAUNCHER_VERSION.into());
174
175 let classpath = build_classpath(version, &builder.libraries);
177 map.insert(KEY_CLASSPATH.into(), classpath);
178 map.insert(KEY_CLASSPATH_SEPARATOR.into(), classpath_separator.to_string());
179
180 map
181}
182
183fn build_classpath<T: VersionInfo>(version: &T, libraries: &[lighty_loaders::types::version_metadata::Library]) -> String {
185 #[cfg(target_os = "windows")]
186 let separator = ";";
187 #[cfg(not(target_os = "windows"))]
188 let separator = ":";
189
190 let lib_dir = version.game_dirs().join("libraries");
191
192 let mut classpath_entries: Vec<String> = libraries
193 .iter()
194 .filter_map(|lib| {
195 lib.path.as_ref().map(|path| {
196 lib_dir.join(path).display().to_string()
197 })
198 })
199 .collect();
200
201 classpath_entries.push(
203 version.game_dirs().join(format!("{}.jar", version.name())).display().to_string()
204 );
205
206 classpath_entries.join(separator)
207}
208
209fn build_default_jvm_args(variables: &HashMap<String, String>) -> Vec<String> {
211 let natives_dir = variables.get(KEY_NATIVES_DIRECTORY).cloned().unwrap_or_default();
212 let launcher_name = variables.get(KEY_LAUNCHER_NAME).cloned().unwrap_or_default();
213 let launcher_version = variables.get(KEY_LAUNCHER_VERSION).cloned().unwrap_or_default();
214 let classpath = variables.get(KEY_CLASSPATH).cloned().unwrap_or_default();
215
216 vec![
217 "-Xms1024M".into(),
218 "-Xmx2048M".into(),
219 format!("-Djava.library.path={}", natives_dir),
220 format!("-Dminecraft.launcher.brand={}", launcher_name),
221 format!("-Dminecraft.launcher.version={}", launcher_version),
222 CP_FLAG.into(),
223 classpath,
224 ]
225}
226
227
228fn replace_variables_in_vec(args: &[String], variables: &HashMap<String, String>) -> Vec<String> {
230 args.iter()
231 .map(|arg| replace_variables_cow(arg, variables).into_owned())
232 .collect()
233}
234
235fn replace_variables_cow<'a>(
238 input: &'a str,
239 variables: &HashMap<String, String>
240) -> Cow<'a, str> {
241 if !input.contains("${") {
243 return Cow::Borrowed(input); }
245
246 let mut result = String::with_capacity(input.len() + 128);
248 let mut last_end = 0;
249
250 for (start, _) in input.match_indices("${") {
252 if let Some(end_offset) = input[start..].find('}') {
253 let end = start + end_offset;
254 let key = &input[start + 2..end];
255
256 result.push_str(&input[last_end..start]);
258
259 if let Some(value) = variables.get(key) {
261 result.push_str(value);
262 } else {
263 result.push_str(&input[start..=end]);
264 }
265
266 last_end = end + 1;
267 }
268 }
269
270 result.push_str(&input[last_end..]);
272 Cow::Owned(result)
273}
274
275fn apply_jvm_overrides(jvm_args: &mut Vec<String>, jvm_overrides: &HashMap<String, String>) {
282 for (key, value) in jvm_overrides {
283 let formatted_option = format_jvm_option(key, value);
284
285 let key_prefix = format!("-{}", key.split('=').next().unwrap_or(key));
287 if let Some(pos) = jvm_args.iter().position(|arg| arg.starts_with(&key_prefix)) {
288 jvm_args[pos] = formatted_option;
289 } else {
290 if let Some(cp_pos) = jvm_args.iter().position(|arg| arg == CP_FLAG) {
292 jvm_args.insert(cp_pos, formatted_option);
293 } else {
294 jvm_args.push(formatted_option);
295 }
296 }
297 }
298}
299
300fn format_jvm_option(key: &str, value: &str) -> String {
308 if value.is_empty() {
309 format!("-{}", key)
310 } else if key.starts_with('X') && !key.contains(':') && !key.contains('=') {
311 format!("-{}{}", key, value)
313 } else {
314 format!("-{}={}", key, value)
316 }
317}
318
319fn apply_jvm_removals(jvm_args: &mut Vec<String>, jvm_removals: &HashSet<String>) {
321 jvm_args.retain(|arg| {
322 let arg_key = if let Some(stripped) = arg.strip_prefix('-') {
324 stripped.split('=').next().unwrap_or(stripped)
326 .split(|c: char| c.is_numeric()).next().unwrap_or(stripped)
327 } else {
328 return true; };
330
331 !jvm_removals.contains(arg_key)
333 });
334}
335
336fn apply_arg_removals(game_args: Vec<String>, arg_removals: &HashSet<String>) -> Vec<String> {
338 game_args.into_iter()
339 .filter(|arg| {
340 !arg_removals.iter().any(|removal| {
342 arg == removal || arg.starts_with(&format!("--{}", removal))
343 })
344 })
345 .collect()
346}