Модлоадер RustexRemake: агент, патч лаунчера, установщик и сборка
Моды грузятся через -javaagent, game.jar не патчится, переживают ре-обфускацию. Что внутри: - агент rsx-bridge.jar: профиль сборки игры, патчи A2/A6, загрузка модов через родной загрузчик игры (ilxq -> special.Main) - патч лаунчера: окно выбора «С модами / Ванилла» по кнопке «Играть» и защита от автоапдейта лаунчера - install/uninstall: всё ставится в %LOCALAPPDATA%\rsx-mod, пути не хардкодятся - build: три готовых артефакта в dist, нужен только JDK - testserver: локальный сервер, принимает любой ник и токен Проверено на живом клиенте: мод грузится и выполняется в игровом потоке, в логе видно игрока и мир.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Self-test for server.py: runs the stand-in server on a spare port and talks
|
||||
to it with a hand-rolled 1.12.2 client, checking handshake, login and the
|
||||
play-state packets the game needs to reach a world."""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
sys.path.insert(0, r"G:\rsx-mod\testserver")
|
||||
|
||||
import server as srv # noqa: E402
|
||||
|
||||
|
||||
def read_varint(sock):
|
||||
value = 0
|
||||
for shift in range(0, 35, 7):
|
||||
chunk = sock.recv(1)
|
||||
if not chunk:
|
||||
raise AssertionError("closed")
|
||||
byte = chunk[0]
|
||||
value |= (byte & 0x7F) << shift
|
||||
if not byte & 0x80:
|
||||
return value
|
||||
raise AssertionError("varint too long")
|
||||
|
||||
|
||||
def read_packet(sock):
|
||||
length = read_varint(sock)
|
||||
data = b""
|
||||
while len(data) < length:
|
||||
chunk = sock.recv(length - len(data))
|
||||
if not chunk:
|
||||
raise AssertionError("closed mid packet")
|
||||
data += chunk
|
||||
pid_val = 0
|
||||
shift = 0
|
||||
pos = 0
|
||||
while True:
|
||||
byte = data[pos]
|
||||
pos += 1
|
||||
pid_val |= (byte & 0x7F) << shift
|
||||
if not byte & 0x80:
|
||||
break
|
||||
shift += 7
|
||||
return pid_val, data[pos:]
|
||||
|
||||
|
||||
def send_packet(sock, pid, payload=b""):
|
||||
body = srv.varint(pid) + payload
|
||||
sock.sendall(srv.varint(len(body)) + body)
|
||||
|
||||
|
||||
def drain(sock, wanted, budget=8.0):
|
||||
seen = {}
|
||||
deadline = time.time() + budget
|
||||
while time.time() < deadline and not wanted.issubset(seen):
|
||||
try:
|
||||
pid, payload = read_packet(sock)
|
||||
except (socket.timeout, TimeoutError, AssertionError, OSError):
|
||||
break
|
||||
seen.setdefault(pid, payload)
|
||||
return seen
|
||||
|
||||
|
||||
def main():
|
||||
port = 25599
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind(("127.0.0.1", port))
|
||||
listener.listen(4)
|
||||
|
||||
def accept_loop():
|
||||
try:
|
||||
sock, peer = listener.accept()
|
||||
srv.handle(srv.Conn(sock, "%s:%s" % peer))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=accept_loop, daemon=True).start()
|
||||
|
||||
sock = socket.create_connection(("127.0.0.1", port), timeout=10)
|
||||
sock.settimeout(10)
|
||||
results = []
|
||||
|
||||
handshake = (srv.varint(srv.PROTOCOL) + srv.mc_string("127.0.0.1")
|
||||
+ struct.pack(">H", port) + srv.varint(2))
|
||||
send_packet(sock, 0x00, handshake)
|
||||
results.append(("handshake sent", True))
|
||||
|
||||
send_packet(sock, 0x00,
|
||||
srv.mc_string("TestPilot")
|
||||
+ srv.mc_string("00000000-0000-0000-0000-000000000000"))
|
||||
pid, payload = read_packet(sock)
|
||||
cursor = srv.Cursor(payload)
|
||||
uuid_txt = cursor.string()
|
||||
nick = cursor.string()
|
||||
results.append(("login success", pid == 0x02 and nick == "TestPilot"))
|
||||
uuid_ok = len(uuid_txt) in (32, 36)
|
||||
try:
|
||||
uuid.UUID(uuid_txt)
|
||||
except (ValueError, AttributeError):
|
||||
uuid_ok = False
|
||||
results.append(("uuid parseable (%d chars)" % len(uuid_txt), uuid_ok))
|
||||
|
||||
send_packet(sock, 0x03, b"")
|
||||
seen = drain(sock, {0x01, 0x08, 0x23})
|
||||
results.append(("join game received", 0x01 in seen))
|
||||
results.append(("spawn position received", 0x08 in seen))
|
||||
results.append(("brand received", 0x23 in seen))
|
||||
|
||||
if 0x01 in seen:
|
||||
cur = srv.Cursor(seen[0x01])
|
||||
entity = struct.unpack(">i", cur.take(4))[0]
|
||||
gamemode = cur.ubyte()
|
||||
dimension = struct.unpack(">i", cur.take(4))[0]
|
||||
cur.ubyte()
|
||||
cur.ubyte()
|
||||
level = cur.string()
|
||||
results.append(("join game parses (entity=%d gm=%d dim=%d %s)"
|
||||
% (entity, gamemode, dimension, level),
|
||||
level == srv.WORLD_NAME))
|
||||
|
||||
if 0x08 in seen:
|
||||
cur = srv.Cursor(seen[0x08])
|
||||
x = struct.unpack(">d", cur.take(8))[0]
|
||||
y = struct.unpack(">d", cur.take(8))[0]
|
||||
z = struct.unpack(">d", cur.take(8))[0]
|
||||
cur.take(8)
|
||||
cur.ubyte()
|
||||
tid = cur.varint()
|
||||
results.append(("spawn parses (%.1f/%.1f/%.1f tid=%d)"
|
||||
% (x, y, z, tid), y == 64.0 and tid == 0))
|
||||
|
||||
send_packet(sock, 0x00, srv.varint(0x1234))
|
||||
send_packet(sock, 0x03, srv.mc_string("ru_ru") + bytes([8])
|
||||
+ srv.varint(0) + b"\x01" + bytes([2]) + bytes([127])
|
||||
+ srv.varint(1))
|
||||
send_packet(sock, 0x07,
|
||||
struct.pack(">dddff?", 0.5, 64.0, 0.5, 0.0, 0.0, True))
|
||||
alive = drain(sock, {0x00}, budget=18.0)
|
||||
results.append(("keep alive flow", 0x00 in alive))
|
||||
|
||||
sock.close()
|
||||
listener.close()
|
||||
|
||||
print("== self-test ==")
|
||||
failed = 0
|
||||
for label, ok in results:
|
||||
print(" %-40s %s" % (label, "OK" if ok else "FAIL"))
|
||||
failed += 0 if ok else 1
|
||||
print(" %d check(s) failed" % failed)
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user