5411923887
Send the join packets in vanilla's order with the permission level entity event, recipe book, server data, ticking state and post effects; announce the newcomer only to the others; list players with every info action; use vanilla's level key order and border warning time; sync the game time every 20 ticks without clocks and the overworld clock only when it jumps. Build the command tree from the commands report for the player's permission level and server kind, with vanilla's suggestion providers (VanillaCommandSuggestionsExporter), and answer command suggestions. The tree matches what a vanilla integrated server sends node for node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.3 KiB
Python
164 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Launch the vanilla 26.3 client offline (no Microsoft account, no auth
|
|
servers: --offlineDeveloperMode) and optionally join a server straight away
|
|
(--quickPlayMultiplayer). Meant to run on a virtual screen:
|
|
|
|
vscreen start python3 tools/vanilla_client.py --name Guest --join localhost:25565
|
|
|
|
Sound goes to OpenAL's null backend. The game directory gets an options.txt
|
|
that skips the first-run screens, keeps the game running unfocused and
|
|
renders few chunks, so a harness can drive it with vscreen keys.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
GAME = ROOT / ".local/minecraft/26.3"
|
|
|
|
OPTIONS = {
|
|
"onboardAccessibility": "false",
|
|
"skipMultiplayerWarning": "true",
|
|
"joinedFirstServer": "true",
|
|
"tutorialStep": "none",
|
|
"pauseOnLostFocus": "false",
|
|
"renderDistance": "6",
|
|
"simulationDistance": "6",
|
|
"maxFps": "30",
|
|
"narrator": "0",
|
|
"soundCategory_master": "0.0",
|
|
"realmsNotifications": "false",
|
|
"hideServerAddress": "false",
|
|
"autoJump": "false",
|
|
"chatLinksPrompt": "false",
|
|
"onlyShowSecureChat": "false",
|
|
"rawMouseInput": "false",
|
|
}
|
|
|
|
|
|
def offline_uuid(name):
|
|
digest = bytearray(hashlib.md5(("OfflinePlayer:" + name).encode()).digest())
|
|
digest[6] = (digest[6] & 0x0F) | 0x30
|
|
digest[8] = (digest[8] & 0x3F) | 0x80
|
|
return uuid.UUID(bytes=bytes(digest))
|
|
|
|
|
|
def linux_library(path):
|
|
name = Path(path).name
|
|
if "natives-" in name:
|
|
return name.endswith("natives-linux.jar")
|
|
if "netty-transport-native" in name and ("osx" in name or "aarch_64" in name):
|
|
return False
|
|
return "java-objc-bridge" not in name
|
|
|
|
|
|
def classpath():
|
|
prepared = json.loads((GAME / "prepared.json").read_text())
|
|
jars = [ROOT / p for p in prepared["libraries"]["client"] if linux_library(p)]
|
|
return [GAME / "jars/client.jar", *jars]
|
|
|
|
|
|
def extract_natives(jars, target):
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
for jar in jars:
|
|
if not jar.name.endswith("natives-linux.jar"):
|
|
continue
|
|
with zipfile.ZipFile(jar) as archive:
|
|
for name in archive.namelist():
|
|
if name.endswith(".so") and "arm" not in name and "aarch" not in name:
|
|
out = target / Path(name).name
|
|
if not out.exists():
|
|
out.write_bytes(archive.read(name))
|
|
|
|
|
|
def write_options(game_dir, extra):
|
|
path = game_dir / "options.txt"
|
|
current = {}
|
|
if path.exists():
|
|
for line in path.read_text().splitlines():
|
|
if ":" in line:
|
|
k, v = line.split(":", 1)
|
|
current[k] = v
|
|
current.update(OPTIONS)
|
|
current.update(extra)
|
|
path.write_text("".join(f"{k}:{v}\n" for k, v in current.items()))
|
|
|
|
|
|
def environment():
|
|
env = dict(os.environ)
|
|
libs = [p for p in (os.environ.get("NIX_LD_LIBRARY_PATH", ""), "/run/opengl-driver/lib",
|
|
"/run/current-system/sw/share/nix-ld/lib") if p]
|
|
env["LD_LIBRARY_PATH"] = ":".join(libs + [env.get("LD_LIBRARY_PATH", "")]).strip(":")
|
|
env["ALSOFT_DRIVERS"] = "null"
|
|
return env
|
|
|
|
|
|
def command(args):
|
|
jars = classpath()
|
|
natives = args.game_dir / "natives"
|
|
extract_natives(jars, natives)
|
|
assets = args.assets or (args.game_dir / "assets")
|
|
(assets / "indexes").mkdir(parents=True, exist_ok=True)
|
|
cmd = ["java", f"-Xmx{args.heap}", "-XX:StackShadowPages=32",
|
|
"--enable-native-access=ALL-UNNAMED",
|
|
"--add-exports", "java.base/jdk.internal.misc=ALL-UNNAMED",
|
|
f"-Djava.library.path={natives}", f"-Dorg.lwjgl.system.SharedLibraryExtractPath={natives}",
|
|
f"-Dio.netty.native.workdir={natives}",
|
|
"-cp", os.pathsep.join(map(str, jars)), "net.minecraft.client.main.Main",
|
|
"--username", args.name, "--uuid", offline_uuid(args.name).hex,
|
|
"--accessToken", "0", "--offlineDeveloperMode",
|
|
"--version", "26.3", "--gameDir", str(args.game_dir),
|
|
"--assetsDir", str(assets), "--assetIndex", "34",
|
|
"--width", str(args.width), "--height", str(args.height)]
|
|
if args.join:
|
|
cmd += ["--quickPlayMultiplayer", args.join]
|
|
if args.world:
|
|
cmd += ["--quickPlaySingleplayer", args.world]
|
|
if args.lan_port is not None:
|
|
# The vanilla client as reference LAN host (OfflineLanHost).
|
|
cp = cmd.index("-cp") + 1
|
|
cmd[cp] = os.pathsep.join([str(args.classes), cmd[cp]])
|
|
main = cmd.index("net.minecraft.client.main.Main")
|
|
cmd[main:main + 1] = [f"-Dhorizon.lanPort={args.lan_port}", "dev.horizonlab.parity.OfflineLanHost"]
|
|
if args.backend:
|
|
cmd += ["--graphicsBackend", args.backend]
|
|
return cmd
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--name", default="Guest")
|
|
parser.add_argument("--join", help="HOST:PORT to join on start")
|
|
parser.add_argument("--world", help="open this saves/ folder in singleplayer on start")
|
|
parser.add_argument("--game-dir", type=Path, default=ROOT / ".local/vanilla-client")
|
|
parser.add_argument("--assets", type=Path)
|
|
parser.add_argument("--heap", default="3G")
|
|
parser.add_argument("--width", type=int, default=1280)
|
|
parser.add_argument("--height", type=int, default=800)
|
|
parser.add_argument("--lan-port", type=int,
|
|
help="open the --world to LAN on this port without authentication (needs --classes)")
|
|
parser.add_argument("--classes", type=Path, default=ROOT / ".local/mp/oracle",
|
|
help="compiled OfflineLanHost (mods/horizon/src/test/java)")
|
|
parser.add_argument("--backend", help="graphics backend (opengl or vulkan)")
|
|
parser.add_argument("--option", action="append", default=[], help="KEY:VALUE for options.txt")
|
|
parser.add_argument("--print", action="store_true", help="print the command and exit")
|
|
args = parser.parse_args()
|
|
args.game_dir = args.game_dir.resolve()
|
|
args.game_dir.mkdir(parents=True, exist_ok=True)
|
|
write_options(args.game_dir, dict(o.split(":", 1) for o in args.option))
|
|
cmd = command(args)
|
|
if args.print:
|
|
print(" ".join(cmd))
|
|
return
|
|
sys.exit(subprocess.run(cmd, cwd=args.game_dir, env=environment()).returncode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|