874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
118 lines
5.2 KiB
Python
118 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""A vanilla loot chest at spawn has its loot when Horizon opens the world.
|
|
|
|
Vanilla rolls a container's loot table when a player first opens it
|
|
(`RandomizableContainer.unpackLootTable`). Horizon rolls a loaded container with a loot table
|
|
and no items as its chunk is read from disk (`save::roll_loaded_loot`), so a container nobody
|
|
opens still saves as vanilla wrote it. The loot tables have to be loaded before the first chunks
|
|
are read: the spawn chunks are read while the world opens, and a chest read before the tables
|
|
came out empty, its loot table lost on the next save.
|
|
|
|
The vanilla 26.3 server writes a flat world with a seeded loot chest beside the world spawn, then
|
|
rolls the chest itself in a copy of that world (`execute if items` unpacks it). Horizon opens the
|
|
untouched world in a new process, so nothing was loaded before the world, and must hold the same
|
|
items in the same slots. Both single-player paths: the default server thread and in-frame.
|
|
|
|
test_spawn_loot_parity.py [--bin-dir DIR] [--keep]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from test_save_roundtrip_parity import Vanilla # noqa: E402
|
|
from test_world_reopen_parity import game_env # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
RESOURCES = ROOT / '.local' / 'horizon-client' / '26.3'
|
|
RUN = ROOT / '.local' / 'spawn-loot'
|
|
CHEST = (2, -60, 1)
|
|
LOOT = '{LootTable:"minecraft:chests/simple_dungeon",LootTableSeed:42L}'
|
|
MODES = {'server thread': [], 'in-frame': ['--in-frame']}
|
|
|
|
|
|
def vanilla_world():
|
|
"""The world with the unopened chest, and what vanilla rolls from it: [(slot, id, count)]."""
|
|
server = Vanilla(RUN / 'v0')
|
|
try:
|
|
server.run('setworldspawn 0 -60 0')
|
|
server.run('forceload add 0 0')
|
|
time.sleep(2)
|
|
server.run('setblock {} {} {} minecraft:chest{}'.format(*CHEST, LOOT))
|
|
server.run('save-all flush')
|
|
time.sleep(2)
|
|
finally:
|
|
server.stop()
|
|
shutil.copytree(RUN / 'v0', RUN / 'vv')
|
|
server = Vanilla(RUN / 'vv')
|
|
try:
|
|
server.run('forceload add 0 0')
|
|
time.sleep(2)
|
|
server.run('execute if items block {} {} {} container.* *'.format(*CHEST))
|
|
text = ' '.join(server.run('data get block {} {} {} Items'.format(*CHEST)))
|
|
finally:
|
|
server.stop()
|
|
rolled = []
|
|
for item in re.finditer(r'\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}', text.split('data:', 1)[-1]):
|
|
found = item.group(0)
|
|
count = re.search(r'count: (\d+)', found)
|
|
rolled.append((int(re.search(r'Slot: (\d+)b', found).group(1)), re.search(r'id: "([^"]+)"', found).group(1),
|
|
int(count.group(1)) if count else 1))
|
|
return sorted(rolled)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('--bin-dir', help='prebuilt horizon-client and horizon-ctl (default: build them with the fast profile)')
|
|
parser.add_argument('--keep', action='store_true', help='keep the run folder')
|
|
args = parser.parse_args()
|
|
bin_dir = Path(args.bin_dir).resolve() if args.bin_dir else None
|
|
if bin_dir is None:
|
|
subprocess.run(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bins'], cwd=ROOT, check=True)
|
|
bin_dir = ROOT / os.environ.get('CARGO_TARGET_DIR', 'target') / 'fast'
|
|
shutil.rmtree(RUN, ignore_errors=True)
|
|
RUN.mkdir(parents=True)
|
|
expected = vanilla_world()
|
|
print('vanilla rolls:', expected)
|
|
if not expected:
|
|
sys.exit('vanilla rolled nothing from the chest')
|
|
env, extra = game_env()
|
|
failures = 0
|
|
for index, (mode, flags) in enumerate(MODES.items()):
|
|
folder = RUN / f'h{index}'
|
|
shutil.copytree(RUN / 'v0' / 'world', folder / 'saves' / 'world')
|
|
socket = Path(tempfile.mkdtemp(prefix='hzsl')) / 'control.sock' # sockets need short paths
|
|
|
|
def ctl(*words, check=True):
|
|
result = subprocess.run([str(bin_dir / 'horizon-ctl'), '--socket', str(socket), '--timeout', '600',
|
|
*map(str, words)], cwd=folder, env=env, capture_output=True, text=True)
|
|
if check and result.returncode:
|
|
sys.exit(f'horizon-ctl {" ".join(map(str, words))} failed: {result.stdout}{result.stderr}')
|
|
return json.loads(result.stdout) if result.stdout.strip().startswith('{') else result.stdout
|
|
|
|
ctl('launch', '--resources', RESOURCES, '--world', 'world', '--render-distance', '4', *flags, *extra)
|
|
try:
|
|
ctl('wait-ready', '300')
|
|
slots = ctl('container', *CHEST)['slots']
|
|
finally:
|
|
ctl('quit', check=False)
|
|
got = sorted((slot, stack['item'], stack['count']) for slot, stack in enumerate(slots) if stack)
|
|
ok = got == expected
|
|
failures += not ok
|
|
print(f'{mode}: {len(got)} stacks', 'ok' if ok else f'FAIL: {got}')
|
|
if not args.keep and not failures:
|
|
shutil.rmtree(RUN, ignore_errors=True)
|
|
print('spawn loot:', 'PASS' if not failures else f'{failures} FAILED')
|
|
sys.exit(1 if failures else 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|