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    // TODO: generalize this into a per-loader post-install hook for any
186    // loader that needs one (currently only Forge / NeoForge do).
187    #[cfg(feature = "neoforge")]
188    if matches!(version.loader(), Loader::NeoForge) {
189        let install_profile = NEOFORGE.get_raw(version).await?;
190        let profile_libs = neoforge_install_profile_libraries(install_profile.as_ref());
191        let profile_tasks = collect_library_tasks(version, &profile_libs).await;
192        download_libraries(
193            profile_tasks,
194            #[cfg(feature = "events")]
195            event_bus,
196        )
197        .await?;
198        run_neoforge_install_processors(version, install_profile.as_ref(), java_path.clone())
199            .await?;
200    }
201
202    #[cfg(feature = "forge")]
203    if matches!(version.loader(), Loader::Forge) {
204        let raw = FORGE.get_raw(version).await?;
205        match raw.as_ref() {
206            ForgeRawData::Modern {
207                install_profile, ..
208            } => {
209                // Download processor-only libraries, then run processors.
210                let profile_libs = forge_install_profile_libraries_modern(install_profile);
211                let profile_tasks = collect_library_tasks(version, &profile_libs).await;
212                download_libraries(
213                    profile_tasks,
214                    #[cfg(feature = "events")]
215                    event_bus,
216                )
217                .await?;
218                run_forge_install_processors(version, install_profile, java_path.clone()).await?;
219            }
220            ForgeRawData::Legacy(profile) => {
221                // No processors in the legacy era; the universal JAR
222                // ships inside the installer and must be extracted to
223                // its Maven path so the classpath entry resolves.
224                forge_legacy_extract_universal_jar(version, profile).await?;
225            }
226        }
227    }
228
229    // Launch the game
230    execute_game(
231        version,
232        version_data,
233        profile,
234        java_path,
235        arg_overrides,
236        arg_removals,
237        jvm_overrides,
238        jvm_removals,
239        raw_args,
240        #[cfg(feature = "events")]
241        event_bus,
242    )
243    .await
244}
245
246/// Fetches the loader's full metadata document.
247async fn prepare_metadata<T>(
248    builder: &mut T,
249    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
250) -> InstallerResult<Arc<VersionMetaData>>
251where
252    T: VersionInfo<LoaderType = Loader> + LoaderExtensions,
253{
254    lighty_core::trace_debug!(
255        "[Launch] Fetching metadata for loader: {:?}",
256        builder.loader()
257    );
258
259    #[cfg(feature = "events")]
260    let loader_name = format!("{:?}", builder.loader());
261
262    #[cfg(feature = "events")]
263    if let Some(bus) = event_bus {
264        bus.emit(lighty_event::Event::Loader(
265            lighty_event::LoaderEvent::FetchingData {
266                loader: loader_name.clone(),
267                minecraft_version: builder.minecraft_version().to_string(),
268                loader_version: builder.loader_version().to_string(),
269            },
270        ));
271    }
272
273    // Generic metadata fetching - automatically dispatches to the correct loader
274    let metadata = builder.get_metadata().await?;
275
276    #[cfg(feature = "events")]
277    if let Some(bus) = event_bus {
278        bus.emit(lighty_event::Event::Loader(
279            lighty_event::LoaderEvent::DataFetched {
280                loader: loader_name,
281                minecraft_version: builder.minecraft_version().to_string(),
282                loader_version: builder.loader_version().to_string(),
283            },
284        ));
285    }
286
287    lighty_core::trace_info!(
288        "[Launch] Metadata fetched successfully for {:?}",
289        builder.loader()
290    );
291    Ok(metadata)
292}
293
294/// Ensures Java is installed for `version` and returns the binary path.
295async fn ensure_java_installed<T>(
296    builder: &T,
297    version: &Version,
298    java_distribution: &JavaDistribution,
299    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
300) -> InstallerResult<PathBuf>
301where
302    T: VersionInfo,
303{
304    let java_version = version.java_version.major_version;
305
306    // Look for an existing Java install before downloading
307    match find_java_binary(builder.java_dirs(), java_distribution, &java_version).await {
308        Ok(path) => {
309            lighty_core::trace_info!(
310                "[Java] Java {} already installed at: {:?}",
311                java_version,
312                path
313            );
314
315            #[cfg(feature = "events")]
316            if let Some(bus) = event_bus {
317                bus.emit(lighty_event::Event::Java(
318                    lighty_event::JavaEvent::JavaAlreadyInstalled {
319                        distribution: java_distribution.get_name().to_string(),
320                        version: java_version,
321                        binary_path: path.to_string_lossy().to_string(),
322                    },
323                ));
324            }
325
326            Ok(path)
327        }
328        Err(_) => {
329            lighty_core::trace_info!("[Java] Java {} not found, downloading...", java_version);
330
331            #[cfg(feature = "events")]
332            if let Some(bus) = event_bus {
333                bus.emit(lighty_event::Event::Java(
334                    lighty_event::JavaEvent::JavaNotFound {
335                        distribution: java_distribution.get_name().to_string(),
336                        version: java_version,
337                    },
338                ));
339            }
340
341            #[cfg(feature = "events")]
342            let path = jre_download(
343                builder.java_dirs(),
344                java_distribution,
345                &java_version,
346                |current, total| {
347                    lighty_core::trace_debug!("[Java] Download progress: {}/{}", current, total);
348                },
349                event_bus,
350            )
351            .await?;
352
353            #[cfg(not(feature = "events"))]
354            let path = jre_download(
355                builder.java_dirs(),
356                java_distribution,
357                &java_version,
358                |current, total| {
359                    lighty_core::trace_debug!("[Java] Download progress: {}/{}", current, total);
360                },
361            )
362            .await?;
363
364            lighty_core::trace_info!("[Java] Java {} installed successfully", java_version);
365            Ok(path)
366        }
367    }
368}
369
370/// Spawns the game process and wires up event/console handlers.
371async fn execute_game<T>(
372    builder: &T,
373    version: &Version,
374    profile: &UserProfile,
375    java_path: PathBuf,
376    arg_overrides: &HashMap<String, String>,
377    arg_removals: &HashSet<String>,
378    jvm_overrides: &HashMap<String, String>,
379    jvm_removals: &HashSet<String>,
380    raw_args: &[String],
381    #[cfg(feature = "events")] event_bus: Option<&EventBus>,
382) -> InstallerResult<()>
383where
384    T: VersionInfo + Arguments,
385{
386    use crate::instance::manager::GameInstance;
387    use crate::instance::{handle_console_streams, INSTANCE_MANAGER};
388
389    let username = profile.username.as_str();
390
391    // Build the full argv (JVM args + main class + game args)
392    let arguments = builder.build_arguments(
393        version,
394        Some(profile),
395        arg_overrides,
396        arg_removals,
397        jvm_overrides,
398        jvm_removals,
399        raw_args,
400    );
401
402    // Wrap the Java binary path in a runtime helper
403    let java_runtime = JavaRuntime::new(java_path);
404    lighty_core::trace_info!("[Launch] Executing game...");
405
406    match java_runtime.execute(arguments, builder.game_dirs()).await {
407        Ok(child) => {
408            let pid = child.id().ok_or(InstallerError::NoPid)?;
409
410            lighty_core::trace_info!("[Launch] Game launched successfully, PID: {}", pid);
411
412            // Register the instance (metadata only — the child is owned by the console task)
413            let instance = GameInstance {
414                pid,
415                instance_name: builder.name().to_string(),
416                version: format!(
417                    "{}-{}",
418                    builder.minecraft_version(),
419                    builder.loader_version()
420                ),
421                username: username.to_string(),
422                game_dir: builder.game_dirs().to_path_buf(),
423                started_at: std::time::SystemTime::now(),
424            };
425
426            if let Err(e) = INSTANCE_MANAGER.register_instance(instance).await {
427                lighty_core::trace_warn!(
428                    error = %e,
429                    "Failed to register launched instance — process keeps running"
430                );
431            }
432
433            // Emit InstanceLaunched event
434            #[cfg(feature = "events")]
435            if let Some(bus) = event_bus {
436                use lighty_event::{Event, InstanceLaunchedEvent};
437
438                bus.emit(Event::InstanceLaunched(InstanceLaunchedEvent {
439                    pid,
440                    instance_name: builder.name().to_string(),
441                    version: format!(
442                        "{}-{}",
443                        builder.minecraft_version(),
444                        builder.loader_version()
445                    ),
446                    username: username.to_string(),
447                    timestamp: std::time::SystemTime::now(),
448                }));
449
450                // Spawn the window-appearance watcher
451                let bus_clone = bus.clone();
452                let instance_name = builder.name().to_string();
453                let version = format!(
454                    "{}-{}",
455                    builder.minecraft_version(),
456                    builder.loader_version()
457                );
458                tokio::spawn(super::window::detect_window_appearance(
459                    pid,
460                    instance_name,
461                    version,
462                    bus_clone,
463                ));
464            }
465
466            // Spawn the console-streaming handler. It takes ownership of the
467            // child and handles all stdio until the process exits.
468            tokio::spawn(handle_console_streams(
469                pid,
470                builder.name().to_string(),
471                child,
472                #[cfg(feature = "events")]
473                event_bus.cloned(),
474            ));
475
476            Ok(())
477        }
478        Err(e) => {
479            lighty_core::trace_error!("[Launch] Failed to launch game: {}", e);
480            Err(e.into())
481        }
482    }
483}
484
485/// Extracts the [`Version`] payload from a [`VersionMetaData`] variant.
486fn extract_version(metadata: &VersionMetaData) -> InstallerResult<&Version> {
487    match metadata {
488        VersionMetaData::Version(v) => Ok(v),
489        _ => Err(InstallerError::InvalidMetadata),
490    }
491}