Skip to main content

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