lighty_launch/arguments/
arguments.rs

1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4use lighty_loaders::types::version_metadata::Version;
5use lighty_loaders::types::VersionInfo;
6use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8
9// Constantes publiques pour les clés de HashMap
10pub 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
29// Constantes pour les valeurs
30const 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        // Créer la HashMap avec toutes les variables
67        let mut variables = create_variable_map(self, builder, username, uuid);
68
69        // Appliquer les overrides sur les variables
70        for (key, value) in arg_overrides {
71            variables.insert(key.clone(), value.clone());
72        }
73
74        // Remplacer les variables dans les arguments
75        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        // S'assurer que les arguments JVM critiques sont toujours présents
83
84        // 1. java.library.path (pour les natives LWJGL)
85        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        // 2. Launcher brand et version
91        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        // 3. Classpath (doit être en dernier avant la mainClass)
102        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        // 4. Appliquer les JVM overrides
109        apply_jvm_overrides(&mut jvm_args, jvm_overrides);
110
111        // 5. Appliquer les JVM removals
112        apply_jvm_removals(&mut jvm_args, jvm_removals);
113
114        // 6. Appliquer les arg removals (filtrer les arguments de jeu)
115        let game_args = apply_arg_removals(game_args, arg_removals);
116
117        // Construire le Vec complet : JVM + MainClass + Game + Raw Args
118        let mut full_args = jvm_args;
119        full_args.push(builder.main_class.main_class.clone());
120        full_args.extend(game_args);
121
122        // Ajouter les arguments bruts à la fin
123        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
131/// Crée la HashMap avec toutes les variables de lancement
132fn 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        // Authentification
146        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        // Version
155        map.insert(KEY_VERSION_NAME.into(), version.name().into());
156        map.insert(KEY_VERSION_TYPE.into(), DEFAULT_VERSION_TYPE.into());
157
158        // Directories
159        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        // Assets index
165        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        // Launcher
172        map.insert(KEY_LAUNCHER_NAME.into(), LAUNCHER_NAME.into());
173        map.insert(KEY_LAUNCHER_VERSION.into(), LAUNCHER_VERSION.into());
174
175        // Classpath
176        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
183/// Construit le classpath à partir des libraries
184fn 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        // Ajouter le client.jar à la fin
202        classpath_entries.push(
203            version.game_dirs().join(format!("{}.jar", version.name())).display().to_string()
204        );
205
206        classpath_entries.join(separator)
207}
208
209/// Arguments JVM par défaut (pour anciennes versions sans JVM args)
210fn 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
228/// Replaces variables in a vector of arguments efficiently
229fn 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
235/// Efficient variable replacement using Cow (Copy-on-Write)
236/// Only allocates when replacements are actually needed
237fn replace_variables_cow<'a>(
238    input: &'a str,
239    variables: &HashMap<String, String>
240) -> Cow<'a, str> {
241    // Fast path: no variables to replace
242    if !input.contains("${") {
243        return Cow::Borrowed(input); // Zero allocation!
244    }
245
246    // Pre-allocate with extra capacity for replacements
247    let mut result = String::with_capacity(input.len() + 128);
248    let mut last_end = 0;
249
250    // Find all ${...} patterns
251    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            // Append text before the variable
257            result.push_str(&input[last_end..start]);
258
259            // Replace with value or keep original if not found
260            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    // Append remaining text
271    result.push_str(&input[last_end..]);
272    Cow::Owned(result)
273}
274
275/// Applique les JVM overrides en ajoutant automatiquement le préfixe `-`
276///
277/// Formate automatiquement les options JVM :
278/// - `Xmx` → `-Xmx`
279/// - `XX:+UseG1GC` → `-XX:+UseG1GC`
280/// - `Djava.library.path` → `-Djava.library.path`
281fn 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        // Vérifier si l'option existe déjà et la remplacer
286        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            // Insérer avant le classpath (-cp)
291            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
300/// Formate une option JVM avec le préfixe `-` et la valeur
301///
302/// # Exemples
303/// - `("Xmx", "4G")` → `-Xmx4G`
304/// - `("Xms", "2G")` → `-Xms2G`
305/// - `("XX:+UseG1GC", "")` → `-XX:+UseG1GC`
306/// - `("Djava.library.path", "/path")` → `-Djava.library.path=/path`
307fn 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        // Options -Xmx, -Xms, etc. (pas de séparateur)
312        format!("-{}{}", key, value)
313    } else {
314        // Options -D, -XX:, etc. (avec =)
315        format!("-{}={}", key, value)
316    }
317}
318
319/// Supprime les options JVM qui correspondent aux clés dans jvm_removals
320fn apply_jvm_removals(jvm_args: &mut Vec<String>, jvm_removals: &HashSet<String>) {
321    jvm_args.retain(|arg| {
322        // Extraire la clé de l'argument (sans le '-' et sans la valeur)
323        let arg_key = if let Some(stripped) = arg.strip_prefix('-') {
324            // Gérer les cas -Xmx4G, -Djava.library.path=/path, -XX:+UseG1GC
325            stripped.split('=').next().unwrap_or(stripped)
326                .split(|c: char| c.is_numeric()).next().unwrap_or(stripped)
327        } else {
328            return true; // Garder les arguments qui ne commencent pas par '-'
329        };
330
331        // Garder l'argument si sa clé n'est pas dans jvm_removals
332        !jvm_removals.contains(arg_key)
333    });
334}
335
336/// Filtre les arguments de jeu en supprimant ceux qui correspondent à arg_removals
337fn apply_arg_removals(game_args: Vec<String>, arg_removals: &HashSet<String>) -> Vec<String> {
338    game_args.into_iter()
339        .filter(|arg| {
340            // Supprimer l'argument s'il correspond exactement ou s'il commence par la clé
341            !arg_removals.iter().any(|removal| {
342                arg == removal || arg.starts_with(&format!("--{}", removal))
343            })
344        })
345        .collect()
346}