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