Files
horizon-airdef/tools/test_mob_attributes_parity.py
T
Ludovic Marie 6396e8c161 Run the vanilla harnesses in parallel, only when their inputs change
tools/parity.py traces each harness (its Python, Java oracle sources and
the Rust sources of the probes it runs, from Cargo's dep-info) and reruns
it only when one of those, the vanilla artifacts or the toolchains change.
Harnesses run in parallel within the available memory, and their probes
build without LTO and with 16 codegen units into .local/parity-target: a
one-line change rebuilds every horizon-world probe in 3 seconds instead of
122. The full suite takes about 20 minutes instead of an hour.

tools/gate.py is the check a branch passes before integration, and
tools/supervise.py manages the per-area worktrees: status, new worktrees,
saving unfinished work, and integrating a branch only when the gate passes.

Harnesses find probes through lab.cargo_target() instead of a hard-coded
target/, the save round-trip harness builds the binaries it launches, and
the mob AI harness now fails on divergences like every other harness
(--report-only keeps the old behaviour). Debug builds keep line tables
only, so worktrees stay small.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:05:36 +02:00

96 lines
4.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Compare mob default attributes, dimensions (adult and baby) and the entity inventory with vanilla."""
import argparse
import math
import os
from pathlib import Path
import subprocess
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
# Entity types that are not mobs (or are players): Horizon models them outside the mob table.
# Horizon models these differently on purpose: slimes/magma/sulfur cubes are created already sized (size 1,
# vanilla applies setSize at spawn), and the phantom keeps its move control speed in attrs.speed.
KNOWN = {('minecraft:slime', k) for k in ('max_health', 'movement_speed', 'attack_damage')} | \
{('minecraft:magma_cube', k) for k in ('max_health', 'movement_speed', 'attack_damage')} | \
{('minecraft:sulfur_cube', k) for k in ('max_health', 'movement_speed')} | {('minecraft:phantom', 'movement_speed')}
def parse(path):
rows = {}
for line in path.read_text(encoding='utf-8').splitlines():
id, *fields = line.split('\t')
row = {'raw': fields}
for f in fields:
key, _, rest = f.partition(' ')
if key == 'attrs':
row['attrs'] = {k: float(v) for k, v in (p.split('=') for p in rest.split())}
elif key in ('dim', 'baby'):
try:
row[key] = [float(x) for x in rest.split()]
except ValueError:
pass
elif key == 'scale':
row['scale'] = float(rest)
rows[id] = row
return rows
def close(a, b):
return a == b or abs(a - b) <= 1e-6 * max(1.0, abs(a), abs(b)) or (math.isnan(a) and math.isnan(b))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prepare', action='store_true')
args = parser.parse_args()
build = lab.LOCAL / 'rust-mob-attributes-parity'
build.mkdir(parents=True, exist_ok=True)
if args.prepare:
lab.prepare()
lock, jars, libraries = base.verify_artifacts()
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaMobAttributesOracle.java'
classes = build / 'classes'
classes.mkdir(exist_ok=True)
deps = os.pathsep.join(map(str, libraries))
def run(cmd, name):
with (build / (name + '.log')).open('w', encoding='utf-8') as log:
subprocess.run(list(map(str, cmd)), cwd=lab.ROOT, check=True, timeout=3600, stdout=log, stderr=subprocess.STDOUT)
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx1G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]), 'dev.horizonlab.parity.VanillaMobAttributesOracle', build / 'vanilla.txt'], 'java')
run(['cargo', 'build', '--release', '-p', 'horizon-client', '--example', 'mob_attributes_fixture'], 'compile-rust')
binary = lab.cargo_target() / 'release/examples/mob_attributes_fixture'
resources = lab.ROOT / '.local/horizon-client/26.3'
run([binary, resources, build / 'rust.txt'], 'rust')
vanilla, rust = parse(build / 'vanilla.txt'), parse(build / 'rust.txt')
problems = []
living = [i for i, r in vanilla.items() if 'attrs' in r]
missing = [i for i in vanilla if i not in rust]
(build / 'missing.txt').write_text('\n'.join(missing) + '\n')
failed = [i for i, r in vanilla.items() if 'raw' in r and r['raw'][:1] == ['FAIL']]
for id, v in vanilla.items():
r = rust.get(id)
if r is None:
continue
if 'dim' in v and 'dim' in r and not all(close(a, b) for a, b in zip(v['dim'], r['dim'])):
problems.append(f'{id}: dim vanilla={v["dim"]} rust={r["dim"]}')
if 'baby' in v and not all(close(a, b) for a, b in zip(v['baby'], r['baby'])):
problems.append(f'{id}: baby vanilla={v["baby"]} rust={r["baby"]}')
for k, x in v.get('attrs', {}).items():
if (id, k) not in KNOWN and k in r['attrs'] and not close(x, r['attrs'][k]):
problems.append(f'{id}: {k} vanilla={x} rust={r["attrs"][k]}')
print(f'vanilla types {len(vanilla)} (living {len(living)}), Horizon mob types {len(rust)}, not in mob table {len(missing)}: {" ".join(m.split(":")[1] for m in missing)}')
print('oracle could not instantiate:', ' '.join(f.split(':')[1] for f in failed))
if problems:
print('\n'.join(problems))
raise SystemExit(f'{len(problems)} divergences')
print('Mob attribute/dimension parity passed')
if __name__ == '__main__':
main()