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