Skip to main content

lighty_launch/launch/
runner.rs

1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use lighty_auth::UserProfile;
6use lighty_core::time_it;
7#[cfg(feature = "events")]
8use lighty_event::EventBus;
9use lighty_java::jre_downloader::{find_java_binary, jre_download};
10use lighty_java::runtime::JavaRuntime;
11use lighty_java::JavaDistribution;
12#[cfg(not(feature = "events"))]
13use lighty_java::JreError;
14use lighty_loaders::types::version_metadata::{Version, VersionMetaData};
15use lighty_loaders::types::{Loader, LoaderExtensions, VersionInfo};
16use lighty_modsloader::WithMods;
17
18use crate::arguments::Arguments;
19use crate::errors::{InstallerError, InstallerResult};
20#[cfg(any(feature = "neoforge", feature = "forge"))]
21use crate::installer::ressources::libraries::{collect_library_tasks, download_libraries};
22use crate::installer::Installer;
23
24use super::builder::LaunchBuilder;
25
26#[cfg(feature = "forge")]
27use crate::installer::processors::forge_install::run_forge_install_processors;
28#[cfg(feature = "neoforge")]
29use crate::installer::processors::forge_install::run_neoforge_install_processors;
30
31#[cfg(feature = "forge")]
32use lighty_loaders::forge::forge::{
33    extract_install_profile_libraries_modern as forge_install_profile_libraries_modern,
34    ForgeRawData, FORGE,
35};
36#[cfg(feature = "forge")]
37use lighty_loaders::forge::forge_legacy::extract_universal_jar as forge_legacy_extract_universal_jar;
38#[cfg(feature = "neoforge")]
39use lighty_loaders::neoforge::neoforge::{
40    extract_install_profile_libraries as neoforge_install_profile_libraries, NEOFORGE,
41};
42
43/// Extension trait that adds [`Self::launch`] to any installable instance.
44///
45/// Implemented automatically for every type that satisfies the launch
46/// pipeline's trait bounds (see the blanket impl below).
47pub trait Launch {
48    /// Launch the game with a builder pattern
49    ///
50    /// # Arguments
51    /// - `profile`: User profile from authentication
52    /// - `java_distribution`: Java distribution to use
53    ///
54    /// # Returns
55    /// A `LaunchBuilder` for configuring JVM options and game arguments
56    ///
57    /// # Example
58    /// ```no_run
59    /// // Simple launch
60    /// version.launch(&profile, JavaDistribution::Zulu).await?;
61    ///
62    /// // With custom options
63    /// version.launch(&profile, JavaDistribution::Zulu)
64    ///     .with_jvm_options()
65    ///         .set("Xmx", "4G")
66    ///         .done()
67    ///     .with_arguments()
68    ///         .set(KEY_WIDTH, "1920")
69    ///         .done()
70    ///     .await?;
71    /// ```
72    fn launch<'a>(
73        &'a mut self,
74        profile: &'a UserProfile,
75        java_distribution: JavaDistribution,
76    ) -> LaunchBuilder<'a, Self>
77    where
78        Self: Sized;
79}
80
81// Blanket impl for any type that implements the launch pipeline traits.
82//
83// `WithMods` is always required (provides `mod_requests()` — defaults to
84// `&[]` for types that don't track mods, so it's free for vanilla
85// instances). Modrinth / CurseForge / modpack features only gate which
86// builder methods exist and which API clients compile in.
87impl<T> Launch for T
88where
89    T: VersionInfo<LoaderType = Loader>
90        + LoaderExtensions
91        + Arguments
92        + Installer
93        + WithMods,
94{
95    fn launch<'a>(
96        &'a mut self,
97        profile: &'a UserProfile,
98        java_distribution: JavaDistribution,
99    ) -> LaunchBuilder<'a, Self> {
100        LaunchBuilder::new(self, profile, java_distribution)
101    }
102}
103
104/// Internal function to execute the launch process
105pub(crate) async fn execute_launch<T>(
106    version: &mut T,
107    profile: &UserProfile,
108    java_distribution: JavaDistribution,
109    jvm_overrides: &std::collections::HashMap<String, String>,
110    jvm_removals: &std::collections::HashSet<String>,
111    arg_overrides: &std::collections::HashMap<String, String>,
112    arg_removals: &std::collections::HashSet<String>,
113    raw_args: &[String],
114    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
115) -> InstallerResult<()>
116where
117    T: VersionInfo<LoaderType = Loader>
118        + LoaderExtensions
119        + Arguments
120        + Installer
121        + WithMods,
122{
123    // 1. Fetch the loader metadata
124    let metadata = prepare_metadata(
125        version,
126        #[cfg(feature = "events")]
127        event_bus,
128    )
129    .await?;
130
131    let version_data = extract_version(&metadata)?;
132
133    // 2. Make sure Java is installed
134    let java_path = ensure_java_installed(
135        version,
136        version_data,
137        &java_distribution,
138        #[cfg(feature = "events")]
139        event_bus,
140    )
141    .await?;
142
143    // Reconcile arg_overrides[KEY_GAME_DIRECTORY] back onto the
144    // builder so install + args read the same value via
145    // version.runtime_dir(). `game_dirs.join(custom)` does what we
146    // want: a relative override ("runtime") resolves to
147    // game_dirs/runtime, an absolute override ("/mnt/games") wins
148    // outright (Path::join semantics).
149    if let Some(custom) = arg_overrides.get(crate::arguments::KEY_GAME_DIRECTORY) {
150        let resolved = version.game_dirs().join(custom);
151        if resolved.as_path() != version.runtime_dir() {
152            lighty_core::trace_info!(
153                from = %version.runtime_dir().display(),
154                to = %resolved.display(),
155                source = %custom,
156                "[Launch] Resolved KEY_GAME_DIRECTORY override before install"
157            );
158            version.set_runtime_dir(resolved);
159        }
160    }
161
162    // Modpack + user-attached mods are resolved by `Installer::install`
163    // itself (Phase 0 of its pipeline). The runner just passes the raw
164    // `Version` here — no merge to do.
165
166    // Install Minecraft dependencies (libraries, natives, client, assets)
167    time_it!(
168        "Install delay",
169        version
170            .install(
171                version_data,
172                #[cfg(feature = "events")]
173                event_bus,
174            )
175            .await?
176    );
177
178    // Forge-family install_profile libraries + processors.
179    //
180    // For Forge and NeoForge, the install_profile.json libraries are
181    // downloaded through the shared library installer (parallel +
182    // retry + SHA1) so the processor JARs and the runtime-required
183    // `forge:universal` artifact land on disk. Only the processor
184    // execution stays inside each loader crate (it's a per-loader
185    // Java exec with different maven URLs / extract subdirs).
186    //
187    // TODO: generalize this into a per-loader post-install hook for any
188    // loader that needs one (currently only Forge / NeoForge do).
189    #[cfg(feature = "neoforge")]
190    if matches!(version.loader(), Loader::NeoForge) {
191        let install_profile = NEOFORGE.get_raw(version).await?;
192        let profile_libs = neoforge_install_profile_libraries(install_profile.as_ref());
193        let profile_tasks = collect_library_tasks(version, &profile_libs).await;
194        download_libraries(
195            profile_tasks,
196            #[cfg(feature = "events")]
197            event_bus,
198        )
199        .await?;
200        run_neoforge_install_processors(version, install_profile.as_ref(), java_path.clone())
201            .await?;
202    }
203
204    #[cfg(feature = "forge")]
205    if matches!(version.loader(), Loader::Forge) {
206        let raw = FORGE.get_raw(version).await?;
207        match raw.as_ref() {
208            ForgeRawData::Modern {
209                install_profile, ..
210            } => {
211                // Download processor-only libraries, then run processors.
212                let profile_libs = forge_install_profile_libraries_modern(install_profile);
213                let profile_tasks = collect_library_tasks(version, &profile_libs).await;
214                download_libraries(
215                    profile_tasks,
216                    #[cfg(feature = "events")]
217                    event_bus,
218                )
219                .await?;
220                run_forge_install_processors(version, install_profile, java_path.clone()).await?;
221            }
222            ForgeRawData::Legacy(profile) => {
223                // No processors in the legacy era; the universal JAR
224                // ships inside the installer and must be extracted to
225                // its Maven path so the classpath entry resolves.
226                forge_legacy_extract_universal_jar(version, profile).await?;
227            }
228        }
229    }
230
231    // Launch the game
232    execute_game(
233        version,
234        version_data,
235        profile,
236        java_path,
237        arg_overrides,
238        arg_removals,
239        jvm_overrides,
240        jvm_removals,
241        raw_args,
242        #[cfg(feature = "events")]
243        event_bus,
244    )
245    .await
246}
247
248/// Fetches the loader's full metadata document.
249async fn prepare_metadata<T>(
250    builder: &mut T,
251    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
252) -> InstallerResult<Arc<VersionMetaData>>
253where
254    T: VersionInfo<LoaderType = Loader> + LoaderExtensions,
255{
256    lighty_core::trace_debug!(
257        "[Launch] Fetching metadata for loader: {:?}",
258        builder.loader()
259    );
260
261    #[cfg(feature = "events")]
262    let loader_name = format!("{:?}", builder.loader());
263
264    #[cfg(feature = "events")]
265    if let Some(bus) = event_bus {
266        bus.emit(lighty_event::Event::Loader(
267            lighty_event::LoaderEvent::FetchingData {
268                loader: loader_name.clone(),
269                minecraft_version: builder.minecraft_version().to_string(),
270                loader_version: builder.loader_version().to_string(),
271            },
272        ));
273    }
274
275    // Generic metadata fetching - automatically dispatches to the correct loader
276    let metadata = builder.get_metadata().await?;
277
278    #[cfg(feature = "events")]
279    if let Some(bus) = event_bus {
280        bus.emit(lighty_event::Event::Loader(
281            lighty_event::LoaderEvent::DataFetched {
282                loader: loader_name,
283                minecraft_version: builder.minecraft_version().to_string(),
284                loader_version: builder.loader_version().to_string(),
285            },
286        ));
287    }
288
289    lighty_core::trace_info!(
290        "[Launch] Metadata fetched successfully for {:?}",
291        builder.loader()
292    );
293    Ok(metadata)
294}
295
296/// Ensures Java is installed for `version` and returns the binary path.
297async fn ensure_java_installed<T>(
298    builder: &T,
299    version: &Version,
300    java_distribution: &JavaDistribution,
301    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
302) -> InstallerResult<PathBuf>
303where
304    T: VersionInfo,
305{
306    let java_version = version.java_version.major_version;
307
308    // Look for an existing Java install before downloading
309    match find_java_binary(builder.java_dirs(), java_distribution, &java_version).await {
310        Ok(path) => {
311            lighty_core::trace_info!(
312                "[Java] Java {} already installed at: {:?}",
313                java_version,
314                path
315            );
316
317            #[cfg(feature = "events")]
318            if let Some(bus) = event_bus {
319                bus.emit(lighty_event::Event::Java(
320                    lighty_event::JavaEvent::JavaAlreadyInstalled {
321                        distribution: java_distribution.get_name().to_string(),
322                        version: java_version,
323                        binary_path: path.to_string_lossy().to_string(),
324                    },
325                ));
326            }
327
328            Ok(path)
329        }
330        Err(_) => {
331            lighty_core::trace_info!("[Java] Java {} not found, downloading...", java_version);
332
333            #[cfg(feature = "events")]
334            if let Some(bus) = event_bus {
335                bus.emit(lighty_event::Event::Java(
336                    lighty_event::JavaEvent::JavaNotFound {
337                        distribution: java_distribution.get_name().to_string(),
338                        version: java_version,
339                    },
340                ));
341            }
342
343            #[cfg(feature = "events")]
344            let path = jre_download(
345                builder.java_dirs(),
346                java_distribution,
347                &java_version,
348                |current, total| {
349                    lighty_core::trace_debug!("[Java] Download progress: {}/{}", current, total);
350                },
351                event_bus,
352            )
353            .await
354            .map_err(|e| InstallerError::DownloadFailed(format!("JRE download failed: {}", e)))?;
355
356            #[cfg(not(feature = "events"))]
357            let path = jre_download(
358                builder.java_dirs(),
359                java_distribution,
360                &java_version,
361                |current, total| {
362                    lighty_core::trace_debug!("[Java] Download progress: {}/{}", current, total);
363                },
364            )
365            .await
366            .map_err(|e: JreError| {
367                InstallerError::DownloadFailed(format!("JRE download failed: {}", e))
368            })?;
369
370            lighty_core::trace_info!("[Java] Java {} installed successfully", java_version);
371            Ok(path)
372        }
373    }
374}
375
376/// Spawns the game process and wires up event/console handlers.
377async fn execute_game<T>(
378    builder: &T,
379    version: &Version,
380    profile: &UserProfile,
381    java_path: PathBuf,
382    arg_overrides: &HashMap<String, String>,
383    arg_removals: &HashSet<String>,
384    jvm_overrides: &HashMap<String, String>,
385    jvm_removals: &HashSet<String>,
386    raw_args: &[String],
387    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
388) -> InstallerResult<()>
389where
390    T: VersionInfo + Arguments,
391{
392    use crate::instance::manager::GameInstance;
393    use crate::instance::{handle_console_streams, INSTANCE_MANAGER};
394
395    let username = profile.username.as_str();
396
397    // Build the full argv (JVM args + main class + game args)
398    let arguments = builder.build_arguments(
399        version,
400        Some(profile),
401        arg_overrides,
402        arg_removals,
403        jvm_overrides,
404        jvm_removals,
405        raw_args,
406    );
407
408    // Wrap the Java binary path in a runtime helper
409    let java_runtime = JavaRuntime::new(java_path);
410    lighty_core::trace_info!("[Launch] Executing game...");
411
412    match java_runtime.execute(arguments, builder.game_dirs()).await {
413        Ok(child) => {
414            let pid = child.id().ok_or(InstallerError::NoPid)?;
415
416            lighty_core::trace_info!("[Launch] Game launched successfully, PID: {}", pid);
417
418            // Register the instance (metadata only — the child is owned by the console task)
419            let instance = GameInstance {
420                pid,
421                instance_name: builder.name().to_string(),
422                version: format!(
423                    "{}-{}",
424                    builder.minecraft_version(),
425                    builder.loader_version()
426                ),
427                username: username.to_string(),
428                game_dir: builder.game_dirs().to_path_buf(),
429                started_at: std::time::SystemTime::now(),
430            };
431
432            if let Err(e) = INSTANCE_MANAGER.register_instance(instance).await {
433                lighty_core::trace_warn!(
434                    error = %e,
435                    "Failed to register launched instance — process keeps running"
436                );
437            }
438
439            // Emit InstanceLaunched event
440            #[cfg(feature = "events")]
441            if let Some(bus) = event_bus {
442                use lighty_event::{Event, InstanceLaunchedEvent};
443
444                bus.emit(Event::InstanceLaunched(InstanceLaunchedEvent {
445                    pid,
446                    instance_name: builder.name().to_string(),
447                    version: format!(
448                        "{}-{}",
449                        builder.minecraft_version(),
450                        builder.loader_version()
451                    ),
452                    username: username.to_string(),
453                    timestamp: std::time::SystemTime::now(),
454                }));
455
456                // Spawn the window-appearance watcher
457                let bus_clone = bus.clone();
458                let instance_name = builder.name().to_string();
459                let version = format!(
460                    "{}-{}",
461                    builder.minecraft_version(),
462                    builder.loader_version()
463                );
464                tokio::spawn(super::window::detect_window_appearance(
465                    pid,
466                    instance_name,
467                    version,
468                    bus_clone,
469                ));
470            }
471
472            // Spawn the console-streaming handler. It takes ownership of the
473            // child and handles all stdio until the process exits.
474            tokio::spawn(handle_console_streams(
475                pid,
476                builder.name().to_string(),
477                child,
478                #[cfg(feature = "events")]
479                event_bus.cloned(),
480            ));
481
482            Ok(())
483        }
484        Err(e) => {
485            lighty_core::trace_error!("[Launch] Failed to launch game: {}", e);
486            Err(InstallerError::DownloadFailed(format!(
487                "Launch failed: {}",
488                e
489            )))
490        }
491    }
492}
493
494/// Extracts the [`Version`] payload from a [`VersionMetaData`] variant.
495fn extract_version(metadata: &VersionMetaData) -> InstallerResult<&Version> {
496    match metadata {
497        VersionMetaData::Version(v) => Ok(v),
498        _ => Err(InstallerError::InvalidMetadata),
499    }
500}