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    /// When `true` and `gameData.json` already exists on disk, skip the
85    /// bundle integrity check and load directly from cache (fast launch).
86    /// Falls through to the normal download path when the cache is absent.
87    /// Default: `false` (always verify — current behaviour preserved).
88    #[serde(default)]
89    pub skip_bundle_check: bool,
90
91    /// Force all HTTP traffic over IPv4, ignoring DNS AAAA (IPv6) records.
92    ///
93    /// Enable when downloads fail with connection errors ("error sending
94    /// request") on networks whose IPv6 route is broken even though IPv4 works
95    /// — a frequent cause of failures that vanish under a VPN or in a browser
96    /// (which does Happy Eyeballs; reqwest does not). Default: `false`.
97    #[serde(default)]
98    pub force_ipv4: bool,
99}
100
101impl LaunchOptions {
102    /// Directory where `gameData.json` is stored.
103    /// Returns `<path>/instances/<instance>` when instanced, otherwise `<path>`.
104    pub fn save_dir(&self) -> PathBuf {
105        match &self.instance {
106            Some(inst) => self.path.join("instances").join(inst),
107            None => self.path.clone(),
108        }
109    }
110
111    /// Root directory for a specific mod loader's files.
112    ///
113    /// Returns `<path>/loader/<name>` unless `loader.path` is set explicitly.
114    pub fn loader_dir(&self, name: &str) -> PathBuf {
115        match &self.loader.path {
116            Some(p) => PathBuf::from(p),
117            None => self.path.join("loader").join(name),
118        }
119    }
120
121    /// `download_concurrency` clamped to the valid range 1–64.
122    ///
123    /// The upper bound matches the ceiling in [`adaptive_concurrency`], which
124    /// further reduces the effective value based on available CPU cores.
125    pub fn clamped_concurrency(&self) -> u32 {
126        self.download_concurrency.clamp(1, 64)
127    }
128
129    /// `verify_concurrency` clamped to the valid range 1–16.
130    pub fn clamped_verify_concurrency(&self) -> u32 {
131        self.verify_concurrency.clamp(1, 16)
132    }
133}
134
135// ── Memory ───────────────────────────────────────────────────────────────────
136
137#[derive(Debug, Clone, Deserialize, Serialize)]
138pub struct MemoryConfig {
139    /// JVM minimum heap (`-Xms`), e.g. `"1G"`, `"512M"` (default: `"1G"`).
140    #[serde(default = "defaults::memory_min")]
141    pub min: String,
142    /// JVM maximum heap (`-Xmx`), e.g. `"2G"` (default: `"2G"`).
143    #[serde(default = "defaults::memory_max")]
144    pub max: String,
145}
146
147impl Default for MemoryConfig {
148    fn default() -> Self {
149        Self {
150            min: defaults::memory_min(),
151            max: defaults::memory_max(),
152        }
153    }
154}
155
156// ── Screen ───────────────────────────────────────────────────────────────────
157
158#[derive(Debug, Clone, Deserialize, Serialize, Default)]
159pub struct ScreenConfig {
160    pub width: Option<u32>,
161    pub height: Option<u32>,
162    /// Launch in fullscreen mode (default: false).
163    #[serde(default)]
164    pub fullscreen: bool,
165}
166
167// ── Java ─────────────────────────────────────────────────────────────────────
168
169#[derive(Debug, Clone, Deserialize, Serialize)]
170pub struct JavaOptions {
171    /// Path to a pre-installed `java` executable — skips automatic download.
172    #[serde(default)]
173    pub path: Option<PathBuf>,
174
175    /// Force a specific Java major version, e.g. `"21"`.
176    #[serde(default)]
177    pub version: Option<String>,
178
179    /// Adoptium image type: `"jre"` or `"jdk"` (default: `"jre"`).
180    #[serde(default = "defaults::java_image_type")]
181    pub image_type: String,
182}
183
184impl Default for JavaOptions {
185    fn default() -> Self {
186        Self {
187            path: None,
188            version: None,
189            image_type: defaults::java_image_type(),
190        }
191    }
192}
193
194// ── Loader ───────────────────────────────────────────────────────────────────
195
196#[derive(Debug, Clone, Deserialize, Serialize, Default)]
197pub struct LoaderConfig {
198    /// Which mod loader to install (`None` = no loader).
199    pub loader_type: Option<LoaderType>,
200
201    /// Build selector: `"latest"`, `"recommended"`, or an exact version string
202    /// (default: `"latest"`).
203    #[serde(default = "defaults::loader_build")]
204    pub build: String,
205
206    /// Whether to run the loader installer (default: false).
207    #[serde(default)]
208    pub enable: bool,
209
210    /// Loader-local directory prefix, e.g. `"./loader/forge"`.
211    /// Auto-set to `"./loader/<type>"` if not provided.
212    #[serde(default)]
213    pub path: Option<String>,
214
215    /// Paths populated by the installer after a successful install.
216    /// Passed back to the argument builder.
217    #[serde(default)]
218    pub config: Option<LoaderInnerConfig>,
219}
220
221/// File paths set by the mod loader installer.
222#[derive(Debug, Clone, Deserialize, Serialize)]
223pub struct LoaderInnerConfig {
224    pub java_path: String,
225    pub minecraft_jar: String,
226    pub minecraft_json: String,
227}
228
229// ── Defaults (free functions required by serde's `default = "..."`) ─────────
230
231mod defaults {
232    pub fn timeout_secs() -> u64 {
233        10
234    }
235    pub fn download_concurrency() -> u32 {
236        5
237    }
238    pub fn verify_concurrency() -> u32 {
239        4
240    }
241    pub fn memory_min() -> String {
242        "1G".into()
243    }
244    pub fn memory_max() -> String {
245        "2G".into()
246    }
247    pub fn java_image_type() -> String {
248        "jre".into()
249    }
250    pub fn loader_build() -> String {
251        "latest".into()
252    }
253}
254
255// ── Tests ────────────────────────────────────────────────────────────────────
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn save_dir_without_instance() {
263        let opts = make_opts(None);
264        assert_eq!(opts.save_dir(), PathBuf::from("/mc"));
265    }
266
267    #[test]
268    fn save_dir_with_instance() {
269        let opts = make_opts(Some("test-world".into()));
270        assert_eq!(opts.save_dir(), PathBuf::from("/mc/instances/test-world"));
271    }
272
273    #[test]
274    fn concurrency_clamp() {
275        let mut opts = make_opts(None);
276        opts.download_concurrency = 0;
277        assert_eq!(opts.clamped_concurrency(), 1);
278        opts.download_concurrency = 99;
279        assert_eq!(opts.clamped_concurrency(), 64);
280        opts.download_concurrency = 5;
281        assert_eq!(opts.clamped_concurrency(), 5);
282    }
283
284    #[test]
285    fn verify_concurrency_clamp() {
286        let mut opts = make_opts(None);
287        opts.verify_concurrency = 0;
288        assert_eq!(opts.clamped_verify_concurrency(), 1);
289        opts.verify_concurrency = 99;
290        assert_eq!(opts.clamped_verify_concurrency(), 16);
291        opts.verify_concurrency = 4;
292        assert_eq!(opts.clamped_verify_concurrency(), 4);
293    }
294
295    #[test]
296    fn memory_defaults() {
297        let m = MemoryConfig::default();
298        assert_eq!(m.min, "1G");
299        assert_eq!(m.max, "2G");
300    }
301
302    fn make_opts(instance: Option<String>) -> LaunchOptions {
303        use crate::models::minecraft::Authenticator;
304        LaunchOptions {
305            path: PathBuf::from("/mc"),
306            version: "1.20.4".into(),
307            authenticator: Authenticator {
308                access_token: "tok".into(),
309                name: "Player".into(),
310                uuid: "uuid".into(),
311                xbox_account: None,
312                user_properties: None,
313                client_id: None,
314                client_token: None,
315            },
316            timeout_secs: 10,
317            download_concurrency: 5,
318            verify_concurrency: 4,
319            memory: MemoryConfig::default(),
320            java: JavaOptions::default(),
321            loader: LoaderConfig::default(),
322            screen: ScreenConfig::default(),
323            verify: false,
324            game_args: vec![],
325            jvm_args: vec![],
326            instance,
327            url: None,
328            mcp: None,
329            intel_enabled_mac: false,
330            bypass_offline: false,
331            skip_bundle_check: false,
332            force_ipv4: false,
333        }
334    }
335}