lighty_launch/launch/builder.rs
1// Copyright (c) 2025 Hamadi
2// Licensed under the MIT License
3
4//! Launch builder for game arguments and JVM options.
5
6use std::collections::{HashMap, HashSet};
7use lighty_auth::UserProfile;
8use lighty_java::JavaDistribution;
9use crate::errors::InstallerResult;
10use lighty_loaders::types::{VersionInfo, Loader, LoaderExtensions};
11use crate::arguments::Arguments;
12use crate::installer::Installer;
13
14#[cfg(feature = "events")]
15use lighty_event::EventBus;
16
17/// Launch builder. Created by `version.launch(&profile, java_distribution)`.
18pub struct LaunchBuilder<'a, T> {
19 pub(crate) version: &'a mut T,
20 pub(crate) profile: &'a UserProfile,
21 pub(crate) java_distribution: JavaDistribution,
22 pub(crate) jvm_overrides: HashMap<String, String>,
23 pub(crate) jvm_removals: HashSet<String>,
24 pub(crate) arg_overrides: HashMap<String, String>,
25 pub(crate) arg_removals: HashSet<String>,
26 pub(crate) raw_args: Vec<String>,
27 #[cfg(feature = "events")]
28 pub(crate) event_bus: Option<&'a EventBus>,
29}
30
31impl<'a, T> LaunchBuilder<'a, T>
32where
33 T: VersionInfo<LoaderType = Loader>
34 + LoaderExtensions
35 + Arguments
36 + Installer
37 + lighty_modsloader::WithMods,
38{
39 /// Create a new launch builder
40 pub(crate) fn new(
41 version: &'a mut T,
42 profile: &'a UserProfile,
43 java_distribution: JavaDistribution,
44 ) -> Self {
45 Self {
46 version,
47 profile,
48 java_distribution,
49 jvm_overrides: HashMap::new(),
50 jvm_removals: HashSet::new(),
51 arg_overrides: HashMap::new(),
52 arg_removals: HashSet::new(),
53 raw_args: Vec::new(),
54 #[cfg(feature = "events")]
55 event_bus: None,
56 }
57 }
58
59 /// Set an event bus to receive download progress events
60 ///
61 /// # Example
62 /// ```rust,no_run
63 /// # use lighty_auth::UserProfile;
64 /// # use lighty_core::AppState;
65 /// # use lighty_launch::errors::InstallerResult;
66 /// # use lighty_event::EventBus;
67 /// # use lighty_java::JavaDistribution;
68 /// # use lighty_launch::launch::Launch;
69 /// # use lighty_loaders::types::Loader;
70 /// # use lighty_version::VersionBuilder;
71 /// # async fn run() -> InstallerResult<()> {
72 /// # AppState::init("MyLauncher").ok();
73 /// # let profile = UserProfile::offline("Player", "");
74 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
75 /// let event_bus = EventBus::new(100);
76 /// version.launch(&profile, JavaDistribution::Zulu)
77 /// .with_event_bus(&event_bus)
78 /// .run()
79 /// .await?;
80 /// # Ok(()) }
81 /// ```
82 #[cfg(feature = "events")]
83 pub fn with_event_bus(mut self, event_bus: &'a EventBus) -> Self {
84 self.event_bus = Some(event_bus);
85 self
86 }
87
88 /// Configure JVM options
89 ///
90 /// # Example
91 /// ```rust,no_run
92 /// # use lighty_auth::UserProfile;
93 /// # use lighty_core::AppState;
94 /// # use lighty_launch::errors::InstallerResult;
95 /// # use lighty_java::JavaDistribution;
96 /// # use lighty_launch::launch::Launch;
97 /// # use lighty_loaders::types::Loader;
98 /// # use lighty_version::VersionBuilder;
99 /// # async fn run() -> InstallerResult<()> {
100 /// # AppState::init("MyLauncher").ok();
101 /// # let profile = UserProfile::offline("Player", "");
102 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
103 /// version.launch(&profile, JavaDistribution::Zulu)
104 /// .with_jvm_options()
105 /// .set("Xmx", "4G")
106 /// .set("Xms", "2G")
107 /// .set("XX:+UseG1GC", "")
108 /// .done()
109 /// .run()
110 /// .await?;
111 /// # Ok(()) }
112 /// ```
113 pub fn with_jvm_options(self) -> JvmOptionsBuilder<'a, T> {
114 JvmOptionsBuilder {
115 parent: self,
116 overrides: HashMap::new(),
117 removals: HashSet::new(),
118 }
119 }
120
121 /// Configure game arguments
122 ///
123 /// # Example
124 /// ```rust,no_run
125 /// # use lighty_auth::UserProfile;
126 /// # use lighty_core::AppState;
127 /// # use lighty_launch::errors::InstallerResult;
128 /// # use lighty_java::JavaDistribution;
129 /// # use lighty_launch::launch::Launch;
130 /// # use lighty_loaders::types::Loader;
131 /// # use lighty_version::VersionBuilder;
132 /// # async fn run() -> InstallerResult<()> {
133 /// # AppState::init("MyLauncher").ok();
134 /// # let profile = UserProfile::offline("Player", "");
135 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
136 /// version.launch(&profile, JavaDistribution::Zulu)
137 /// .with_arguments()
138 /// .set("width", "1920")
139 /// .set("height", "1080")
140 /// .done()
141 /// .run()
142 /// .await?;
143 /// # Ok(()) }
144 /// ```
145 pub fn with_arguments(self) -> ArgumentsBuilder<'a, T> {
146 ArgumentsBuilder {
147 parent: self,
148 overrides: HashMap::new(),
149 removals: HashSet::new(),
150 raw_args: Vec::new(),
151 }
152 }
153
154 /// Execute the launch
155 ///
156 /// # Example
157 /// ```rust,no_run
158 /// # use lighty_auth::UserProfile;
159 /// # use lighty_core::AppState;
160 /// # use lighty_launch::errors::InstallerResult;
161 /// # use lighty_java::JavaDistribution;
162 /// # use lighty_launch::launch::Launch;
163 /// # use lighty_loaders::types::Loader;
164 /// # use lighty_version::VersionBuilder;
165 /// # async fn run() -> InstallerResult<()> {
166 /// # AppState::init("MyLauncher").ok();
167 /// # let profile = UserProfile::offline("Player", "");
168 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
169 /// version.launch(&profile, JavaDistribution::Zulu).run().await?;
170 /// # Ok(()) }
171 /// ```
172 pub async fn run(self) -> InstallerResult<()> {
173 crate::launch::execute_launch(
174 self.version,
175 self.profile,
176 self.java_distribution,
177 &self.jvm_overrides,
178 &self.jvm_removals,
179 &self.arg_overrides,
180 &self.arg_removals,
181 &self.raw_args,
182 #[cfg(feature = "events")]
183 self.event_bus,
184 )
185 .await
186 }
187}
188
189/// JVM options builder
190///
191/// Configure JVM options like memory, garbage collection, etc.
192pub struct JvmOptionsBuilder<'a, T> {
193 parent: LaunchBuilder<'a, T>,
194 overrides: HashMap<String, String>,
195 removals: HashSet<String>,
196}
197
198impl<'a, T> JvmOptionsBuilder<'a, T>
199where
200 T: VersionInfo<LoaderType = Loader> + LoaderExtensions + Arguments + Installer,
201{
202 /// Set a JVM option
203 ///
204 /// The `-` prefix is added automatically based on the key format:
205 /// - `Xmx`, `Xms` → `-Xmx`, `-Xms`
206 /// - `XX:+UseG1GC` → `-XX:+UseG1GC`
207 /// - `Djava.library.path` → `-Djava.library.path`
208 ///
209 /// # Arguments
210 /// - `key`: JVM option key (without the `-` prefix)
211 /// - `value`: Option value (empty string for flags)
212 ///
213 /// # Example
214 /// ```rust,no_run
215 /// # use lighty_auth::UserProfile;
216 /// # use lighty_core::AppState;
217 /// # use lighty_launch::errors::InstallerResult;
218 /// # use lighty_java::JavaDistribution;
219 /// # use lighty_launch::launch::Launch;
220 /// # use lighty_loaders::types::Loader;
221 /// # use lighty_version::VersionBuilder;
222 /// # async fn run() -> InstallerResult<()> {
223 /// # AppState::init("MyLauncher").ok();
224 /// # let profile = UserProfile::offline("Player", "");
225 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
226 /// version.launch(&profile, JavaDistribution::Zulu)
227 /// .with_jvm_options()
228 /// .set("Xmx", "4G") // → -Xmx4G
229 /// .set("XX:+UseG1GC", "") // → -XX:+UseG1GC
230 /// .set("Djava.library.path", "/path") // → -Djava.library.path=/path
231 /// .done()
232 /// .run()
233 /// .await?;
234 /// # Ok(()) }
235 /// ```
236 pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
237 self.overrides.insert(key.into(), value.into());
238 self
239 }
240
241 /// Remove a JVM option
242 ///
243 /// # Arguments
244 /// - `key`: JVM option key to remove
245 ///
246 /// # Example
247 /// ```rust,no_run
248 /// # use lighty_auth::UserProfile;
249 /// # use lighty_core::AppState;
250 /// # use lighty_launch::errors::InstallerResult;
251 /// # use lighty_java::JavaDistribution;
252 /// # use lighty_launch::launch::Launch;
253 /// # use lighty_loaders::types::Loader;
254 /// # use lighty_version::VersionBuilder;
255 /// # async fn run() -> InstallerResult<()> {
256 /// # AppState::init("MyLauncher").ok();
257 /// # let profile = UserProfile::offline("Player", "");
258 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
259 /// version.launch(&profile, JavaDistribution::Zulu)
260 /// .with_jvm_options()
261 /// .remove("Xmx")
262 /// .done()
263 /// .run()
264 /// .await?;
265 /// # Ok(()) }
266 /// ```
267 pub fn remove(mut self, key: impl Into<String>) -> Self {
268 self.removals.insert(key.into());
269 self
270 }
271
272 /// Finish configuring JVM options and return to the launch builder
273 pub fn done(self) -> LaunchBuilder<'a, T> {
274 let mut parent = self.parent;
275 parent.jvm_overrides = self.overrides;
276 parent.jvm_removals = self.removals;
277 parent
278 }
279}
280
281/// Game arguments builder
282///
283/// Configure game arguments like resolution, game directory, etc.
284pub struct ArgumentsBuilder<'a, T> {
285 parent: LaunchBuilder<'a, T>,
286 overrides: HashMap<String, String>,
287 removals: HashSet<String>,
288 raw_args: Vec<String>,
289}
290
291impl<'a, T> ArgumentsBuilder<'a, T>
292where
293 T: VersionInfo<LoaderType = Loader> + LoaderExtensions + Arguments + Installer,
294{
295 /// Set a game argument or placeholder value
296 ///
297 /// This method intelligently handles two cases:
298 /// - If the key is a known placeholder constant (like KEY_LAUNCHER_NAME), it overrides the placeholder value
299 /// - Otherwise, it adds a raw argument with automatic `--` prefix
300 ///
301 /// # Arguments
302 /// - `key`: Either a placeholder constant or a custom argument name
303 /// - `value`: The value for the argument
304 ///
305 /// # Example
306 /// ```rust,no_run
307 /// # use lighty_auth::UserProfile;
308 /// # use lighty_core::AppState;
309 /// # use lighty_launch::errors::InstallerResult;
310 /// # use lighty_java::JavaDistribution;
311 /// # use lighty_launch::launch::Launch;
312 /// # use lighty_loaders::types::Loader;
313 /// # use lighty_version::VersionBuilder;
314 /// use lighty_launch::arguments::KEY_LAUNCHER_NAME;
315 /// # async fn run() -> InstallerResult<()> {
316 /// # AppState::init("MyLauncher").ok();
317 /// # let profile = UserProfile::offline("Player", "");
318 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
319 /// version.launch(&profile, JavaDistribution::Zulu)
320 /// .with_arguments()
321 /// .set(KEY_LAUNCHER_NAME, "MyLauncher") // override ${launcher_name}
322 /// .set("width", "1920") // adds --width 1920
323 /// .set("fullscreen", "") // adds --fullscreen
324 /// .done()
325 /// .run()
326 /// .await?;
327 /// # Ok(()) }
328 /// ```
329 pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
330 let key_str = key.into();
331 let value_str = value.into();
332
333 // Known launch placeholder keys (overrides target the variable map)
334 const KNOWN_PLACEHOLDERS: &[&str] = &[
335 "auth_player_name", "auth_uuid", "auth_access_token", "auth_xuid",
336 "clientid", "user_type", "user_properties",
337 "version_name", "version_type",
338 "game_directory", "assets_root", "natives_directory", "library_directory",
339 "assets_index_name", "launcher_name", "launcher_version",
340 "classpath", "classpath_separator",
341 ];
342
343 // Known placeholders are recorded as substitutions
344 if KNOWN_PLACEHOLDERS.contains(&key_str.as_str()) {
345 self.overrides.insert(key_str, value_str);
346 } else {
347 // Anything else is appended as a raw `--key [value]` argument
348 let formatted_arg = if key_str.starts_with("--") {
349 key_str
350 } else if key_str.starts_with('-') {
351 format!("-{}", key_str)
352 } else {
353 format!("--{}", key_str)
354 };
355
356 self.raw_args.push(formatted_arg);
357
358 if !value_str.is_empty() {
359 self.raw_args.push(value_str);
360 }
361 }
362
363 self
364 }
365
366 /// Remove a game argument
367 ///
368 /// # Arguments
369 /// - `key`: Argument key to remove
370 ///
371 /// # Example
372 /// ```rust,no_run
373 /// # use lighty_auth::UserProfile;
374 /// # use lighty_core::AppState;
375 /// # use lighty_launch::errors::InstallerResult;
376 /// # use lighty_java::JavaDistribution;
377 /// # use lighty_launch::launch::Launch;
378 /// # use lighty_loaders::types::Loader;
379 /// # use lighty_version::VersionBuilder;
380 /// # async fn run() -> InstallerResult<()> {
381 /// # AppState::init("MyLauncher").ok();
382 /// # let profile = UserProfile::offline("Player", "");
383 /// # let mut version = VersionBuilder::new("inst", Loader::Vanilla, "", "1.21.1");
384 /// version.launch(&profile, JavaDistribution::Zulu)
385 /// .with_arguments()
386 /// .remove("width")
387 /// .done()
388 /// .run()
389 /// .await?;
390 /// # Ok(()) }
391 /// ```
392 pub fn remove(mut self, key: impl Into<String>) -> Self {
393 self.removals.insert(key.into());
394 self
395 }
396
397 /// Finish configuring arguments and return to the launch builder
398 pub fn done(self) -> LaunchBuilder<'a, T> {
399 let mut parent = self.parent;
400 parent.arg_overrides = self.overrides;
401 parent.arg_removals = self.removals;
402 parent.raw_args = self.raw_args;
403 parent
404 }
405}
406