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}