Skip to main content

lighty_version/
version_builder.rs

1use std::{
2    fmt::Debug,
3    path::{Path, PathBuf},
4    time::Duration,
5};
6
7use lighty_core::AppState;
8use lighty_loaders::types::VersionInfo;
9use lighty_modsloader::{ModRequest, WithMods};
10
11#[cfg(any(feature = "modrinth", feature = "curseforge"))]
12use lighty_modsloader::ModpackSource;
13
14/// Configures a Minecraft instance: name, loader, versions, and on-disk paths.
15///
16/// Default directories are derived from the global [`AppState`]:
17/// - `game_dirs`   = `AppState::data_dir().join(name)`
18/// - `runtime_dir` = alias of `game_dirs` until overridden
19/// - `java_dirs`   = `AppState::config_dir().join("jre")`
20///
21/// Call [`AppState::init`] once at startup before constructing any
22/// `VersionBuilder`.
23#[derive(Debug, Clone)]
24pub struct VersionBuilder<L = ()> {
25    pub name: String,
26    pub loader: L,
27    pub loader_version: String,
28    pub minecraft_version: String,
29    pub game_dirs: PathBuf,
30    pub java_dirs: PathBuf,
31    pub runtime_dir: PathBuf,
32    pub mod_requests: Vec<ModRequest>,
33    #[cfg(any(feature = "modrinth", feature = "curseforge"))]
34    pub modpack: Option<ModpackSource>,
35    pub ttl_override: Option<Duration>,
36}
37
38impl<L> VersionBuilder<L> {
39    /// Creates a new `VersionBuilder` with default paths derived
40    /// from the global [`AppState`].
41    ///
42    /// Panics if [`AppState::init`] hasn't been called yet.
43    ///
44    /// # Example
45    /// ```rust,no_run
46    /// use lighty_core::AppState;
47    /// use lighty_loaders::types::Loader;
48    /// use lighty_version::VersionBuilder;
49    ///
50    /// AppState::init("MyLauncher").unwrap();
51    /// let builder = VersionBuilder::new("my-instance", Loader::Vanilla, "", "1.21.1");
52    /// ```
53    pub fn new(
54        name: &str,
55        loader: L,
56        loader_version: &str,
57        minecraft_version: &str,
58    ) -> Self {
59        let game_dirs = AppState::data_dir().join(name);
60        let java_dirs = AppState::config_dir().join("jre");
61        Self {
62            name: name.to_string(),
63            loader,
64            loader_version: loader_version.to_string(),
65            minecraft_version: minecraft_version.to_string(),
66            runtime_dir: game_dirs.clone(),
67            game_dirs,
68            java_dirs,
69            mod_requests: Vec::new(),
70            #[cfg(any(feature = "modrinth", feature = "curseforge"))]
71            modpack: None,
72            ttl_override: None,
73        }
74    }
75
76    /// Opens the mod-sources sub-builder.
77    ///
78    /// # Example
79    /// ```rust,no_run
80    /// # use lighty_core::AppState;
81    /// # use lighty_loaders::types::Loader;
82    /// # use lighty_version::VersionBuilder;
83    /// # AppState::init("MyLauncher").unwrap();
84    /// let builder = VersionBuilder::new("modded", Loader::Fabric, "0.16.0", "1.21.1")
85    ///     .with_mod()
86    ///         .done();
87    /// ```
88    pub fn with_mod(self) -> ModSourcesBuilder<L> {
89        ModSourcesBuilder {
90            parent: self,
91            pending: Vec::new(),
92            #[cfg(any(feature = "modrinth", feature = "curseforge"))]
93            pending_modpack: None,
94        }
95    }
96
97    /// Overrides the Java install directory.
98    ///
99    /// # Example
100    /// ```rust,no_run
101    /// # use lighty_core::AppState;
102    /// # use lighty_loaders::types::Loader;
103    /// # use lighty_version::VersionBuilder;
104    /// use std::path::PathBuf;
105    /// # AppState::init("MyLauncher").unwrap();
106    /// let builder = VersionBuilder::new("my-instance", Loader::Vanilla, "", "1.21.1")
107    ///     .with_custom_java_dir(PathBuf::from("/opt/java"));
108    /// ```
109    pub fn with_custom_java_dir(mut self, java_dir: PathBuf) -> Self {
110        self.java_dirs = java_dir;
111        self
112    }
113
114    /// Replaces the loader.
115    ///
116    /// # Example
117    /// ```rust,no_run
118    /// # use lighty_core::AppState;
119    /// # use lighty_loaders::types::Loader;
120    /// # use lighty_version::VersionBuilder;
121    /// # AppState::init("MyLauncher").unwrap();
122    /// let builder = VersionBuilder::new("my-instance", Loader::Vanilla, "", "1.21.1")
123    ///     .with_loader(Loader::Fabric);
124    /// ```
125    pub fn with_loader(mut self, loader: L) -> Self {
126        self.loader = loader;
127        self
128    }
129
130    /// Replaces the loader version.
131    ///
132    /// # Example
133    /// ```rust,no_run
134    /// # use lighty_core::AppState;
135    /// # use lighty_loaders::types::Loader;
136    /// # use lighty_version::VersionBuilder;
137    /// # AppState::init("MyLauncher").unwrap();
138    /// let builder = VersionBuilder::new("my-instance", Loader::Fabric, "0.16.0", "1.21.1")
139    ///     .with_loader_version("0.17.2");
140    /// ```
141    pub fn with_loader_version(mut self, version: &str) -> Self {
142        self.loader_version = version.to_string();
143        self
144    }
145
146    /// Replaces the Minecraft version.
147    ///
148    /// # Example
149    /// ```rust,no_run
150    /// # use lighty_core::AppState;
151    /// # use lighty_loaders::types::Loader;
152    /// # use lighty_version::VersionBuilder;
153    /// # AppState::init("MyLauncher").unwrap();
154    /// let builder = VersionBuilder::new("my-instance", Loader::Vanilla, "", "1.21.1")
155    ///     .with_minecraft_version("1.20.4");
156    /// ```
157    pub fn with_minecraft_version(mut self, version: &str) -> Self {
158        self.minecraft_version = version.to_string();
159        self
160    }
161
162    /// Overrides the TTL applied to every cache associated with this
163    /// instance. Default = 24h.
164    ///
165    /// # Example
166    /// ```rust,no_run
167    /// # use lighty_core::AppState;
168    /// # use lighty_loaders::types::Loader;
169    /// # use lighty_version::VersionBuilder;
170    /// use std::time::Duration;
171    /// # AppState::init("MyLauncher").unwrap();
172    /// let builder = VersionBuilder::new("my-instance", Loader::Vanilla, "", "1.21.1")
173    ///     .with_ttl_duration(Duration::from_secs(3600));
174    /// ```
175    pub fn with_ttl_duration(mut self, ttl: Duration) -> Self {
176        self.ttl_override = Some(ttl);
177        self
178    }
179}
180
181impl<L: Clone + Send + Sync + Debug> VersionInfo for VersionBuilder<L> {
182    type LoaderType = L;
183
184    fn name(&self) -> &str {
185        &self.name
186    }
187
188    fn loader_version(&self) -> &str {
189        &self.loader_version
190    }
191
192    fn minecraft_version(&self) -> &str {
193        &self.minecraft_version
194    }
195
196    fn game_dirs(&self) -> &Path {
197        &self.game_dirs
198    }
199
200    fn java_dirs(&self) -> &Path {
201        &self.java_dirs
202    }
203
204    fn loader(&self) -> &Self::LoaderType {
205        &self.loader
206    }
207
208    fn runtime_dir(&self) -> &Path {
209        &self.runtime_dir
210    }
211
212    fn set_runtime_dir(&mut self, path: PathBuf) {
213        self.runtime_dir = path;
214    }
215
216    fn ttl(&self) -> Duration {
217        self.ttl_override.unwrap_or(Duration::from_secs(86_400))
218    }
219}
220
221// Read-only impl on &VersionBuilder: the no-op `set_runtime_dir` default
222// applies because we can't mutate through a shared reference.
223impl<'b, L: Clone + Send + Sync + Debug> VersionInfo for &'b VersionBuilder<L> {
224    type LoaderType = L;
225
226    fn name(&self) -> &str {
227        &self.name
228    }
229
230    fn loader_version(&self) -> &str {
231        &self.loader_version
232    }
233
234    fn minecraft_version(&self) -> &str {
235        &self.minecraft_version
236    }
237
238    fn game_dirs(&self) -> &Path {
239        &self.game_dirs
240    }
241
242    fn java_dirs(&self) -> &Path {
243        &self.java_dirs
244    }
245
246    fn loader(&self) -> &Self::LoaderType {
247        &self.loader
248    }
249
250    fn runtime_dir(&self) -> &Path {
251        &self.runtime_dir
252    }
253
254    fn ttl(&self) -> Duration {
255        self.ttl_override.unwrap_or(Duration::from_secs(86_400))
256    }
257}
258
259impl<L: Clone + Send + Sync + Debug> WithMods for VersionBuilder<L> {
260    fn mod_requests(&self) -> &[ModRequest] {
261        &self.mod_requests
262    }
263
264    #[cfg(any(feature = "modrinth", feature = "curseforge"))]
265    fn modpack(&self) -> Option<&ModpackSource> {
266        self.modpack.as_ref()
267    }
268}
269
270impl<'b, L: Clone + Send + Sync + Debug> WithMods for &'b VersionBuilder<L> {
271    fn mod_requests(&self) -> &[ModRequest] {
272        &self.mod_requests
273    }
274
275    #[cfg(any(feature = "modrinth", feature = "curseforge"))]
276    fn modpack(&self) -> Option<&ModpackSource> {
277        self.modpack.as_ref()
278    }
279}
280
281/// Sub-builder accumulating mod sources and an optional modpack.
282pub struct ModSourcesBuilder<L> {
283    parent: VersionBuilder<L>,
284    pending: Vec<ModRequest>,
285    #[cfg(any(feature = "modrinth", feature = "curseforge"))]
286    pending_modpack: Option<ModpackSource>,
287}
288
289impl<L> ModSourcesBuilder<L> {
290    /// Adds Modrinth mod requests.
291    ///
292    /// Each tuple is `(project-slug-or-id, optional-mod-version-id)`.
293    /// `version` is the Modrinth release id, not the Minecraft version.
294    ///
295    /// # Example
296    /// ```rust,no_run
297    /// # use lighty_core::AppState;
298    /// # use lighty_loaders::types::Loader;
299    /// # use lighty_version::VersionBuilder;
300    /// # AppState::init("MyLauncher").unwrap();
301    /// let builder = VersionBuilder::new("modded", Loader::Fabric, "0.17.2", "1.21.1")
302    ///     .with_mod()
303    ///         .with_modrinth_mods(vec![
304    ///             ("sodium", None),
305    ///             ("lithium", Some("mc1.21.1-0.13.0".into())),
306    ///         ])
307    ///         .done();
308    /// ```
309    #[cfg(feature = "modrinth")]
310    pub fn with_modrinth_mods<S>(mut self, list: Vec<(S, Option<String>)>) -> Self
311    where
312        S: Into<String>,
313    {
314        for (id_or_slug, version) in list {
315            self.pending.push(ModRequest::Modrinth {
316                id_or_slug: id_or_slug.into(),
317                version,
318            });
319        }
320        self
321    }
322
323    /// Adds CurseForge mod requests.
324    ///
325    /// Each tuple is `(numeric-mod-id, optional-numeric-file-id)`.
326    /// Requires [`lighty_modsloader::curseforge::set_api_key`] to
327    /// have been called before `.run()`.
328    ///
329    /// # Example
330    /// ```rust,no_run
331    /// # use lighty_core::AppState;
332    /// # use lighty_loaders::types::Loader;
333    /// # use lighty_version::VersionBuilder;
334    /// # AppState::init("MyLauncher").unwrap();
335    /// let builder = VersionBuilder::new("modded", Loader::Fabric, "0.17.2", "1.21.1")
336    ///     .with_mod()
337    ///         .with_curseforge_mods(vec![
338    ///             (238222, None),       // JEI, latest compatible
339    ///             (306612, Some(4000000)),
340    ///         ])
341    ///         .done();
342    /// ```
343    #[cfg(feature = "curseforge")]
344    pub fn with_curseforge_mods(mut self, list: Vec<(u32, Option<u32>)>) -> Self {
345        for (mod_id, file_id) in list {
346            self.pending.push(ModRequest::CurseForge { mod_id, file_id });
347        }
348        self
349    }
350
351    /// Attaches a Modrinth `.mrpack` modpack.
352    ///
353    /// Accepts either a CDN URL or an explicit
354    /// [`ModpackSource::ModrinthPinned`] for `(project, version_id)` pinning.
355    ///
356    /// # Example
357    /// ```rust,no_run
358    /// # use lighty_core::AppState;
359    /// # use lighty_loaders::types::Loader;
360    /// # use lighty_version::VersionBuilder;
361    /// use lighty_modsloader::ModpackSource;
362    /// # AppState::init("MyLauncher").unwrap();
363    /// let builder = VersionBuilder::new("pack", Loader::Fabric, "0.17.2", "1.21.1")
364    ///     .with_mod()
365    ///         .with_modrinth_modpack(ModpackSource::ModrinthPinned {
366    ///             project: "fabulously-optimized".into(),
367    ///             version: Some("5.10.0".into()),
368    ///         })
369    ///         .done();
370    /// ```
371    #[cfg(feature = "modrinth")]
372    pub fn with_modrinth_modpack(mut self, source: impl Into<ModpackSource>) -> Self {
373        self.pending_modpack = Some(source.into());
374        self
375    }
376
377    /// Attaches a CurseForge `.zip` modpack by `(project_id, file_id)`.
378    ///
379    /// Requires [`lighty_modsloader::curseforge::set_api_key`] to have
380    /// been called before `.run()`.
381    ///
382    /// # Example
383    /// ```rust,no_run
384    /// # use lighty_core::AppState;
385    /// # use lighty_loaders::types::Loader;
386    /// # use lighty_version::VersionBuilder;
387    /// # AppState::init("MyLauncher").unwrap();
388    /// let builder = VersionBuilder::new("pack", Loader::Forge, "47.3.0", "1.20.1")
389    ///     .with_mod()
390    ///         .with_curseforge_modpack(715572, 4769518)
391    ///         .done();
392    /// ```
393    #[cfg(feature = "curseforge")]
394    pub fn with_curseforge_modpack(mut self, project_id: u32, file_id: u32) -> Self {
395        self.pending_modpack = Some(ModpackSource::CurseForgePinned { project_id, file_id });
396        self
397    }
398
399    /// Threads the accumulated mod requests and modpack source back
400    /// into the parent builder.
401    pub fn done(self) -> VersionBuilder<L> {
402        let mut parent = self.parent;
403        let mut pending = self.pending;
404        parent.mod_requests.append(&mut pending);
405        #[cfg(any(feature = "modrinth", feature = "curseforge"))]
406        if self.pending_modpack.is_some() {
407            parent.modpack = self.pending_modpack;
408        }
409        parent
410    }
411}