Files
horizon-airdef/tools/compare_structure_blocks.py
T
Ludovic Marie d3659d9652 Match vanilla structure placement block for block
- Legacy jigsaw elements apply STRUCTURE_AND_AIR last, after jigsaw
  replacement (outposts no longer carve air where jigsaws sat).
- Templates without a known shape run StructureTemplate's neighbor shape
  updates (updateShapeAtEdge, then updateFromNeighbourShapes) through
  level::Behaviours: snowy grass under igloos, stair shapes and fence
  connections in shipwrecks.
- The placement oracle keeps going through structure entities (a level
  stand-in with no enabled features) and compares brushable-block loot.

With block behavior exports present, 18 chunks of seed 1 covering
villages, igloo, outpost, mineshafts, shipwreck, warm ocean ruin, buried
treasure, ruined portal, trial chambers, monument, mansion, stronghold,
ancient city, trail ruins and a jungle temple match vanilla exactly,
blocks and block-entity loot seeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:46:03 +02:00

114 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Compare structure placement (terrain + structure pieces, no placed features)
between vanilla and Rust for target chunks.
usage: compare_structure_blocks.py RESOURCES SEED DIMENSION CX,CZ...
Runs VanillaStructurePlacementOracle and the structure_blocks example and
reports, per chunk, the blocks and block entities that differ.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
def build_dir():
return Path(os.environ['HORIZON_ORACLE_BUILD']) if os.environ.get('HORIZON_ORACLE_BUILD') else lab.LOCAL/'structure-placement-oracle'
def run_oracle(out, args):
lock, jars, libraries = base.verify_artifacts()
source = lab.ROOT/'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaStructurePlacementOracle.java'
classes = build_dir()/'classes'
classes.mkdir(parents=True, exist_ok=True)
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
compiled = subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-cp', dependencies,
'-d', str(classes), str(source)], capture_output=True, text=True)
if compiled.returncode:
raise SystemExit('javac failed:\n' + compiled.stderr[-3000:])
result = subprocess.run(['java', '-Xmx4G', '-cp', str(classes)+os.pathsep+dependencies,
'dev.horizonlab.parity.VanillaStructurePlacementOracle', str(out), *args],
capture_output=True, text=True)
if result.returncode:
lines = [line for line in (result.stdout + result.stderr).splitlines() if 'Exception' in line or 'Error' in line or '\tat ' in line]
raise SystemExit('oracle failed: ' + ' | '.join(lines[:8]))
def parse(lines):
chunks, current = {}, None
for line in lines:
t = line.split()
if not t:
continue
if t[0] == 'chunk':
current = (int(t[1]), int(t[2]))
chunks[current] = {'rows': {}, 'be': []}
elif t[0] == 'y' and current:
chunks[current]['rows'][int(t[1])] = t[2:]
elif t[0] == 'be' and current:
# Position, loot table and seed; the block entity type follows
# from the (separately compared) block state.
chunks[current]['be'].append(' '.join(t[1:4] + t[5:]))
return chunks
def state_names(resources):
names = {}
report = lab.LOCAL/'datagen/out/reports/blocks.json'
if report.exists():
for block, info in json.loads(report.read_text()).items():
for state in info['states']:
props = ','.join(f'{k}={v}' for k, v in sorted(state.get('properties', {}).items()))
names[str(state['id'])] = block + (f'[{props}]' if props else '')
return names
def main():
resources, seed, dimension, targets = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:]
out = build_dir()/'vanilla.txt'
out.parent.mkdir(parents=True, exist_ok=True)
run_oracle(out, [seed, dimension, *targets])
vanilla = parse(out.read_text().splitlines())
subprocess.run(['cargo', 'build', '--profile', 'fast', '-q', '-p', 'horizon-world', '--example', 'structure_blocks'],
check=True, cwd=lab.ROOT)
rust_out = subprocess.run([str(lab.ROOT/'target/fast/examples/structure_blocks'), resources, seed, dimension, *targets],
check=True, capture_output=True, text=True).stdout
(build_dir()/'rust.txt').write_text(rust_out)
rust = parse(rust_out.splitlines())
names = state_names(resources)
failed = 0
for key in sorted(vanilla):
v, r = vanilla[key], rust.get(key)
if r is None:
print(key, 'missing from Rust')
failed += 1
continue
diffs = []
for y, row in v['rows'].items():
other = r['rows'].get(y, [])
for i, (a, b) in enumerate(zip(row, other)):
if a != b:
diffs.append((key[0] * 16 + i % 16, y, key[1] * 16 + i // 16, a, b))
total = sum(len(row) for row in v['rows'].values())
be_v, be_r = set(v['be']), set(r['be'])
status = 'match' if not diffs and be_v == be_r else 'DIFF'
print(f'chunk {key}: {status}; {total - len(diffs)} of {total} blocks equal; block entities {len(be_v & be_r)} of {len(be_v)} equal')
for x, y, z, a, b in diffs[:12]:
print(f' {x} {y} {z} vanilla {names.get(a, a)} rust {names.get(b, b)}')
for e in sorted(be_v - be_r)[:6]:
print(' only vanilla be', e)
for e in sorted(be_r - be_v)[:6]:
print(' only rust be', e)
failed += status != 'match'
return 1 if failed else 0
if __name__ == '__main__':
sys.exit(main())