Skip to main content

minecraft_java_rs_core/launcher/
options.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use crate::models::{loader::LoaderType, minecraft::Authenticator};
6
7/// Complete configuration for a launcher session.
8///
9/// Pass to `Launcher::new()`. Every field except `path`, `version`, and
10/// `authenticator` has a sensible default so callers only need to set what
11/// differs from the defaults.
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct LaunchOptions {
14    /// Absolute base path for all launcher data
15    /// (libraries/, assets/, versions/, runtime/, …).
16    pub path: PathBuf,
17
18    /// Minecraft version: concrete (`"1.20.4"`) or alias
19    /// (`"latest_release"` / `"r"` / `"lr"` / `"latest_snapshot"` / `"s"` / `"ls"`).
20    pub version: String,
21
22    /// Authentication credentials — required.
23    pub authenticator: Authenticator,
24
25    /// HTTP request timeout in seconds (default: 10).
26    #[serde(default = "defaults::timeout_secs")]
27    pub timeout_secs: u64,
28
29    /// Concurrent download workers, clamped to 1–30 (default: 5).
30    #[serde(default = "defaults::download_concurrency")]
31    pub download_concurrency: u32,
32
33    /// Concurrent SHA-1 verify workers, clamped to 1–16 (default: 4).
34    /// Lower than `download_concurrency` to avoid disk seek thrashing on HDDs.
35    #[serde(default = "defaults::verify_concurrency")]
36    pub verify_concurrency: u32,
37
38    #[serde(default)]
39    pub memory: MemoryConfig,
40
41    #[serde(default)]
42    pub java: JavaOptions,
43
44    #[serde(default)]
45    pub loader: LoaderConfig,
46
47    #[serde(default)]
48    pub screen: ScreenConfig,
49
50    /// Re-verify SHA-1 integrity of every file after download (default: false).
51    #[serde(default)]
52    pub verify: bool,
53
54    /// Extra arguments appended after the vanilla game arg list.
55    #[serde(default)]
56    pub game_args: Vec<String>,
57
58    /// Extra arguments prepended to the JVM arg list.
59    #[serde(default)]
60    pub jvm_args: Vec<String>,
61
62    /// Named instance for multi-instance support.
63    /// When set, data lives under `<path>/instances/<instance>/`.
64    #[serde(default)]
65    pub instance: Option<String>,
66
67    /// URL for custom additional assets (optional).
68    #[serde(default)]
69    pub url: Option<String>,
70
71    /// Path to a custom Minecraft JAR (mod compatibility parameter).
72    #[serde(default)]
73    pub mcp: Option<String>,
74
75    /// macOS only: force x64 Java even on Apple Silicon (Rosetta 2).
76    #[serde(default)]
77    pub intel_enabled_mac: bool,
78
79    /// Redirect Mojang auth endpoints to an invalid domain so offline
80    /// multiplayer works without a valid session (default: false).
81    #[serde(default)]
82    pub bypass_offline: bool,
83}
84
85impl LaunchOptions {
86    /// Directory where `gameData.json` is stored.
87    /// Returns `<path>/instances/<instance>` when instanced, otherwise `<path>`.
88    pub fn save_dir(&self) -> PathBuf {
89        match &self.instance {
90            Some(inst) => self.path.join("instances").join(inst),
91            None => self.path.clone(),
92        }
93    }
94
95    /// Root directory for a specific mod loader's files.
96    ///
97    /// Returns `<path>/loader/<name>` unless `loader.path` is set explicitly.
98    pub fn loader_dir(&self, name: &str) -> PathBuf {
99        match &self.loader.path {
100            Some(p) => PathBuf::from(p),
101            None => self.path.join("loader").join(name),
102        }
103    }
104
105    /// `download_concurrency` clamped to the valid range 1–30.
106    pub fn clamped_concurrency(&self) -> u32 {
107        self.download_concurrency.clamp(1, 30)
108    }
109
110    /// `verify_concurrency` clamped to the valid range 1–16.
111    pub fn clamped_verify_concurrency(&self) -> u32 {
112        self.verify_concurrency.clamp(1, 16)
113    }
114}
115
116// ── Memory ───────────────────────────────────────────────────────────────────
117
118#[derive(Debug, Clone, Deserialize, Serialize)]
119pub struct MemoryConfig {
120    /// JVM minimum heap (`-Xms`), e.g. `"1G"`, `"512M"` (default: `"1G"`).
121    #[serde(default = "defaults::memory_min")]
122    pub min: String,
123    /// JVM maximum heap (`-Xmx`), e.g. `"2G"` (default: `"2G"`).
124    #[serde(default = "defaults::memory_max")]
125    pub max: String,
126}
127
128impl Default for MemoryConfig {
129    fn default() -> Self {
130        Self {
131            min: defaults::memory_min(),
132            max: defaults::memory_max(),
133        }
134    }
135}
136
137// ── Screen ───────────────────────────────────────────────────────────────────
138
139#[derive(Debug, Clone, Deserialize, Serialize, Default)]
140pub struct ScreenConfig {
141    pub width: Option<u32>,
142    pub height: Option<u32>,
143    /// Launch in fullscreen mode (default: false).
144    #[serde(default)]
145    pub fullscreen: bool,
146}
147
148// ── Java ─────────────────────────────────────────────────────────────────────
149
150#[derive(Debug, Clone, Deserialize, Serialize)]
151pub struct JavaOptions {
152    /// Path to a pre-installed `java` executable — skips automatic download.
153    #[serde(default)]
154    pub path: Option<PathBuf>,
155
156    /// Force a specific Java major version, e.g. `"21"`.
157    #[serde(default)]
158    pub version: Option<String>,
159
160    /// Adoptium image type: `"jre"` or `"jdk"` (default: `"jre"`).
161    #[serde(default = "defaults::java_image_type")]
162    pub image_type: String,
163}
164
165impl Default for JavaOptions {
166    fn default() -> Self {
167        Self {
168            path: None,
169            version: None,
170            image_type: defaults::java_image_type(),
171        }
172    }
173}
174
175// ── Loader ───────────────────────────────────────────────────────────────────
176
177#[derive(Debug, Clone, Deserialize, Serialize, Default)]
178pub struct LoaderConfig {
179    /// Which mod loader to install (`None` = no loader).
180    pub loader_type: Option<LoaderType>,
181
182    /// Build selector: `"latest"`, `"recommended"`, or an exact version string
183    /// (default: `"latest"`).
184    #[serde(default = "defaults::loader_build")]
185    pub build: String,
186
187    /// Whether to run the loader installer (default: false).
188    #[serde(default)]
189    pub enable: bool,
190
191    /// Loader-local directory prefix, e.g. `"./loader/forge"`.
192    /// Auto-set to `"./loader/<type>"` if not provided.
193    #[serde(default)]
194    pub path: Option<String>,
195
196    /// Paths populated by the installer after a successful install.
197    /// Passed back to the argument builder.
198    #[serde(default)]
199    pub config: Option<LoaderInnerConfig>,
200}
201
202/// File paths set by the mod loader installer.
203#[derive(Debug, Clone, Deserialize, Serialize)]
204pub struct LoaderInnerConfig {
205    pub java_path: String,
206    pub minecraft_jar: String,
207    pub minecraft_json: String,
208}
209
210// ── Defaults (free functions required by serde's `default = "..."`) ─────────
211
212mod defaults {
213    pub fn timeout_secs() -> u64 { 10 }
214    pub fn download_concurrency() -> u32 { 5 }
215    pub fn verify_concurrency() -> u32 { 4 }
216    pub fn memory_min() -> String { "1G".into() }
217    pub fn memory_max() -> String { "2G".into() }
218    pub fn java_image_type() -> String { "jre".into() }
219    pub fn loader_build() -> String { "latest".into() }
220}
221
222// ── Tests ────────────────────────────────────────────────────────────────────
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn save_dir_without_instance() {
230        let opts = make_opts(None);
231        assert_eq!(opts.save_dir(), PathBuf::from("/mc"));
232    }
233
234    #[test]
235    fn save_dir_with_instance() {
236        let opts = make_opts(Some("test-world".into()));
237        assert_eq!(opts.save_dir(), PathBuf::from("/mc/instances/test-world"));
238    }
239
240    #[test]
241    fn concurrency_clamp() {
242        let mut opts = make_opts(None);
243        opts.download_concurrency = 0;
244        assert_eq!(opts.clamped_concurrency(), 1);
245        opts.download_concurrency = 99;
246        assert_eq!(opts.clamped_concurrency(), 30);
247        opts.download_concurrency = 5;
248        assert_eq!(opts.clamped_concurrency(), 5);
249    }
250
251    #[test]
252    fn verify_concurrency_clamp() {
253        let mut opts = make_opts(None);
254        opts.verify_concurrency = 0;
255        assert_eq!(opts.clamped_verify_concurrency(), 1);
256        opts.verify_concurrency = 99;
257        assert_eq!(opts.clamped_verify_concurrency(), 16);
258        opts.verify_concurrency = 4;
259        assert_eq!(opts.clamped_verify_concurrency(), 4);
260    }
261
262    #[test]
263    fn memory_defaults() {
264        let m = MemoryConfig::default();
265        assert_eq!(m.min, "1G");
266        assert_eq!(m.max, "2G");
267    }
268
269    fn make_opts(instance: Option<String>) -> LaunchOptions {
270        use crate::models::minecraft::Authenticator;
271        LaunchOptions {
272            path: PathBuf::from("/mc"),
273            version: "1.20.4".into(),
274            authenticator: Authenticator {
275                access_token: "tok".into(),
276                name: "Player".into(),
277                uuid: "uuid".into(),
278                xbox_account: None,
279                user_properties: None,
280                client_id: None,
281                client_token: None,
282            },
283            timeout_secs: 10,
284            download_concurrency: 5,
285            verify_concurrency: 4,
286            memory: MemoryConfig::default(),
287            java: JavaOptions::default(),
288            loader: LoaderConfig::default(),
289            screen: ScreenConfig::default(),
290            verify: false,
291            game_args: vec![],
292            jvm_args: vec![],
293            instance,
294            url: None,
295            mcp: None,
296            intel_enabled_mac: false,
297            bypass_offline: false,
298        }
299    }
300}