diff --git a/decode_hex.py b/decode_hex.py new file mode 100644 index 0000000..63439d9 --- /dev/null +++ b/decode_hex.py @@ -0,0 +1,28 @@ +import zlib + +def decode_varint(data, offset=0): + val = 0 + shift = 0 + while True: + b = data[offset] + offset += 1 + val |= (b & 0x7F) << shift + shift += 7 + if (b & 0x80) == 0: + break + return val, offset + +hex_str = "81 9b 01 78 9c ed 5b cd 92 db 44 10 9e 14 f9 71 9c 4a 41 48" +data = bytes.fromhex(hex_str.replace(" ", "")) + +uncompressed_len, offset = decode_varint(data) +compressed_data = data[offset:] + +d = zlib.decompressobj() +dec = d.decompress(compressed_data) +if dec: + pkt_id, dec_offset = decode_varint(dec) + print(f"Packet ID: {pkt_id} (0x{pkt_id:02x})") + print(f"Next bytes: {dec[dec_offset:dec_offset+10].hex()}") +else: + print("Could not decompress anything (need more bytes).") diff --git a/parse_codec.py b/parse_codec.py new file mode 100644 index 0000000..c7672f3 --- /dev/null +++ b/parse_codec.py @@ -0,0 +1,25 @@ +import re + +with open("ignore/MCProtocolLib/protocol/src/main/java/org/geysermc/mcprotocollib/protocol/codec/MinecraftCodec.java", "r") as f: + lines = f.readlines() + +in_game = False +clientbound_index = 0 + +for line in lines: + if "ProtocolState.GAME" in line: + in_game = True + continue + + if in_game: + # Check if another state starts + if "ProtocolState." in line and "ProtocolState.GAME" not in line: + break + + if "registerClientboundPacket" in line: + # Extract the class name + m = re.search(r'registerClientboundPacket\((.*?)\.class', line) + if m: + packet_class = m.group(1) + print(f"{packet_class} -> {clientbound_index} (0x{clientbound_index:02x})") + clientbound_index += 1 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2b4f51f..0e4c7ae 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -272,6 +283,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -284,6 +301,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -539,6 +562,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -564,6 +597,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -688,6 +727,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -788,6 +839,17 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -850,7 +912,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -986,12 +1050,46 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.8" @@ -1016,8 +1114,10 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" name = "emerald-legacy-launcher" version = "1.2.1" dependencies = [ + "aes", "base64 0.21.7", "byteorder", + "cipher", "flate2", "futures-util", "http 1.4.0", @@ -1025,9 +1125,11 @@ dependencies = [ "libc", "lzxd", "once_cell", + "p256", "rand 0.8.5", "reqwest 0.11.27", "rfd", + "rsa", "serde", "serde_json", "sha1", @@ -1043,6 +1145,7 @@ dependencies = [ "tokio-tungstenite", "tokio-util", "url", + "uuid 1.23.0", ] [[package]] @@ -1179,6 +1282,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -1494,6 +1607,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1685,6 +1799,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gtk" version = "0.18.2" @@ -1812,6 +1937,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -2228,6 +2362,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -2402,6 +2545,15 @@ dependencies = [ "selectors 0.24.0", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2464,6 +2616,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.15" @@ -2741,12 +2899,48 @@ dependencies = [ "nom", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2754,6 +2948,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3017,6 +3212,18 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "pango" version = "0.18.3" @@ -3077,6 +3284,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3287,6 +3503,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3392,6 +3629,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3800,6 +4046,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rfd" version = "0.15.4" @@ -3851,6 +4107,26 @@ dependencies = [ "uuid 0.8.2", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -4054,6 +4330,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -4341,6 +4631,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -4439,6 +4739,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d21ee0..04e375d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,7 +19,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-gamepad = "0.0.5" tauri-plugin-drpc = "*" -reqwest = { version = "0.11", features = ["stream"] } +reqwest = { version = "0.11", features = ["stream", "json"] } tokio = { version = "1", features = ["full"] } futures-util = "0.3" tokio-util = { version = "0.7.18", features = ["rt"] } @@ -34,10 +34,15 @@ image = "0.24" steam_shortcuts_util = "1.1.8" tauri-plugin-process = "2" byteorder = "1" +p256 = "0.13" flate2 = "1" sha1 = "0.10" once_cell = "1" lzxd = "0.2.6" +uuid = { version = "1", features = ["v4"] } +rsa = { version = "0.9", default-features = false, features = ["std"] } +aes = "0.8" +cipher = "0.4" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/src-tauri/src/commands/bridge.rs b/src-tauri/src/commands/bridge.rs new file mode 100644 index 0000000..cbba0c8 --- /dev/null +++ b/src-tauri/src/commands/bridge.rs @@ -0,0 +1,63 @@ +use tauri::{AppHandle, Emitter}; +use crate::lce_bridge::{start_bridge, stop_bridge, bridge_status as lce_bridge_status, config::BridgeConfig, msa_auth}; +use std::sync::OnceLock; + +static AUTH_PROFILE: OnceLock = OnceLock::new(); + +#[allow(dead_code)] +pub fn get_auth_profile() -> Option<&'static msa_auth::MinecraftProfile> { + AUTH_PROFILE.get() +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn bridge_start( + app: AppHandle, + listen_address: String, + port: u16, + remote_host: String, + remote_port: u16, + auth_type: String, + remote_protocol_version: i32, +) -> Result { + if auth_type == "online" { + let mut rx = msa_auth::subscribe_device_code(); + let app2 = app.clone(); + tokio::spawn(async move { + while let Ok(msg) = rx.recv().await { + let _ = app2.emit("lcebridge-msa-code", &msg); + } + }); + let profile = msa_auth::authenticate().await + .map_err(|e| format!("MSA auth failed: {e}"))?; + let _ = AUTH_PROFILE.set(profile); + } + + let config = BridgeConfig { + lce: crate::lce_bridge::config::LceConfig { + listen_address, + port, + motd: "LCEBridge".to_string(), + }, + remote: crate::lce_bridge::config::RemoteConfig { + host: remote_host, + port: remote_port, + auth_type, + protocol_version: remote_protocol_version, + }, + ..Default::default() + }; + start_bridge(config).await +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn bridge_stop() -> Result { + stop_bridge().await +} + +#[tauri::command] +#[allow(non_snake_case)] +pub async fn bridge_status() -> Result { + lce_bridge_status().await +} diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs index 2f5c7a0..17838e4 100644 --- a/src-tauri/src/commands/game.rs +++ b/src-tauri/src/commands/game.rs @@ -11,6 +11,7 @@ use crate::platform::linux; use crate::state::GameState; use crate::types::McServer; use crate::util; +use crate::workshop_server; #[tauri::command] #[allow(non_snake_case)] pub async fn launch_game( @@ -35,6 +36,14 @@ pub async fn launch_game( } } ensure_server_list(&working_dir, servers); + + let ws_cancel = workshop_server::start().await; + let _ws_guard = workshop_server::Guard::new(ws_cancel.clone()); + { + let mut lock = state.workshop_cancel.lock().await; + *lock = Some(ws_cancel); + } + let game_exe = working_dir.join("Minecraft.Client.exe"); if !game_exe.exists() { return Err("Game executable not found in instance folder.".into()); @@ -297,6 +306,12 @@ pub async fn stop_game( linux::kill_process_tree(&app, &instance_id); let _ = child.kill().await; } + drop(lock); + + let mut lock = state.workshop_cancel.lock().await; + if let Some(cancel) = lock.take() { + cancel.cancel(); + } Ok(()) } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 41461d5..d6802b0 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod bridge; pub mod config; pub mod console2lce; pub mod download; diff --git a/src-tauri/src/lce_bridge/bridge.rs b/src-tauri/src/lce_bridge/bridge.rs new file mode 100644 index 0000000..fc4ab5a --- /dev/null +++ b/src-tauri/src/lce_bridge/bridge.rs @@ -0,0 +1,622 @@ +use crate::lce_bridge::lce_packets::*; +use crate::lce_bridge::java_protocol::*; +use crate::lce_bridge::java_session::JavaSession; +use crate::lce_bridge::chunk::{translate_java_chunk, CachedLceChunk}; +use crate::lce_bridge::config::BridgeConfig; +use crate::lce_bridge::crafting::InventoryManager; +use std::collections::{HashMap, VecDeque}; +use uuid::Uuid; + +pub struct LceBridgeSession { + pub config: BridgeConfig, + pub lce_tx: VecDeque, + pub java_session: Option, + pub player_eid: i32, + pub tracked_entities: HashMap, + pub chunk_cache: HashMap<(i32, i32), CachedLceChunk>, + pub inventory: InventoryManager, + pub render_distance: i32, + pub teleport_id: i32, + pub health: f32, + pub food: i32, + pub saturation: f32, + pub game_time: i64, + pub day_time: i64, + pub has_joined: bool, + pub player_name: String, + pub pos_x: f64, + pub pos_y: f64, + pub pos_z: f64, + pub yaw: f32, + pub pitch: f32, + pub container_id_map: HashMap, + pub lce_container_id: u8, +} + +pub struct TrackedEntity { + pub entity_id: i32, + pub kind: TrackedEntityKind, + pub x: f64, + pub y: f64, + pub z: f64, + pub yaw: u8, + pub pitch: u8, +} + +pub enum TrackedEntityKind { + Object, + Mob(u8), + Item, + Player, +} + +impl LceBridgeSession { + pub fn new(config: BridgeConfig) -> Self { + Self { + config, + lce_tx: VecDeque::new(), + java_session: None, + player_eid: 0, + tracked_entities: HashMap::new(), + chunk_cache: HashMap::new(), + inventory: InventoryManager::new(), + render_distance: 8, + teleport_id: 0, + health: 20.0, + food: 20, + saturation: 5.0, + game_time: 0, + day_time: 1000, + has_joined: false, + player_name: String::new(), + pos_x: 0.0, pos_y: 64.0, pos_z: 0.0, + yaw: 0.0, pitch: 0.0, + container_id_map: HashMap::new(), + lce_container_id: 1, + } + } + + pub fn send_lce(&mut self, packet: LcePacket) { + self.lce_tx.push_back(packet); + } + + pub fn on_connected(&mut self) { + eprintln!("[LCEBridge] on_connected"); + let prelogin = LcePacket::PreLogin(PreLoginPacket { + net_version: 560, + player_name: "Player".to_string(), + offline_xuid: 0, + online_xuid: 0, + }); + self.send_lce(prelogin); + } + + pub async fn handle_lce_packet(&mut self, packet: LcePacket) { + match packet { + LcePacket::PreLogin(p) => { + self.on_lce_prelogin(p).await; + } + LcePacket::Login(p) => { + self.on_lce_login(p).await; + } + LcePacket::MovePlayer(p) => { + self.pos_x = p.x; + self.pos_y = p.y; + self.pos_z = p.z; + self.yaw = p.yaw; + self.pitch = p.pitch; + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::AcceptTeleportation(AcceptTeleportationPacket { + teleport_id: self.teleport_id, + })).await; + } + } + LcePacket::Chat(p) => { + if let Some(ref mut js) = self.java_session { + let msg = p.string_args.first().cloned().unwrap_or_default(); + let _ = js.send(JavaPacket::ChatCommand(ChatCommandPacket { command: msg })); + } + } + LcePacket::PlayerAction(p) => { + if let Some(ref mut js) = self.java_session { + if p.action == 1 { + let _x = p.x as f64; + let _y = p.y as f64; + let _z = p.z as f64; + let _face = p.face; + let _ = js.send(JavaPacket::Unknown { id: 0x36, data: VarInt::write(0) }); + } + } + } + LcePacket::Interact(p) => { + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::Unknown { + id: 0x11, + data: pack_varint(p.target), + }).await; + } + } + LcePacket::ContainerClick(p) => { + if let Some(ref mut js) = self.java_session { + let mut data = Vec::new(); + VarInt::write_to(p.container_id as i32, &mut data); + VarInt::write_to(0, &mut data); + VarInt::write_to(p.slot_num as i32, &mut data); + data.push(p.click_type); + data.push(p.button_num); + VarInt::write_to(0, &mut data); + let _ = js.send(JavaPacket::Unknown { id: 0x0E, data }).await; + } + } + LcePacket::ContainerClose(p) => { + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::ContainerCloseS2C( + ContainerCloseS2CPacket { container_id: p.container_id as i32 } + )).await; + } + } + LcePacket::SetCarriedItem(p) => { + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::Unknown { + id: 0x28, + data: vec![p.slot], + }).await; + } + } + LcePacket::ClientCommand(_p) => { + //neo: do NOT think of sending here. it causes premature FinishConfiguration + } + LcePacket::RespawnRequest(_) => {} + _ => {} + } + } + + async fn on_lce_prelogin(&mut self, p: PreLoginPacket) { + eprintln!("[LCEBridge] PreLogin from client: player={}, net_ver={}, offline_xuid={}, online_xuid={}", p.player_name, p.net_version, p.offline_xuid, p.online_xuid); + self.player_name = p.player_name.clone(); + eprintln!("[LCEBridge] Connecting to Java server at {}:{}", self.config.remote.host, self.config.remote.port); + match JavaSession::connect(&self.config.remote).await { + Ok(mut js) => { + eprintln!("[LCEBridge] Connected to Java server, sending Handshake+LoginStart"); + let handshake = JavaPacket::Handshake(HandshakePacket { + protocol_version: self.config.remote.protocol_version, + server_address: self.config.remote.host.clone(), + server_port: self.config.remote.port, + next_state: 2, + }); + let _ = js.send(handshake).await; + let login_start = JavaPacket::LoginStart(LoginStartPacket { + username: p.player_name.clone(), + profile_id: Uuid::nil(), + protocol_version: self.config.remote.protocol_version, + }); + let _ = js.send(login_start).await; + eprintln!("[LCEBridge] Handshake+LoginStart sent to Java server"); + self.java_session = Some(js); + self.has_joined = true; + } + Err(e) => { + eprintln!("[LCEBridge] Failed to connect to Java server: {}", e); + self.send_lce(LcePacket::Disconnect(DisconnectPacket { reason: 1 })); + } + } + } + + async fn on_lce_login(&mut self, p: LoginPacket) { + if self.java_session.is_none() { + eprintln!("[LCEBridge] Client sent Login without PreLogin, connecting to Java server now"); + match JavaSession::connect(&self.config.remote).await { + Ok(mut js) => { + let handshake = JavaPacket::Handshake(HandshakePacket { + protocol_version: self.config.remote.protocol_version, + server_address: self.config.remote.host.clone(), + server_port: self.config.remote.port, + next_state: 2, + }); + let _ = js.send(handshake).await; + let login_start = JavaPacket::LoginStart(LoginStartPacket { + username: p.username.clone(), + profile_id: Uuid::nil(), + protocol_version: self.config.remote.protocol_version, + }); + let _ = js.send(login_start).await; + self.java_session = Some(js); + self.has_joined = true; + } + Err(e) => { + eprintln!("[LCEBridge] Failed to connect to Java server: {}", e); + } + } + } + self.send_lce_login_response(&p.username).await; + self.player_name.clear(); + } + + async fn send_lce_login_response(&mut self, username: &str) { + self.send_lce(LcePacket::Login(LoginPacket { + protocol_version: 560, + username: username.to_string(), + map_seed: 0, + game_type: 0, + world_name: "bridge".to_string(), + dimension: 0, + difficulty: 2, + max_players: 1, + world_width: 0, + world_length: 0, + })); + self.send_lce(LcePacket::SetSpawnPosition(SetSpawnPositionPacket { + x: 0, y: 64, z: 0, + })); + self.send_lce(LcePacket::GameEvent(GameEventPacket { + reason: 3, param: 0.0, + })); + self.send_lce(LcePacket::SetTime(SetTimePacket { + game_time: 0, day_time: 1000, + })); + self.send_lce(LcePacket::PlayerAbilities(PlayerAbilitiesPacket { + flags: 0x04, fly_speed: 0.05, walk_speed: 0.1, + })); + self.send_lce(LcePacket::SetHealth(SetHealthPacket { + health: 20.0, food: 20, saturation: 5.0, damage_source: 0, + })); + let _ = self.spawn_player().await; + } + + async fn spawn_player(&mut self) { + let (cx, cz) = (0i32, 0i32); + self.send_lce(LcePacket::ChunkVisibilityArea(ChunkVisibilityAreaPacket { + min_cx: cx - self.render_distance, + max_cx: cx + self.render_distance, + min_cz: cz - self.render_distance, + max_cz: cz + self.render_distance, + })); + for dx in -self.render_distance..=self.render_distance { + for dz in -self.render_distance..=self.render_distance { + self.send_lce(LcePacket::ChunkVisibility(ChunkVisibilityPacket { + chunk_x: cx + dx, + chunk_z: cz + dz, + visible: true, + })); + } + } + let builder = crate::lce_bridge::chunk::LceChunkBuilder::new(); + let raw = builder.build_raw_data(); + let compressed = crate::lce_bridge::chunk::compress_rle_zlib(&raw); + self.send_lce(LcePacket::BlockRegionUpdate(BlockRegionUpdatePacket { + x: 0, y: -64, z: 0, xs: 16, ys: 24, zs: 16, + level_idx: 0, is_full_chunk: true, compressed_data: compressed, + })); + } + + pub async fn handle_java_packet(&mut self, packet: JavaPacket) { + match packet { + JavaPacket::KeepAliveS2C(p) => { + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::KeepAliveC2S(KeepAliveC2SPacket { id: p.id })).await; + } + } + JavaPacket::LoginPlay(p) => { + self.player_eid = p.entity_id; + self.render_distance = p.view_distance.min(self.render_distance); + if !self.player_name.is_empty() { + let name = self.player_name.clone(); + self.send_lce_login_response(&name).await; + self.player_name.clear(); + } + } + JavaPacket::SynchronizePlayerPosition(p) => { + self.teleport_id = p.teleport_id; + self.pos_x = p.x; + self.pos_y = p.y; + self.pos_z = p.z; + self.yaw = p.yaw; + self.pitch = p.pitch; + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::ClientInformation(ClientInformationPacket { + locale: "en_GB".to_string(), + view_distance: self.render_distance as u8, + chat_mode: 0, + chat_colors: true, + displayed_skin_parts: 0x7F, + main_hand: 1, + enable_text_filtering: false, + allow_listing: false, + particle_status: 0, + })).await; + let _ = js.send(JavaPacket::AcceptTeleportation(AcceptTeleportationPacket { + teleport_id: p.teleport_id, + })).await; + } + } + JavaPacket::PlayerPosition(p) => { + self.teleport_id = p.teleport_id; + if let Some(ref mut js) = self.java_session { + let _ = js.send(JavaPacket::ClientInformation(ClientInformationPacket { + locale: "en_GB".to_string(), + view_distance: self.render_distance as u8, + chat_mode: 0, + chat_colors: true, + displayed_skin_parts: 0x7F, + main_hand: 1, + enable_text_filtering: false, + allow_listing: false, + particle_status: 0, + })).await; + let _ = js.send(JavaPacket::AcceptTeleportation(AcceptTeleportationPacket { + teleport_id: p.teleport_id, + })).await; + } + self.send_lce(LcePacket::SetSpawnPosition(SetSpawnPositionPacket { + x: p.x as i32, y: p.y as i32, z: p.z as i32, + })); + } + JavaPacket::ChunkData(p) => { + let (_builder, compressed) = translate_java_chunk( + p.chunk_x, p.chunk_z, &p.chunk_data, + ); + self.chunk_cache.insert( + (p.chunk_x, p.chunk_z), + CachedLceChunk::new(p.chunk_x, p.chunk_z), + ); + self.send_lce(LcePacket::ChunkVisibility(ChunkVisibilityPacket { + chunk_x: p.chunk_x, + chunk_z: p.chunk_z, + visible: true, + })); + self.send_lce(LcePacket::BlockRegionUpdate(BlockRegionUpdatePacket { + x: p.chunk_x * 16, y: -64, z: p.chunk_z * 16, + xs: 16, ys: 24, zs: 16, + level_idx: 0, is_full_chunk: true, + compressed_data: compressed, + })); + } + JavaPacket::ChunkBatchFinished => {} + JavaPacket::SetContainerContent(p) => { + let mut items = Vec::new(); + for slot in &p.slots { + if slot.present { + items.push(LceItemStack { + id: slot.item_id as i16, + count: slot.count, + damage: 0, + }); + } else { + items.push(LceItemStack::empty()); + } + } + let cid = *self.container_id_map.get(&p.container_id).unwrap_or(&1); + self.send_lce(LcePacket::ContainerSetContent(ContainerSetContentPacket { + container_id: cid, + items, + })); + } + JavaPacket::SetContainerSlot(p) => { + let item = if p.item.present { + LceItemStack { + id: p.item.item_id as i16, + count: p.item.count, + damage: 0, + } + } else { + LceItemStack::empty() + }; + let cid = *self.container_id_map.get(&p.container_id).unwrap_or(&1); + self.send_lce(LcePacket::ContainerSetSlot(ContainerSetSlotPacket { + container_id: cid, + slot: p.slot, + item, + })); + } + JavaPacket::OpenScreen(p) => { + let lce_type = match p.screen_id { + 2 | 3 | 4 | 5 => 16, + 7 => 17, + 8 => 15, + 9 | 10 | 11 | 12 | 13 | 14 => 14, + _ => 14, + }; + let cid = self.lce_container_id; + self.lce_container_id += 1; + self.container_id_map.insert(p.container_id, cid); + self.send_lce(LcePacket::ContainerOpen(ContainerOpenPacket { + container_id: cid, + container_type: lce_type, + size: 0, + custom_name: String::new(), + title: String::new(), + entity_id: -1, + })); + } + JavaPacket::SystemChat(p) => { + let text = extract_chat_text(&p.content_json); + self.send_lce(LcePacket::Chat(ChatPacket::set_message(&text))); + } + JavaPacket::PlayerChat(p) => { + let text = extract_chat_text(&p.content_json); + self.send_lce(LcePacket::Chat(ChatPacket::set_message(&text))); + } + JavaPacket::SetHealth(p) => { + self.health = p.health; + self.food = p.food; + self.saturation = p.saturation; + self.send_lce(LcePacket::SetHealth(SetHealthPacket { + health: p.health, food: p.food, saturation: p.saturation, damage_source: 0, + })); + } + JavaPacket::SetTime(p) => { + self.game_time = p.world_age; + self.day_time = p.day_time; + self.send_lce(LcePacket::SetTime(SetTimePacket { + game_time: p.world_age, day_time: p.day_time, + })); + } + JavaPacket::AddEntity(p) => { + let lce_type = map_java_object_type(p.entity_type); + let fx = (p.x * 32.0) as i32; + let fy = (p.y * 32.0) as i32; + let fz = (p.z * 32.0) as i32; + self.tracked_entities.insert(p.entity_id, TrackedEntity { + entity_id: p.entity_id, + kind: TrackedEntityKind::Object, + x: p.x, y: p.y, z: p.z, + yaw: p.yaw, pitch: p.pitch, + }); + self.send_lce(LcePacket::AddEntity(AddEntityPacket { + entity_id: p.entity_id, + entity_type: lce_type, + x: fx, y: fy, z: fz, + yaw: p.yaw, pitch: p.pitch, + data: p.data, + motion_x: p.vel_x, motion_y: p.vel_y, motion_z: p.vel_z, + })); + } + JavaPacket::AddMob(p) => { + let lce_type = map_java_mob_type(p.entity_type); + let fx = (p.x * 32.0) as i32; + let fy = (p.y * 32.0) as i32; + let fz = (p.z * 32.0) as i32; + self.tracked_entities.insert(p.entity_id, TrackedEntity { + entity_id: p.entity_id, + kind: TrackedEntityKind::Mob(lce_type), + x: p.x, y: p.y, z: p.z, + yaw: p.yaw, pitch: p.pitch, + }); + self.send_lce(LcePacket::AddMob(AddMobPacket { + entity_id: p.entity_id, + entity_type: lce_type, + x: fx, y: fy, z: fz, + yaw: p.yaw, pitch: p.pitch, head_yaw: p.head_yaw, + motion_x: p.vel_x, motion_y: p.vel_y, motion_z: p.vel_z, + metadata: Vec::new(), + })); + } + JavaPacket::AddPlayer(p) => { + let fx = (p.x * 32.0) as i32; + let fy = (p.y * 32.0) as i32; + let fz = (p.z * 32.0) as i32; + self.send_lce(LcePacket::AddPlayer(AddPlayerPacket { + entity_id: p.entity_id, + name: String::new(), + x: fx, y: fy, z: fz, + yaw: p.yaw, pitch: p.pitch, head_yaw: p.yaw, + carried_item: 0, + offline_xuid: p.entity_id as i64, + online_xuid: 0, + player_index: 0, + skin_id: String::new(), + cape_id: String::new(), + game_privileges: 0xFFFFFFFF, + metadata: Vec::new(), + })); + } + JavaPacket::RemoveEntities(p) => { + for id in &p.entity_ids { + self.tracked_entities.remove(id); + } + self.send_lce(LcePacket::RemoveEntities(RemoveEntitiesPacket { + entity_ids: p.entity_ids, + })); + } + JavaPacket::SetEntityMotion(p) => { + self.send_lce(LcePacket::SetEntityMotion(SetEntityMotionPacket { + entity_id: p.entity_id, + xa: p.xa, ya: p.ya, za: p.za, + })); + } + JavaPacket::TeleportEntity(p) => { + let fx = (p.x * 32.0) as i32; + let fy = (p.y * 32.0) as i32; + let fz = (p.z * 32.0) as i32; + if let Some(e) = self.tracked_entities.get_mut(&p.entity_id) { + e.x = p.x; e.y = p.y; e.z = p.z; + } + self.send_lce(LcePacket::TeleportEntity(TeleportEntityPacket { + entity_id: p.entity_id, + x: fx, y: fy, z: fz, + yaw: p.yaw, pitch: p.pitch, + })); + } + JavaPacket::SetHeadRotation(p) => { + self.send_lce(LcePacket::RotateHead(RotateHeadPacket { + entity_id: p.entity_id, + y_head_rot: p.head_yaw, + })); + } + JavaPacket::SetEntityData(_) => {} + JavaPacket::GameEventS2C(p) => { + self.send_lce(LcePacket::GameEvent(GameEventPacket { + reason: p.reason, param: p.param, + })); + } + JavaPacket::SetDefaultSpawnPosition(p) => { + self.send_lce(LcePacket::SetSpawnPosition(SetSpawnPositionPacket { + x: p.x, y: p.y, z: p.z, + })); + } + JavaPacket::ContainerCloseS2C(p) => { + self.send_lce(LcePacket::ContainerClose(ContainerClosePacket { + container_id: p.container_id as u8, + })); + } + JavaPacket::ContainerSetDataS2C(p) => { + self.send_lce(LcePacket::ContainerSetData(ContainerSetDataPacket { + container_id: p.container_id as u8, + id: p.key as i16, + value: p.value as i16, + })); + } + JavaPacket::ContainerAckS2C(p) => { + self.send_lce(LcePacket::ContainerAck(ContainerAckPacket { + container_id: p.container_id, + uid: p.uid, + accepted: p.accepted, + })); + } + JavaPacket::SoundEffect(_) => {} + JavaPacket::RegistryData(_) => {} + _ => {} + } + } +} + +fn map_java_mob_type(java_type: i32) -> u8 { + match java_type { + 0 => 60, 1 => 61, 2 => 62, 3 => 63, 4 => 64, 5 => 65, 6 => 66, + 7 => 67, 8 => 68, 10 => 69, 11 => 70, 12 => 71, 13 => 72, + 14 => 73, 15 => 74, 16 => 75, 17 => 76, 18 => 77, 19 => 78, + 20 => 79, 21 => 80, 22 => 81, 23 => 82, 24 => 83, 25 => 84, + 26 => 85, 27 => 86, 28 => 87, 29 => 88, 30 => 89, 31 => 90, + 32 => 91, 33 => 92, 34 => 93, 35 => 94, 36 => 95, 37 => 96, + 38 => 97, 39 => 98, 40 => 99, + _ => 60, + } +} + +fn map_java_object_type(java_type: i32) -> u8 { + match java_type { + 0 => 50, 1 => 51, 2 => 52, 3 => 53, 4 => 54, 5 => 55, + 6 => 56, 7 => 57, 8 => 58, 9 => 59, 10 => 60, 11 => 61, + _ => 50, + } +} + +fn extract_chat_text(nbt_data: &[u8]) -> String { + if nbt_data.is_empty() || nbt_data[0] == 0 { + return String::new(); + } + let text = String::from_utf8_lossy(nbt_data); + if let Some(start) = text.find("text\":\"") { + let rest = &text[start + 7..]; + if let Some(end) = rest.find('"') { + return rest[..end].to_string(); + } + } + String::new() +} + +fn pack_varint(val: i32) -> Vec { + VarInt::write(val) +} + +use crate::lce_bridge::util::VarInt; diff --git a/src-tauri/src/lce_bridge/chunk.rs b/src-tauri/src/lce_bridge/chunk.rs new file mode 100644 index 0000000..9aeb3ae --- /dev/null +++ b/src-tauri/src/lce_bridge/chunk.rs @@ -0,0 +1,239 @@ +use crate::lce_bridge::registry; +use flate2::write::ZlibEncoder; +use flate2::Compression; +use std::io::Write; + +const SECTIONS: usize = 24; +const BLOCKS_PER_SECTION: usize = 4096; +pub const FULL_CHUNK_SIZE: usize = SECTIONS * BLOCKS_PER_SECTION; + +pub struct LceChunkBuilder { + blocks: Vec, + block_light: Vec, + sky_light: Vec, + biomes: Vec, +} + +impl LceChunkBuilder { + pub fn new() -> Self { + Self { + blocks: vec![0u16; FULL_CHUNK_SIZE], + block_light: vec![0u8; FULL_CHUNK_SIZE / 2], + sky_light: vec![0u8; FULL_CHUNK_SIZE / 2], + biomes: vec![0u8; 256], + } + } + + pub fn set_block(&mut self, x: usize, y: usize, z: usize, block: u16) { + if y >= SECTIONS * 16 { return; } + let seci = y / 16; + let lcy = y % 16; + let idx = seci * BLOCKS_PER_SECTION + (x * 16 + z) * 16 + lcy; + if idx < self.blocks.len() { + self.blocks[idx] = block; + } + } + + pub fn set_sky_light(&mut self, x: usize, y: usize, z: usize, light: u8) { + if y >= SECTIONS * 16 { return; } + let idx = (y / 16) * 2048 + (x * 16 + z) * 16 + (y % 16); + if idx < self.sky_light.len() { + let nibble_idx = idx / 2; + if idx % 2 == 0 { + self.sky_light[nibble_idx] = (self.sky_light[nibble_idx] & 0xF0) | (light & 0x0F); + } else { + self.sky_light[nibble_idx] = (self.sky_light[nibble_idx] & 0x0F) | ((light & 0x0F) << 4); + } + } + } + + pub fn set_biome(&mut self, x: usize, z: usize, biome: u8) { + let idx = z * 16 + x; + if idx < self.biomes.len() { + self.biomes[idx] = biome; + } + } + + pub fn build_raw_data(&self) -> Vec { + let mut data = Vec::with_capacity( + self.blocks.len() * 2 + self.sky_light.len() + self.block_light.len() + self.biomes.len() + ); + for &b in &self.blocks { + data.extend_from_slice(&b.to_be_bytes()); + } + data.extend_from_slice(&self.sky_light); + data.extend_from_slice(&self.block_light); + data.extend_from_slice(&self.biomes); + data + } + + pub fn snapshot(&self) -> Self { + Self { + blocks: self.blocks.clone(), + block_light: self.block_light.clone(), + sky_light: self.sky_light.clone(), + biomes: self.biomes.clone(), + } + } + + pub fn pack_half_height_nibbles(&self) -> (Vec, Vec) { + let full_skylight = &self.sky_light; + let half_len = full_skylight.len() / 2; + let mut top = Vec::with_capacity(half_len); + let mut bottom = Vec::with_capacity(half_len); + for i in 0..half_len { + top.push(full_skylight[i * 2]); + bottom.push(full_skylight[i * 2 + 1]); + } + (top, bottom) + } +} + +pub struct CachedLceChunk { + pub chunk_x: i32, + pub chunk_z: i32, + builder: LceChunkBuilder, +} + +impl CachedLceChunk { + pub fn new(chunk_x: i32, chunk_z: i32) -> Self { + Self { chunk_x, chunk_z, builder: LceChunkBuilder::new() } + } + + pub fn set_block(&mut self, x: usize, y: usize, z: usize, block: u16) { + self.builder.set_block(x, y, z, block); + } + + pub fn snapshot_builder(&self) -> LceChunkBuilder { + self.builder.snapshot() + } +} + +pub fn translate_java_chunk( + _chunk_x: i32, + _chunk_z: i32, + chunk_data: &[u8], +) -> (LceChunkBuilder, Vec) { + let mut builder = LceChunkBuilder::new(); + let mut offset: usize = 0; + for section_y in 0..SECTIONS { + if offset >= chunk_data.len() { break; } + if offset + 2 > chunk_data.len() { break; } + let block_count = i16::from_be_bytes([chunk_data[offset], chunk_data[offset + 1]]); + offset += 2; + if block_count == 0 { + if offset >= chunk_data.len() { break; } + let _bits_per_entry = chunk_data[offset]; + offset += 1; + if offset >= chunk_data.len() { break; } + let (palette_len, used) = read_varint_offset(chunk_data, offset); + offset = used; + offset += palette_len as usize * 4; + if offset >= chunk_data.len() { break; } + let (data_len, used2) = read_varint_offset(chunk_data, offset); + offset = used2; + offset += data_len as usize; + continue; + } + if offset >= chunk_data.len() { break; } + let bits_per_entry = chunk_data[offset]; + offset += 1; + if bits_per_entry == 0 || bits_per_entry > 14 { + if offset + 4 <= chunk_data.len() { + offset += 4; + } + if offset >= chunk_data.len() { break; } + let (data_len, used) = read_varint_offset(chunk_data, offset); + offset = used; + offset += data_len as usize; + continue; + } + let (palette_len, used) = read_varint_offset(chunk_data, offset); + offset = used; + let mut palette = Vec::with_capacity(palette_len as usize); + for _ in 0..palette_len { + if offset + 4 > chunk_data.len() { break; } + palette.push(i32::from_be_bytes([ + chunk_data[offset], chunk_data[offset+1], chunk_data[offset+2], chunk_data[offset+3] + ])); + offset += 4; + } + if offset >= chunk_data.len() { break; } + let (data_len, used2) = read_varint_offset(chunk_data, offset); + offset = used2; + if offset + data_len as usize > chunk_data.len() { break; } + let compact_data = &chunk_data[offset..offset + data_len as usize]; + offset += data_len as usize; + let values_per_long = 64 / bits_per_entry as usize; + let mask = (1u64 << bits_per_entry) - 1; + for i in 0..4096usize { + let long_idx = i / values_per_long; + let bit_offset = (i % values_per_long) * bits_per_entry as usize; + if long_idx >= compact_data.len() / 8 { continue; } + let mut val = 0u64; + for b in 0..8 { + let byte_idx = long_idx * 8 + b; + if byte_idx < compact_data.len() { + val |= (compact_data[byte_idx] as u64) << (b * 8); + } + } + let palette_idx = ((val >> bit_offset) & mask) as usize; + let java_state = palette.get(palette_idx).copied().unwrap_or(0); + let bx = i & 0xF; + let by = (i >> 8) & 0xF; + let bz = (i >> 4) & 0xF; + let world_y = section_y * 16 + by; + let lce_block = registry::registry().get_lce(java_state); + builder.set_block(bx, world_y, bz, lce_block); + } + } + let raw_data = builder.build_raw_data(); + let compressed = compress_rle_zlib(&raw_data); + (builder, compressed) +} + +pub fn compress_rle_zlib(data: &[u8]) -> Vec { + let rle_encoded = rle_encode(data); + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + let _ = encoder.write_all(&rle_encoded); + encoder.finish().unwrap_or_else(|_| rle_encoded) +} + +fn rle_encode(data: &[u8]) -> Vec { + let mut out = Vec::with_capacity(data.len()); + let mut i = 0; + while i < data.len() { + let byte = data[i]; + if byte == 0xFF || (i + 1 < data.len() && data[i + 1] == byte) { + let mut count = 1usize; + while i + count < data.len() && data[i + count] == byte && count < 255 { + count += 1; + } + out.push(0xFF); + out.push((count - 1) as u8); + out.push(byte); + i += count; + } else { + out.push(byte); + i += 1; + } + } + out +} + +fn read_varint_offset(data: &[u8], offset: usize) -> (i32, usize) { + let mut result = 0i32; + let mut shift = 0; + let mut pos = offset; + while pos < data.len() { + let byte = data[pos]; + pos += 1; + result |= ((byte & 0x7F) as i32) << shift; + shift += 7; + if byte & 0x80 == 0 { + return (result, pos); + } + if shift >= 35 { break; } + } + (result, pos) +} diff --git a/src-tauri/src/lce_bridge/config.rs b/src-tauri/src/lce_bridge/config.rs new file mode 100644 index 0000000..e5b5d86 --- /dev/null +++ b/src-tauri/src/lce_bridge/config.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BridgeConfig { + pub lce: LceConfig, + pub remote: RemoteConfig, + pub world: WorldConfig, + pub performance: PerformanceConfig, + pub logging: LoggingConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LceConfig { + pub listen_address: String, + pub port: u16, + pub motd: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RemoteConfig { + pub host: String, + pub port: u16, + pub auth_type: String, + pub protocol_version: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorldConfig { + pub render_distance: i32, + pub simulation_distance: i32, + pub force_gamemode: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerformanceConfig { + pub chunk_threads: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoggingConfig { + pub level: String, +} + +impl Default for BridgeConfig { + fn default() -> Self { + Self { + lce: LceConfig { + listen_address: "0.0.0.0".to_string(), + port: 25656, + motd: "LCEBridge".to_string(), + }, + remote: RemoteConfig { + host: "127.0.0.1".to_string(), + port: 25565, + auth_type: "offline".to_string(), + protocol_version: 47, + }, + world: WorldConfig { + render_distance: 8, + simulation_distance: 8, + force_gamemode: None, + }, + performance: PerformanceConfig { + chunk_threads: 2, + }, + logging: LoggingConfig { + level: "info".to_string(), + }, + } + } +} diff --git a/src-tauri/src/lce_bridge/crafting.rs b/src-tauri/src/lce_bridge/crafting.rs new file mode 100644 index 0000000..b19552a --- /dev/null +++ b/src-tauri/src/lce_bridge/crafting.rs @@ -0,0 +1,240 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct LegacyRecipe { + pub index: u8, + pub input: Vec>, + pub output: (ItemSpec, u8), + pub width: u8, + pub height: u8, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct IngredientKey { + pub item_id: i16, + pub data: i16, + pub accept_any_data: bool, +} + +#[derive(Debug, Clone)] +pub struct ItemSpec { + pub id: i16, + pub data: i16, +} + +pub struct RecipeCatalog { + pub recipes: Vec, + by_index: HashMap, +} + +impl RecipeCatalog { + pub fn new() -> Self { + let recipes = build_catalog(); + let mut by_index = HashMap::new(); + for (i, r) in recipes.iter().enumerate() { + by_index.insert(r.index, i); + } + Self { recipes, by_index } + } + + pub fn recipe_for_index(&self, index: u8) -> Option<&LegacyRecipe> { + self.by_index.get(&index).map(|&i| &self.recipes[i]) + } +} + +fn build_catalog() -> Vec { + let mut recipes = Vec::new(); + recipes.push(LegacyRecipe { + index: 0, width: 1, height: 1, + input: vec![Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true })], + output: (ItemSpec { id: 281, data: 0 }, 4), + }); + recipes.push(LegacyRecipe { + index: 1, width: 2, height: 1, + input: vec![ + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 280, data: 0 }, 4), + }); + recipes.push(LegacyRecipe { + index: 2, width: 2, height: 2, + input: vec![ + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 5, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 3, width: 3, height: 3, + input: vec![ + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 4, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 4, width: 2, height: 1, + input: vec![ + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 256, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 5, width: 3, height: 2, + input: vec![ + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + None, + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + None, + ], + output: (ItemSpec { id: 257, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 6, width: 3, height: 3, + input: vec![ + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + None, + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + None, + None, + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + None, + ], + output: (ItemSpec { id: 258, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 7, width: 2, height: 2, + input: vec![ + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 265, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 266, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 8, width: 1, height: 1, + input: vec![ + Some(IngredientKey { item_id: 263, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 263, data: 1 }, 1), + }); + recipes.push(LegacyRecipe { + index: 9, width: 3, height: 2, + input: vec![ + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + None, + Some(IngredientKey { item_id: 280, data: 0, accept_any_data: true }), + None, + ], + output: (ItemSpec { id: 290, data: 0 }, 1), + }); + recipes.push(LegacyRecipe { + index: 10, width: 3, height: 1, + input: vec![ + Some(IngredientKey { item_id: 336, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 336, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 336, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 30, data: 0 }, 6), + }); + recipes.push(LegacyRecipe { + index: 11, width: 1, height: 1, + input: vec![ + Some(IngredientKey { item_id: 337, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 339, data: 0 }, 4), + }); + recipes.push(LegacyRecipe { + index: 12, width: 1, height: 1, + input: vec![ + Some(IngredientKey { item_id: 338, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 339, data: 0 }, 9), + }); + recipes.push(LegacyRecipe { + index: 13, width: 3, height: 3, + input: vec![ + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + None, + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 3, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 17, data: 0 }, 3), + }); + recipes.push(LegacyRecipe { + index: 14, width: 2, height: 1, + input: vec![ + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + Some(IngredientKey { item_id: 5, data: 0, accept_any_data: true }), + ], + output: (ItemSpec { id: 280, data: 0 }, 4), + }); + recipes +} + +pub struct InventoryManager { + pub slots: Vec, + pub catalog: RecipeCatalog, +} + +use crate::lce_bridge::lce_packets::LceItemStack; + +impl InventoryManager { + pub fn new() -> Self { + Self { + slots: Vec::new(), + catalog: RecipeCatalog::new(), + } + } + + pub fn set_slots(&mut self, slots: Vec) { + self.slots = slots; + } + + pub fn can_craft(&self, recipe_index: u8, player_inventory: &[LceItemStack]) -> bool { + if let Some(recipe) = self.catalog.recipe_for_index(recipe_index) { + let mut needed: HashMap<(i16, i16), u8> = HashMap::new(); + for ing in &recipe.input { + if let Some(key) = ing { + *needed.entry((key.item_id, key.data)).or_insert(0) += 1; + } + } + let mut available: HashMap<(i16, i16), u8> = HashMap::new(); + for item in player_inventory { + if item.id > 0 && item.count > 0 { + *available.entry((item.id, item.damage)).or_insert(0) += item.count; + } + } + for ((id, data), count) in &needed { + let avail = available.get(&(*id, *data)).copied().unwrap_or(0); + if avail < *count { return false; } + } + true + } else { + false + } + } +} diff --git a/src-tauri/src/lce_bridge/java_protocol.rs b/src-tauri/src/lce_bridge/java_protocol.rs new file mode 100644 index 0000000..a21ae11 --- /dev/null +++ b/src-tauri/src/lce_bridge/java_protocol.rs @@ -0,0 +1,1066 @@ +use crate::lce_bridge::util::*; + +use std::io::Read; + +pub struct JavaConnection { + stream: tokio::net::TcpStream, + compression_threshold: i32, + pub state: JavaProtocolState, +} + +#[derive(Debug)] +pub enum JavaProtocolState { + Handshaking, + Login, + Config, + Play, +} + +impl JavaConnection { + pub async fn connect(host: &str, port: u16) -> std::io::Result { + let stream = tokio::net::TcpStream::connect((host, port)).await?; + Ok(Self { + stream, + compression_threshold: -1, + state: JavaProtocolState::Handshaking, + }) + } + + pub async fn send_packet(&mut self, packet: &JavaPacket) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + let mut payload = Vec::new(); + packet.encode(&mut payload)?; + let mut frame = Vec::new(); + frame.extend_from_slice(&VarInt::write(packet.id())); + if self.compression_threshold >= 0 { + if payload.len() as i32 > self.compression_threshold { + use flate2::write::ZlibEncoder; + use flate2::Compression; + use std::io::Write; + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&payload)?; + let compressed = encoder.finish()?; + let mut compressed_frame = Vec::new(); + compressed_frame.extend_from_slice(&VarInt::write(payload.len() as i32)); + compressed_frame.extend_from_slice(&compressed); + payload = compressed_frame; + } else { + let mut uncompressed_frame = Vec::new(); + uncompressed_frame.extend_from_slice(&VarInt::write(0)); + uncompressed_frame.extend_from_slice(&payload); + payload = uncompressed_frame; + } + } + frame.extend_from_slice(&payload); + let mut len_prefix = VarInt::write(frame.len() as i32); + len_prefix.extend_from_slice(&frame); + self.stream.write_all(&len_prefix).await?; + Ok(()) + } + + pub async fn read_packet(&mut self) -> std::io::Result { + use tokio::io::AsyncReadExt; + let len = read_varint_stream(&mut self.stream).await?; + let mut packet_data = vec![0u8; len as usize]; + self.stream.read_exact(&mut packet_data).await?; + let decompressed = if self.compression_threshold >= 0 { + let mut data = &packet_data[..]; + let uncompressed_len = VarInt::read(&mut data)?; + if uncompressed_len > 0 { + use flate2::read::ZlibDecoder; + use std::io::Read; + let mut decoder = ZlibDecoder::new(data); + let mut buf = Vec::with_capacity(uncompressed_len as usize); + decoder.read_to_end(&mut buf)?; + buf + } else { + data.to_vec() + } + } else { + packet_data.clone() + }; + let mut data = &decompressed[..]; + let packet_id = VarInt::read(&mut data)?; + JavaPacket::decode(packet_id, data, &self.state) + } +} + +async fn read_varint_stream(stream: &mut tokio::net::TcpStream) -> std::io::Result { + use tokio::io::AsyncReadExt; + let mut result = 0i32; + let mut shift = 0; + loop { + let byte = stream.read_u8().await?; + result |= ((byte & 0x7F) as i32) << shift; + shift += 7; + if byte & 0x80 == 0 { + return Ok(result); + } + if shift >= 35 { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "varint too long")); + } + } +} + +#[derive(Debug, Clone)] +pub enum JavaPacket { + Handshake(HandshakePacket), + LoginStart(LoginStartPacket), + LoginSuccess(LoginSuccessPacket), + LoginAcknowledged, + SetCompression(SetCompressionPacket), + FinishConfig, + KeepAliveC2S(KeepAliveC2SPacket), + KeepAliveS2C(KeepAliveS2CPacket), + ChunkData(ChunkDataPacket), + ChunkBatchFinished, + RegistryData(RegistryDataPacket), + PlayerPosition(PlayerPositionPacket), + AcceptTeleportation(AcceptTeleportationPacket), + ClientInformation(ClientInformationPacket), + SynchronizePlayerPosition(SynchronizePlayerPositionPacket), + SetContainerContent(SetContainerContentPacket), + SetContainerSlot(SetContainerSlotPacket), + OpenScreen(OpenScreenPacket), + SystemChat(SystemChatPacket), + PlayerChat(PlayerChatPacket), + ChatCommand(ChatCommandPacket), + SetHealth(SetHealthS2CPacket), + SetTime(SetTimeS2CPacket), + AddEntity(AddEntityS2CPacket), + AddMob(AddMobS2CPacket), + AddPlayer(AddPlayerS2CPacket), + RemoveEntities(RemoveEntitiesS2CPacket), + SetEntityMotion(SetEntityMotionS2CPacket), + TeleportEntity(TeleportEntityS2CPacket), + SetHeadRotation(SetHeadRotationS2CPacket), + SetEntityData(SetEntityDataS2CPacket), + LoginPlay(LoginPlayPacket), + SoundEffect(SoundEffectS2CPacket), + SetDefaultSpawnPosition(SetDefaultSpawnPositionS2CPacket), + GameEventS2C(GameEventS2CPacket), + ContainerCloseS2C(ContainerCloseS2CPacket), + ContainerSetDataS2C(ContainerSetDataS2CPacket), + ContainerAckS2C(ContainerAckS2CPacket), + PingPong(PingPongPacket), + EncryptionRequest(EncryptionRequestPacket), + EncryptionResponse(EncryptionResponsePacket), + Unknown { id: i32, data: Vec }, +} + +impl JavaPacket { + pub fn id(&self) -> i32 { + use JavaPacket::*; + match self { + Handshake(_) => 0x00, + LoginStart(_) => 0x00, + LoginSuccess(_) => 0x03, + LoginAcknowledged => 0x03, + SetCompression(_) => 0x03, + FinishConfig => 0x03, + KeepAliveC2S(_) => 0x18, + KeepAliveS2C(_) => 0x26, + ChunkData(_) => 0x24, + ChunkBatchFinished => 0x71, + RegistryData(_) => 0x7C, + PlayerPosition(_) => 0x40, + AcceptTeleportation(_) => 0x00, + ClientInformation(_) => 0x09, + SynchronizePlayerPosition(_) => 0x3E, + SetContainerContent(_) => 0x13, + SetContainerSlot(_) => 0x14, + OpenScreen(_) => 0x32, + SystemChat(_) => 0x6F, + PlayerChat(_) => 0x37, + ChatCommand(_) => 0x04, + SetHealth(_) => 0x5A, + SetTime(_) => 0x5C, + AddEntity(_) => 0x01, + AddMob(_) => 0x03, + AddPlayer(_) => 0x04, + RemoveEntities(_) => 0x41, + SetEntityMotion(_) => 0x2E, + TeleportEntity(_) => 0x6E, + SetHeadRotation(_) => 0x4A, + SetEntityData(_) => 0x4C, + LoginPlay(_) => 0x2C, + SoundEffect(_) => 0x64, + SetDefaultSpawnPosition(_) => 0x5B, + GameEventS2C(_) => 0x1F, + ContainerCloseS2C(_) => 0x12, + ContainerSetDataS2C(_) => 0x15, + ContainerAckS2C(_) => 0x07, + EncryptionRequest(_) => 0x01, + EncryptionResponse(_) => 0x01, + PingPong(_) => 0x26, + Unknown { id, .. } => *id, + } + } + + pub fn encode(&self, buf: &mut Vec) -> std::io::Result<()> { + use JavaPacket::*; + match self { + Handshake(p) => { + VarInt::write_to(p.protocol_version, buf); + write_utf8_string(buf, &p.server_address); + buf.extend_from_slice(&p.server_port.to_be_bytes()); + VarInt::write_to(p.next_state, buf); + } + LoginStart(p) => { + write_utf8_string(buf, &p.username); + if p.protocol_version >= 761 && p.protocol_version <= 765 { + buf.push(0); + } + if p.protocol_version >= 761 { + buf.extend_from_slice(p.profile_id.as_bytes()); + } + } + LoginSuccess(p) => { + write_uuid(buf, &p.uuid); + write_utf8_string(buf, &p.username); + VarInt::write_to(0, buf); + } + LoginAcknowledged => {} + SetCompression(p) => { VarInt::write_to(p.threshold, buf); } + FinishConfig => {} + KeepAliveC2S(p) => { buf.extend_from_slice(&p.id.to_be_bytes()); } + KeepAliveS2C(p) => { buf.extend_from_slice(&p.id.to_be_bytes()); } + ClientInformation(p) => { + write_utf8_string(buf, &p.locale); + buf.push(p.view_distance); + VarInt::write_to(p.chat_mode, buf); + buf.push(p.chat_colors as u8); + buf.push(p.displayed_skin_parts); + VarInt::write_to(p.main_hand, buf); + buf.push(p.enable_text_filtering as u8); + buf.push(p.allow_listing as u8); + VarInt::write_to(p.particle_status, buf); + } + AcceptTeleportation(p) => { VarInt::write_to(p.teleport_id, buf); } + PingPong(p) => { buf.extend_from_slice(&p.id.to_be_bytes()); } + EncryptionRequest(p) => { + write_utf8_string(buf, &p.server_id); + VarInt::write_to(p.public_key.len() as i32, buf); + buf.extend_from_slice(&p.public_key); + VarInt::write_to(p.verify_token.len() as i32, buf); + buf.extend_from_slice(&p.verify_token); + } + EncryptionResponse(p) => { + VarInt::write_to(p.shared_secret.len() as i32, buf); + buf.extend_from_slice(&p.shared_secret); + VarInt::write_to(p.verify_token.len() as i32, buf); + buf.extend_from_slice(&p.verify_token); + } + _ => { + if let Unknown { data, .. } = self { + buf.extend_from_slice(data); + } + } + } + Ok(()) + } + + pub fn decode(id: i32, data: &[u8], state: &JavaProtocolState) -> std::io::Result { + let mut r = data; + Ok(match (state, id) { + (JavaProtocolState::Login, 0x00) => { + let reason = read_utf8_string(&mut r)?; + eprintln!("[LCEBridge] Java server disconnected: {}", reason); + JavaPacket::Unknown { id, data: data.to_vec() } + } + (JavaProtocolState::Login, 0x01) => { + let server_id = read_utf8_string(&mut r)?; + let key_len = VarInt::read(&mut r)? as usize; + let mut public_key = vec![0u8; key_len]; + r.read_exact(&mut public_key)?; + let token_len = VarInt::read(&mut r)? as usize; + let mut verify_token = vec![0u8; token_len]; + r.read_exact(&mut verify_token)?; + JavaPacket::EncryptionRequest(EncryptionRequestPacket { + server_id, public_key, verify_token, + }) + } + (JavaProtocolState::Login, 0x02) => { + let saved = data; + let mut r2 = saved; + if let Ok(uuid) = read_uuid(&mut r2) { + if let Ok(username) = read_utf8_string(&mut r2) { + return Ok(JavaPacket::LoginSuccess(LoginSuccessPacket { uuid, username })); + } + } + let uuid_str = read_utf8_string(&mut r)?; + let uuid = uuid::Uuid::try_parse(&uuid_str).unwrap_or(uuid::Uuid::nil()); + let username = read_utf8_string(&mut r)?; + JavaPacket::LoginSuccess(LoginSuccessPacket { uuid, username }) + } + (JavaProtocolState::Login, 0x03) => { + let peek = r.first().copied().unwrap_or(0); + if (peek & 0x80) != 0 || peek >= 4 { + let threshold = VarInt::read(&mut r)?; + JavaPacket::SetCompression(SetCompressionPacket { threshold }) + } else { + let uuid = read_uuid(&mut r)?; + let username = read_utf8_string(&mut r)?; + JavaPacket::LoginSuccess(LoginSuccessPacket { uuid, username }) + } + } + (JavaProtocolState::Config, 0x03) => { + JavaPacket::FinishConfig + } + (JavaProtocolState::Play, 0x26) => { + let id_val = read_i64be(&mut r)?; + JavaPacket::KeepAliveS2C(KeepAliveS2CPacket { id: id_val }) + } + (JavaProtocolState::Play, 0x24) => { + let chunk_x = read_i32be(&mut r)?; + let chunk_z = read_i32be(&mut r)?; + let _heightmaps = read_nbt(&mut r)?; + let data_size = VarInt::read(&mut r)? as usize; + let mut chunk_data = vec![0u8; data_size]; + r.read_exact(&mut chunk_data)?; + let block_entities_count = VarInt::read(&mut r)?; + for _ in 0..block_entities_count { + let _packed_xyz = read_u8be(&mut r)?; + let _block_entity_type = VarInt::read(&mut r)?; + let _nbt = read_nbt(&mut r)?; + } + let _trust_edges = read_u8be(&mut r)? != 0; + let _sky_light_mask = read_varint_array(&mut r)?; + let _block_light_mask = read_varint_array(&mut r)?; + let _empty_sky_light_mask = read_varint_array(&mut r)?; + let _empty_block_light_mask = read_varint_array(&mut r)?; + let sky_light_arrays_count = VarInt::read(&mut r)?; + let mut light_data = Vec::new(); + for _ in 0..sky_light_arrays_count { + let light_len = VarInt::read(&mut r)? as usize; + let mut light = vec![0u8; light_len]; + r.read_exact(&mut light)?; + light_data.extend_from_slice(&light); + } + let block_light_arrays_count = VarInt::read(&mut r)?; + for _ in 0..block_light_arrays_count { + let light_len = VarInt::read(&mut r)? as usize; + let mut light = vec![0u8; light_len]; + r.read_exact(&mut light)?; + } + JavaPacket::ChunkData(ChunkDataPacket { + chunk_x, chunk_z, chunk_data, light_data, + trust_edges: false, block_entities: Vec::new(), + }) + } + (JavaProtocolState::Play, 0x71) => { + JavaPacket::ChunkBatchFinished + } + (JavaProtocolState::Config, 0x07) => { + let _registry_codec = read_nbt(&mut r)?; + JavaPacket::RegistryData(RegistryDataPacket {}) + } + (JavaProtocolState::Play, 0x40) => { + let teleport_id = VarInt::read(&mut r)?; + let x = read_f64be(&mut r)?; + let y = read_f64be(&mut r)?; + let z = read_f64be(&mut r)?; + let yaw = read_f32be(&mut r)?; + let pitch = read_f32be(&mut r)?; + let _flags = read_u8be(&mut r)?; + let _ = VarInt::read(&mut r)?; + JavaPacket::PlayerPosition(PlayerPositionPacket { + teleport_id, x, y, z, yaw, pitch, flags: _flags, + }) + } + (JavaProtocolState::Play, 0x3E) => { + let teleport_id = VarInt::read(&mut r)?; + let x = read_f64be(&mut r)?; + let y = read_f64be(&mut r)?; + let z = read_f64be(&mut r)?; + let yaw = read_f32be(&mut r)?; + let pitch = read_f32be(&mut r)?; + let _dismount_vehicle = read_u8be(&mut r)?; + let _ = VarInt::read(&mut r)?; + JavaPacket::SynchronizePlayerPosition(SynchronizePlayerPositionPacket { + teleport_id, x, y, z, yaw, pitch, + }) + } + (JavaProtocolState::Play, 0x13) => { + let container_id = VarInt::read(&mut r)?; + let _state_id = VarInt::read(&mut r)?; + let slots_count = VarInt::read(&mut r)? as usize; + let mut slots = Vec::with_capacity(slots_count); + for _ in 0..slots_count { + slots.push(read_java_slot(&mut r)?); + } + let _carried_item = read_java_slot(&mut r)?; + JavaPacket::SetContainerContent(SetContainerContentPacket { + container_id, state_id: _state_id, slots, + }) + } + (JavaProtocolState::Play, 0x14) => { + let container_id = VarInt::read(&mut r)?; + let _state_id = VarInt::read(&mut r)?; + let slot = i16::try_from(VarInt::read(&mut r)?).unwrap_or(0); + let item = read_java_slot(&mut r)?; + JavaPacket::SetContainerSlot(SetContainerSlotPacket { + container_id, state_id: _state_id, slot, item, + }) + } + (JavaProtocolState::Play, 0x32) => { + let container_id = VarInt::read(&mut r)?; + let _screen_id = VarInt::read(&mut r)?; + let _title = read_nbt(&mut r)?; + JavaPacket::OpenScreen(OpenScreenPacket { + container_id, screen_id: _screen_id, title: _title, + }) + } + (JavaProtocolState::Play, 0x6F) => { + let content = read_nbt(&mut r)?; + let is_actionbar = read_u8be(&mut r)? != 0; + JavaPacket::SystemChat(SystemChatPacket { + content_json: content, is_actionbar, + }) + } + (JavaProtocolState::Play, 0x37) => { + let content = read_nbt(&mut r)?; + let _position = read_i64be(&mut r)?; + let mut uuid_bytes = [0u8; 16]; + r.read_exact(&mut uuid_bytes)?; + let sender = uuid::Uuid::from_bytes(uuid_bytes); + JavaPacket::PlayerChat(PlayerChatPacket { + content_json: content, sender, + }) + } + (JavaProtocolState::Play, 0x5A) => { + let health = read_f32be(&mut r)?; + let food = VarInt::read(&mut r)?; + let saturation = read_f32be(&mut r)?; + JavaPacket::SetHealth(SetHealthS2CPacket { health, food, saturation }) + } + (JavaProtocolState::Play, 0x5C) => { + let world_age = read_i64be(&mut r)?; + let day_time = read_i64be(&mut r)?; + JavaPacket::SetTime(SetTimeS2CPacket { world_age, day_time }) + } + (JavaProtocolState::Play, 0x01) => { + let entity_id = VarInt::read(&mut r)?; + let entity_type = VarInt::read(&mut r)?; + let x = read_f64be(&mut r)?; + let y = read_f64be(&mut r)?; + let z = read_f64be(&mut r)?; + let yaw = read_u8be(&mut r)?; + let pitch = read_u8be(&mut r)?; + let head_yaw = read_u8be(&mut r)?; + let data = VarInt::read(&mut r)?; + let vel_x = read_i16be(&mut r)?; + let vel_y = read_i16be(&mut r)?; + let vel_z = read_i16be(&mut r)?; + JavaPacket::AddEntity(AddEntityS2CPacket { + entity_id, entity_type, x, y, z, yaw, pitch, head_yaw, data, + vel_x, vel_y, vel_z, + }) + } + (JavaProtocolState::Play, 0x03) => { + let entity_id = VarInt::read(&mut r)?; + let _uuid = read_uuid(&mut r)?; + let entity_type = VarInt::read(&mut r)?; + let x = read_f64be(&mut r)?; + let y = read_f64be(&mut r)?; + let z = read_f64be(&mut r)?; + let yaw = read_u8be(&mut r)?; + let pitch = read_u8be(&mut r)?; + let head_yaw = read_u8be(&mut r)?; + let vel_x = read_i16be(&mut r)?; + let vel_y = read_i16be(&mut r)?; + let vel_z = read_i16be(&mut r)?; + let _metadata = read_java_metadata(&mut r)?; + JavaPacket::AddMob(AddMobS2CPacket { + entity_id, entity_type, x, y, z, yaw, pitch, head_yaw, + vel_x, vel_y, vel_z, + }) + } + (JavaProtocolState::Play, 0x04) => { + let entity_id = VarInt::read(&mut r)?; + let _uuid = read_uuid(&mut r)?; + let x = read_f64be(&mut r)?; + let y = read_f64be(&mut r)?; + let z = read_f64be(&mut r)?; + let yaw = read_u8be(&mut r)?; + let pitch = read_u8be(&mut r)?; + let _metadata = read_java_metadata(&mut r)?; + JavaPacket::AddPlayer(AddPlayerS2CPacket { + entity_id, x, y, z, yaw, pitch, + }) + } + (JavaProtocolState::Play, 0x41) => { + let count = VarInt::read(&mut r)?; + let mut entity_ids = Vec::with_capacity(count as usize); + for _ in 0..count { + entity_ids.push(VarInt::read(&mut r)?); + } + JavaPacket::RemoveEntities(RemoveEntitiesS2CPacket { entity_ids }) + } + (JavaProtocolState::Play, 0x2E) => { + let entity_id = VarInt::read(&mut r)?; + let xa = read_i16be(&mut r)?; + let ya = read_i16be(&mut r)?; + let za = read_i16be(&mut r)?; + JavaPacket::SetEntityMotion(SetEntityMotionS2CPacket { entity_id, xa, ya, za }) + } + (JavaProtocolState::Play, 0x6E) => { + let entity_id = VarInt::read(&mut r)?; + let x = read_f64be(&mut r)?; + let y = read_f64be(&mut r)?; + let z = read_f64be(&mut r)?; + let yaw = read_u8be(&mut r)?; + let pitch = read_u8be(&mut r)?; + let _on_ground = read_u8be(&mut r)? != 0; + JavaPacket::TeleportEntity(TeleportEntityS2CPacket { + entity_id, x, y, z, yaw, pitch, + }) + } + (JavaProtocolState::Play, 0x4A) => { + let entity_id = VarInt::read(&mut r)?; + let head_yaw = read_u8be(&mut r)?; + JavaPacket::SetHeadRotation(SetHeadRotationS2CPacket { entity_id, head_yaw }) + } + (JavaProtocolState::Play, 0x4C) => { + let entity_id = VarInt::read(&mut r)?; + let _metadata = read_java_metadata(&mut r)?; + JavaPacket::SetEntityData(SetEntityDataS2CPacket { entity_id }) + } + (JavaProtocolState::Play, 0x2C) => { + let entity_id = VarInt::read(&mut r)?; + let _is_hardcore = read_u8be(&mut r)? != 0; + let _dim_count = VarInt::read(&mut r)?; + for _ in 0.._dim_count { + let _ = read_utf8_string(&mut r)?; + } + let _max_players = VarInt::read(&mut r)?; + let view_distance = VarInt::read(&mut r)?; + let simulation_distance = VarInt::read(&mut r)?; + let _reduced_debug = read_u8be(&mut r)? != 0; + let _enable_respawn_screen = read_u8be(&mut r)? != 0; + let _do_limited_crafting = read_u8be(&mut r)? != 0; + let _dim_type = VarInt::read(&mut r)?; + let _dim_name = read_utf8_string(&mut r)?; + let _hashed_seed = read_i64be(&mut r)?; + let _ = VarInt::read(&mut r)?; + let _is_debug = read_u8be(&mut r)? != 0; + let _is_flat = read_u8be(&mut r)? != 0; + JavaPacket::LoginPlay(LoginPlayPacket { + entity_id, is_hardcore: _is_hardcore, + view_distance, simulation_distance, + reduced_debug: _reduced_debug, + enable_respawn_screen: _enable_respawn_screen, + do_limited_crafting: _do_limited_crafting, + hashed_seed: _hashed_seed, + is_debug: _is_debug, + is_flat: _is_flat, + }) + } + (JavaProtocolState::Play, 0x64) => { + let _sound_id = VarInt::read(&mut r)?; + let _sound_category = VarInt::read(&mut r)?; + let _x = read_i32be(&mut r)?; + let _y = read_i32be(&mut r)?; + let _z = read_i32be(&mut r)?; + let _volume = read_f32be(&mut r)?; + let _pitch = read_f32be(&mut r)?; + let _seed = read_i64be(&mut r)?; + JavaPacket::SoundEffect(SoundEffectS2CPacket { + sound_id: _sound_id, sound_category: _sound_category, + x: _x, y: _y, z: _z, volume: _volume, pitch: _pitch, seed: _seed, + }) + } + (JavaProtocolState::Play, 0x5B) => { + let x = read_i32be(&mut r)?; + let y = read_i32be(&mut r)?; + let z = read_i32be(&mut r)?; + let _angle = read_f32be(&mut r)?; + JavaPacket::SetDefaultSpawnPosition(SetDefaultSpawnPositionS2CPacket { x, y, z }) + } + (JavaProtocolState::Play, 0x1F) => { + let reason = read_u8be(&mut r)?; + let param = read_f32be(&mut r)?; + JavaPacket::GameEventS2C(GameEventS2CPacket { reason, param }) + } + (JavaProtocolState::Play, 0x12) => { + let container_id = VarInt::read(&mut r)?; + JavaPacket::ContainerCloseS2C(ContainerCloseS2CPacket { container_id }) + } + (JavaProtocolState::Play, 0x15) => { + let container_id = VarInt::read(&mut r)?; + let key = VarInt::read(&mut r)?; + let value = VarInt::read(&mut r)?; + JavaPacket::ContainerSetDataS2C(ContainerSetDataS2CPacket { container_id, key, value }) + } + (JavaProtocolState::Play, 0x07) => { + let container_id = read_u8be(&mut r)?; + let uid = read_i16be(&mut r)?; + let accepted = read_u8be(&mut r)? != 0; + JavaPacket::ContainerAckS2C(ContainerAckS2CPacket { container_id, uid, accepted }) + } + _ => { + JavaPacket::Unknown { id, data: data.to_vec() } + } + }) + } +} + +#[derive(Debug, Clone)] +pub struct HandshakePacket { + pub protocol_version: i32, + pub server_address: String, + pub server_port: u16, + pub next_state: i32, +} + +#[derive(Debug, Clone)] +pub struct LoginStartPacket { + pub username: String, + pub profile_id: uuid::Uuid, + pub protocol_version: i32, +} + +#[derive(Debug, Clone)] +pub struct LoginSuccessPacket { + pub uuid: uuid::Uuid, + pub username: String, +} + +#[derive(Debug, Clone)] +pub struct SetCompressionPacket { + pub threshold: i32, +} + +#[derive(Debug, Clone)] +pub struct KeepAliveC2SPacket { + pub id: i64, +} + +#[derive(Debug, Clone)] +pub struct KeepAliveS2CPacket { + pub id: i64, +} + +#[derive(Debug, Clone)] +pub struct ChunkDataPacket { + pub chunk_x: i32, + pub chunk_z: i32, + pub chunk_data: Vec, + pub light_data: Vec, + pub trust_edges: bool, + pub block_entities: Vec<()>, +} + +#[derive(Debug, Clone)] +pub struct RegistryDataPacket {} + +#[derive(Debug, Clone)] +pub struct PlayerPositionPacket { + pub teleport_id: i32, + pub x: f64, pub y: f64, pub z: f64, + pub yaw: f32, pub pitch: f32, + pub flags: u8, +} + +#[derive(Debug, Clone)] +pub struct AcceptTeleportationPacket { + pub teleport_id: i32, +} + +#[derive(Debug, Clone)] +pub struct ClientInformationPacket { + pub locale: String, + pub view_distance: u8, + pub chat_mode: i32, + pub chat_colors: bool, + pub displayed_skin_parts: u8, + pub main_hand: i32, + pub enable_text_filtering: bool, + pub allow_listing: bool, + pub particle_status: i32, +} + +#[derive(Debug, Clone)] +pub struct SynchronizePlayerPositionPacket { + pub teleport_id: i32, + pub x: f64, pub y: f64, pub z: f64, + pub yaw: f32, pub pitch: f32, +} + +#[derive(Debug, Clone)] +pub struct SetContainerContentPacket { + pub container_id: i32, + pub state_id: i32, + pub slots: Vec, +} + +#[derive(Debug, Clone)] +pub struct SetContainerSlotPacket { + pub container_id: i32, + pub state_id: i32, + pub slot: i16, + pub item: JavaSlot, +} + +#[derive(Debug, Clone)] +pub struct OpenScreenPacket { + pub container_id: i32, + pub screen_id: i32, + pub title: Vec, +} + +#[derive(Debug, Clone)] +pub struct SystemChatPacket { + pub content_json: Vec, + pub is_actionbar: bool, +} + +#[derive(Debug, Clone)] +pub struct PlayerChatPacket { + pub content_json: Vec, + pub sender: uuid::Uuid, +} + +#[derive(Debug, Clone)] +pub struct ChatCommandPacket { + pub command: String, +} + +#[derive(Debug, Clone)] +pub struct SetHealthS2CPacket { + pub health: f32, + pub food: i32, + pub saturation: f32, +} + +#[derive(Debug, Clone)] +pub struct SetTimeS2CPacket { + pub world_age: i64, + pub day_time: i64, +} + +#[derive(Debug, Clone)] +pub struct AddEntityS2CPacket { + pub entity_id: i32, + pub entity_type: i32, + pub x: f64, pub y: f64, pub z: f64, + pub yaw: u8, pub pitch: u8, pub head_yaw: u8, + pub data: i32, + pub vel_x: i16, pub vel_y: i16, pub vel_z: i16, +} + +#[derive(Debug, Clone)] +pub struct AddMobS2CPacket { + pub entity_id: i32, + pub entity_type: i32, + pub x: f64, pub y: f64, pub z: f64, + pub yaw: u8, pub pitch: u8, pub head_yaw: u8, + pub vel_x: i16, pub vel_y: i16, pub vel_z: i16, +} + +#[derive(Debug, Clone)] +pub struct AddPlayerS2CPacket { + pub entity_id: i32, + pub x: f64, pub y: f64, pub z: f64, + pub yaw: u8, pub pitch: u8, +} + +#[derive(Debug, Clone)] +pub struct RemoveEntitiesS2CPacket { + pub entity_ids: Vec, +} + +#[derive(Debug, Clone)] +pub struct SetEntityMotionS2CPacket { + pub entity_id: i32, + pub xa: i16, pub ya: i16, pub za: i16, +} + +#[derive(Debug, Clone)] +pub struct TeleportEntityS2CPacket { + pub entity_id: i32, + pub x: f64, pub y: f64, pub z: f64, + pub yaw: u8, pub pitch: u8, +} + +#[derive(Debug, Clone)] +pub struct SetHeadRotationS2CPacket { + pub entity_id: i32, + pub head_yaw: u8, +} + +#[derive(Debug, Clone)] +pub struct SetEntityDataS2CPacket { + pub entity_id: i32, +} + +#[derive(Debug, Clone)] +pub struct LoginPlayPacket { + pub entity_id: i32, + pub is_hardcore: bool, + pub view_distance: i32, + pub simulation_distance: i32, + pub reduced_debug: bool, + pub enable_respawn_screen: bool, + pub do_limited_crafting: bool, + pub hashed_seed: i64, + pub is_debug: bool, + pub is_flat: bool, +} + +#[derive(Debug, Clone)] +pub struct SoundEffectS2CPacket { + pub sound_id: i32, + pub sound_category: i32, + pub x: i32, pub y: i32, pub z: i32, + pub volume: f32, + pub pitch: f32, + pub seed: i64, +} + +#[derive(Debug, Clone)] +pub struct SetDefaultSpawnPositionS2CPacket { + pub x: i32, pub y: i32, pub z: i32, +} + +#[derive(Debug, Clone)] +pub struct GameEventS2CPacket { + pub reason: u8, + pub param: f32, +} + +#[derive(Debug, Clone)] +pub struct ContainerCloseS2CPacket { + pub container_id: i32, +} + +#[derive(Debug, Clone)] +pub struct ContainerSetDataS2CPacket { + pub container_id: i32, + pub key: i32, + pub value: i32, +} + +#[derive(Debug, Clone)] +pub struct ContainerAckS2CPacket { + pub container_id: u8, + pub uid: i16, + pub accepted: bool, +} + +#[derive(Debug, Clone)] +pub struct PingPongPacket { + pub id: i64, +} + +#[derive(Debug, Clone)] +pub struct EncryptionRequestPacket { + pub server_id: String, + pub public_key: Vec, + pub verify_token: Vec, +} + +#[derive(Debug, Clone)] +pub struct EncryptionResponsePacket { + pub shared_secret: Vec, + pub verify_token: Vec, +} + +#[derive(Debug, Clone)] +pub struct JavaSlot { + pub present: bool, + pub item_id: i32, + pub count: u8, + pub components: Vec, +} + +impl JavaSlot { + pub fn empty() -> Self { + Self { present: false, item_id: 0, count: 0, components: Vec::new() } + } +} + +fn write_uuid(buf: &mut Vec, uuid: &uuid::Uuid) { + buf.extend_from_slice(uuid.as_bytes()); +} + +pub fn write_utf8_string(buf: &mut Vec, s: &str) { + let bytes = s.as_bytes(); + VarInt::write_to(bytes.len() as i32, buf); + buf.extend_from_slice(bytes); +} + +fn read_uuid(r: &mut &[u8]) -> std::io::Result { + let mut bytes = [0u8; 16]; + r.read_exact(&mut bytes)?; + Ok(uuid::Uuid::from_bytes(bytes)) +} + +fn read_u8be(r: &mut &[u8]) -> std::io::Result { + if r.is_empty() { return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof")); } + let v = r[0]; + *r = &r[1..]; + Ok(v) +} + +fn read_i16be(r: &mut &[u8]) -> std::io::Result { + let mut buf = [0u8; 2]; + r.read_exact(&mut buf)?; + Ok(i16::from_be_bytes(buf)) +} + +fn read_i32be(r: &mut &[u8]) -> std::io::Result { + let mut buf = [0u8; 4]; + r.read_exact(&mut buf)?; + Ok(i32::from_be_bytes(buf)) +} + +fn read_i64be(r: &mut &[u8]) -> std::io::Result { + let mut buf = [0u8; 8]; + r.read_exact(&mut buf)?; + Ok(i64::from_be_bytes(buf)) +} + +fn read_f32be(r: &mut &[u8]) -> std::io::Result { + let mut buf = [0u8; 4]; + r.read_exact(&mut buf)?; + Ok(f32::from_be_bytes(buf)) +} + +fn read_f64be(r: &mut &[u8]) -> std::io::Result { + let mut buf = [0u8; 8]; + r.read_exact(&mut buf)?; + Ok(f64::from_be_bytes(buf)) +} + +fn read_utf8_string(r: &mut &[u8]) -> std::io::Result { + let len = VarInt::read(r)? as usize; + if len == 0 { return Ok(String::new()); } + let mut buf = vec![0u8; len]; + r.read_exact(&mut buf)?; + Ok(String::from_utf8(buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?) +} + +fn read_varint_array(r: &mut &[u8]) -> std::io::Result> { + let count = VarInt::read(r)?; + let mut v = Vec::with_capacity(count as usize); + for _ in 0..count { v.push(VarInt::read(r)?); } + Ok(v) +} + +fn read_nbt(r: &mut &[u8]) -> std::io::Result> { + if r.is_empty() { return Ok(Vec::new()); } + let tag_type = r[0]; + if tag_type == 0 { *r = &r[1..]; return Ok(Vec::new()); } + let start_len = r.len(); + let mut depth = 1; + let mut i = 1; + if tag_type == 10 { + if r.len() < 3 { return Ok(Vec::new()); } + let name_len = i16::from_be_bytes([r[1], r[2]]) as usize; + i = 3 + name_len; + } + let skip = |pos: &mut usize, n: usize| -> bool { + if *pos + n > r.len() { return false; } + *pos += n; + true + }; + while i < r.len() && depth > 0 { + let tag = r[i]; + i += 1; + match tag { + 0 => { depth -= 1; } + 1 => { if !skip(&mut i, 1) { break; } } + 2 => { if !skip(&mut i, 2) { break; } } + 3 => { if !skip(&mut i, 4) { break; } } + 4 => { if !skip(&mut i, 8) { break; } } + 5 => { if !skip(&mut i, 4) { break; } } + 6 => { if !skip(&mut i, 8) { break; } } + 7 => { + if i + 4 > r.len() { break; } + let len = i32::from_be_bytes([r[i], r[i+1], r[i+2], r[i+3]]) as usize; + if !skip(&mut i, 4 + len) { break; } + } + 8 => { + if i + 2 > r.len() { break; } + let len = i16::from_be_bytes([r[i], r[i+1]]) as usize; + if !skip(&mut i, 2 + len) { break; } + } + 9 => { + if i + 5 > r.len() { break; } + let _inner = r[i]; i += 1; + let len = i32::from_be_bytes([r[i], r[i+1], r[i+2], r[i+3]]) as usize; + i += 4; + depth += len; + } + 10 => { + if i + 2 > r.len() { break; } + let name_len = i16::from_be_bytes([r[i], r[i+1]]) as usize; + if !skip(&mut i, 2 + name_len) { break; } + depth += 1; + } + 11 => { + if i + 4 > r.len() { break; } + let len = i32::from_be_bytes([r[i], r[i+1], r[i+2], r[i+3]]) as usize; + if !skip(&mut i, 4 + len * 4) { break; } + } + 12 => { + if i + 4 > r.len() { break; } + let len = i32::from_be_bytes([r[i], r[i+1], r[i+2], r[i+3]]) as usize; + if !skip(&mut i, 4 + len * 8) { break; } + } + _ => { break; } + } + } + let consumed = start_len.saturating_sub(r.len() - i).min(start_len); + let result = r[..consumed].to_vec(); + *r = &r[consumed..]; + Ok(result) +} + +fn read_java_slot(r: &mut &[u8]) -> std::io::Result { + let present = read_u8be(r)? != 0; + if !present { + Ok(JavaSlot::empty()) + } else { + let item_id = VarInt::read(r)?; + let count = read_u8be(r)?; + let components_len = VarInt::read(r)? as usize; + let mut components = vec![0u8; components_len]; + r.read_exact(&mut components)?; + Ok(JavaSlot { present, item_id, count, components }) + } +} + +fn read_java_metadata(r: &mut &[u8]) -> std::io::Result<()> { + loop { + if r.is_empty() { break; } + let item = r[0]; + if item == 0xFF { *r = &r[1..]; break; } + *r = &r[1..]; + let type_id = item & 0x1F; + match type_id { + 0 => { let _ = read_u8be(r)?; } + 1 => { let _ = VarInt::read(r)?; } + 2 => { let _ = read_f32be(r)?; } + 3 => { let _ = read_utf8_string(r)?; } + 4 => { let _ = read_utf8_string(r)?; } + 5 => { let _ = read_nbt(r)?; } + 6 => { let _ = VarInt::read(r)?; } + 7 => { let _ = read_f32be(r)?; let _ = read_f32be(r)?; } + 8 => { let _ = VarInt::read(r)?; } + 9 => { let _ = VarInt::read(r)?; } + 10 => { let _ = VarInt::read(r)?; } + 11 => { let _ = read_u8be(r)?; } + 12 => { let mut u = [0u8; 16]; r.read_exact(&mut u)?; } + 13 => { let _ = VarInt::read(r)?; } + 14 => { let _ = read_nbt(r)?; } + 15 => { let _ = VarInt::read(r)?; } + 16 => { let _ = read_i64be(r)?; } + 17 => { let _ = VarInt::read(r)?; } + _ => { break; } + } + } + Ok(()) +} diff --git a/src-tauri/src/lce_bridge/java_session.rs b/src-tauri/src/lce_bridge/java_session.rs new file mode 100644 index 0000000..4b6f3f1 --- /dev/null +++ b/src-tauri/src/lce_bridge/java_session.rs @@ -0,0 +1,443 @@ +use crate::lce_bridge::java_protocol::*; +use crate::lce_bridge::config::RemoteConfig; +use crate::lce_bridge::msa_auth; +use crate::lce_bridge::util::VarInt; +use tokio::sync::mpsc; +use tokio::net::TcpStream; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::sync::Mutex; +use aes::Aes128; +use cipher::{KeyInit, BlockEncrypt}; + +pub struct EncryptionCtx { + pub key: [u8; 16], + pub enc_iv: [u8; 16], + pub dec_iv: [u8; 16], +} + +fn aes_cfb8_encrypt(key: &[u8; 16], iv: &mut [u8; 16], data: &[u8]) -> Vec { + let cipher = Aes128::new_from_slice(key).unwrap(); + let mut result = data.to_vec(); + for i in 0..result.len() { + let mut block = *iv; + cipher.encrypt_block((&mut block).into()); + let keystream = block[0]; + let ct = result[i] ^ keystream; + result[i] = ct; + iv.copy_within(1.., 0); + iv[15] = ct; + } + result +} + +fn aes_cfb8_decrypt(key: &[u8; 16], iv: &mut [u8; 16], data: &[u8]) -> Vec { + let cipher = Aes128::new_from_slice(key).unwrap(); + let mut result = Vec::with_capacity(data.len()); + for &byte in data { + let mut block = *iv; + cipher.encrypt_block((&mut block).into()); + let keystream = block[0]; + let pt = byte ^ keystream; + result.push(pt); + iv.copy_within(1.., 0); + iv[15] = byte; + } + result +} + +fn rsa_encrypt(public_key_der: &[u8], data: &[u8]) -> std::io::Result> { + use rsa::RsaPublicKey; + use rsa::pkcs8::DecodePublicKey; + use rsa::Pkcs1v15Encrypt; + let pubkey = RsaPublicKey::from_public_key_der(public_key_der) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + let mut rng = rand::thread_rng(); + pubkey.encrypt(&mut rng, Pkcs1v15Encrypt, data) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) +} + +pub struct JavaSession { + pub tx: mpsc::Sender, + pub rx: mpsc::Receiver, +} + +impl JavaSession { + pub async fn connect(config: &RemoteConfig) -> std::io::Result { + let stream = tokio::time::timeout( + std::time::Duration::from_secs(5), + TcpStream::connect((config.host.as_str(), config.port)), + ).await + .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "connect timeout"))? + .map_err(|e| e)?; + let (reader, writer) = stream.into_split(); + let (packet_tx, packet_rx) = mpsc::channel::(256); + let (write_tx, write_rx) = mpsc::channel::(256); + + let enc_ctx: Arc>> = Arc::new(Mutex::new(None)); + + if config.auth_type == "online" { + let profile = msa_auth::authenticate().await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + eprintln!("[LCEBridge] Authenticated as {} ({})", profile.username, profile.uuid); + } + + let enc_w = enc_ctx.clone(); + let write_tx_clone = write_tx.clone(); + let comp_state: Arc> = Arc::new(Mutex::new(-1i32)); + tokio::spawn(write_loop(writer, write_rx, enc_w, comp_state.clone())); + + tokio::spawn(read_loop(reader, packet_tx, write_tx_clone, enc_ctx, comp_state, config.clone())); + + Ok(Self { + tx: write_tx, + rx: packet_rx, + }) + } + + pub async fn send(&mut self, packet: JavaPacket) -> std::io::Result<()> { + self.tx.send(packet).await + .map_err(|_| std::io::Error::new(std::io::ErrorKind::ConnectionReset, "send failed")) + } + + pub async fn recv(&mut self) -> Option { + self.rx.recv().await + } +} + +fn encode_frame(packet: &JavaPacket, compression: i32) -> Option> { + let mut payload = Vec::new(); + packet.encode(&mut payload).ok()?; + let packet_id = packet.id(); + let mut frame = VarInt::write(packet_id); + frame.extend_from_slice(&payload); + if compression >= 0 { + let raw_data_len = frame.len() as i32; + let mut compressed_frame = Vec::new(); + if raw_data_len > compression { + use flate2::write::ZlibEncoder; + use flate2::Compression; + use std::io::Write; + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&frame).ok()?; + let compressed = encoder.finish().ok()?; + VarInt::write_to(raw_data_len, &mut compressed_frame); + compressed_frame.extend_from_slice(&compressed); + } else { + VarInt::write_to(0, &mut compressed_frame); + compressed_frame.extend_from_slice(&frame); + } + frame = compressed_frame; + } + let mut len_prefix = VarInt::write(frame.len() as i32); + len_prefix.extend_from_slice(&frame); + Some(len_prefix) +} + +async fn read_loop( + mut reader: tokio::net::tcp::OwnedReadHalf, + tx: mpsc::Sender, + write_tx: mpsc::Sender, + enc_ctx: Arc>>, + comp_state: Arc>, + config: RemoteConfig, +) { + let mut state: u8 = 1; + let mut compression = -1i32; + let sent_finish_config = Arc::new(AtomicBool::new(false)); + loop { + let mut raw = Vec::new(); + let mut dec = Vec::new(); + loop { + let b = match read_raw_bytes(&mut reader, 1).await { + Ok(b) => b[0], + Err(e) => { + eprintln!("[LCEBridge] Java read loop error reading byte: {}", e); + break; + } + }; + raw.push(b); + let db = { + let mut ctx = enc_ctx.lock().await; + if let Some(ref mut ec) = *ctx { + let result = aes_cfb8_decrypt(&ec.key, &mut ec.dec_iv, &[b]); + result[0] + } else { + b + } + }; + dec.push(db); + if dec.len() == 1 { + eprintln!("[LCEBridge] Java read loop: first byte 0x{:02x} decrypted 0x{:02x}", b, db); + } + if db & 0x80 == 0 || dec.len() >= 5 { + break; + } + } + eprintln!("[LCEBridge] Java read: VarInt raw={:02x?} dec={:02x?}", raw, dec); + if raw.is_empty() { break; } + + let len = match VarInt::read(&mut &dec[..]) { + Ok(l) => { + eprintln!("[LCEBridge] Java read: decoded packet length={}", l); + l + } + Err(e) => { + eprintln!("[LCEBridge] Java read: VarInt failed on first byte 0x{:02x}: {}", raw[0], e); + if let Ok(()) = try_handshake(raw[0], &mut reader, &write_tx, &enc_ctx, &config).await { + continue; + } + eprintln!("[LCEBridge] Java read: try_handshake failed (offline?), breaking read loop"); + break; + } + }; + + let mut packet_data = vec![0u8; len as usize]; + if read_exact_decrypt(&mut reader, &mut packet_data, &enc_ctx).await.is_err() { break; } + if len > 0 { + let show = &packet_data[..packet_data.len().min(20)]; + eprintln!("[LCEBridge] Java read: packet_data len={} hex={:02x?}", len, show); + } + + let decompressed = if compression >= 0 { + let mut data = &packet_data[..]; + let uncompressed_len = match VarInt::read(&mut data) { + Ok(l) => l, + Err(_) => break, + }; + if uncompressed_len > 0 { + use flate2::read::ZlibDecoder; + use std::io::Read; + let mut decoder = ZlibDecoder::new(data); + let mut buf = Vec::with_capacity(uncompressed_len as usize); + if decoder.read_to_end(&mut buf).is_err() { break; } + buf + } else { + data.to_vec() + } + } else { + packet_data + }; + + let mut data = &decompressed[..]; + if data.is_empty() { continue; } + let packet_id = match VarInt::read(&mut data) { + Ok(id) => id, + Err(_) => break, + }; + + let proto_state = match state { + 0 => JavaProtocolState::Handshaking, + 1 => JavaProtocolState::Login, + 2 => JavaProtocolState::Config, + 3 => JavaProtocolState::Play, + _ => JavaProtocolState::Play, + }; + + let packet = match JavaPacket::decode(packet_id, data, &proto_state) { + Ok(p) => p, + Err(e) => { + eprintln!("[LCEBridge] Java read: decode failed for id=0x{:02x} state={:?}: {}", packet_id, proto_state, e); + continue; + } + }; + + match &packet { + JavaPacket::SetCompression(p) => { + eprintln!("[LCEBridge] Java read: SetCompression threshold={}", p.threshold); + compression = p.threshold; + *comp_state.lock().await = p.threshold; + let _ = tx.send(packet).await; + continue; + } + JavaPacket::LoginSuccess(p) => { + eprintln!("[LCEBridge] Java read: LoginSuccess user={}", p.username); + let _ = write_tx.send(JavaPacket::LoginAcknowledged).await; + let _ = tx.send(packet).await; + state = 2; + continue; + } + JavaPacket::FinishConfig => { + eprintln!("[LCEBridge] Java read: FinishConfig from server, sending ours"); + let _ = write_tx.send(JavaPacket::FinishConfig).await; + let _ = tx.send(packet).await; + state = 3; + continue; + } + JavaPacket::Unknown { id: 0x0E, .. } if state == 2 && config.protocol_version >= 766 => { + eprintln!("[LCEBridge] Java read: SelectKnownPacks (0x0E), sending empty response"); + let _ = write_tx.send(JavaPacket::Unknown { id: 0x07, data: vec![0] }).await; + continue; + } + JavaPacket::Unknown { id: 0x07, .. } if state == 2 && config.protocol_version < 766 => { + eprintln!("[LCEBridge] Java read: SelectKnownPacks (0x07), sending empty response"); + let _ = write_tx.send(JavaPacket::Unknown { id: 0x08, data: vec![0] }).await; + continue; + } + JavaPacket::EncryptionRequest(p) => { + handle_encryption_request(p, &write_tx, &enc_ctx).await; + continue; + } + JavaPacket::Unknown { id: 0x09, data } if state == 2 => { + eprintln!("[LCEBridge] Java read: ResourcePackPush, auto-accepting"); + let pack_id = if data.len() >= 16 { + data[..16].to_vec() + } else { + data.clone() + }; + let sid: i32 = if config.protocol_version >= 766 { 0x06 } else { 0x0C }; + for status in &[3, 4, 0] { + let mut resp = pack_id.clone(); + VarInt::write_to(*status, &mut resp); + let _ = write_tx.send(JavaPacket::Unknown { id: sid, data: resp }).await; + } + continue; + } + JavaPacket::Unknown { id: 0x04, data } if state == 2 && data.len() == 8 => { + eprintln!("[LCEBridge] Java read: KeepAlive (Config 0x04), echoing"); + let _ = write_tx.send(JavaPacket::Unknown { id: 0x04, data: data.clone() }).await; + continue; + } + JavaPacket::Unknown { id: 0x04, data } if state == 2 && data.len() == 4 => { + eprintln!("[LCEBridge] Java read: Ping (Config 0x04), sending Pong"); + let _ = write_tx.send(JavaPacket::Unknown { id: 0x05, data: data.clone() }).await; + continue; + } + _ => {} + } + if state == 2 && !sent_finish_config.swap(true, Ordering::Relaxed) { + eprintln!("[LCEBridge] Java read: first server config, waiting for FinishConfig from server"); + } + eprintln!("[LCEBridge] Java read: forwarding packet id={}", packet.id()); + if tx.send(packet).await.is_err() { break; } + } +} + +async fn read_raw_bytes(reader: &mut tokio::net::tcp::OwnedReadHalf, n: usize) -> std::io::Result> { + let mut buf = vec![0u8; n]; + reader.read_exact(&mut buf).await?; + Ok(buf) +} + +async fn read_exact_decrypt(reader: &mut tokio::net::tcp::OwnedReadHalf, buf: &mut [u8], enc_ctx: &Arc>>) -> std::io::Result<()> { + let mut offset = 0; + while offset < buf.len() { + let n = reader.read(&mut buf[offset..]).await?; + if n == 0 { return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof")); } + offset += n; + } + let mut ctx = enc_ctx.lock().await; + if let Some(ref mut ec) = *ctx { + let decrypted = aes_cfb8_decrypt(&ec.key, &mut ec.dec_iv, buf); + buf.copy_from_slice(&decrypted); + } + Ok(()) +} + +async fn try_handshake( + first_byte: u8, + reader: &mut tokio::net::tcp::OwnedReadHalf, + write_tx: &mpsc::Sender, + enc_ctx: &Arc>>, + config: &RemoteConfig, +) -> std::io::Result<()> { + if config.auth_type != "online" { + return Err(std::io::Error::new(std::io::ErrorKind::Other, "not online")); + } + let mut more = [0u8; 2]; + reader.read_exact(&mut more).await?; + let mut full = vec![first_byte]; + full.extend_from_slice(&more); + let len = VarInt::read(&mut &full[..]).map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "bad varint"))?; + let mut packet_data = vec![0u8; len as usize]; + reader.read_exact(&mut packet_data).await?; + let mut rest = &packet_data[..]; + let _sid = read_utf8_string(&mut rest)?; + let key_len = VarInt::read(&mut rest)? as usize; + let mut public_key = vec![0u8; key_len]; + std::io::Read::read_exact(&mut rest, &mut public_key)?; + let token_len = VarInt::read(&mut rest)? as usize; + let mut verify_token = vec![0u8; token_len]; + std::io::Read::read_exact(&mut rest, &mut verify_token)?; + + let shared_secret: [u8; 16] = rand::random(); + let encrypted_secret = rsa_encrypt(&public_key, &shared_secret)?; + let encrypted_token = { + let mut iv = shared_secret; + aes_cfb8_encrypt(&shared_secret, &mut iv, &verify_token) + }; + + let response = JavaPacket::EncryptionResponse(EncryptionResponsePacket { + shared_secret: encrypted_secret, + verify_token: encrypted_token, + }); + let _ = write_tx.send(response).await; + + *enc_ctx.lock().await = Some(EncryptionCtx { + key: shared_secret, + enc_iv: shared_secret, + dec_iv: shared_secret, + }); + Ok(()) +} + +async fn handle_encryption_request( + p: &EncryptionRequestPacket, + write_tx: &mpsc::Sender, + enc_ctx: &Arc>>, +) { + let shared_secret: [u8; 16] = rand::random(); + let encrypted_secret = match rsa_encrypt(&p.public_key, &shared_secret) { + Ok(s) => s, + Err(_) => return, + }; + let encrypted_token = { + let mut iv = shared_secret; + aes_cfb8_encrypt(&shared_secret, &mut iv, &p.verify_token) + }; + let response = JavaPacket::EncryptionResponse(EncryptionResponsePacket { + shared_secret: encrypted_secret, + verify_token: encrypted_token, + }); + let _ = write_tx.send(response).await; + *enc_ctx.lock().await = Some(EncryptionCtx { + key: shared_secret, + enc_iv: shared_secret, + dec_iv: shared_secret, + }); +} + +async fn write_loop( + mut writer: tokio::net::tcp::OwnedWriteHalf, + mut rx: mpsc::Receiver, + enc_ctx: Arc>>, + comp_state: Arc>, +) { + while let Some(packet) = rx.recv().await { + let threshold = *comp_state.lock().await; + if let Some(mut frame) = encode_frame(&packet, threshold) { + let needs_encryption = !matches!(packet, JavaPacket::EncryptionResponse(_)); + if needs_encryption { + let mut ctx = enc_ctx.lock().await; + if let Some(ref mut ec) = *ctx { + let encrypted = aes_cfb8_encrypt(&ec.key, &mut ec.enc_iv, &frame); + frame = encrypted; + } + } + let hex: String = frame.iter().map(|b| format!("{:02x}", b)).collect::>().join(" "); + eprintln!("[LCEBridge] Java write: sending len={} hex=[{}]", frame.len(), hex); + if writer.write_all(&frame).await.is_err() { break; } + } + } + let _ = writer.shutdown().await; +} + +fn read_utf8_string(r: &mut &[u8]) -> std::io::Result { + let len = VarInt::read(r)? as usize; + if len == 0 { return Ok(String::new()); } + let mut buf = vec![0u8; len]; + std::io::Read::read_exact(r, &mut buf)?; + Ok(String::from_utf8(buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?) +} diff --git a/src-tauri/src/lce_bridge/lce_codec.rs b/src-tauri/src/lce_bridge/lce_codec.rs new file mode 100644 index 0000000..310b88d --- /dev/null +++ b/src-tauri/src/lce_bridge/lce_codec.rs @@ -0,0 +1,600 @@ +use crate::lce_bridge::lce_packets::*; +use crate::lce_bridge::util::*; +use crate::lce_bridge::bridge::LceBridgeSession; +use crate::lce_bridge::config::BridgeConfig; +use std::sync::Arc; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::Mutex; + +pub fn decode_lce_packet(id: u8, data: &[u8]) -> std::io::Result { + let mut r = LceReader::new(data); + Ok(match id { + 0 => LcePacket::KeepAlive(KeepAlivePacket { + keep_alive_id: r.read_i32()?, + }), + 1 => LcePacket::Login(LoginPacket { + protocol_version: r.read_i32()?, + username: r.read_string_utf16()?, + map_seed: r.read_u64()?, + game_type: r.read_i32()?, + world_name: r.read_string_utf16()?, + dimension: r.read_i32()?, + difficulty: r.read_i32()?, + max_players: r.read_u8()?, + world_width: r.read_i32()?, + world_length: r.read_i32()?, + }), + 2 => { + let net_version = r.read_i16()?; + let player_name = r.read_string_utf16()?; + let mut offline_xuid = r.read_u8()? as i64; + r.read_i32()?; + let player_count = r.read_u8()?; + let mut online_xuid = 0i64; + for i in 0..player_count { + let off = r.read_i64()?; + let on = r.read_i64()?; + if i == 0 { + offline_xuid = off; + online_xuid = on; + } + } + for _ in 0..14 { r.read_u8()?; } + r.read_i32()?; + r.read_u8()?; + r.read_i32()?; + LcePacket::PreLogin(PreLoginPacket { net_version, player_name, offline_xuid, online_xuid }) + } + 3 => LcePacket::Chat(ChatPacket { + message_type: r.read_u8()?, + string_args: { + let count = r.read_u16()? as usize; + let mut v = Vec::with_capacity(count); + for _ in 0..count { v.push(r.read_string_utf16()?); } + v + }, + int_args: { + let count = r.read_u16()? as usize; + let mut v = Vec::with_capacity(count); + for _ in 0..count { v.push(r.read_i32()?); } + v + }, + }), + 7 => LcePacket::Interact(InteractPacket { + source: r.read_u8()?, + target: r.read_i32()?, + action: r.read_u8()?, + }), + 9 => LcePacket::RespawnRequest(RespawnRequestPacket), + 10 | 11 | 12 | 13 => LcePacket::MovePlayer(MovePlayerPacket { + id, + x: r.read_f64()?, + y: r.read_f64()?, + y_view: r.read_f64()?, + z: r.read_f64()?, + yaw: r.read_f32()?, + pitch: r.read_f32()?, + flags: if id == 13 { r.read_u8()? } else { 0 }, + }), + 14 => LcePacket::PlayerAction(PlayerActionPacket { + action: r.read_u8()?, + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + face: r.read_u8()?, + }), + 15 => LcePacket::UseItem(UseItemPacket { + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + face: r.read_u8()?, + item: read_lce_item(&mut r)?, + click_x: r.read_f32()?, + click_y: r.read_f32()?, + click_z: r.read_f32()?, + }), + 16 => LcePacket::SetCarriedItem(SetCarriedItemPacket { + slot: r.read_u8()?, + }), + 18 => LcePacket::Animate(AnimatePacket { + entity_id: r.read_i32()?, + action: r.read_u8()?, + }), + 19 => LcePacket::PlayerCommand(PlayerCommandPacket { + entity_id: r.read_i32()?, + action: r.read_u8()?, + data: r.read_i32()?, + }), + 20 => LcePacket::AddPlayer(AddPlayerPacket { + entity_id: r.read_i32()?, + name: r.read_string_utf16()?, + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + yaw: r.read_u8()?, + pitch: r.read_u8()?, + head_yaw: r.read_u8()?, + carried_item: r.read_u16()?, + offline_xuid: r.read_i64()?, + online_xuid: r.read_i64()?, + player_index: r.read_i32()?, + skin_id: r.read_string_utf16()?, + cape_id: r.read_string_utf16()?, + game_privileges: r.read_u32()?, + metadata: read_entity_metadata(&mut r)?, + }), + 23 => LcePacket::AddEntity(AddEntityPacket { + entity_id: r.read_i32()?, + entity_type: r.read_u8()?, + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + yaw: r.read_u8()?, + pitch: r.read_u8()?, + data: r.read_i32()?, + motion_x: r.read_i16()?, + motion_y: r.read_i16()?, + motion_z: r.read_i16()?, + }), + 24 => LcePacket::AddMob(AddMobPacket { + entity_id: r.read_i32()?, + entity_type: r.read_u8()?, + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + yaw: r.read_u8()?, + pitch: r.read_u8()?, + head_yaw: r.read_u8()?, + motion_x: r.read_i16()?, + motion_y: r.read_i16()?, + motion_z: r.read_i16()?, + metadata: read_entity_metadata(&mut r)?, + }), + 28 => LcePacket::SetEntityMotion(SetEntityMotionPacket { + entity_id: r.read_i32()?, + xa: r.read_i16()?, + ya: r.read_i16()?, + za: r.read_i16()?, + }), + 29 => { + let count = r.read_u16()? as usize; + let mut entity_ids = Vec::with_capacity(count); + for _ in 0..count { entity_ids.push(r.read_i32()?); } + LcePacket::RemoveEntities(RemoveEntitiesPacket { entity_ids }) + } + 34 => LcePacket::TeleportEntity(TeleportEntityPacket { + entity_id: r.read_i32()?, + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + yaw: r.read_u8()?, + pitch: r.read_u8()?, + }), + 35 => LcePacket::RotateHead(RotateHeadPacket { + entity_id: r.read_i32()?, + y_head_rot: r.read_u8()?, + }), + 38 => LcePacket::EntityEvent(EntityEventPacket { + entity_id: r.read_i32()?, + event_id: r.read_u8()?, + }), + 40 => { + let entity_id = r.read_i32()?; + let values = read_entity_metadata(&mut r)?; + LcePacket::SetEntityData(SetEntityDataPacket { entity_id, values }) + } + 50 => LcePacket::ChunkVisibility(ChunkVisibilityPacket { + chunk_x: r.read_i32()?, + chunk_z: r.read_i32()?, + visible: r.read_bool()?, + }), + 51 => LcePacket::BlockRegionUpdate(BlockRegionUpdatePacket { + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + xs: r.read_u8()?, + ys: r.read_u8()?, + zs: r.read_u8()?, + level_idx: r.read_u8()?, + is_full_chunk: r.read_bool()?, + compressed_data: { + let len = r.read_u32()? as usize; + r.read_bytes(len)? + }, + }), + 53 => LcePacket::TileUpdate(TileUpdatePacket { + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + block: r.read_u16()?, + data: r.read_u16()?, + level_idx: r.read_u8()?, + }), + 70 => LcePacket::GameEvent(GameEventPacket { + reason: r.read_u8()?, + param: r.read_f32()?, + }), + 100 => LcePacket::ContainerOpen(ContainerOpenPacket { + container_id: r.read_u8()?, + container_type: r.read_u8()?, + size: r.read_u8()?, + custom_name: r.read_string_utf16()?, + title: r.read_string_utf16()?, + entity_id: r.read_i32()?, + }), + 101 => LcePacket::ContainerClose(ContainerClosePacket { + container_id: r.read_u8()?, + }), + 102 => LcePacket::ContainerClick(ContainerClickPacket { + container_id: r.read_u8()?, + slot_num: r.read_i16()?, + button_num: r.read_u8()?, + uid: r.read_i16()?, + click_type: r.read_u8()?, + item: read_lce_item(&mut r)?, + }), + 103 => LcePacket::ContainerSetSlot(ContainerSetSlotPacket { + container_id: r.read_u8()?, + slot: r.read_i16()?, + item: read_lce_item(&mut r)?, + }), + 104 => LcePacket::ContainerSetContent(ContainerSetContentPacket { + container_id: r.read_u8()?, + items: { + let count = r.read_u16()? as usize; + let mut v = Vec::with_capacity(count); + for _ in 0..count { v.push(read_lce_item(&mut r)?); } + v + }, + }), + 105 => LcePacket::ContainerSetData(ContainerSetDataPacket { + container_id: r.read_u8()?, + id: r.read_i16()?, + value: r.read_i16()?, + }), + 106 => LcePacket::ContainerAck(ContainerAckPacket { + container_id: r.read_u8()?, + uid: r.read_i16()?, + accepted: r.read_bool()?, + }), + 108 => LcePacket::ContainerButtonClick(ContainerButtonClickPacket { + container_id: r.read_u8()?, + button_id: r.read_u8()?, + }), + 150 => LcePacket::CraftItem(CraftItemPacket { + uid: r.read_i16()?, + recipe: r.read_u8()?, + }), + 152 => LcePacket::DebugOptions(DebugOptionsPacket { + options_mask: r.read_u32()?, + }), + 155 => LcePacket::ChunkVisibilityArea(ChunkVisibilityAreaPacket { + min_cx: r.read_i32()?, + max_cx: r.read_i32()?, + min_cz: r.read_i32()?, + max_cz: r.read_i32()?, + }), + 202 => LcePacket::PlayerAbilities(PlayerAbilitiesPacket { + flags: r.read_u8()?, + fly_speed: r.read_f32()?, + walk_speed: r.read_f32()?, + }), + 205 => LcePacket::ClientCommand(ClientCommandPacket { + action: r.read_u8()?, + }), + 255 => LcePacket::Disconnect(DisconnectPacket { + reason: r.read_u8()?, + }), + 4 => LcePacket::SetTime(SetTimePacket { + game_time: r.read_i64()?, + day_time: r.read_i64()?, + }), + 6 => LcePacket::SetSpawnPosition(SetSpawnPositionPacket { + x: r.read_i32()?, + y: r.read_i32()?, + z: r.read_i32()?, + }), + 8 => LcePacket::SetHealth(SetHealthPacket { + health: r.read_f32()?, + food: r.read_i32()?, + saturation: r.read_f32()?, + damage_source: r.read_u8()?, + }), + _ => LcePacket::Raw { id, data: data.to_vec() }, + }) +} + +pub fn encode_lce_packet(packet: &LcePacket) -> Vec { + let id = packet.id(); + let mut w = LceWriter::new(); + match packet { + LcePacket::KeepAlive(p) => { w.write_i32(p.keep_alive_id); } + LcePacket::Login(p) => { + w.write_i32(p.protocol_version); + w.write_utf16(&p.username); + w.write_u64(p.map_seed); + w.write_i32(p.game_type); + w.write_utf16(&p.world_name); + w.write_i32(p.dimension); + w.write_i32(p.difficulty); + w.write_u8(p.max_players); + w.write_i32(p.world_width); + w.write_i32(p.world_length); + } + LcePacket::PreLogin(p) => { + w.write_i16(p.net_version); + w.write_utf16(&p.player_name); + w.write_i64(p.offline_xuid); + w.write_i64(p.online_xuid); + } + LcePacket::Chat(p) => { + w.write_u8(p.message_type); + w.write_u16(p.string_args.len() as u16); + for s in &p.string_args { w.write_utf16(s); } + w.write_u16(p.int_args.len() as u16); + for v in &p.int_args { w.write_i32(*v); } + } + LcePacket::Disconnect(p) => { w.write_u8(p.reason); } + LcePacket::Animate(p) => { w.write_i32(p.entity_id); w.write_u8(p.action); } + LcePacket::MovePlayer(p) => { + w.write_f64(p.x); w.write_f64(p.y); w.write_f64(p.y_view); w.write_f64(p.z); + w.write_f32(p.yaw); w.write_f32(p.pitch); + if p.id == 13 { w.write_u8(p.flags); } + } + LcePacket::PlayerAction(p) => { + w.write_u8(p.action); w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); w.write_u8(p.face); + } + LcePacket::Interact(p) => { w.write_u8(p.source); w.write_i32(p.target); w.write_u8(p.action); } + LcePacket::UseItem(p) => { + w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); w.write_u8(p.face); + write_lce_item(&mut w, &p.item); + w.write_f32(p.click_x); w.write_f32(p.click_y); w.write_f32(p.click_z); + } + LcePacket::PlayerCommand(p) => { w.write_i32(p.entity_id); w.write_u8(p.action); w.write_i32(p.data); } + LcePacket::SetCarriedItem(p) => { w.write_u8(p.slot); } + LcePacket::PlayerAbilities(p) => { w.write_u8(p.flags); w.write_f32(p.fly_speed); w.write_f32(p.walk_speed); } + LcePacket::DebugOptions(p) => { w.write_u32(p.options_mask); } + LcePacket::ClientCommand(p) => { w.write_u8(p.action); } + LcePacket::RespawnRequest(_) => {} + LcePacket::ContainerClose(p) => { w.write_u8(p.container_id); } + LcePacket::ContainerClick(p) => { + w.write_u8(p.container_id); w.write_i16(p.slot_num); w.write_u8(p.button_num); + w.write_i16(p.uid); w.write_u8(p.click_type); + write_lce_item(&mut w, &p.item); + } + LcePacket::ContainerButtonClick(p) => { w.write_u8(p.container_id); w.write_u8(p.button_id); } + LcePacket::CraftItem(p) => { w.write_i16(p.uid); w.write_u8(p.recipe); } + LcePacket::SetTime(p) => { w.write_i64(p.game_time); w.write_i64(p.day_time); } + LcePacket::SetSpawnPosition(p) => { w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); } + LcePacket::SetHealth(p) => { w.write_f32(p.health); w.write_i32(p.food); w.write_f32(p.saturation); w.write_u8(p.damage_source); } + LcePacket::GameEvent(p) => { w.write_u8(p.reason); w.write_f32(p.param); } + LcePacket::EntityEvent(p) => { w.write_i32(p.entity_id); w.write_u8(p.event_id); } + LcePacket::ContainerOpen(p) => { + w.write_u8(p.container_id); w.write_u8(p.container_type); w.write_u8(p.size); + w.write_utf16(&p.custom_name); w.write_utf16(&p.title); w.write_i32(p.entity_id); + } + LcePacket::ContainerSetSlot(p) => { w.write_u8(p.container_id); w.write_i16(p.slot); write_lce_item(&mut w, &p.item); } + LcePacket::ContainerSetContent(p) => { + w.write_u8(p.container_id); + w.write_u16(p.items.len() as u16); + for item in &p.items { write_lce_item(&mut w, item); } + } + LcePacket::ContainerSetData(p) => { w.write_u8(p.container_id); w.write_i16(p.id); w.write_i16(p.value); } + LcePacket::ContainerAck(p) => { w.write_u8(p.container_id); w.write_i16(p.uid); w.write_bool(p.accepted); } + LcePacket::ChunkVisibility(p) => { w.write_i32(p.chunk_x); w.write_i32(p.chunk_z); w.write_bool(p.visible); } + LcePacket::ChunkVisibilityArea(p) => { w.write_i32(p.min_cx); w.write_i32(p.max_cx); w.write_i32(p.min_cz); w.write_i32(p.max_cz); } + LcePacket::BlockRegionUpdate(p) => { + w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); + w.write_u8(p.xs); w.write_u8(p.ys); w.write_u8(p.zs); + w.write_u8(p.level_idx); w.write_bool(p.is_full_chunk); + w.write_u32(p.compressed_data.len() as u32); + w.write_bytes(&p.compressed_data); + } + LcePacket::TileUpdate(p) => { w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); w.write_u16(p.block); w.write_u16(p.data); w.write_u8(p.level_idx); } + LcePacket::AddPlayer(p) => { + w.write_i32(p.entity_id); w.write_utf16(&p.name); + w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); + w.write_u8(p.yaw); w.write_u8(p.pitch); w.write_u8(p.head_yaw); + w.write_u16(p.carried_item); + w.write_i64(p.offline_xuid); w.write_i64(p.online_xuid); + w.write_i32(p.player_index); + w.write_utf16(&p.skin_id); w.write_utf16(&p.cape_id); + w.write_u32(p.game_privileges); + write_entity_metadata(&mut w, &p.metadata); + } + LcePacket::AddEntity(p) => { + w.write_i32(p.entity_id); w.write_u8(p.entity_type); + w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); + w.write_u8(p.yaw); w.write_u8(p.pitch); w.write_i32(p.data); + w.write_i16(p.motion_x); w.write_i16(p.motion_y); w.write_i16(p.motion_z); + } + LcePacket::AddMob(p) => { + w.write_i32(p.entity_id); w.write_u8(p.entity_type); + w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); + w.write_u8(p.yaw); w.write_u8(p.pitch); w.write_u8(p.head_yaw); + w.write_i16(p.motion_x); w.write_i16(p.motion_y); w.write_i16(p.motion_z); + write_entity_metadata(&mut w, &p.metadata); + } + LcePacket::TeleportEntity(p) => { w.write_i32(p.entity_id); w.write_i32(p.x); w.write_i32(p.y); w.write_i32(p.z); w.write_u8(p.yaw); w.write_u8(p.pitch); } + LcePacket::RotateHead(p) => { w.write_i32(p.entity_id); w.write_u8(p.y_head_rot); } + LcePacket::SetEntityMotion(p) => { w.write_i32(p.entity_id); w.write_i16(p.xa); w.write_i16(p.ya); w.write_i16(p.za); } + LcePacket::RemoveEntities(p) => { + w.write_u16(p.entity_ids.len() as u16); + for id in &p.entity_ids { w.write_i32(*id); } + } + LcePacket::SetEntityData(p) => { + w.write_i32(p.entity_id); + write_entity_metadata(&mut w, &p.values); + } + LcePacket::Raw { data, .. } => { w.write_bytes(data); } + } + let payload = w.into_bytes(); + let mut frame = Vec::with_capacity(4 + 1 + payload.len()); + frame.extend_from_slice(&(payload.len() as u32 + 1).to_be_bytes()); + frame.push(id); + frame.extend_from_slice(&payload); + frame +} + +fn read_lce_item(r: &mut LceReader) -> std::io::Result { + let id = r.read_i16()?; + let count = r.read_u8()?; + let damage = r.read_i16()?; + let _nbt_size = r.read_i16()?; + if _nbt_size > 0 { r.skip(_nbt_size as usize)?; } + Ok(LceItemStack { id, count, damage }) +} + +fn write_lce_item(w: &mut LceWriter, item: &LceItemStack) { + w.write_i16(item.id); + w.write_u8(item.count); + w.write_i16(item.damage); + w.write_i16(0); +} + +fn read_entity_metadata(r: &mut LceReader) -> std::io::Result> { + let mut values = Vec::new(); + loop { + let id = r.read_u8()?; + if id == 0x7F { break; } + let data_type = r.read_u8()?; + let value = match data_type { + 0 => EntityDataValueType::Byte(r.read_u8()?), + 1 => EntityDataValueType::Short(r.read_i16()?), + 2 => EntityDataValueType::Int(r.read_i32()?), + 3 => EntityDataValueType::Float(r.read_f32()?), + 4 => EntityDataValueType::String(r.read_string_utf16()?), + 5 => EntityDataValueType::ItemStack(read_lce_item(r)?), + _ => { r.skip(1)?; EntityDataValueType::Byte(0) } + }; + values.push(EntityDataValue { id, data_type, value }); + } + Ok(values) +} + +fn write_entity_metadata(w: &mut LceWriter, values: &[EntityDataValue]) { + for v in values { + w.write_u8(v.id); + w.write_u8(v.data_type); + match &v.value { + EntityDataValueType::Byte(b) => w.write_u8(*b), + EntityDataValueType::Short(s) => w.write_i16(*s), + EntityDataValueType::Int(i) => w.write_i32(*i), + EntityDataValueType::Float(f) => w.write_f32(*f), + EntityDataValueType::String(s) => w.write_utf16(s), + EntityDataValueType::ItemStack(item) => write_lce_item(w, item), + } + } + w.write_u8(0x7F); +} + +pub async fn run_lce_server(config: BridgeConfig) -> std::io::Result<()> { + let addr = format!("{}:{}", config.lce.listen_address, config.lce.port); + let listener = TcpListener::bind(&addr).await?; + eprintln!("[LCEBridge] Listening on {}", addr); + loop { + let (stream, addr) = listener.accept().await?; + eprintln!("[LCEBridge] Connection from {}", addr); + let config = config.clone(); + tokio::spawn(async move { + if let Err(e) = handle_connection(stream, config).await { + eprintln!("[LCEBridge] Connection error: {}", e); + } + }); + } +} + +async fn handle_connection(stream: TcpStream, config: BridgeConfig) -> std::io::Result<()> { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let (mut reader, mut writer) = stream.into_split(); + let small_id: u8 = 1; + writer.write_all(&small_id.to_be_bytes()).await?; + eprintln!("[LCEBridge] small_id sent"); + let session = Arc::new(Mutex::new(LceBridgeSession::new(config))); + { + let mut s = session.lock().await; + s.on_connected(); + } + let session_clone = session.clone(); + let read_handle = tokio::spawn(async move { + let mut header = [0u8; 4]; + loop { + match reader.read_exact(&mut header).await { + Ok(_) => {} + Err(_) => { + eprintln!("[LCEBridge] LCE read error (connection closed?)"); + break; + } + } + let payload_len = u32::from_be_bytes(header) as usize; + eprintln!("[LCEBridge] received frame len={}", payload_len); + if payload_len > 2_097_152 { + break; + } + let mut payload = vec![0u8; payload_len]; + if reader.read_exact(&mut payload).await.is_err() { + break; + } + let hex: String = payload.iter().map(|b| format!("{:02x}", b)).collect::>().join(" "); + eprintln!("[LCEBridge] frame hex=[{}]", hex); + let mut offset = 0; + while offset < payload_len { + if offset >= payload_len { break; } + let packet_id = payload[offset]; + offset += 1; + let remaining = payload_len - offset; + let packet_data = &payload[offset..offset + remaining]; + match decode_lce_packet(packet_id, packet_data) { + Ok(packet) => { + let mut s = session_clone.lock().await; + s.handle_lce_packet(packet).await; + } + Err(e) => { + eprintln!("[LCEBridge] decode failed for packet_id={} len={}: {}", packet_id, remaining, e); + } + } + offset += remaining; + } + } + }); + let write_clone = session.clone(); + let _write_handle = tokio::spawn(async move { + let mut out_buf: Vec = Vec::with_capacity(4096); + loop { + out_buf.clear(); + let mut s = write_clone.lock().await; + while let Some(packet) = s.lce_tx.pop_front() { + let frame = encode_lce_packet(&packet); + out_buf.extend_from_slice(&frame); + } + drop(s); + if !out_buf.is_empty() { + if writer.write_all(&out_buf).await.is_err() { break; } + } + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + }); + let java_clone = session.clone(); + tokio::spawn(async move { + use tokio::sync::mpsc::error::TryRecvError; + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + let packet = { + let mut s = java_clone.lock().await; + let js = match s.java_session.as_mut() { + Some(js) => js, + None => continue, + }; + match js.rx.try_recv() { + Ok(p) => p, + Err(TryRecvError::Empty) => continue, + Err(TryRecvError::Disconnected) => return, + } + }; + let mut s = java_clone.lock().await; + s.handle_java_packet(packet).await; + } + }); + let _ = read_handle.await; + Ok(()) +} diff --git a/src-tauri/src/lce_bridge/lce_packets.rs b/src-tauri/src/lce_bridge/lce_packets.rs new file mode 100644 index 0000000..04f88c2 --- /dev/null +++ b/src-tauri/src/lce_bridge/lce_packets.rs @@ -0,0 +1,478 @@ +#[derive(Debug, Clone)] +pub enum LcePacket { + KeepAlive(KeepAlivePacket), + Login(LoginPacket), + PreLogin(PreLoginPacket), + Chat(ChatPacket), + Disconnect(DisconnectPacket), + Animate(AnimatePacket), + MovePlayer(MovePlayerPacket), + PlayerAction(PlayerActionPacket), + Interact(InteractPacket), + UseItem(UseItemPacket), + PlayerCommand(PlayerCommandPacket), + SetCarriedItem(SetCarriedItemPacket), + PlayerAbilities(PlayerAbilitiesPacket), + DebugOptions(DebugOptionsPacket), + ClientCommand(ClientCommandPacket), + RespawnRequest(RespawnRequestPacket), + ContainerClose(ContainerClosePacket), + ContainerClick(ContainerClickPacket), + ContainerButtonClick(ContainerButtonClickPacket), + CraftItem(CraftItemPacket), + SetTime(SetTimePacket), + SetSpawnPosition(SetSpawnPositionPacket), + SetHealth(SetHealthPacket), + GameEvent(GameEventPacket), + EntityEvent(EntityEventPacket), + ContainerOpen(ContainerOpenPacket), + ContainerSetSlot(ContainerSetSlotPacket), + ContainerSetContent(ContainerSetContentPacket), + ContainerSetData(ContainerSetDataPacket), + ContainerAck(ContainerAckPacket), + ChunkVisibility(ChunkVisibilityPacket), + ChunkVisibilityArea(ChunkVisibilityAreaPacket), + BlockRegionUpdate(BlockRegionUpdatePacket), + TileUpdate(TileUpdatePacket), + AddPlayer(AddPlayerPacket), + AddEntity(AddEntityPacket), + AddMob(AddMobPacket), + TeleportEntity(TeleportEntityPacket), + RotateHead(RotateHeadPacket), + SetEntityMotion(SetEntityMotionPacket), + RemoveEntities(RemoveEntitiesPacket), + SetEntityData(SetEntityDataPacket), + Raw { id: u8, data: Vec }, +} + +impl LcePacket { + pub fn id(&self) -> u8 { + match self { + LcePacket::KeepAlive(_) => 0, + LcePacket::Login(_) => 1, + LcePacket::PreLogin(_) => 2, + LcePacket::Chat(_) => 3, + LcePacket::Disconnect(_) => 255, + LcePacket::Animate(_) => 18, + LcePacket::MovePlayer(_) => 10, + LcePacket::PlayerAction(_) => 14, + LcePacket::Interact(_) => 7, + LcePacket::UseItem(_) => 15, + LcePacket::PlayerCommand(_) => 19, + LcePacket::SetCarriedItem(_) => 16, + LcePacket::PlayerAbilities(_) => 202, + LcePacket::DebugOptions(_) => 152, + LcePacket::ClientCommand(_) => 205, + LcePacket::RespawnRequest(_) => 9, + LcePacket::ContainerClose(_) => 101, + LcePacket::ContainerClick(_) => 102, + LcePacket::ContainerButtonClick(_) => 108, + LcePacket::CraftItem(_) => 150, + LcePacket::SetTime(_) => 4, + LcePacket::SetSpawnPosition(_) => 6, + LcePacket::SetHealth(_) => 8, + LcePacket::GameEvent(_) => 70, + LcePacket::EntityEvent(_) => 38, + LcePacket::ContainerOpen(_) => 100, + LcePacket::ContainerSetSlot(_) => 103, + LcePacket::ContainerSetContent(_) => 104, + LcePacket::ContainerSetData(_) => 105, + LcePacket::ContainerAck(_) => 106, + LcePacket::ChunkVisibility(_) => 50, + LcePacket::ChunkVisibilityArea(_) => 155, + LcePacket::BlockRegionUpdate(_) => 51, + LcePacket::TileUpdate(_) => 53, + LcePacket::AddPlayer(_) => 20, + LcePacket::AddEntity(_) => 23, + LcePacket::AddMob(_) => 24, + LcePacket::TeleportEntity(_) => 34, + LcePacket::RotateHead(_) => 35, + LcePacket::SetEntityMotion(_) => 28, + LcePacket::RemoveEntities(_) => 29, + LcePacket::SetEntityData(_) => 40, + LcePacket::Raw { id, .. } => *id, + } + } +} + +#[derive(Debug, Clone)] +pub struct KeepAlivePacket { + pub keep_alive_id: i32, +} + +#[derive(Debug, Clone)] +pub struct LoginPacket { + pub protocol_version: i32, + pub username: String, + pub map_seed: u64, + pub game_type: i32, + pub world_name: String, + pub dimension: i32, + pub difficulty: i32, + pub max_players: u8, + pub world_width: i32, + pub world_length: i32, +} + +#[derive(Debug, Clone)] +pub struct PreLoginPacket { + pub net_version: i16, + pub player_name: String, + pub offline_xuid: i64, + pub online_xuid: i64, +} + +#[derive(Debug, Clone)] +pub struct ChatPacket { + pub message_type: u8, + pub string_args: Vec, + pub int_args: Vec, +} + +impl ChatPacket { + pub fn set_message(msg: &str) -> Self { + Self { + message_type: 1, + string_args: vec![msg.to_string()], + int_args: vec![], + } + } +} + +#[derive(Debug, Clone)] +pub struct DisconnectPacket { + pub reason: u8, +} + +#[derive(Debug, Clone)] +pub struct AnimatePacket { + pub entity_id: i32, + pub action: u8, +} + +#[derive(Debug, Clone)] +pub struct MovePlayerPacket { + pub id: u8, + pub x: f64, + pub y: f64, + pub y_view: f64, + pub z: f64, + pub yaw: f32, + pub pitch: f32, + pub flags: u8, +} + +#[derive(Debug, Clone)] +pub struct PlayerActionPacket { + pub action: u8, + pub x: i32, + pub y: i32, + pub z: i32, + pub face: u8, +} + +#[derive(Debug, Clone)] +pub struct PlayerCommandPacket { + pub entity_id: i32, + pub action: u8, + pub data: i32, +} + +#[derive(Debug, Clone)] +pub struct InteractPacket { + pub source: u8, + pub target: i32, + pub action: u8, +} + +#[derive(Debug, Clone)] +pub struct UseItemPacket { + pub x: i32, + pub y: i32, + pub z: i32, + pub face: u8, + pub item: LceItemStack, + pub click_x: f32, + pub click_y: f32, + pub click_z: f32, +} + +#[derive(Debug, Clone)] +pub struct SetCarriedItemPacket { + pub slot: u8, +} + +#[derive(Debug, Clone)] +pub struct PlayerAbilitiesPacket { + pub flags: u8, + pub fly_speed: f32, + pub walk_speed: f32, +} + +#[derive(Debug, Clone)] +pub struct DebugOptionsPacket { + pub options_mask: u32, +} + +#[derive(Debug, Clone)] +pub struct ClientCommandPacket { + pub action: u8, +} + +#[derive(Debug, Clone)] +pub struct RespawnRequestPacket; + +#[derive(Debug, Clone)] +pub struct ContainerClosePacket { + pub container_id: u8, +} + +#[derive(Debug, Clone)] +pub struct ContainerClickPacket { + pub container_id: u8, + pub slot_num: i16, + pub button_num: u8, + pub uid: i16, + pub click_type: u8, + pub item: LceItemStack, +} + +#[derive(Debug, Clone)] +pub struct ContainerButtonClickPacket { + pub container_id: u8, + pub button_id: u8, +} + +#[derive(Debug, Clone)] +pub struct CraftItemPacket { + pub uid: i16, + pub recipe: u8, +} + +#[derive(Debug, Clone)] +pub struct SetTimePacket { + pub game_time: i64, + pub day_time: i64, +} + +#[derive(Debug, Clone)] +pub struct SetSpawnPositionPacket { + pub x: i32, + pub y: i32, + pub z: i32, +} + +#[derive(Debug, Clone)] +pub struct SetHealthPacket { + pub health: f32, + pub food: i32, + pub saturation: f32, + pub damage_source: u8, +} + +#[derive(Debug, Clone)] +pub struct GameEventPacket { + pub reason: u8, + pub param: f32, +} + +#[derive(Debug, Clone)] +pub struct EntityEventPacket { + pub entity_id: i32, + pub event_id: u8, +} + +#[derive(Debug, Clone)] +pub struct ContainerOpenPacket { + pub container_id: u8, + pub container_type: u8, + pub size: u8, + pub custom_name: String, + pub title: String, + pub entity_id: i32, +} + +#[derive(Debug, Clone)] +pub struct ContainerSetSlotPacket { + pub container_id: u8, + pub slot: i16, + pub item: LceItemStack, +} + +#[derive(Debug, Clone)] +pub struct ContainerSetContentPacket { + pub container_id: u8, + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub struct ContainerSetDataPacket { + pub container_id: u8, + pub id: i16, + pub value: i16, +} + +#[derive(Debug, Clone)] +pub struct ContainerAckPacket { + pub container_id: u8, + pub uid: i16, + pub accepted: bool, +} + +#[derive(Debug, Clone)] +pub struct ChunkVisibilityPacket { + pub chunk_x: i32, + pub chunk_z: i32, + pub visible: bool, +} + +#[derive(Debug, Clone)] +pub struct ChunkVisibilityAreaPacket { + pub min_cx: i32, + pub max_cx: i32, + pub min_cz: i32, + pub max_cz: i32, +} + +#[derive(Debug, Clone)] +pub struct BlockRegionUpdatePacket { + pub x: i32, + pub y: i32, + pub z: i32, + pub xs: u8, + pub ys: u8, + pub zs: u8, + pub level_idx: u8, + pub is_full_chunk: bool, + pub compressed_data: Vec, +} + +#[derive(Debug, Clone)] +pub struct TileUpdatePacket { + pub x: i32, + pub y: i32, + pub z: i32, + pub block: u16, + pub data: u16, + pub level_idx: u8, +} + +#[derive(Debug, Clone)] +pub struct AddPlayerPacket { + pub entity_id: i32, + pub name: String, + pub x: i32, + pub y: i32, + pub z: i32, + pub yaw: u8, + pub pitch: u8, + pub head_yaw: u8, + pub carried_item: u16, + pub offline_xuid: i64, + pub online_xuid: i64, + pub player_index: i32, + pub skin_id: String, + pub cape_id: String, + pub game_privileges: u32, + pub metadata: Vec, +} + +#[derive(Debug, Clone)] +pub struct AddEntityPacket { + pub entity_id: i32, + pub entity_type: u8, + pub x: i32, + pub y: i32, + pub z: i32, + pub yaw: u8, + pub pitch: u8, + pub data: i32, + pub motion_x: i16, + pub motion_y: i16, + pub motion_z: i16, +} + +#[derive(Debug, Clone)] +pub struct AddMobPacket { + pub entity_id: i32, + pub entity_type: u8, + pub x: i32, + pub y: i32, + pub z: i32, + pub yaw: u8, + pub pitch: u8, + pub head_yaw: u8, + pub motion_x: i16, + pub motion_y: i16, + pub motion_z: i16, + pub metadata: Vec, +} + +#[derive(Debug, Clone)] +pub struct TeleportEntityPacket { + pub entity_id: i32, + pub x: i32, + pub y: i32, + pub z: i32, + pub yaw: u8, + pub pitch: u8, +} + +#[derive(Debug, Clone)] +pub struct RotateHeadPacket { + pub entity_id: i32, + pub y_head_rot: u8, +} + +#[derive(Debug, Clone)] +pub struct SetEntityMotionPacket { + pub entity_id: i32, + pub xa: i16, + pub ya: i16, + pub za: i16, +} + +#[derive(Debug, Clone)] +pub struct RemoveEntitiesPacket { + pub entity_ids: Vec, +} + +#[derive(Debug, Clone)] +pub struct SetEntityDataPacket { + pub entity_id: i32, + pub values: Vec, +} + +#[derive(Debug, Clone)] +pub struct EntityDataValue { + pub id: u8, + pub data_type: u8, + pub value: EntityDataValueType, +} + +#[derive(Debug, Clone)] +pub enum EntityDataValueType { + Byte(u8), + Short(i16), + Int(i32), + Float(f32), + String(String), + ItemStack(LceItemStack), +} + +#[derive(Debug, Clone, Default)] +pub struct LceItemStack { + pub id: i16, + pub count: u8, + pub damage: i16, +} + +impl LceItemStack { + pub fn is_empty(&self) -> bool { + self.id <= 0 || self.count == 0 + } + + pub fn empty() -> Self { + Self { id: 0, count: 0, damage: 0 } + } +} diff --git a/src-tauri/src/lce_bridge/mod.rs b/src-tauri/src/lce_bridge/mod.rs new file mode 100644 index 0000000..eab7368 --- /dev/null +++ b/src-tauri/src/lce_bridge/mod.rs @@ -0,0 +1,61 @@ +pub mod config; +pub mod util; +pub mod lce_packets; +pub mod lce_codec; +pub mod java_protocol; +pub mod java_session; +pub mod registry; +pub mod chunk; +pub mod crafting; +pub mod bridge; +pub mod msa_auth; + +use crate::lce_bridge::config::BridgeConfig; +use crate::lce_bridge::lce_codec::run_lce_server; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct BridgeHandle { + cancel: tokio::sync::watch::Sender, +} + +static BRIDGE_INSTANCE: std::sync::OnceLock>>> = std::sync::OnceLock::new(); + +fn bridge_instance() -> &'static Arc>> { + BRIDGE_INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))) +} + +pub async fn start_bridge(config: BridgeConfig) -> Result { + let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false); + let handle = BridgeHandle { cancel: cancel_tx.clone() }; + *bridge_instance().lock().await = Some(handle); + let config_clone = config.clone(); + tokio::spawn(async move { + let mut cancel_rx = cancel_rx; + let server_fut = run_lce_server(config_clone); + tokio::select! { + _ = server_fut => {} + _ = cancel_rx.changed() => {} + } + }); + Ok(format!("LCEBridge started on {}:{}", config.lce.listen_address, config.lce.port)) +} + +pub async fn stop_bridge() -> Result { + let mut guard = bridge_instance().lock().await; + if let Some(handle) = guard.take() { + let _ = handle.cancel.send(true); + Ok("LCEBridge stopped".to_string()) + } else { + Err("LCEBridge is not running".to_string()) + } +} + +pub async fn bridge_status() -> Result { + let guard = bridge_instance().lock().await; + if guard.is_some() { + Ok("running".to_string()) + } else { + Ok("stopped".to_string()) + } +} diff --git a/src-tauri/src/lce_bridge/msa_auth.rs b/src-tauri/src/lce_bridge/msa_auth.rs new file mode 100644 index 0000000..4f61d1d --- /dev/null +++ b/src-tauri/src/lce_bridge/msa_auth.rs @@ -0,0 +1,384 @@ +use base64::Engine; +use p256::ecdsa::{SigningKey, Signature, signature::Signer}; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::SecretKey; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::OnceLock; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::broadcast; + +const CLIENT_ID: &str = "00000000402b5328"; +const SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL"; +const CACHE_FILE: &str = "msa_cache.json"; + +static DEVICE_CODE_TX: OnceLock> = OnceLock::new(); + +fn device_code_tx() -> broadcast::Sender { + DEVICE_CODE_TX.get_or_init(|| { + let (tx, _) = broadcast::channel(8); + tx + }).clone() +} + +pub fn subscribe_device_code() -> broadcast::Receiver { + device_code_tx().subscribe() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinecraftProfile { + pub uuid: String, + pub username: String, + pub access_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct TokenCache { + access_token: String, + refresh_token: Option, + mc_uuid: Option, + mc_username: Option, + mc_access_token: Option, + expires_at: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct DeviceCodeResponse { + device_code: String, + user_code: String, + verification_uri: String, + expires_in: u64, + interval: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct TokenResponse { + access_token: String, + refresh_token: Option, + expires_in: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct TokenError { + error: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +struct DeviceAuthResponse { + token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct McLoginResponse { + access_token: String, + username: String, + role: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct McProfileResponse { + id: String, + name: String, +} + +struct AuthKey { + signing: SigningKey, + verifying: p256::PublicKey, +} + +static AUTH_KEY: OnceLock = OnceLock::new(); + +fn auth_key() -> &'static AuthKey { + AUTH_KEY.get_or_init(|| { + let secret = SecretKey::random(&mut rand::rngs::OsRng); + let verifying = secret.public_key(); + let signing = secret.into(); + AuthKey { signing, verifying } + }) +} + +fn windows_timestamp() -> i64 { + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + ((secs + 11644473600) * 10_000_000) as i64 +} + +fn build_signature(method: &str, path: &str, body: &[u8]) -> String { + let key = auth_key(); + let ts = windows_timestamp(); + + let mut content = Vec::new(); + content.extend_from_slice(&1i32.to_be_bytes()); + content.push(0); + content.extend_from_slice(&ts.to_be_bytes()); + content.push(0); + content.extend_from_slice(method.as_bytes()); + content.push(0); + content.extend_from_slice(path.as_bytes()); + content.push(0); + content.push(0); + content.extend_from_slice(body); + content.push(0); + + let sig: Signature = key.signing.sign(&content); + let sig_bytes = sig.to_bytes(); + + let mut header = Vec::new(); + header.extend_from_slice(&1i32.to_be_bytes()); + header.extend_from_slice(&ts.to_be_bytes()); + header.extend_from_slice(&sig_bytes[..]); + + base64::engine::general_purpose::STANDARD.encode(&header) +} + +fn proof_key_value() -> serde_json::Value { + let key = auth_key(); + let encoded = key.verifying.to_encoded_point(false); + serde_json::json!({ + "kty": "EC", + "alg": "ES256", + "crv": "P-256", + "use": "sig", + "x": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.x().unwrap()), + "y": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(encoded.y().unwrap()), + }) +} + +fn cache_path() -> PathBuf { + let mut p = std::env::current_exe().unwrap_or_default(); + p.pop(); + p.push(CACHE_FILE); + p +} + +fn load_cache() -> Option { + let path = cache_path(); + std::fs::read_to_string(&path).ok().and_then(|s| serde_json::from_str(&s).ok()) +} + +fn save_cache(cache: &TokenCache) { + if let Ok(s) = serde_json::to_string(cache) { + let _ = std::fs::write(cache_path(), &s); + } +} + +async fn request_device_code() -> Result { + let client = reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| format!("build client: {e}"))?; + let mut params = vec![ + ("client_id", CLIENT_ID), + ("scope", SCOPE), + ]; + params.push(("response_type", "device_code")); + let resp = client + .post("https://login.live.com/oauth20_connect.srf") + .form(¶ms) + .send() + .await + .map_err(|e| format!("device code request failed: {e}"))?; + let status = resp.status(); + let text = resp.text().await.map_err(|e| format!("read device code response: {e}"))?; + if !status.is_success() { + return Err(format!("device code HTTP {status}: {text}")); + } + let dc: DeviceCodeResponse = serde_json::from_str(&text) + .map_err(|e| format!("parse device code: {e} (raw: {text})"))?; + let msg = format!("{}|{}", dc.user_code, dc.verification_uri); + eprintln!("[MSA] device code: {msg}"); + let _ = device_code_tx().send(msg); + Ok(dc) +} + +async fn poll_token(device_code: &str) -> Result { + let client = reqwest::Client::new(); + loop { + let params = [ + ("client_id", CLIENT_ID), + ("grant_type", "device_code"), + ("device_code", device_code), + ]; + let resp = client + .post("https://login.live.com/oauth20_token.srf") + .form(¶ms) + .send() + .await + .map_err(|e| format!("token poll failed: {e}"))?; + let text = resp.text().await.map_err(|e| format!("read token response: {e}"))?; + if let Ok(tok) = serde_json::from_str::(&text) { + return Ok(tok); + } + if let Ok(err) = serde_json::from_str::(&text) { + if err.error == "authorization_pending" || err.error == "slow_down" { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + continue; + } + return Err(format!("token error: {}", err.error)); + } + return Err(format!("unexpected token response: {text}")); + } +} + +async fn device_authenticate() -> Result { + let client = reqwest::Client::new(); + let device_id = format!("{{{}}}", uuid::Uuid::new_v4()); + let pk = proof_key_value(); + let body = serde_json::json!({ + "Properties": { + "DeviceType": "Win32", + "Id": device_id, + "AuthMethod": "ProofOfPossession", + "ProofKey": pk, + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT", + }); + let body_bytes = serde_json::to_vec(&body).map_err(|e| format!("serialize device body: {e}"))?; + let sig = build_signature("POST", "/device/authenticate", &body_bytes); + + let resp = client + .post("https://device.auth.xboxlive.com/device/authenticate") + .header("x-xbl-contract-version", "1") + .header("Signature", &sig) + .header("Accept", "application/json") + .body(body_bytes) + .send() + .await + .map_err(|e| format!("device authenticate: {e}"))?; + + let status = resp.status(); + let text = resp.text().await.map_err(|e| format!("device authenticate read: {e}"))?; + if !status.is_success() { + return Err(format!("device auth HTTP {status}: {text}")); + } + let auth: DeviceAuthResponse = serde_json::from_str(&text) + .map_err(|e| format!("parse device auth: {e} {text}"))?; + Ok(auth.token) +} + +async fn sisu_authorize(msa_token: &str, device_token: &str) -> Result<(String, String), String> { + let client = reqwest::Client::new(); + let pk = proof_key_value(); + let body = serde_json::json!({ + "Sandbox": "RETAIL", + "UseModernGamertag": true, + "AppId": CLIENT_ID, + "AccessToken": format!("t={msa_token}"), + "DeviceToken": device_token, + "ProofKey": pk, + "RelyingParty": "rp://api.minecraftservices.com/", + }); + let body_bytes = serde_json::to_vec(&body).map_err(|e| format!("serialize sisu body: {e}"))?; + let sig = build_signature("POST", "/authorize", &body_bytes); + + let resp = client + .post("https://sisu.xboxlive.com/authorize") + .header("Signature", &sig) + .header("Accept", "application/json") + .body(body_bytes) + .send() + .await + .map_err(|e| format!("sisu auth: {e}"))?; + + let status = resp.status(); + let text = resp.text().await.map_err(|e| format!("sisu auth read: {e}"))?; + if !status.is_success() { + return Err(format!("sisu auth HTTP {status}: {text}")); + } + + #[derive(Deserialize)] + struct SisuResp { + #[serde(rename = "UserToken")] + user_token: SisuTok, + #[serde(rename = "AuthorizationToken")] + auth_token: SisuTok, + } + #[derive(Deserialize)] + struct SisuTok { + #[serde(rename = "Token")] + token: String, + #[serde(rename = "DisplayClaims")] + claims: SisuClaims, + } + #[derive(Deserialize)] + struct SisuClaims { + xui: Vec, + } + #[derive(Deserialize)] + struct SisuXui { + uhs: String, + } + + let sisu: SisuResp = serde_json::from_str(&text) + .map_err(|e| format!("parse sisu: {e} {text}"))?; + let xsts_token = sisu.auth_token.token; + let uhs = sisu.user_token.claims.xui.first() + .ok_or_else(|| "no uhs in sisu user token".to_string())? + .uhs.clone(); + Ok((xsts_token, uhs)) +} + +async fn mc_login(xsts_token: &str, uhs: &str) -> Result<(String, String, String), String> { + let client = reqwest::Client::new(); + let identity = format!("XBL3.0 x={uhs};{xsts_token}"); + let body = serde_json::json!({ "identityToken": identity }); + let resp = client + .post("https://api.minecraftservices.com/authentication/login_with_xbox") + .json(&body) + .send() + .await + .map_err(|e| format!("mc login: {e}"))?; + let text = resp.text().await.map_err(|e| format!("mc login read: {e}"))?; + let login: McLoginResponse = serde_json::from_str(&text) + .map_err(|e| format!("parse mc login: {e} {text}"))?; + + let profile_resp = client + .get("https://api.minecraftservices.com/minecraft/profile") + .header("Authorization", format!("Bearer {}", login.access_token)) + .send() + .await + .map_err(|e| format!("mc profile: {e}"))?; + let ptext = profile_resp.text().await.map_err(|e| format!("mc profile read: {e}"))?; + let profile: McProfileResponse = serde_json::from_str(&ptext) + .map_err(|e| format!("parse mc profile: {e} {ptext}"))?; + + Ok((profile.id, profile.name, login.access_token)) +} + +pub async fn authenticate() -> Result { + if let Some(cached) = load_cache() { + if let (Some(uuid), Some(uname), Some(token)) = + (cached.mc_uuid, cached.mc_username, cached.mc_access_token) + { + return Ok(MinecraftProfile { uuid, username: uname, access_token: token }); + } + } + + let device = request_device_code().await?; + + let tok = poll_token(&device.device_code).await?; + let expires = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + tok.expires_in; + + let device_token = device_authenticate().await?; + let (xsts_token, uhs) = sisu_authorize(&tok.access_token, &device_token).await?; + let (uuid, username, mc_token) = mc_login(&xsts_token, &uhs).await?; + + let cache = TokenCache { + access_token: tok.access_token, + refresh_token: tok.refresh_token, + mc_uuid: Some(uuid.clone()), + mc_username: Some(username.clone()), + mc_access_token: Some(mc_token.clone()), + expires_at: expires, + }; + save_cache(&cache); + + Ok(MinecraftProfile { uuid, username, access_token: mc_token }) +} diff --git a/src-tauri/src/lce_bridge/registry.rs b/src-tauri/src/lce_bridge/registry.rs new file mode 100644 index 0000000..0cd2168 --- /dev/null +++ b/src-tauri/src/lce_bridge/registry.rs @@ -0,0 +1,149 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + +static MAPPINGS: OnceLock = OnceLock::new(); + +pub fn registry() -> &'static MappingRegistry { + MAPPINGS.get_or_init(|| { + let mut r = MappingRegistry { + block_map: HashMap::new(), + biome_map: HashMap::new(), + fallback_block: u16::MAX, + }; + r.load_default(); + r + }) +} + +pub struct MappingRegistry { + block_map: HashMap, + biome_map: HashMap, + fallback_block: u16, +} + +impl MappingRegistry { + fn load_default(&mut self) { + for (java_id, lce_id) in DEFAULT_BLOCK_MAP { + self.block_map.insert(*java_id, *lce_id); + } + for (name, id) in DEFAULT_BIOME_MAP { + self.biome_map.insert(name.to_string(), *id); + } + self.fallback_block = (1 << 8) | 0; + } + + pub fn is_air(&self, java_state_id: i32) -> bool { + java_state_id == 0 || java_state_id == 1024 + } + + pub fn get_lce(&self, java_state_id: i32) -> u16 { + self.block_map.get(&java_state_id).copied().unwrap_or(self.fallback_block) + } + + pub fn get_lce_id(&self, java_state_id: i32) -> u8 { + (self.get_lce(java_state_id) >> 8) as u8 + } + + pub fn get_lce_data(&self, java_state_id: i32) -> u8 { + (self.get_lce(java_state_id) & 0xFF) as u8 + } + + pub fn get_biome(&self, java_name: &str) -> u8 { + self.biome_map.get(java_name).copied().unwrap_or(1) + } + + pub fn register_block(&mut self, java_state_id: i32, packed_lce: u16) { + self.block_map.insert(java_state_id, packed_lce); + } +} + +pub fn get_lce_block_id(java_state_id: i32) -> u8 { + registry().get_lce_id(java_state_id) +} + +pub fn get_lce_block_data(java_state_id: i32) -> u8 { + registry().get_lce_data(java_state_id) +} + +pub fn get_lce_biome(java_name: &str) -> u8 { + registry().get_biome(java_name) +} + +static DEFAULT_BLOCK_MAP: &[(i32, u16)] = &[ + (0, 0), (1, 1<<8|0), (2, 2<<8|0), (3, 3<<8|0), (4, 4<<8|0), + (5, 5<<8|0), (5, 5<<8|1), (5, 5<<8|2), (5, 5<<8|3), (5, 5<<8|4), (5, 5<<8|5), + (7, 7<<8|0), (8, 8<<8|0), (9, 9<<8|0), (10, 10<<8|0), (11, 11<<8|0), + (12, 12<<8|0), (13, 13<<8|0), (14, 14<<8|0), (15, 15<<8|0), + (16, 16<<8|0), (16, 16<<8|1), (16, 16<<8|2), (16, 16<<8|3), (16, 16<<8|4), (16, 16<<8|5), + (17, 17<<8|0), (17, 17<<8|1), (17, 17<<8|2), (17, 17<<8|3), + (18, 18<<8|0), (18, 18<<8|1), (18, 18<<8|2), (18, 18<<8|3), + (19, 19<<8|0), (19, 19<<8|1), (19, 19<<8|2), + (20, 20<<8|0), (21, 21<<8|0), + (22, 22<<8|0), (24, 24<<8|0), (24, 24<<8|1), (24, 24<<8|2), + (25, 25<<8|0), (35, 35<<8|0), (35, 35<<8|1), (35, 35<<8|2), (35, 35<<8|3), + (35, 35<<8|4), (35, 35<<8|5), (35, 35<<8|6), (35, 35<<8|7), + (35, 35<<8|8), (35, 35<<8|9), (35, 35<<8|10), (35, 35<<8|11), + (35, 35<<8|12), (35, 35<<8|13), (35, 35<<8|14), (35, 35<<8|15), + (41, 41<<8|0), (42, 42<<8|0), (43, 43<<8|0), (43, 43<<8|8), + (44, 44<<8|0), (44, 44<<8|1), (44, 44<<8|2), (44, 44<<8|3), + (44, 44<<8|4), (44, 44<<8|5), (44, 44<<8|6), (44, 44<<8|7), + (45, 45<<8|0), (46, 46<<8|0), (47, 47<<8|0), (48, 48<<8|0), + (49, 49<<8|0), (50, 50<<8|0), (50, 50<<8|1), (50, 50<<8|2), (50, 50<<8|3), (50, 50<<8|4), (50, 50<<8|5), + (52, 52<<8|0), (53, 53<<8|0), (53, 53<<8|1), (53, 53<<8|2), (53, 53<<8|3), + (56, 56<<8|0), (57, 57<<8|0), (58, 58<<8|0), (60, 60<<8|0), + (61, 61<<8|0), (62, 62<<8|0), (73, 73<<8|0), (74, 74<<8|0), + (78, 78<<8|0), (79, 79<<8|0), (80, 80<<8|0), (81, 81<<8|0), + (82, 82<<8|0), (83, 83<<8|0), (84, 84<<8|0), (85, 85<<8|0), + (86, 86<<8|0), (87, 87<<8|0), (88, 88<<8|0), (89, 89<<8|0), + (91, 91<<8|0), (95, 95<<8|0), (97, 97<<8|0), (98, 98<<8|0), + (98, 98<<8|1), (98, 98<<8|2), (98, 98<<8|3), + (99, 99<<8|0), (100, 100<<8|0), (101, 101<<8|0), (102, 102<<8|0), + (103, 103<<8|0), (107, 107<<8|0), (108, 108<<8|0), (109, 109<<8|0), + (110, 110<<8|0), (111, 111<<8|0), (112, 112<<8|0), (113, 113<<8|0), + (114, 114<<8|0), (121, 121<<8|0), (123, 123<<8|0), (124, 124<<8|0), + (125, 125<<8|0), (126, 126<<8|0), (128, 128<<8|0), (129, 129<<8|0), + (130, 130<<8|0), (133, 133<<8|0), (134, 134<<8|0), (135, 135<<8|0), + (136, 136<<8|0), (138, 138<<8|0), (139, 139<<8|0), (140, 140<<8|0), + (141, 141<<8|0), (142, 142<<8|0), (144, 144<<8|0), (145, 145<<8|0), + (147, 147<<8|0), (148, 148<<8|0), (151, 151<<8|0), (152, 152<<8|0), + (153, 153<<8|0), (154, 154<<8|0), (155, 155<<8|0), (156, 156<<8|0), + (158, 158<<8|0), (159, 159<<8|0), (159, 159<<8|1), (159, 159<<8|2), (159, 159<<8|3), + (159, 159<<8|4), (159, 159<<8|5), (159, 159<<8|6), (159, 159<<8|7), + (159, 159<<8|8), (159, 159<<8|9), (159, 159<<8|10), (159, 159<<8|11), + (159, 159<<8|12), (159, 159<<8|13), (159, 159<<8|14), (159, 159<<8|15), + (161, 161<<8|0), (161, 161<<8|1), (162, 162<<8|0), (162, 162<<8|1), + (163, 163<<8|0), (164, 164<<8|0), (165, 165<<8|0), (167, 167<<8|0), + (168, 168<<8|0), (168, 168<<8|1), (168, 168<<8|2), + (169, 169<<8|0), (170, 170<<8|0), (171, 171<<8|0), (171, 171<<8|1), + (171, 171<<8|2), (171, 171<<8|3), (171, 171<<8|4), (171, 171<<8|5), + (171, 171<<8|6), (171, 171<<8|7), (171, 171<<8|8), (171, 171<<8|9), + (171, 171<<8|10), (171, 171<<8|11), (171, 171<<8|12), (171, 171<<8|13), + (171, 171<<8|14), (171, 171<<8|15), + (172, 172<<8|0), (173, 173<<8|0), +]; + +static DEFAULT_BIOME_MAP: &[(&str, u8)] = &[ + ("minecraft:plains", 1), ("minecraft:desert", 2), ("minecraft:forest", 4), + ("minecraft:taiga", 5), ("minecraft:swamp", 6), ("minecraft:river", 7), + ("minecraft:frozen_river", 7), ("minecraft:ocean", 0), + ("minecraft:cold_ocean", 0), ("minecraft:deep_ocean", 0), + ("minecraft:frozen_ocean", 10), ("minecraft:deep_frozen_ocean", 10), + ("minecraft:beach", 16), ("minecraft:stone_beach", 16), + ("minecraft:snowy_beach", 16), ("minecraft:birch_forest", 27), + ("minecraft:dark_forest", 29), ("minecraft:flower_forest", 28), + ("minecraft:ice_spikes", 12), ("minecraft:jungle", 21), + ("minecraft:mushroom_fields", 14), ("minecraft:nether_wastes", 8), + ("minecraft:savanna", 36), ("minecraft:snowy_taiga", 30), + ("minecraft:snowy_plains", 11), ("minecraft:sunflower_plains", 1), + ("minecraft:the_end", 9), ("minecraft:the_void", 9), + ("minecraft:warm_ocean", 0), ("minecraft:lukewarm_ocean", 0), + ("minecraft:badlands", 39), ("minecraft:wooded_badlands", 39), + ("minecraft:eroded_badlands", 39), ("minecraft:meadow", 1), + ("minecraft:windswept_hills", 15), ("minecraft:windswept_forest", 15), + ("minecraft:windswept_gravelly_hills", 15), ("minecraft:windswept_savanna", 36), + ("minecraft:grove", 5), ("minecraft:snowy_slopes", 12), + ("minecraft:jagged_peaks", 24), ("minecraft:frozen_peaks", 24), + ("minecraft:stony_peaks", 15), ("minecraft:cherry_grove", 28), + ("minecraft:mangrove_swamp", 6), ("minecraft:deep_dark", 2), + ("minecraft:dripstone_caves", 2), ("minecraft:lush_caves", 2), +]; diff --git a/src-tauri/src/lce_bridge/util.rs b/src-tauri/src/lce_bridge/util.rs new file mode 100644 index 0000000..64fff3a --- /dev/null +++ b/src-tauri/src/lce_bridge/util.rs @@ -0,0 +1,226 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::io::{Cursor, Read}; + +pub struct LceReader<'a> { + cursor: Cursor<&'a [u8]>, +} + +impl<'a> LceReader<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { cursor: Cursor::new(data) } + } + + pub fn remaining(&self) -> usize { + self.cursor.get_ref().len() - self.cursor.position() as usize + } + + pub fn read_u8(&mut self) -> std::io::Result { + self.cursor.read_u8() + } + + pub fn read_i8(&mut self) -> std::io::Result { + self.cursor.read_i8() + } + + pub fn read_u16(&mut self) -> std::io::Result { + self.cursor.read_u16::() + } + + pub fn read_i16(&mut self) -> std::io::Result { + self.cursor.read_i16::() + } + + pub fn read_u32(&mut self) -> std::io::Result { + self.cursor.read_u32::() + } + + pub fn read_i32(&mut self) -> std::io::Result { + self.cursor.read_i32::() + } + + pub fn read_u64(&mut self) -> std::io::Result { + self.cursor.read_u64::() + } + + pub fn read_i64(&mut self) -> std::io::Result { + self.cursor.read_i64::() + } + + pub fn read_f32(&mut self) -> std::io::Result { + self.cursor.read_f32::() + } + + pub fn read_f64(&mut self) -> std::io::Result { + self.cursor.read_f64::() + } + + pub fn read_bytes(&mut self, len: usize) -> std::io::Result> { + let mut buf = vec![0u8; len]; + self.cursor.read_exact(&mut buf)?; + Ok(buf) + } + + pub fn read_bool(&mut self) -> std::io::Result { + Ok(self.cursor.read_u8()? != 0) + } + + pub fn read_utf16(&mut self, max_length: u32) -> std::io::Result { + let len = self.read_u16()? as usize; + if len > max_length as usize { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, + format!("UTF-16 string too long: {} > {}", len, max_length))); + } + let mut chars = Vec::with_capacity(len); + for _ in 0..len { + chars.push(self.read_u16()?); + } + String::from_utf16(&chars) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + } + + pub fn read_string_utf16(&mut self) -> std::io::Result { + self.read_utf16(0xFFFF) + } + + pub fn skip(&mut self, count: usize) -> std::io::Result<()> { + let pos = self.cursor.position() as usize + count; + if pos > self.cursor.get_ref().len() { + return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "skip past end")); + } + self.cursor.set_position(pos as u64); + Ok(()) + } +} + +pub struct LceWriter { + buf: Vec, +} + +impl LceWriter { + pub fn new() -> Self { + Self { buf: Vec::new() } + } + + pub fn into_bytes(self) -> Vec { + self.buf + } + + pub fn write_u8(&mut self, v: u8) { + let _ = self.buf.write_u8(v); + } + + pub fn write_i8(&mut self, v: i8) { + let _ = self.buf.write_i8(v); + } + + pub fn write_u16(&mut self, v: u16) { + let _ = self.buf.write_u16::(v); + } + + pub fn write_i16(&mut self, v: i16) { + let _ = self.buf.write_i16::(v); + } + + pub fn write_u32(&mut self, v: u32) { + let _ = self.buf.write_u32::(v); + } + + pub fn write_i32(&mut self, v: i32) { + let _ = self.buf.write_i32::(v); + } + + pub fn write_u64(&mut self, v: u64) { + let _ = self.buf.write_u64::(v); + } + + pub fn write_i64(&mut self, v: i64) { + let _ = self.buf.write_i64::(v); + } + + pub fn write_f32(&mut self, v: f32) { + let _ = self.buf.write_f32::(v); + } + + pub fn write_f64(&mut self, v: f64) { + let _ = self.buf.write_f64::(v); + } + + pub fn write_bytes(&mut self, data: &[u8]) { + self.buf.extend_from_slice(data); + } + + pub fn write_bool(&mut self, v: bool) { + self.write_u8(if v { 1 } else { 0 }); + } + + pub fn write_utf16(&mut self, s: &str) { + let encoded: Vec = s.encode_utf16().collect(); + self.write_u16(encoded.len() as u16); + for c in encoded { + self.write_u16(c); + } + } +} + +pub struct VarInt; + +impl VarInt { + pub fn read(reader: &mut &[u8]) -> std::io::Result { + let mut result = 0i32; + let mut shift = 0; + loop { + if reader.is_empty() { + return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "varint truncated")); + } + let byte = reader[0]; + *reader = &reader[1..]; + result |= ((byte & 0x7F) as i32) << shift; + shift += 7; + if byte & 0x80 == 0 { + return Ok(result); + } + } + } + + pub fn write_to(val: i32, buf: &mut Vec) { + let mut v = val as u32; + loop { + if v & !0x7F == 0 { + buf.push(v as u8); + return; + } + buf.push((v & 0x7F) as u8 | 0x80); + v >>= 7; + } + } + + pub fn write(val: i32) -> Vec { + let mut buf = Vec::new(); + let mut v = val as u32; + loop { + if v & !0x7F == 0 { + buf.push(v as u8); + return buf; + } + buf.push((v & 0x7F) as u8 | 0x80); + v >>= 7; + } + } + + pub fn encode_len(val: i32) -> usize { + let mut len = 1; + let mut v = val as u32 >> 7; + while v != 0 { + v >>= 7; + len += 1; + } + len + } +} + +pub fn read_varint_from_slice(data: &[u8]) -> std::io::Result<(i32, &[u8])> { + let mut reader = data; + let val = VarInt::read(&mut reader)?; + let consumed = data.len() - reader.len(); + Ok((val, &data[consumed..])) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8c86541..857fde5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,11 +4,14 @@ mod config; mod util; mod platform; mod networking; +pub mod lce_bridge; +mod workshop_server; mod commands; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; use tauri::Manager; +use commands::bridge as bridge_cmds; use commands::config as config_cmds; use commands::download; use commands::file_dialogs; @@ -33,6 +36,7 @@ pub fn run() { }) .manage(GameState { child: Arc::new(Mutex::new(None)), + workshop_cancel: Arc::new(Mutex::new(None)), }) .manage(ProxyGuard { cancel_tokens: Arc::new(Mutex::new(HashMap::new())), @@ -62,6 +66,9 @@ pub fn run() { workshop::workshop_install, workshop::workshop_uninstall, workshop::workshop_list_installed, + bridge_cmds::bridge_start, + bridge_cmds::bridge_stop, + bridge_cmds::bridge_status, skin::get_screenshots, skin::delete_screenshot, skin::open_screenshot_folder, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 2e70003..ab777c1 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -8,6 +8,7 @@ pub struct DownloadState { pub struct GameState { pub child: Arc>>, + pub workshop_cancel: Arc>>, } pub struct ProxyGuard { diff --git a/src-tauri/src/workshop_server.rs b/src-tauri/src/workshop_server.rs new file mode 100644 index 0000000..f868f22 --- /dev/null +++ b/src-tauri/src/workshop_server.rs @@ -0,0 +1,131 @@ +use once_cell::sync::Lazy; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; +const REGISTRY_URL: &str = "https://raw.githubusercontent.com/LCE-Hub/LCE-Workshop/refs/heads/main"; +static CLIENT: Lazy = Lazy::new(|| reqwest::Client::new()); +pub struct Guard { + cancel: Option, +} + +impl Guard { + pub fn new(cancel: CancellationToken) -> Self { + Self { cancel: Some(cancel) } + } +} + +impl Drop for Guard { + fn drop(&mut self) { + if let Some(cancel) = self.cancel.take() { + cancel.cancel(); + } + } +} + +pub async fn start() -> CancellationToken { + let cancel = CancellationToken::new(); + let server_cancel = cancel.clone(); + tokio::spawn(async move { + serve(server_cancel).await; + }); + + cancel +} + +async fn serve(cancel: CancellationToken) { + let listener = match TcpListener::bind("127.0.0.1:5582").await { + Ok(l) => l, + Err(e) => { + eprintln!("[WorkshopServer] Failed to bind: {e}"); + return; + } + }; + + loop { + tokio::select! { + result = listener.accept() => { + match result { + Ok((stream, _)) => { + tokio::spawn(handle(stream)); + } + Err(e) => { + eprintln!("[WorkshopServer] Accept error: {e}"); + } + } + } + _ = cancel.cancelled() => break, + } + } +} + +async fn handle(stream: tokio::net::TcpStream) { + let (reader, mut writer) = stream.into_split(); + let mut buf_reader = BufReader::new(reader); + let mut request_line = String::new(); + if buf_reader.read_line(&mut request_line).await.is_err() { + return; + } + let request_line = request_line.trim(); + loop { + let mut line = String::new(); + if buf_reader.read_line(&mut line).await.is_err() { + return; + } + if line == "\r\n" || line == "\n" { + break; + } + } + + if request_line.starts_with("GET /workshop/") && request_line.ends_with(" HTTP/1.1") { + let path = &request_line["GET /workshop/".len()..request_line.len() - " HTTP/1.1".len()]; + match fetch_workshop_file(path).await { + Ok(body) => { + let body_bytes = &body; + let content_type = if path.ends_with(".json") { + "application/json" + } else if path.ends_with(".png") { + "image/png" + } else { + "application/octet-stream" + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n", + content_type, + body.len(), + ); + let _ = writer.write_all(response.as_bytes()).await; + let _ = writer.write_all(body_bytes).await; + } + Err(e) => { + let status = if e.contains("404") { 404 } else { 502 }; + let body = format!("{{\"error\": \"{e}\"}}"); + let response = format!( + "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + status, + if status == 404 { "Not Found" } else { "Bad Gateway" }, + body.len(), + body + ); + let _ = writer.write_all(response.as_bytes()).await; + } + } + } else { + let body = "{\"error\": \"not found\"}"; + let response = format!( + "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + let _ = writer.write_all(response.as_bytes()).await; + } +} + +async fn fetch_workshop_file(path: &str) -> Result, String> { + let url = format!("{}/{}", REGISTRY_URL, path); + let resp = CLIENT.get(&url).send().await.map_err(|e| e.to_string())?; + if resp.status().is_success() { + resp.bytes().await.map(|b| b.to_vec()).map_err(|e| e.to_string()) + } else { + Err(format!("GitHub returned {}", resp.status())) + } +} diff --git a/src/components/common/SkinViewer.tsx b/src/components/common/SkinViewer.tsx index 23cbfe0..c29560b 100644 --- a/src/components/common/SkinViewer.tsx +++ b/src/components/common/SkinViewer.tsx @@ -1,8 +1,9 @@ -import { useEffect, useRef, useState, memo } from 'react'; +import { useEffect, useRef, useState, memo, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import { motion } from 'framer-motion'; import * as THREE from 'three'; -import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useConfig } from '../../context/LauncherContext'; +import { TauriService } from '../../services/TauriService'; type UVSet = Record; @@ -18,15 +19,66 @@ interface SkinViewerProps { onNavigateRight: () => void; } -const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSound, skinUrl, capeUrl, setSkinUrl, setActiveView, isFocusedSection, onNavigateRight }: SkinViewerProps) { +const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSound, skinUrl, capeUrl, setActiveView, isFocusedSection, onNavigateRight }: SkinViewerProps) { const mountRef = useRef(null); const containerRef = useRef(null); const [focusIndex, setFocusIndex] = useState(0); const { legacyMode } = useConfig(); - const [showLayers, setShowLayers] = useLocalStorage('lce-show-layers', true); const overlaysRef = useRef([]); const capeRef = useRef(null); const requestRenderRef = useRef<(() => void) | null>(null); + + const [showBridgeMenu, setShowBridgeMenu] = useState(false); + const [bridgeActive, setBridgeActive] = useState(false); + const [bridgeStarting, setBridgeStarting] = useState(false); + const [bridgeRemoteHost, setBridgeRemoteHost] = useState("127.0.0.1"); + const [bridgeRemotePort, setBridgeRemotePort] = useState("25565"); + const [bridgeProtocolVersion, setBridgeProtocolVersion] = useState(767); + const [bridgeAuthType, setBridgeAuthType] = useState("offline"); + const [msaCode, setMsaCode] = useState(""); + + useEffect(() => { + let unlisten: () => void; + (async () => { + const { listen } = await import("@tauri-apps/api/event"); + const u = await listen("lcebridge-msa-code", (e) => { + setMsaCode(e.payload); + }); + unlisten = u; + })(); + return () => { unlisten?.(); }; + }, []); + + const toggleBridge = useCallback(async () => { + if (bridgeActive) { + await TauriService.bridgeStop(); + setBridgeActive(false); + } else { + setBridgeStarting(true); + setMsaCode(""); + try { + await TauriService.bridgeStart( + "0.0.0.0", 25656, + bridgeRemoteHost, + parseInt(bridgeRemotePort) || 25565, + bridgeAuthType, + bridgeProtocolVersion, + ); + setBridgeActive(true); + } catch (e) { + console.error("Bridge start failed:", e); + } finally { + setBridgeStarting(false); + } + } + }, [bridgeActive, bridgeRemoteHost, bridgeRemotePort, bridgeProtocolVersion, bridgeAuthType]); + + useEffect(() => { + TauriService.bridgeStatus().then((s) => { + setBridgeActive(s === "running"); + }).catch(() => {}); + }, []); + useEffect(() => { if (!mountRef.current) return; const width = 260; @@ -68,12 +120,12 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo const getMats = (uvSet: UVSet) => { const flipX = isLegacyMirror; return [ - createFaceMaterial(swapMats ? uvSet.right[0] : uvSet.left[0], uvSet.left[1], uvSet.left[2], uvSet.left[3], flipX), // +x (L) - createFaceMaterial(swapMats ? uvSet.left[0] : uvSet.right[0], uvSet.right[1], uvSet.right[2], uvSet.right[3], flipX), // -x (R) - createFaceMaterial(uvSet.top[0], uvSet.top[1], uvSet.top[2], uvSet.top[3], flipX, true), // +y (T) - createFaceMaterial(uvSet.bottom[0], uvSet.bottom[1], uvSet.bottom[2], uvSet.bottom[3], flipX, true), // -y (B) - createFaceMaterial(uvSet.front[0], uvSet.front[1], uvSet.front[2], uvSet.front[3], flipX), // +z (F) - createFaceMaterial(uvSet.back[0], uvSet.back[1], uvSet.back[2], uvSet.back[3], !flipX) // -z (B) + createFaceMaterial(swapMats ? uvSet.right[0] : uvSet.left[0], uvSet.left[1], uvSet.left[2], uvSet.left[3], flipX), + createFaceMaterial(swapMats ? uvSet.left[0] : uvSet.right[0], uvSet.right[1], uvSet.right[2], uvSet.right[3], flipX), + createFaceMaterial(uvSet.top[0], uvSet.top[1], uvSet.top[2], uvSet.top[3], flipX, true), + createFaceMaterial(uvSet.bottom[0], uvSet.bottom[1], uvSet.bottom[2], uvSet.bottom[3], flipX, true), + createFaceMaterial(uvSet.front[0], uvSet.front[1], uvSet.front[2], uvSet.front[3], flipX), + createFaceMaterial(uvSet.back[0], uvSet.back[1], uvSet.back[2], uvSet.back[3], !flipX) ]; }; @@ -82,7 +134,7 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo if (overlayUv) { const oGeo = new THREE.BoxGeometry(w + 0.5, h + 0.5, d + 0.5); const oMesh = new THREE.Mesh(oGeo, getMats(overlayUv)); - oMesh.visible = showLayers; + oMesh.visible = true; overlaysRef.current.push(oMesh); group.add(oMesh); } @@ -208,13 +260,6 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo }; }, [skinUrl, capeUrl]); - useEffect(() => { - overlaysRef.current.forEach(overlay => { - overlay.visible = showLayers; - }); - requestRenderRef.current?.(); - }, [showLayers]); - useEffect(() => { if (!isFocusedSection) { setFocusIndex(legacyMode ? 1 : 0); @@ -222,6 +267,10 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo } const handleKeyDown = (e: KeyboardEvent) => { + if (showBridgeMenu) { + if (e.key === "Escape") setShowBridgeMenu(false); + return; + } if (document.activeElement?.tagName === 'INPUT') return; if (e.key === 'ArrowRight') { @@ -232,17 +281,9 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo if (legacyMode) return; if (focusIndex > 1 && focusIndex <= 4) setFocusIndex(prev => prev - 1); } else if (e.key === 'ArrowDown') { - if (legacyMode) { - setFocusIndex(prev => (prev < 4 ? prev + 1 : prev)); - } else { - setFocusIndex(prev => (prev < 4 ? prev + 1 : prev)); - } + setFocusIndex(prev => (prev < 4 ? prev + 1 : prev)); } else if (e.key === 'ArrowUp') { - if (legacyMode) { - return; - } else { - setFocusIndex(prev => (prev > 0 ? prev - 1 : prev)); - } + setFocusIndex(prev => (prev > 0 ? prev - 1 : prev)); } else if (e.key === 'Enter') { if (focusIndex === 0) { (containerRef.current?.querySelector('input') as HTMLElement)?.focus(); @@ -251,14 +292,11 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo setActiveView('skins'); } else if (focusIndex === 2) { playPressSound(); - setShowLayers(!showLayers); + setActiveView('screenshots'); } else if (focusIndex === 3) { playPressSound(); - setSkinUrl('/images/Default.png'); + setShowBridgeMenu(true); } else if (focusIndex === 4) { - playPressSound(); - setActiveView('screenshots'); - } else if (focusIndex === 5) { playPressSound(); setActiveView('lcelive'); } @@ -267,7 +305,7 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [isFocusedSection, focusIndex, onNavigateRight, playPressSound, setActiveView, setShowLayers, showLayers, setSkinUrl, legacyMode]); + }, [isFocusedSection, focusIndex, onNavigateRight, playPressSound, setActiveView, legacyMode, showBridgeMenu]); useEffect(() => { if (isFocusedSection) { @@ -317,53 +355,180 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo > Skin - {!legacyMode && ( - - )} - {!legacyMode && ( - - )} + + + {showBridgeMenu && createPortal( + setShowBridgeMenu(false)} + > +
e.stopPropagation()} + style={{ + backgroundImage: "url('/images/frame_background.png')", + backgroundSize: "100% 100%", + imageRendering: "pixelated", + }} + > +

+ LCEBridge +

+ +
+
+ + {msaCode ? "Authenticating..." : bridgeActive ? "Running on port 25656" : "Stopped"} + +
+ + {msaCode && ( +
+ +
+ {msaCode.split("|")[0]} +
+
+ + Enter code at microsoft.com/link + + +
+
+ )} + +
+
+ + setBridgeRemoteHost(e.target.value)} + className="w-full bg-black/20 border-4 border-[#555] text-white p-3 text-lg outline-none focus:border-[#FFFF55] transition-colors mc-text-shadow" + disabled={bridgeActive} + /> +
+
+ + setBridgeRemotePort(e.target.value)} + className="w-full bg-black/20 border-4 border-[#555] text-white p-3 text-lg outline-none focus:border-[#FFFF55] transition-colors mc-text-shadow" + disabled={bridgeActive} + /> +
+
+ + setBridgeProtocolVersion(parseInt(e.target.value) || 767)} + className="w-full bg-black/20 border-4 border-[#555] text-white p-3 text-lg outline-none focus:border-[#FFFF55] transition-colors mc-text-shadow" + disabled={bridgeActive} + /> +
+
+ +
+ {(["offline", "online", "floodgate"] as const).map((t) => ( + + ))} +
+
+
+ +
+ + +
+
+ , + document.body + )} ); }); -export default SkinViewer; \ No newline at end of file +export default SkinViewer; diff --git a/src/components/views/HomeView.tsx b/src/components/views/HomeView.tsx index 6f87699..55fdfdf 100644 --- a/src/components/views/HomeView.tsx +++ b/src/components/views/HomeView.tsx @@ -30,9 +30,10 @@ const HomeView = memo(function HomeView() { const isInstalled = installs.includes(profile); const isDownloading = downloadingId === profile; const [menuFocus, setMenuFocus] = useState(null); + const hasAnyInstall = installs.length > 0; - const buttons = useMemo( + const buttonsVal = useMemo( () => [ { label: !hasAnyInstall @@ -82,16 +83,9 @@ const HomeView = memo(function HomeView() { }, ], [ - isDownloading, - hasAnyInstall, - isInstalled, - selectedVersionName, - handleLaunch, - toggleInstall, - profile, - setActiveView, - isGameRunning, - stopGame, + isDownloading, hasAnyInstall, isInstalled, selectedVersionName, + handleLaunch, toggleInstall, profile, setActiveView, + isGameRunning, stopGame, ], ); @@ -100,25 +94,22 @@ const HomeView = memo(function HomeView() { setMenuFocus(null); return; } - - const handleKeyDown = (e: KeyboardEvent) => { + const handleKey = (e: KeyboardEvent) => { if (document.activeElement?.tagName === "INPUT") return; if (e.key === "ArrowDown") setMenuFocus((prev) => - prev === null ? 0 : prev < buttons.length - 1 ? prev + 1 : prev, + prev === null ? 0 : prev < buttonsVal.length - 1 ? prev + 1 : prev, ); if (e.key === "ArrowUp") setMenuFocus((prev) => - prev === null ? buttons.length - 1 : prev > 0 ? prev - 1 : prev, + prev === null ? buttonsVal.length - 1 : prev > 0 ? prev - 1 : prev, ); if (e.key === "ArrowLeft") onNavigateToSkin(); - if (e.key === "Enter" && menuFocus !== null) { - buttons[menuFocus].action(); - } + if (e.key === "Enter" && menuFocus !== null) buttonsVal[menuFocus].action(); }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [menuFocus, buttons, playPressSound, isFocusedSection, onNavigateToSkin]); + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [menuFocus, buttonsVal, isFocusedSection, onNavigateToSkin]); return ( - {buttons.map((btn: { label: string; action: () => void; isDanger?: boolean; disabled: boolean; id?: string }, i: number) => ( + {buttonsVal.map((btn, i) => (