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