mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
lab.VERSION, lab.GAME (.local/minecraft/<version>) and lab.RESOURCES (.local/horizon-client/<version>) come from the lock, and lab.version_info() reads the pinned client jar's version.json. Every harness, generator and tool that named 26.3 (resource folders, jar and source paths, result records, the vanilla client's --version and asset index, the bare login client's protocol, the benchmark's options.txt data version, the packet id table's header) uses them, so a candidate worktree pinned to another version runs them unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
170 lines
8.1 KiB
Python
170 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare a new world's spawn with vanilla's, seed by seed.
|
|
|
|
A new overworld's spawn comes from two steps (MinecraftServer.setInitialSpawn):
|
|
|
|
1. the origin: NoiseBasedChunkGenerator.getOrigin runs NoiseSpawnFinder over
|
|
the noise settings' `spawn_target` points (a coarse then a fine radial
|
|
search of the climate fields) and takes the chunk of the best column;
|
|
2. the spawn: an 11 by 11 chunk spiral around that chunk, the first chunk
|
|
with a column PlayerSpawnFinder.getSpawnPosInChunk accepts (a full-topped
|
|
block under open sky, no liquid on top) gives the world spawn.
|
|
|
|
Origins: VanillaWorldSpawnOracle calls NoiseSpawnFinder.findSpawnPosition and
|
|
getOrigin for every seed; the spawn_origin example asks Horizon's generator.
|
|
World spawns: the pinned vanilla dedicated server creates each world and
|
|
stops; Horizon's (`horizon-client --dedicated --nogui`) creates its own from
|
|
the same server.properties and stops; the `spawn` position of both
|
|
level.dat files is compared.
|
|
|
|
Vanilla is not deterministic everywhere: trees of neighbouring chunks
|
|
overlap on chunk corners, and which one is placed last depends on the order
|
|
the server's worker threads decorate the chunks. A spawn column on such a
|
|
corner can then stand on different leaves from one vanilla run to the next.
|
|
When the spawns differ, vanilla creates the world up to RETRIES more times;
|
|
Horizon's spawn passes if one of vanilla's runs gives it too, and the seed is
|
|
reported as varying in vanilla.
|
|
|
|
Seeds: the player report's seed (1891623539983467248: Horizon spawned at
|
|
-48 -48, vanilla at 152 145), the chunk generation harness's seeds and
|
|
--random seeds from a fixed generator.
|
|
|
|
usage: test_world_spawn_parity.py [--random N] [--jobs N] [--origins-only]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import random
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from . import anvil_dump, lab, oracle_harness as oh, test_rust_parity as base
|
|
from .test_dedicated_server_parity import Server, free_port
|
|
except ImportError:
|
|
import anvil_dump
|
|
import lab
|
|
import oracle_harness as oh
|
|
import test_rust_parity as base
|
|
from test_dedicated_server_parity import Server, free_port
|
|
|
|
NAME = 'world-spawn-parity'
|
|
REPORT = 1891623539983467248
|
|
RETRIES = 3
|
|
SEEDS = [REPORT, 12345, -7046029254386353131, 987654321987, 4242, -1, 0x5DEECE66D, 20260607, 777, 0]
|
|
RESOURCES = lab.RESOURCES
|
|
|
|
|
|
def seeds(count):
|
|
rng = random.Random(20261014)
|
|
return SEEDS + [rng.randrange(-2**63, 2**63) for _ in range(count)]
|
|
|
|
|
|
def target_dir():
|
|
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'],
|
|
cwd=lab.ROOT, text=True))
|
|
return Path(metadata['target_directory']) / 'release'
|
|
|
|
|
|
def origins(build, classes, dependencies, all_seeds):
|
|
vanilla_out = build / 'origins-vanilla.txt'
|
|
oh.run_logged(['java', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaWorldSpawnOracle',
|
|
vanilla_out, *all_seeds], build, 'origins-vanilla')
|
|
rust = subprocess.run([target_dir() / 'examples/spawn_origin', RESOURCES, *map(str, all_seeds)],
|
|
capture_output=True, text=True, check=True, cwd=lab.ROOT).stdout
|
|
vanilla = vanilla_out.read_text()
|
|
parse = lambda text: {int(l.split()[0]): tuple(map(int, l.split()[1:])) for l in text.splitlines() if l}
|
|
return parse(vanilla), parse(rust)
|
|
|
|
|
|
def world_spawn(command, directory, seed, log_name):
|
|
"""Creates a world with `seed` in `directory`, stops, returns its level.dat spawn position."""
|
|
if directory.exists():
|
|
shutil.rmtree(directory)
|
|
directory.mkdir(parents=True)
|
|
props = {'online-mode': 'false', 'level-seed': str(seed), 'server-port': str(free_port()),
|
|
'view-distance': '4', 'simulation-distance': '4', 'pause-when-empty-seconds': '-1'}
|
|
(directory / 'server.properties').write_text(''.join(f'{k}={v}\n' for k, v in props.items()), encoding='iso-8859-1')
|
|
(directory / 'eula.txt').write_text('eula=true\n')
|
|
server = Server(command, directory, log_name)
|
|
try:
|
|
server.wait_for(r'Done \(', timeout=900)
|
|
finally:
|
|
server.stop()
|
|
data = anvil_dump.read_dat(directory / 'world/level.dat')['Data']
|
|
return tuple(data['spawn']['pos'][1])
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--random', type=int, default=50, help='random seeds besides the fixed ones')
|
|
parser.add_argument('--jobs', type=int, default=max(1, min(4, int(os.environ.get('FLEET_LOAD_JOBS', '4')) // 4)),
|
|
help='worlds created at once (each side)')
|
|
parser.add_argument('--origins-only', action='store_true', help='compare the spawn search origins only')
|
|
parser.add_argument('--seed', type=int, action='append', help='only these seeds (repeatable)')
|
|
args = parser.parse_args()
|
|
lock, jars, libraries = base.verify_artifacts()
|
|
build = lab.LOCAL / NAME
|
|
build.mkdir(parents=True, exist_ok=True)
|
|
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
|
classes = build / 'classes'
|
|
classes.mkdir(exist_ok=True)
|
|
oh.run_logged(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-nowarn', '-cp', dependencies,
|
|
'-d', classes, lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaWorldSpawnOracle.java'],
|
|
build, 'compile-java')
|
|
oh.run_logged(['cargo', 'build', '--release', '-p', 'horizon-world', '--example', 'spawn_origin'], build, 'compile-probe')
|
|
all_seeds = args.seed or seeds(args.random)
|
|
started = time.time()
|
|
|
|
vanilla, rust = origins(build, classes, dependencies, all_seeds)
|
|
origin_diffs = [s for s in all_seeds if vanilla.get(s) != rust.get(s)]
|
|
for s in origin_diffs:
|
|
print(f'origin {s}: vanilla column {vanilla.get(s)} / horizon {rust.get(s)}')
|
|
print(f'Origins: {len(all_seeds) - len(origin_diffs)}/{len(all_seeds)} seeds match '
|
|
f'(column x z, chunk x z) in {time.time() - started:.0f}s', flush=True)
|
|
if args.origins_only:
|
|
sys.exit(1 if origin_diffs else 0)
|
|
|
|
oh.run_logged(['cargo', 'build', '--release', '-p', 'horizon-client', '--bin', 'horizon-client'], build,
|
|
'compile-rust', timeout=3600)
|
|
binary = target_dir() / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client')
|
|
vanilla_server = ['java', '-Xmx2g', '-cp', dependencies, 'net.minecraft.server.Main', 'nogui']
|
|
horizon_server = [binary, '--resources', RESOURCES, '--dedicated', '--nogui']
|
|
os.environ['HORIZON_NO_AUDIO'] = '1'
|
|
|
|
def one(seed):
|
|
t0 = time.time()
|
|
a = world_spawn(vanilla_server, build / f'vanilla-{seed}', seed, f'vanilla-{seed}')
|
|
b = world_spawn(horizon_server, build / f'horizon-{seed}', seed, f'horizon-{seed}')
|
|
runs = [a]
|
|
while b not in runs and len(runs) <= RETRIES:
|
|
runs.append(world_spawn(vanilla_server, build / f'vanilla-{seed}-{len(runs)}', seed, f'vanilla-{seed}-{len(runs)}'))
|
|
return seed, runs, b, time.time() - t0
|
|
|
|
spawn_diffs, varying = [], []
|
|
with ThreadPoolExecutor(args.jobs) as pool:
|
|
for seed, runs, b, took in pool.map(one, all_seeds):
|
|
ok = b in runs
|
|
if not ok:
|
|
spawn_diffs.append(seed)
|
|
elif len(set(runs)) > 1:
|
|
varying.append(seed)
|
|
seen = ', '.join(' '.join(map(str, r)) for r in dict.fromkeys(runs))
|
|
print(f'{"ok " if ok else "DIFF"} {seed}: vanilla {seen} / horizon {b[0]} {b[1]} {b[2]} '
|
|
f'(origin chunk {vanilla[seed][2]} {vanilla[seed][3]}; {took:.0f}s)', flush=True)
|
|
if ok:
|
|
for path in build.glob(f'*-{seed}*'):
|
|
shutil.rmtree(path, ignore_errors=True)
|
|
print(f'World spawns: {len(all_seeds) - len(spawn_diffs)}/{len(all_seeds)} seeds match in {time.time() - started:.0f}s'
|
|
+ (f'; vanilla itself gave several spawns for {len(varying)}: {varying}' if varying else ''))
|
|
if origin_diffs or spawn_diffs:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|