mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-18 00:07:09 +00:00
feat: java bridge
This commit is contained in:
parent
3a7335f43d
commit
114e775b2a
28
decode_hex.py
Normal file
28
decode_hex.py
Normal 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
25
parse_codec.py
Normal 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
316
src-tauri/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
63
src-tauri/src/commands/bridge.rs
Normal file
63
src-tauri/src/commands/bridge.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bridge;
|
||||
pub mod config;
|
||||
pub mod console2lce;
|
||||
pub mod download;
|
||||
|
|
|
|||
622
src-tauri/src/lce_bridge/bridge.rs
Normal file
622
src-tauri/src/lce_bridge/bridge.rs
Normal 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;
|
||||
239
src-tauri/src/lce_bridge/chunk.rs
Normal file
239
src-tauri/src/lce_bridge/chunk.rs
Normal 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)
|
||||
}
|
||||
71
src-tauri/src/lce_bridge/config.rs
Normal file
71
src-tauri/src/lce_bridge/config.rs
Normal 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
240
src-tauri/src/lce_bridge/crafting.rs
Normal file
240
src-tauri/src/lce_bridge/crafting.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1066
src-tauri/src/lce_bridge/java_protocol.rs
Normal file
1066
src-tauri/src/lce_bridge/java_protocol.rs
Normal file
File diff suppressed because it is too large
Load diff
443
src-tauri/src/lce_bridge/java_session.rs
Normal file
443
src-tauri/src/lce_bridge/java_session.rs
Normal 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))?)
|
||||
}
|
||||
600
src-tauri/src/lce_bridge/lce_codec.rs
Normal file
600
src-tauri/src/lce_bridge/lce_codec.rs
Normal 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(())
|
||||
}
|
||||
478
src-tauri/src/lce_bridge/lce_packets.rs
Normal file
478
src-tauri/src/lce_bridge/lce_packets.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
61
src-tauri/src/lce_bridge/mod.rs
Normal file
61
src-tauri/src/lce_bridge/mod.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
384
src-tauri/src/lce_bridge/msa_auth.rs
Normal file
384
src-tauri/src/lce_bridge/msa_auth.rs
Normal 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(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("device code request failed: {e}"))?;
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.map_err(|e| format!("read device code response: {e}"))?;
|
||||
if !status.is_success() {
|
||||
return Err(format!("device code HTTP {status}: {text}"));
|
||||
}
|
||||
let dc: DeviceCodeResponse = serde_json::from_str(&text)
|
||||
.map_err(|e| format!("parse device code: {e} (raw: {text})"))?;
|
||||
let msg = format!("{}|{}", dc.user_code, dc.verification_uri);
|
||||
eprintln!("[MSA] device code: {msg}");
|
||||
let _ = device_code_tx().send(msg);
|
||||
Ok(dc)
|
||||
}
|
||||
|
||||
async fn poll_token(device_code: &str) -> Result<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(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("token poll failed: {e}"))?;
|
||||
let text = resp.text().await.map_err(|e| format!("read token response: {e}"))?;
|
||||
if let Ok(tok) = serde_json::from_str::<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 })
|
||||
}
|
||||
149
src-tauri/src/lce_bridge/registry.rs
Normal file
149
src-tauri/src/lce_bridge/registry.rs
Normal 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),
|
||||
];
|
||||
226
src-tauri/src/lce_bridge/util.rs
Normal file
226
src-tauri/src/lce_bridge/util.rs
Normal 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..]))
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
131
src-tauri/src/workshop_server.rs
Normal file
131
src-tauri/src/workshop_server.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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={() =>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue