874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
169 lines
8.1 KiB
Python
169 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.
|
|
|
|
Trees of neighbouring chunks overlap on chunk corners, and which one is
|
|
placed first depends on the order the chunks are decorated. With several
|
|
worker threads vanilla's order changes from run to run (seed
|
|
-2080727421395798566 spawned at Y 64, 66 or 72), so its server runs with one
|
|
(`-Dmax.bg.threads`, Util.maxAllowedExecutorThreads): the chunks are then
|
|
decorated in its scheduler's order, the one Horizon follows
|
|
(decoration_order::vanilla_order), and every run gives the same spawn.
|
|
|
|
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.
|
|
|
|
tools/parity.py runs it in PARITY_SHARDS parts, each creating the worlds of
|
|
every PARITY_SHARDS-th seed: each seed is a vanilla and a Horizon server start,
|
|
~15 s, and 60 seeds in one run were the longest harness (~25 min).
|
|
|
|
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, parity_shard, 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 parity_shard
|
|
import test_rust_parity as base
|
|
from test_dedicated_server_parity import Server, free_port
|
|
|
|
NAME = 'world-spawn-parity'
|
|
PARITY_SHARDS = 4
|
|
REPORT = 1891623539983467248
|
|
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(profile='release'):
|
|
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'],
|
|
cwd=lab.ROOT, text=True))
|
|
return Path(metadata['target_directory']) / profile
|
|
|
|
|
|
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/world_probe', '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 / parity_shard.label() # shards run at once
|
|
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', 'world_probe'], build, 'compile-probe')
|
|
all_seeds = parity_shard.shard(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', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'], build,
|
|
'compile-rust', timeout=3600)
|
|
binary = target_dir('fast') / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client')
|
|
# One worker thread: the chunks decorated in a fixed order (see above).
|
|
vanilla_server = ['java', '-Xmx2g', '-Dmax.bg.threads=1', '-cp', dependencies, 'net.minecraft.server.Main', 'nogui']
|
|
horizon_server = [binary, '--resources', RESOURCES, '--dedicated', '--nogui']
|
|
os.environ.update(lab.game_env())
|
|
|
|
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}')
|
|
return seed, a, b, time.time() - t0
|
|
|
|
spawn_diffs = []
|
|
with ThreadPoolExecutor(args.jobs) as pool:
|
|
for seed, a, b, took in pool.map(one, all_seeds):
|
|
ok = a == b
|
|
if not ok:
|
|
spawn_diffs.append(seed)
|
|
print(f'{"ok " if ok else "DIFF"} {seed}: vanilla {a[0]} {a[1]} {a[2]} / 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')
|
|
if origin_diffs or spawn_diffs:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|