Skip to main content

hexomc_lib/install/
loader.rs

1use async_trait::async_trait;
2use std::{path::{Path, PathBuf}, sync::Arc};
3
4use crate::error::Result;
5use crate::install::vanilla::LoaderType;
6
7/// Progress callback. `Arc` so it can be cloned across async boundaries and
8/// satisfy the `Clone + 'static` bounds of `install_vanilla` / `download_batch`.
9pub type ProgressFn = Arc<dyn Fn(usize, usize, &str) + Send + Sync + 'static>;
10
11pub fn no_progress() -> ProgressFn {
12    Arc::new(|_, _, _| {})
13}
14
15/// Unified interface for all loaders (dependency-injection layer): callers only
16/// deal with `&dyn LoaderInstaller` and never need to know the concrete loader.
17///
18/// ```ignore
19/// let loader: Box<dyn LoaderInstaller> = Box::new(FabricInstaller::latest());
20/// loader.install("1.21.4", &base_dir, no_progress()).await?;
21/// ```
22#[async_trait]
23pub trait LoaderInstaller: Send + Sync {
24    fn loader_type(&self) -> LoaderType;
25
26    /// Versions of this loader available for the given MC version.
27    /// Vanilla returns an empty vec (versions come from the MC manifest).
28    async fn available_versions(&self, mc_version: &str) -> Result<Vec<String>>;
29
30    /// Install this loader. Vanilla does the full install; others overlay on top
31    /// of an installed vanilla. `instance_name` is caller-chosen to avoid
32    /// same-version/different-loader collisions.
33    async fn install(
34        &self,
35        mc_version: &str,
36        instance_name: &str,
37        base_dir: &Path,
38        progress: ProgressFn,
39    ) -> Result<()>;
40}
41
42pub struct VanillaInstaller;
43
44#[async_trait]
45impl LoaderInstaller for VanillaInstaller {
46    fn loader_type(&self) -> LoaderType {
47        LoaderType::Vanilla
48    }
49
50    async fn available_versions(&self, _mc_version: &str) -> Result<Vec<String>> {
51        Ok(vec![])
52    }
53
54    async fn install(
55        &self,
56        mc_version: &str,
57        instance_name: &str,
58        base_dir: &Path,
59        progress: ProgressFn,
60    ) -> Result<()> {
61        crate::install::vanilla::install_vanilla(mc_version, instance_name, base_dir, move |d, t, s| {
62            progress(d, t, s)
63        })
64        .await?;
65        Ok(())
66    }
67}
68
69pub struct FabricInstaller {
70    /// None uses the latest stable loader.
71    pub loader_version: Option<String>,
72}
73
74impl FabricInstaller {
75    pub fn new(loader_version: impl Into<String>) -> Self {
76        Self { loader_version: Some(loader_version.into()) }
77    }
78
79    pub fn latest() -> Self {
80        Self { loader_version: None }
81    }
82}
83
84#[async_trait]
85impl LoaderInstaller for FabricInstaller {
86    fn loader_type(&self) -> LoaderType {
87        LoaderType::Fabric
88    }
89
90    async fn available_versions(&self, mc_version: &str) -> Result<Vec<String>> {
91        let versions = crate::install::fabric::get_fabric_loader_versions(mc_version).await?;
92        Ok(versions.into_iter().map(|v| v.version).collect())
93    }
94
95    async fn install(
96        &self,
97        mc_version: &str,
98        instance_name: &str,
99        base_dir: &Path,
100        _progress: ProgressFn,
101    ) -> Result<()> {
102        let loader_ver = match &self.loader_version {
103            Some(v) => v.clone(),
104            None => {
105                let versions =
106                    crate::install::fabric::get_fabric_loader_versions(mc_version).await?;
107                versions
108                    .into_iter()
109                    .find(|v| v.stable)
110                    .map(|v| v.version)
111                    .unwrap_or_default()
112            }
113        };
114        crate::install::fabric::install_fabric(mc_version, &loader_ver, instance_name, base_dir).await
115    }
116}
117
118pub struct ForgeInstaller {
119    /// None uses the newest version.
120    pub forge_version: Option<String>,
121    pub java_path: PathBuf,
122}
123
124impl ForgeInstaller {
125    pub fn new(java_path: impl Into<PathBuf>) -> Self {
126        Self { forge_version: None, java_path: java_path.into() }
127    }
128
129    pub fn with_version(java_path: impl Into<PathBuf>, version: impl Into<String>) -> Self {
130        Self { forge_version: Some(version.into()), java_path: java_path.into() }
131    }
132}
133
134#[async_trait]
135impl LoaderInstaller for ForgeInstaller {
136    fn loader_type(&self) -> LoaderType {
137        LoaderType::Forge
138    }
139
140    async fn available_versions(&self, mc_version: &str) -> Result<Vec<String>> {
141        crate::install::forge::get_forge_versions(mc_version).await
142    }
143
144    async fn install(
145        &self,
146        mc_version: &str,
147        instance_name: &str,
148        base_dir: &Path,
149        _progress: ProgressFn,
150    ) -> Result<()> {
151        crate::install::forge::install_forge(
152            mc_version,
153            self.forge_version.as_deref(),
154            instance_name,
155            &self.java_path,
156            base_dir,
157        )
158        .await
159    }
160}
161
162pub struct NeoForgeInstaller {
163    /// None uses the newest version.
164    pub neoforge_version: Option<String>,
165    pub java_path: PathBuf,
166}
167
168impl NeoForgeInstaller {
169    pub fn new(java_path: impl Into<PathBuf>) -> Self {
170        Self { neoforge_version: None, java_path: java_path.into() }
171    }
172
173    pub fn with_version(java_path: impl Into<PathBuf>, version: impl Into<String>) -> Self {
174        Self { neoforge_version: Some(version.into()), java_path: java_path.into() }
175    }
176}
177
178#[async_trait]
179impl LoaderInstaller for NeoForgeInstaller {
180    fn loader_type(&self) -> LoaderType {
181        LoaderType::NeoForge
182    }
183
184    async fn available_versions(&self, mc_version: &str) -> Result<Vec<String>> {
185        crate::install::neoforge::get_neoforge_versions(mc_version).await
186    }
187
188    async fn install(
189        &self,
190        mc_version: &str,
191        instance_name: &str,
192        base_dir: &Path,
193        _progress: ProgressFn,
194    ) -> Result<()> {
195        crate::install::neoforge::install_neoforge(
196            mc_version,
197            self.neoforge_version.as_deref(),
198            instance_name,
199            &self.java_path,
200            base_dir,
201        )
202        .await
203    }
204}
205
206/// Build the installer matching a `LoaderType`.
207pub fn create_loader(
208    loader_type: LoaderType,
209    java_path: Option<PathBuf>,
210) -> Box<dyn LoaderInstaller> {
211    match loader_type {
212        LoaderType::Vanilla  => Box::new(VanillaInstaller),
213        LoaderType::Fabric   => Box::new(FabricInstaller::latest()),
214        LoaderType::Forge    => Box::new(ForgeInstaller::new(java_path.unwrap_or_default())),
215        LoaderType::NeoForge => Box::new(NeoForgeInstaller::new(java_path.unwrap_or_default())),
216    }
217}
218
219/// Full install: vanilla first, then overlay the injected loader.
220///
221/// `instance_name` (caller-chosen) names the subdirectory under `instance/` and
222/// avoids same-version/different-loader collisions, e.g. `"1.21.1-vanilla"` vs
223/// `"1.21.1-fabric"`.
224///
225/// ```ignore
226/// install_with_loader("1.21.4", "1.21.4-fabric", &base_dir, &FabricInstaller::latest(), no_progress()).await?;
227///
228/// let loader = ForgeInstaller::new("/usr/lib/jvm/java-17/bin/java");
229/// install_with_loader("1.20.1", "1.20.1-forge", &base_dir, &loader, no_progress()).await?;
230/// ```
231pub async fn install_with_loader(
232    mc_version: &str,
233    instance_name: &str,
234    base_dir: &Path,
235    loader: &dyn LoaderInstaller,
236    progress: ProgressFn,
237) -> Result<()> {
238    // For non-vanilla loaders, ensure vanilla is installed first (same instance_name).
239    if loader.loader_type() != LoaderType::Vanilla {
240        VanillaInstaller
241            .install(mc_version, instance_name, base_dir, progress.clone())
242            .await?;
243    }
244
245    // Overlay the loader (vanilla's own install is already complete).
246    loader.install(mc_version, instance_name, base_dir, progress).await
247}