feat: java bridge

This commit is contained in:
neoapps-dev 2026-05-25 13:23:01 +03:00
parent 3a7335f43d
commit 114e775b2a
26 changed files with 5441 additions and 90 deletions

28
decode_hex.py Normal file
View file

@ -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).")

25
parse_codec.py Normal file
View file

@ -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

316
src-tauri/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<msa_auth::MinecraftProfile> = 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<String, String> {
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<String, String> {
stop_bridge().await
}
#[tauri::command]
#[allow(non_snake_case)]
pub async fn bridge_status() -> Result<String, String> {
lce_bridge_status().await
}

View file

@ -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(())
}

View file

@ -1,3 +1,4 @@
pub mod bridge;
pub mod config;
pub mod console2lce;
pub mod download;

View file

@ -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<LcePacket>,
pub java_session: Option<JavaSession>,
pub player_eid: i32,
pub tracked_entities: HashMap<i32, TrackedEntity>,
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<i32, u8>,
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<u8> {
VarInt::write(val)
}
use crate::lce_bridge::util::VarInt;

View file

@ -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<u16>,
block_light: Vec<u8>,
sky_light: Vec<u8>,
biomes: Vec<u8>,
}
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<u8> {
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<u8>, Vec<u8>) {
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<u8>) {
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<u8> {
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<u8> {
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)
}

View file

@ -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<i32>,
}
#[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(),
},
}
}
}

View file

@ -0,0 +1,240 @@
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct LegacyRecipe {
pub index: u8,
pub input: Vec<Option<IngredientKey>>,
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<LegacyRecipe>,
by_index: HashMap<u8, usize>,
}
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<LegacyRecipe> {
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<LceItemStack>,
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<LceItemStack>) {
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
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<u8> {
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<u8> {
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<Vec<u8>> {
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<JavaPacket>,
pub rx: mpsc::Receiver<JavaPacket>,
}
impl JavaSession {
pub async fn connect(config: &RemoteConfig) -> std::io::Result<Self> {
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::<JavaPacket>(256);
let (write_tx, write_rx) = mpsc::channel::<JavaPacket>(256);
let enc_ctx: Arc<Mutex<Option<EncryptionCtx>>> = 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<Mutex<i32>> = 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<JavaPacket> {
self.rx.recv().await
}
}
fn encode_frame(packet: &JavaPacket, compression: i32) -> Option<Vec<u8>> {
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<JavaPacket>,
write_tx: mpsc::Sender<JavaPacket>,
enc_ctx: Arc<Mutex<Option<EncryptionCtx>>>,
comp_state: Arc<Mutex<i32>>,
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<Vec<u8>> {
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<Mutex<Option<EncryptionCtx>>>) -> 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<JavaPacket>,
enc_ctx: &Arc<Mutex<Option<EncryptionCtx>>>,
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<JavaPacket>,
enc_ctx: &Arc<Mutex<Option<EncryptionCtx>>>,
) {
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<JavaPacket>,
enc_ctx: Arc<Mutex<Option<EncryptionCtx>>>,
comp_state: Arc<Mutex<i32>>,
) {
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::<Vec<_>>().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<String> {
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))?)
}

View file

@ -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<LcePacket> {
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<u8> {
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<LceItemStack> {
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<Vec<EntityDataValue>> {
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::<Vec<_>>().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<u8> = 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(())
}

View file

@ -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<u8> },
}
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<String>,
pub int_args: Vec<i32>,
}
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<LceItemStack>,
}
#[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<u8>,
}
#[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<EntityDataValue>,
}
#[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<EntityDataValue>,
}
#[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<i32>,
}
#[derive(Debug, Clone)]
pub struct SetEntityDataPacket {
pub entity_id: i32,
pub values: Vec<EntityDataValue>,
}
#[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 }
}
}

View file

@ -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<bool>,
}
static BRIDGE_INSTANCE: std::sync::OnceLock<Arc<Mutex<Option<BridgeHandle>>>> = std::sync::OnceLock::new();
fn bridge_instance() -> &'static Arc<Mutex<Option<BridgeHandle>>> {
BRIDGE_INSTANCE.get_or_init(|| Arc::new(Mutex::new(None)))
}
pub async fn start_bridge(config: BridgeConfig) -> Result<String, String> {
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<String, String> {
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<String, String> {
let guard = bridge_instance().lock().await;
if guard.is_some() {
Ok("running".to_string())
} else {
Ok("stopped".to_string())
}
}

View file

@ -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<broadcast::Sender<String>> = OnceLock::new();
fn device_code_tx() -> broadcast::Sender<String> {
DEVICE_CODE_TX.get_or_init(|| {
let (tx, _) = broadcast::channel(8);
tx
}).clone()
}
pub fn subscribe_device_code() -> broadcast::Receiver<String> {
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<String>,
mc_uuid: Option<String>,
mc_username: Option<String>,
mc_access_token: Option<String>,
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<String>,
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<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct McProfileResponse {
id: String,
name: String,
}
struct AuthKey {
signing: SigningKey,
verifying: p256::PublicKey,
}
static AUTH_KEY: OnceLock<AuthKey> = 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<TokenCache> {
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<DeviceCodeResponse, String> {
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(&params)
.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<TokenResponse, String> {
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(&params)
.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::<TokenResponse>(&text) {
return Ok(tok);
}
if let Ok(err) = serde_json::from_str::<TokenError>(&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<String, String> {
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<SisuXui>,
}
#[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<MinecraftProfile, String> {
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 })
}

View file

@ -0,0 +1,149 @@
use std::collections::HashMap;
use std::sync::OnceLock;
static MAPPINGS: OnceLock<MappingRegistry> = 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<i32, u16>,
biome_map: HashMap<String, u8>,
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),
];

View file

@ -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<u8> {
self.cursor.read_u8()
}
pub fn read_i8(&mut self) -> std::io::Result<i8> {
self.cursor.read_i8()
}
pub fn read_u16(&mut self) -> std::io::Result<u16> {
self.cursor.read_u16::<BigEndian>()
}
pub fn read_i16(&mut self) -> std::io::Result<i16> {
self.cursor.read_i16::<BigEndian>()
}
pub fn read_u32(&mut self) -> std::io::Result<u32> {
self.cursor.read_u32::<BigEndian>()
}
pub fn read_i32(&mut self) -> std::io::Result<i32> {
self.cursor.read_i32::<BigEndian>()
}
pub fn read_u64(&mut self) -> std::io::Result<u64> {
self.cursor.read_u64::<BigEndian>()
}
pub fn read_i64(&mut self) -> std::io::Result<i64> {
self.cursor.read_i64::<BigEndian>()
}
pub fn read_f32(&mut self) -> std::io::Result<f32> {
self.cursor.read_f32::<BigEndian>()
}
pub fn read_f64(&mut self) -> std::io::Result<f64> {
self.cursor.read_f64::<BigEndian>()
}
pub fn read_bytes(&mut self, len: usize) -> std::io::Result<Vec<u8>> {
let mut buf = vec![0u8; len];
self.cursor.read_exact(&mut buf)?;
Ok(buf)
}
pub fn read_bool(&mut self) -> std::io::Result<bool> {
Ok(self.cursor.read_u8()? != 0)
}
pub fn read_utf16(&mut self, max_length: u32) -> std::io::Result<String> {
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<String> {
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<u8>,
}
impl LceWriter {
pub fn new() -> Self {
Self { buf: Vec::new() }
}
pub fn into_bytes(self) -> Vec<u8> {
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::<BigEndian>(v);
}
pub fn write_i16(&mut self, v: i16) {
let _ = self.buf.write_i16::<BigEndian>(v);
}
pub fn write_u32(&mut self, v: u32) {
let _ = self.buf.write_u32::<BigEndian>(v);
}
pub fn write_i32(&mut self, v: i32) {
let _ = self.buf.write_i32::<BigEndian>(v);
}
pub fn write_u64(&mut self, v: u64) {
let _ = self.buf.write_u64::<BigEndian>(v);
}
pub fn write_i64(&mut self, v: i64) {
let _ = self.buf.write_i64::<BigEndian>(v);
}
pub fn write_f32(&mut self, v: f32) {
let _ = self.buf.write_f32::<BigEndian>(v);
}
pub fn write_f64(&mut self, v: f64) {
let _ = self.buf.write_f64::<BigEndian>(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<u16> = 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<i32> {
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<u8>) {
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<u8> {
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..]))
}

View file

@ -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,

View file

@ -8,6 +8,7 @@ pub struct DownloadState {
pub struct GameState {
pub child: Arc<Mutex<Option<tokio::process::Child>>>,
pub workshop_cancel: Arc<Mutex<Option<CancellationToken>>>,
}
pub struct ProxyGuard {

View file

@ -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<reqwest::Client> = Lazy::new(|| reqwest::Client::new());
pub struct Guard {
cancel: Option<CancellationToken>,
}
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<Vec<u8>, 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()))
}
}

View file

@ -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<string, number[]>;
@ -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<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [focusIndex, setFocusIndex] = useState(0);
const { legacyMode } = useConfig();
const [showLayers, setShowLayers] = useLocalStorage('lce-show-layers', true);
const overlaysRef = useRef<THREE.Mesh[]>([]);
const capeRef = useRef<THREE.Group | null>(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<string>("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
>
<img src="/images/Change_Skin_Icon.png" alt="Skin" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
{!legacyMode && (
<button
data-focus="2" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(2)}
onClick={() => { playPressSound(); setShowLayers(!showLayers); }}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 2 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 2 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
title="Toggle Layers"
>
<img src="/images/Layer_Icon.png" alt="Layers" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
)}
{!legacyMode && (
<button
data-focus="3" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(3)}
onClick={() => { playPressSound(); setSkinUrl('/images/Default.png'); }}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 3 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 3 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
title="Reset to Default"
>
<img src="/images/Trash_Bin_Icon.png" alt="Delete" className="w-8 h-8 object-contain brightness-200" style={{ imageRendering: 'pixelated' }} />
</button>
)}
<button
data-focus="4" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(4)}
data-focus="2" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(2)}
onClick={() => { playPressSound(); setActiveView('screenshots'); }}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 4 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 4 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 2 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 2 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
title="Screenshots"
>
<img src="/images/Screenshots_Icon.png" alt="Screenshots" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
<button
data-focus="5" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(5)}
data-focus="3" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(3)}
onClick={() => { playPressSound(); setShowBridgeMenu(true); }}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 3 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 3 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
title="LCEBridge"
>
<img src="/images/sga_w.png" alt="Bridge" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
<button
data-focus="4" tabIndex={0}
onMouseEnter={() => isFocusedSection && setFocusIndex(4)}
onClick={() => { playPressSound(); setActiveView('lcelive'); }}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 5 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 5 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 4 ? 'scale-110' : ''}`}
style={isFocusedSection && focusIndex === 4 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
title="LCELive"
>
<img src="/images/friends.png" alt="LCELive" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
</button>
</div>
{showBridgeMenu && createPortal(
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md"
onClick={() => setShowBridgeMenu(false)}
>
<div
className="relative w-[420px] p-6 flex flex-col items-center"
onClick={(e) => e.stopPropagation()}
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
LCEBridge
</h2>
<div className="w-full flex items-center gap-3 px-4 py-3 bg-black/30 mb-5">
<div className={`w-3 h-3 rounded-full shrink-0 ${bridgeActive ? "bg-green-400" : "bg-gray-500"}`} />
<span className="text-white text-sm mc-text-shadow">
{msaCode ? "Authenticating..." : bridgeActive ? "Running on port 25656" : "Stopped"}
</span>
</div>
{msaCode && (
<div className="w-full mb-5 space-y-2">
<label className="text-gray-400 text-sm mc-text-shadow uppercase tracking-widest mb-1 block">
Microsoft Login Code
</label>
<div className="w-full bg-black/20 border-4 border-[#555] text-white p-3 text-lg mc-text-shadow tracking-[0.2em] font-bold">
{msaCode.split("|")[0]}
</div>
<div className="flex items-center justify-between">
<span className="text-gray-500 text-xs mc-text-shadow">
Enter code at microsoft.com/link
</span>
<button
onClick={() => TauriService.openUrl(msaCode.split("|")[1] || "https://microsoft.com/link")}
className="h-8 px-4 flex items-center justify-center text-xs text-[#FFFF55] mc-text-shadow outline-none border-none cursor-pointer"
style={{
backgroundImage: "url('/images/button_highlighted.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Open in Browser
</button>
</div>
</div>
)}
<div className="w-full mb-5 space-y-4">
<div>
<label className="text-gray-400 text-sm mc-text-shadow uppercase tracking-widest mb-1 block">Java Server IP</label>
<input type="text" value={bridgeRemoteHost}
onChange={(e) => 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}
/>
</div>
<div>
<label className="text-gray-400 text-sm mc-text-shadow uppercase tracking-widest mb-1 block">Java Server Port</label>
<input type="number" value={bridgeRemotePort}
onChange={(e) => 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}
/>
</div>
<div>
<label className="text-gray-400 text-sm mc-text-shadow uppercase tracking-widest mb-1 block">Protocol Version</label>
<input type="number" value={bridgeProtocolVersion}
onChange={(e) => 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}
/>
</div>
<div>
<label className="text-gray-400 text-sm mc-text-shadow uppercase tracking-widest mb-2 block">Auth Type</label>
<div className="flex gap-2">
{(["offline", "online", "floodgate"] as const).map((t) => (
<button
key={t}
disabled={bridgeActive}
onClick={() => { playPressSound(); setBridgeAuthType(t); }}
className={`flex-1 h-10 text-sm mc-text-shadow tracking-wider outline-none border-none transition-all ${
bridgeAuthType === t
? "text-[#FFFF55] scale-105"
: "text-gray-400 hover:text-white"
} ${bridgeActive ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
style={{
backgroundImage: bridgeAuthType === t
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
{t === "offline" ? "Offline" : t === "online" ? "MSA" : "Floodgate"}
</button>
))}
</div>
</div>
</div>
<div className="flex gap-4 w-full justify-center">
<button
onClick={() => { playPressSound(); setShowBridgeMenu(false); }}
className="w-32 h-10 flex items-center justify-center text-xl text-white mc-text-shadow"
style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Close
</button>
<button
onClick={() => { playPressSound(); toggleBridge(); }}
disabled={bridgeStarting}
className={`w-40 h-10 flex items-center justify-center text-xl mc-text-shadow ${
bridgeStarting ? "text-gray-500 cursor-not-allowed" : bridgeActive ? "text-red-400" : "text-[#FFFF55]"
}`}
style={{
backgroundImage: "url('/images/button_highlighted.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
opacity: bridgeStarting ? 0.5 : 1,
}}
>
{bridgeStarting ? "Starting..." : bridgeActive ? "Stop Bridge" : "Start Bridge"}
</button>
</div>
</div>
</motion.div>,
document.body
)}
</motion.div>
);
});
export default SkinViewer;
export default SkinViewer;

View file

@ -30,9 +30,10 @@ const HomeView = memo(function HomeView() {
const isInstalled = installs.includes(profile);
const isDownloading = downloadingId === profile;
const [menuFocus, setMenuFocus] = useState<number | null>(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 (
<motion.div
@ -129,7 +120,7 @@ const HomeView = memo(function HomeView() {
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
className="relative w-full max-w-[540px] flex flex-col space-y-3 outline-none"
>
{buttons.map((btn: { label: string; action: () => void; isDanger?: boolean; disabled: boolean; id?: string }, i: number) => (
{buttonsVal.map((btn, i) => (
<div key={i} className="relative w-full group">
<button
onMouseEnter={() =>

View file

@ -60,6 +60,11 @@ const BASE_EDITIONS = [
];
const PARTNERSHIP_SERVERS = [
{
name: "Emerald Java Server",
ip: "127.0.0.1",
port: 25656,
},
{
name: "Kowhaifans Clubhouse",
ip: "lce.kowhaifan.net",

View file

@ -297,4 +297,23 @@ export class TauriService {
): Promise<string> {
return invoke("import_world", { inputPath, outputPath });
}
static async bridgeStart(
listenAddress: string,
port: number,
remoteHost: string,
remotePort: number,
authType: string,
remoteProtocolVersion: number,
): Promise<string> {
return invoke("bridge_start", { listenAddress, port, remoteHost, remotePort, authType, remoteProtocolVersion });
}
static async bridgeStop(): Promise<string> {
return invoke("bridge_stop");
}
static async bridgeStatus(): Promise<string> {
return invoke("bridge_status");
}
}