lighty_launch/
arguments.rs

1use lighty_version::version_metadata::VersionBuilder;
2use lighty_loaders::version::Version;
3use std::borrow::Cow;
4use std::collections::HashMap;
5
6pub trait Arguments<'a> {
7    fn build_arguments(
8        &self,
9        builder: &VersionBuilder,
10        username: &str,
11        uuid: &str,
12    ) -> Vec<String>;
13}
14
15impl<'a> Arguments<'a> for Version<'a> {
16    fn build_arguments(
17        &self,
18        builder: &VersionBuilder,
19        username: &str,
20        uuid: &str,
21    ) -> Vec<String> {
22        // Créer la HashMap avec toutes les variables
23        let variables = create_variable_map(self, builder, username, uuid);
24
25        // Remplacer les variables dans les arguments
26        let game_args = replace_variables_in_vec(&builder.arguments.game, &variables);
27
28        let mut jvm_args = builder.arguments.jvm
29            .as_ref()
30            .map(|jvm| replace_variables_in_vec(jvm, &variables))
31            .unwrap_or_else(|| build_default_jvm_args(&variables));
32
33        // S'assurer que les arguments JVM critiques sont toujours présents
34
35        // 1. java.library.path (pour les natives LWJGL)
36        if !jvm_args.iter().any(|arg| arg.starts_with("-Djava.library.path=")) {
37            let natives_dir = variables.get("natives_directory").cloned().unwrap_or_default();
38            jvm_args.insert(0, format!("-Djava.library.path={}", natives_dir));
39        }
40
41        // 2. Launcher brand et version
42        if !jvm_args.iter().any(|arg| arg.starts_with("-Dminecraft.launcher.brand=")) {
43            let launcher_name = variables.get("launcher_name").cloned().unwrap_or_default();
44            jvm_args.insert(0, format!("-Dminecraft.launcher.brand={}", launcher_name));
45        }
46
47        if !jvm_args.iter().any(|arg| arg.starts_with("-Dminecraft.launcher.version=")) {
48            let launcher_version = variables.get("launcher_version").cloned().unwrap_or_default();
49            jvm_args.insert(0, format!("-Dminecraft.launcher.version={}", launcher_version));
50        }
51
52        // 3. Classpath (doit être en dernier avant la mainClass)
53        if !jvm_args.contains(&"-cp".to_string()) {
54            let classpath = variables.get("classpath").cloned().unwrap_or_default();
55            jvm_args.push("-cp".to_string());
56            jvm_args.push(classpath);
57        }
58
59        // Construire le Vec complet : JVM + MainClass + Game
60        let mut full_args = jvm_args;
61        full_args.push(builder.main_class.main_class.clone());
62        full_args.extend(game_args);
63        tracing::debug!(args = ?full_args, "Launch arguments built");
64
65        full_args
66    }
67}
68
69/// Crée la HashMap avec toutes les variables de lancement
70fn create_variable_map<'a>(
71    version: &Version<'a>,
72    builder: &VersionBuilder,
73    username: &str,
74    uuid: &str,
75) -> HashMap<String, String> {
76        let mut map = HashMap::new();
77
78        #[cfg(target_os = "windows")]
79        let classpath_separator = ";";
80        #[cfg(not(target_os = "windows"))]
81        let classpath_separator = ":";
82
83        // Authentification
84        map.insert("auth_player_name".to_string(), username.to_string());
85        map.insert("auth_uuid".to_string(), uuid.to_string());
86        map.insert("auth_access_token".to_string(), "0".to_string());
87        map.insert("auth_xuid".to_string(), "0".to_string());
88        map.insert("clientid".to_string(), "{client-id}".to_string());
89        map.insert("user_type".to_string(), "legacy".to_string());
90        map.insert("user_properties".to_string(), "{}".to_string());
91
92        // Version
93        map.insert("version_name".to_string(), version.name.to_string());
94        map.insert("version_type".to_string(), "release".to_string());
95
96        // Directories
97        map.insert("game_directory".to_string(), version.game_dirs.display().to_string());
98        map.insert("assets_root".to_string(), version.game_dirs.join("assets").display().to_string());
99        map.insert("natives_directory".to_string(), version.game_dirs.join("natives").display().to_string());
100        map.insert("library_directory".to_string(), version.game_dirs.join("libraries").display().to_string());
101
102        // Assets index
103        let assets_index_name = builder.assets_index
104            .as_ref()
105            .map(|idx| idx.id.clone())
106            .unwrap_or_else(|| version.minecraft_version.to_string());
107        map.insert("assets_index_name".to_string(), assets_index_name);
108
109        // Launcher
110        map.insert("launcher_name".to_string(), "LightyLauncher".to_string());
111        map.insert("launcher_version".to_string(), "1.0.0".to_string());
112
113        // Classpath
114        let classpath = build_classpath(version, &builder.libraries);
115        map.insert("classpath".to_string(), classpath);
116        map.insert("classpath_separator".to_string(), classpath_separator.to_string());
117
118        map
119}
120
121/// Construit le classpath à partir des libraries
122fn build_classpath<'a>(version: &Version<'a>, libraries: &[lighty_version::version_metadata::Library]) -> String {
123        #[cfg(target_os = "windows")]
124        let separator = ";";
125        #[cfg(not(target_os = "windows"))]
126        let separator = ":";
127
128        let lib_dir = version.game_dirs.join("libraries");
129
130        let mut classpath_entries: Vec<String> = libraries
131            .iter()
132            .filter_map(|lib| {
133                lib.path.as_ref().map(|path| {
134                    lib_dir.join(path).display().to_string()
135                })
136            })
137            .collect();
138
139        // Ajouter le client.jar à la fin
140        classpath_entries.push(
141            version.game_dirs.join(format!("{}.jar", version.name)).display().to_string()
142        );
143
144        classpath_entries.join(separator)
145}
146
147/// Arguments JVM par défaut (pour anciennes versions sans JVM args)
148fn build_default_jvm_args(variables: &HashMap<String, String>) -> Vec<String> {
149        let natives_dir = variables.get("natives_directory").cloned().unwrap_or_default();
150        let launcher_name = variables.get("launcher_name").cloned().unwrap_or_default();
151        let launcher_version = variables.get("launcher_version").cloned().unwrap_or_default();
152        let classpath = variables.get("classpath").cloned().unwrap_or_default();
153
154        vec![
155            "-Xms1024M".to_string(),
156            "-Xmx2048M".to_string(),
157            format!("-Djava.library.path={}", natives_dir),
158            format!("-Dminecraft.launcher.brand={}", launcher_name),
159            format!("-Dminecraft.launcher.version={}", launcher_version),
160            "-cp".to_string(),
161            classpath,
162        ]
163}
164
165
166/// Replaces variables in a vector of arguments efficiently
167fn replace_variables_in_vec(args: &[String], variables: &HashMap<String, String>) -> Vec<String> {
168    args.iter()
169        .map(|arg| replace_variables_cow(arg, variables).into_owned())
170        .collect()
171}
172
173/// Efficient variable replacement using Cow (Copy-on-Write)
174/// Only allocates when replacements are actually needed
175fn replace_variables_cow<'a>(
176    input: &'a str,
177    variables: &HashMap<String, String>
178) -> Cow<'a, str> {
179    // Fast path: no variables to replace
180    if !input.contains("${") {
181        return Cow::Borrowed(input); // Zero allocation!
182    }
183
184    // Pre-allocate with extra capacity for replacements
185    let mut result = String::with_capacity(input.len() + 128);
186    let mut last_end = 0;
187
188    // Find all ${...} patterns
189    for (start, _) in input.match_indices("${") {
190        if let Some(end_offset) = input[start..].find('}') {
191            let end = start + end_offset;
192            let key = &input[start + 2..end];
193
194            // Append text before the variable
195            result.push_str(&input[last_end..start]);
196
197            // Replace with value or keep original if not found
198            if let Some(value) = variables.get(key) {
199                result.push_str(value);
200            } else {
201                result.push_str(&input[start..=end]);
202            }
203
204            last_end = end + 1;
205        }
206    }
207
208    // Append remaining text
209    result.push_str(&input[last_end..]);
210    Cow::Owned(result)
211}
212
213// Note: For regex-based replacement (if needed in the future):
214// 1. Add `regex = "1"` to Cargo.toml dependencies
215// 2. Use this implementation:
216//
217// use once_cell::sync::Lazy;
218// use regex::Regex;
219//
220// static VAR_PATTERN: Lazy<Regex> = Lazy::new(|| {
221//     Regex::new(r"\$\{([^}]+)\}").expect("Invalid regex pattern")
222// });
223//
224// fn replace_variables_regex(input: &str, variables: &HashMap<String, String>) -> String {
225//     VAR_PATTERN.replace_all(input, |caps: &regex::Captures| {
226//         variables.get(&caps[1]).map(|s| s.as_str()).unwrap_or(&caps[0])
227//     }).into_owned()
228// }
229//
230// However, the Cow-based approach above is faster and doesn't require regex.