This repository has been archived on 2026-10-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
SashegDev b8e5df778e Модлоадер RustexRemake: агент, патч лаунчера, установщик и сборка
Моды грузятся через -javaagent, game.jar не патчится, переживают ре-обфускацию.

Что внутри:
- агент rsx-bridge.jar: профиль сборки игры, патчи A2/A6, загрузка модов
  через родной загрузчик игры (ilxq -> special.Main)
- патч лаунчера: окно выбора «С модами / Ванилла» по кнопке «Играть»
  и защита от автоапдейта лаунчера
- install/uninstall: всё ставится в %LOCALAPPDATA%\rsx-mod, пути не хардкодятся
- build: три готовых артефакта в dist, нужен только JDK
- testserver: локальный сервер, принимает любой ник и токен

Проверено на живом клиенте: мод грузится и выполняется в игровом потоке,
в логе видно игрока и мир.
2026-10-17 00:32:10 +03:00

416 lines
12 KiB
Python

#!/usr/bin/env python3
"""Offline stand-in for the RustexRemake server: accepts any nickname and
token and drops the client into an empty world, so the modloader can be
verified end to end without the live server.
Protocol: Minecraft 1.12.2 (340), no compression, no encryption.
Run: python server.py [--port 25566] [--host 0.0.0.0]
"""
import argparse
import hashlib
import select
import socket
import struct
import sys
import threading
import time
import uuid as uuidlib
PROTOCOL = 340
GAMEMODE_SURVIVAL = 0
DIMENSION_OVERWORLD = 0
DIFFICULTY_NORMAL = 1
WORLD_NAME = "flat"
CLIENTBOUND_PLAY = {
0x00: "KEEP_ALIVE",
0x01: "JOIN_GAME",
0x06: "SPAWN_POSITION",
0x08: "PLAYER_POS_LOOK",
0x23: "PLUGIN_MESSAGE",
}
SERVERBOUND_PLAY = {
0x00: "KEEP_ALIVE",
0x01: "CHAT",
0x02: "CLIENT_STATUS",
0x03: "CLIENT_SETTINGS",
0x04: "PLUGIN_MESSAGE",
0x06: "POSITION",
0x07: "POSITION_LOOK",
0x1A: "TELEPORT_CONFIRM",
}
class ProtocolError(Exception):
pass
class Conn:
def __init__(self, sock, peer):
self.sock = sock
self.peer = peer
self.buf = b""
def close(self):
try:
self.sock.close()
except OSError:
pass
def _fill(self, n):
while len(self.buf) < n:
chunk = self.sock.recv(65536)
if not chunk:
raise ProtocolError("closed by peer")
self.buf += chunk
def read_exact(self, n):
self._fill(n)
out, self.buf = self.buf[:n], self.buf[n:]
return out
def read_varint(self):
value = 0
for shift in range(0, 35, 7):
byte = self.read_exact(1)[0]
value |= (byte & 0x7F) << shift
if not byte & 0x80:
return value
raise ProtocolError("varint too long")
def read_string(self):
return self.read_exact(self.read_varint()).decode("utf-8", "replace")
def read_ushort(self):
return struct.unpack(">H", self.read_exact(2))[0]
def read_packet(self):
length = self.read_varint()
if length <= 0 or length > 2_000_000:
raise ProtocolError("bad packet length %d" % length)
pid = self.read_varint()
payload = self.read_exact(length - varint_size(pid))
return pid, payload
def send(self, pid, payload=b""):
body = varint(pid) + payload
self.sock.sendall(varint(len(body)) + body)
def varint(value):
out = b""
while True:
byte = value & 0x7F
value >>= 7
out += bytes([byte | (0x80 if value else 0)])
if not value:
return out
def varint_size(value):
size = 1
while value >= 0x80:
value >>= 7
size += 1
return size
def mc_string(text):
raw = text.encode("utf-8")
return varint(len(raw)) + raw
def mc_bool(value):
return b"\x01" if value else b"\x00"
def offline_uuid(name):
return uuidlib.uuid3(uuidlib.NAMESPACE_DNS, "OfflinePlayer." + name)
def log(name, fmt, *args):
stamp = time.strftime("%H:%M:%S")
sys.stdout.write("[%s] %-12s %s\n" % (stamp, name, fmt % args if args else fmt))
sys.stdout.flush()
def send_status(conn):
payload = (
mc_string(json_status())
)
conn.send(0x00, payload)
def json_status():
return (
'{"version":{"name":"RustexRemake","protocol":%d},'
'"players":{"max":20,"online":1},'
'"description":{"text":"rsx modloader test server"}}' % PROTOCOL
)
def send_keep_alive(conn, ident):
conn.send(0x00, varint(ident & 0x7FFFFFFF))
def send_join_game(conn, entity_id):
payload = b"".join([
struct.pack(">i", entity_id),
bytes([GAMEMODE_SURVIVAL]),
struct.pack(">i", DIMENSION_OVERWORLD),
bytes([DIFFICULTY_NORMAL]),
bytes([20]),
mc_string(WORLD_NAME),
mc_bool(False),
])
conn.send(0x01, payload)
def send_spawn_position(conn, x=0.5, y=64.0, z=0.5):
payload = struct.pack(">ddd", x, y, z)
conn.send(0x06, payload)
def send_pos_look(conn, x, y, z, yaw, pitch, teleport_id):
payload = b"".join([
struct.pack(">ddd", x, y, z),
struct.pack(">ff", yaw, pitch),
bytes([0]),
varint(teleport_id),
])
conn.send(0x08, payload)
def send_brand(conn, brand):
inner = mc_string(brand)
conn.send(0x23, mc_string("MC|Brand") + varint(len(inner)) + inner)
def handle(conn):
name = None
entity_id = None
teleport_id = 0
keepalive_id = 0x1234
last_keepalive = 0.0
try:
pid, payload = conn.read_packet()
if pid != 0x00:
raise ProtocolError("expected handshake, got %d" % pid)
cursor = Cursor(payload)
protocol = cursor.varint()
host = cursor.string()
port = cursor.ushort()
next_state = cursor.varint()
log("handshake", "protocol=%d host=%s port=%d next_state=%d",
protocol, host, port, next_state)
if next_state == 1:
pid, _ = conn.read_packet()
send_status(conn)
log("status", "ping answered")
return
if next_state != 2:
raise ProtocolError("unsupported next_state %d" % next_state)
pid, payload = conn.read_packet()
if pid != 0x00:
raise ProtocolError("expected login start, got %d" % pid)
cursor = Cursor(payload)
name = cursor.string()
profile_id = None
if cursor.remaining() > 0:
try:
profile_id = cursor.string()
except ProtocolError:
pass
if name is None:
raise ProtocolError("no nickname")
profile_id = profile_id or str(offline_uuid(name)).replace("-", "")
log("login", "nick=%r token-ignored uuid=%s", name, profile_id)
conn.send(0x02, mc_string(profile_id) + mc_string(name))
log("login", "login success sent, any token accepted")
deadline = time.time() + 20
while time.time() < deadline:
pid, payload = conn.read_packet()
if pid == 0x03:
log("login", "client acknowledged")
break
if pid == 0x00:
continue
log("login", "unexpected packet 0x%02X before play" % pid)
else:
raise ProtocolError("no login acknowledgement")
entity_id = int.from_bytes(hashlib.sha256(name.encode()).digest()[:4],
"big") & 0x7FFFFFFF
send_brand(conn, "rsx-testserver")
send_join_game(conn, entity_id)
send_pos_look(conn, 0.5, 64.0, 0.5, 0.0, 0.0, teleport_id)
log("play", "join game sent, entity=%d, spawn sent" % entity_id)
while True:
if time.time() - last_keepalive > 15:
last_keepalive = time.time()
keepalive_id = (keepalive_id + 1) & 0x7FFFFFFF
send_keep_alive(conn, keepalive_id)
pid, payload = conn.read_packet()
label = SERVERBOUND_PLAY.get(pid, "0x%02X" % pid)
if pid == 0x00:
send_keep_alive(conn, keepalive_id)
log("play", "keep alive")
elif pid == 0x1A:
log("play", "teleport confirmed")
teleport_id += 1
send_pos_look(conn, 0.5, 64.0, 0.5, 0.0, 0.0, teleport_id)
log("play", "position sent after teleport confirm")
elif pid == 0x02:
log("play", "client status (respawn?): %s", payload.hex())
elif pid == 0x07:
log("play", "player position+look received")
else:
log("play", "%s (%d bytes)", label, len(payload))
except ProtocolError as exc:
log("close", "%s: %s", conn.peer, exc)
except OSError as exc:
log("close", "%s: %s", conn.peer, exc)
except Exception as exc: # noqa: BLE001 - stand-in server, log and move on
log("error", "%s: %r", conn.peer, exc)
finally:
if name:
log("bye", "nick=%r entity=%s", name, entity_id)
conn.close()
class Cursor:
def __init__(self, data):
self.data = data
self.pos = 0
def remaining(self):
return len(self.data) - self.pos
def take(self, n):
if self.pos + n > len(self.data):
raise ProtocolError("packet truncated")
out = self.data[self.pos:self.pos + n]
self.pos += n
return out
def byte(self):
return self.take(1)[0]
def ubyte(self):
return self.byte()
def ushort(self):
return struct.unpack(">H", self.take(2))[0]
def varint(self):
value = 0
for shift in range(0, 35, 7):
byte = self.byte()
value |= (byte & 0x7F) << shift
if not byte & 0x80:
return value
raise ProtocolError("varint too long")
def string(self):
return self.take(self.varint()).decode("utf-8", "replace")
def parse_ports(spec):
ports = []
for chunk in spec.split(","):
chunk = chunk.strip()
if "-" in chunk:
first, last = chunk.split("-", 1)
ports.extend(range(int(first), int(last) + 1))
elif chunk:
ports.append(int(chunk))
return ports
def serve_ping(ports, host):
"""The client asks http://<host>/server-ping for its server list and reads
lines of port~mode~x~y. 'Offline' skips the per-server ping, which is what
we want: the stand-in has nothing to measure."""
body = "".join("%d~Offline~0~0\n" % port for port in ports)
try:
from http.server import BaseHTTPRequestHandler, HTTPServer
except ImportError:
log("ping", "http.server unavailable, ping endpoint disabled")
return None
class Handler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802 - required by BaseHTTPRequestHandler
payload = body.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, fmt, *args):
log("ping", self.path)
try:
httpd = HTTPServer((host, 80), Handler)
except OSError as exc:
log("ping", "cannot listen on port 80 (%s); list will stay empty" % exc)
return None
threading.Thread(target=httpd.serve_forever, daemon=True).start()
log("ping", "serving /server-ping on %s:80 with %d server(s)"
% (host, len(ports)))
return httpd
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--ports", default="25566",
help="ports or ranges, e.g. 25566-25573")
parser.add_argument("--no-http", action="store_true",
help="do not answer the client server-ping request")
args = parser.parse_args()
ports = parse_ports(args.ports)
listeners = []
for port in ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((args.host, port))
except OSError as exc:
log("server", "cannot bind %s:%d (%s)" % (args.host, port, exc))
sock.close()
continue
sock.listen(8)
listeners.append(sock)
log("server", "listening on %s:%d, any nick/token accepted",
args.host, port)
if not args.no_http:
serve_ping(ports, args.host)
try:
while True:
ready, _, _ = select.select(listeners, [], [], 1.0)
for listener in ready:
sock, peer = listener.accept()
conn = Conn(sock, "%s:%s" % peer)
threading.Thread(target=handle, args=(conn,),
daemon=True).start()
except KeyboardInterrupt:
log("server", "interrupted")
finally:
for listener in listeners:
listener.close()
if __name__ == "__main__":
main()