Skip to main content

minecraft_java_rs_core/game/
lwjgl_native.rs

1use serde::Deserialize;
2
3use crate::error::LaunchError;
4use crate::models::minecraft::{Library, MinecraftVersionJson};
5
6// ── Embedded LWJGL ARM library manifests ─────────────────────────────────────
7//
8// These JSON files list the ARM-compiled LWJGL/JInput libraries that replace
9// the official x86 ones.  They follow the same `{ "libraries": [...] }` shape
10// as a Minecraft version JSON subset.
11//
12// IMPORTANT: The stub files bundled here contain empty library lists.  A real
13// deployment must replace them with the actual ARM LWJGL libraries.  See
14// `assets/LWJGL/` in the repository root.
15
16macro_rules! lwjgl_bytes {
17    ($arch:literal, $ver:literal) => {
18        include_bytes!(concat!(
19            "../../assets/LWJGL/",
20            $arch,
21            "/",
22            $ver,
23            ".json"
24        ))
25        .as_ref()
26    };
27}
28
29/// Return the embedded JSON bytes for `(arch, version)`, or `None` if the
30/// combination is not bundled.
31fn arm_lwjgl_data(arch: &str, version: &str) -> Option<&'static [u8]> {
32    // Mojang 2.9.x releases are all patched to 2.9.4 (matches JS behaviour).
33    let version = if version.contains("2.9") { "2.9.4" } else { version };
34
35    match (arch, version) {
36        ("aarch64", "2.9.4") => Some(lwjgl_bytes!("aarch64", "2.9.4")),
37        ("aarch64", "3.1.2") => Some(lwjgl_bytes!("aarch64", "3.1.2")),
38        ("aarch64", "3.2.2") => Some(lwjgl_bytes!("aarch64", "3.2.2")),
39        ("aarch64", "3.3.1") => Some(lwjgl_bytes!("aarch64", "3.3.1")),
40        ("aarch64", "3.3.2") => Some(lwjgl_bytes!("aarch64", "3.3.2")),
41        ("aarch", "2.9.4") => Some(lwjgl_bytes!("aarch", "2.9.4")),
42        ("aarch", "3.3.1") => Some(lwjgl_bytes!("aarch", "3.3.1")),
43        _ => None,
44    }
45}
46
47// ── Public API ────────────────────────────────────────────────────────────────
48
49/// Patch `version`'s library list for Linux ARM compatibility.
50///
51/// - Removes official LWJGL and JInput libraries (x86-only binaries).
52/// - Injects ARM-compiled replacements from the bundled JSON manifests.
53///
54/// On non-ARM platforms this is a no-op; the check uses
55/// `std::env::consts::ARCH` so a cross-compiled binary still detects its
56/// actual execution environment at runtime.
57pub fn process_json(version: &mut MinecraftVersionJson) -> Result<(), LaunchError> {
58    let mapped_arch = match std::env::consts::ARCH {
59        "aarch64" => "aarch64",
60        "arm" => "aarch",
61        _ => return Ok(()), // not ARM — nothing to do
62    };
63
64    // Detect LWJGL and JInput versions from the existing library list.
65    let version_jinput = find_version(&version.libraries, &["net.java.jinput:jinput-platform:", "net.java.jinput:jinput:"]);
66    let version_lwjgl = find_version(&version.libraries, &["org.lwjgl:lwjgl:", "org.lwjgl.lwjgl:lwjgl:"]);
67
68    // Remove official JInput libraries (replaced by ARM equivalents).
69    if version_jinput.is_some() {
70        version.libraries.retain(|lib| !lib.name.contains("jinput"));
71    }
72
73    // Remove official LWJGL libraries and inject ARM ones.
74    if let Some(lwjgl_ver) = version_lwjgl {
75        version.libraries.retain(|lib| !lib.name.contains("lwjgl"));
76
77        match arm_lwjgl_data(mapped_arch, &lwjgl_ver) {
78            Some(bytes) => {
79                let set: LwjglLibrarySet = serde_json::from_slice(bytes)?;
80                version.libraries.extend(set.libraries);
81            }
82            None => {
83                // Bundled data missing for this LWJGL version — skip the patch
84                // rather than crashing.  Callers can detect ARM support gaps by
85                // checking whether `version.libraries` still contains x86 LWJGL.
86            }
87        }
88    }
89
90    Ok(())
91}
92
93// ── Helpers ───────────────────────────────────────────────────────────────────
94
95/// Extract the version component (last `:` segment) from the first library
96/// whose `name` starts with any of `prefixes`.
97fn find_version(libs: &[Library], prefixes: &[&str]) -> Option<String> {
98    libs.iter()
99        .find(|lib| prefixes.iter().any(|p| lib.name.starts_with(p)))
100        .and_then(|lib| lib.name.split(':').last())
101        .map(|v| v.to_string())
102}
103
104/// Minimal subset of a Minecraft version JSON — just the `libraries` field.
105#[derive(Deserialize)]
106struct LwjglLibrarySet {
107    libraries: Vec<Library>,
108}
109
110// ── LWJGL 2 / XRandR stub (Linux) ────────────────────────────────────────────
111
112/// Returns `true` if the game uses LWJGL 2 (`org.lwjgl.lwjgl:lwjgl:2.x`).
113///
114/// LWJGL 2's `XRandR.java` runs the `xrandr` binary at startup to enumerate
115/// display modes. On Linux systems where `xrandr` is not installed, the
116/// subprocess returns no output and `getScreenNames()` returns an empty array,
117/// causing `ArrayIndexOutOfBoundsException: 0` in `LinuxDisplay:951`.
118pub fn uses_lwjgl2(version: &MinecraftVersionJson) -> bool {
119    version
120        .libraries
121        .iter()
122        .any(|lib| lib.name.starts_with("org.lwjgl.lwjgl:lwjgl:2."))
123}
124
125/// Returns `true` if the `xrandr` binary is found in the current `PATH`.
126#[cfg(target_os = "linux")]
127pub fn xrandr_in_path() -> bool {
128    std::env::var_os("PATH")
129        .map(|p| {
130            std::env::split_paths(&p)
131                .any(|dir| dir.join("xrandr").is_file())
132        })
133        .unwrap_or(false)
134}
135
136/// Write a minimal `xrandr` stub script into `dir` and make it executable.
137///
138/// The stub outputs just enough xrandr-compatible text for LWJGL 2's
139/// `XRandR.java` parser: a connected screen header and one resolution line.
140/// It tries `xdpyinfo` for the actual resolution and falls back to 1920×1080.
141///
142/// Idempotent — safe to call on every launch; skips the write if the file
143/// already exists.
144#[cfg(target_os = "linux")]
145pub async fn write_xrandr_stub(dir: &std::path::Path) -> Result<(), LaunchError> {
146    use std::os::unix::fs::PermissionsExt;
147
148    let stub = dir.join("xrandr");
149    if stub.exists() {
150        return Ok(());
151    }
152    tokio::fs::create_dir_all(dir).await?;
153
154    let script = "\
155#!/bin/sh
156# Minimal xrandr stub — used by LWJGL 2 on systems without the real xrandr.
157W=1920; H=1080
158if command -v xdpyinfo >/dev/null 2>&1; then
159    RES=$(xdpyinfo 2>/dev/null | awk '/dimensions:/{print $2}' | head -1)
160    if [ -n \"$RES\" ]; then W=${RES%x*}; H=${RES#*x}; fi
161fi
162printf 'Screen 0: minimum 8 x 8, current %s x %s, maximum 32767 x 32767\\n' \"$W\" \"$H\"
163printf 'HDMI-1 connected %sx%s+0+0 (normal left inverted right x axis y axis) 0mm x 0mm\\n' \"$W\" \"$H\"
164printf '   %sx%s       60.00*+\\n' \"$W\" \"$H\"
165";
166    tokio::fs::write(&stub, script).await?;
167    tokio::fs::set_permissions(&stub, std::fs::Permissions::from_mode(0o755)).await?;
168    Ok(())
169}
170
171// ── Tests ─────────────────────────────────────────────────────────────────────
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn make_lib(name: &str) -> Library {
178        Library {
179            name: name.to_string(),
180            rules: None,
181            natives: None,
182            downloads: None,
183            url: None,
184            loader: None,
185        }
186    }
187
188    fn libs(names: &[&str]) -> Vec<Library> {
189        names.iter().map(|n| make_lib(n)).collect()
190    }
191
192    #[test]
193    fn find_version_returns_last_colon_segment() {
194        let l = libs(&["org.lwjgl:lwjgl:3.3.1", "org.lwjgl:lwjgl-opengl:3.3.1"]);
195        assert_eq!(find_version(&l, &["org.lwjgl:lwjgl:"]), Some("3.3.1".into()));
196    }
197
198    #[test]
199    fn find_version_returns_none_when_absent() {
200        let l = libs(&["com.example:something:1.0"]);
201        assert_eq!(find_version(&l, &["org.lwjgl:lwjgl:"]), None);
202    }
203
204    #[test]
205    fn find_version_matches_multiple_prefixes() {
206        let l = libs(&["org.lwjgl.lwjgl:lwjgl:2.9.4"]);
207        let v = find_version(&l, &["org.lwjgl:lwjgl:", "org.lwjgl.lwjgl:lwjgl:"]);
208        assert_eq!(v, Some("2.9.4".into()));
209    }
210
211    #[test]
212    fn arm_lwjgl_data_normalises_29x() {
213        // Any 2.9.x version should resolve to the 2.9.4 bundle.
214        assert!(arm_lwjgl_data("aarch64", "2.9.0").is_some());
215        assert!(arm_lwjgl_data("aarch64", "2.9.1").is_some());
216        assert!(arm_lwjgl_data("aarch64", "2.9.4").is_some());
217    }
218
219    #[test]
220    fn arm_lwjgl_data_returns_none_for_unknown() {
221        assert!(arm_lwjgl_data("aarch64", "4.0.0").is_none());
222        assert!(arm_lwjgl_data("x86_64", "3.3.1").is_none());
223    }
224
225    #[test]
226    fn process_json_noop_on_current_arch() {
227        // On x86_64 (the typical CI/dev machine) this should be a no-op.
228        if matches!(std::env::consts::ARCH, "aarch64" | "arm") {
229            return; // ARM machine — test would modify libraries, skip.
230        }
231
232        let mut version = MinecraftVersionJson {
233            id: "1.20.4".into(),
234            version_type: "release".into(),
235            assets: None,
236            asset_index: None,
237            downloads: None,
238            libraries: libs(&["org.lwjgl:lwjgl:3.3.1", "org.lwjgl:lwjgl-opengl:3.3.1"]),
239            arguments: None,
240            minecraft_arguments: None,
241            java_version: None,
242            main_class: None,
243            has_natives: false,
244        };
245
246        let original_count = version.libraries.len();
247        process_json(&mut version).unwrap();
248        // No change on non-ARM.
249        assert_eq!(version.libraries.len(), original_count);
250    }
251}