Files
horizon-airdef/tools/test_block_entity_tickers_parity.py
Ludovic Marie 0b581f9ffe Give each crate's probe example its own target name
horizon-client, horizon-world and horizon-protocol each had an example
named `probe`. Cargo copies every crate's examples into one
<profile>/examples/ directory, so parallel parity harnesses sharing a
target dir could run another crate's probe (CI parity run 40055656303:
command_parse ran the horizon-world dispatcher). The examples are now
client_probe, world_probe and protocol_probe ([[example]] in each
Cargo.toml; the files stay where they are), and every harness builds
and runs its own crate's name.

tools/parity.py recognises the new names when it counts the probes a
harness ran, and renames the old `probe` in recorded artifacts to the
crate's target (from the record's -p, else its probe sources), so the
prebuild still builds them instead of dropping them as unknown.

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

133 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Compare the order block entities tick in with vanilla's `Level.blockEntityTickers`.
Random scenarios load chunks with block entities in a save order (enough in
one chunk to make its block entity map grow), place, replace and remove block
entities, give jukeboxes a record and take it, and unload chunks, with ticks
in between. `VanillaBlockEntityTickerOracle` runs them on real `LevelChunk`s
of a server level and prints, after each tick, the positions of the live
tickers in list order; `examples/probe/block_entity_tickers_fixture` runs the same
on `horizon_client::block_entities::BlockEntities`. The block entities are
the kinds the game ticks in that list (hoppers, furnaces, beacons, spawners,
trial spawners, vaults, hives, crafters, brewing stands, jukebox), with chests and barrels,
which have no ticker, among them.
"""
import argparse
import os
import random
import subprocess
import sys
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
PARITY = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity'
TICKING = ['hopper', 'furnace', 'blast_furnace', 'smoker', 'beacon', 'spawner', 'trial_spawner', 'vault',
'beehive', 'bee_nest', 'crafter', 'brewing_stand', 'jukebox_record']
KINDS = TICKING + ['jukebox', 'chest', 'barrel']
def scenario(rng):
"""One scenario's lines: a few chunks near the origin and the ops on them."""
lines = ['reset']
loaded, blocks = set(), {}
def free(cx, cz):
while True:
p = (cx * 16 + rng.randrange(16), rng.randrange(-64, 320), cz * 16 + rng.randrange(16))
if p not in blocks:
return p
def load():
options = [(x, z) for x in range(-2, 3) for z in range(-2, 3) if (x, z) not in loaded]
if not options:
return
cx, cz = rng.choice(options)
loaded.add((cx, cz))
words = ['load', str(cx), str(cz)]
for _ in range(rng.choice([0, 1, 3, 8, 20, 30, 60])):
p, kind = free(cx, cz), rng.choice(KINDS)
blocks[p] = kind
words += [kind, *map(str, p)]
lines.append(' '.join(words))
for _ in range(rng.randrange(1, 4)):
load()
lines.append('tick')
for _ in range(rng.randrange(10, 60)):
op = rng.random()
mine = sorted(blocks)
if op < 0.25 and loaded:
cx, cz = rng.choice(sorted(loaded))
p, kind = free(cx, cz), rng.choice(KINDS)
blocks[p] = kind
lines.append(f'place {kind} {p[0]} {p[1]} {p[2]}')
elif op < 0.4 and mine:
p = rng.choice(mine)
lines.append(f'replace {p[0]} {p[1]} {p[2]}')
elif op < 0.6 and mine:
p = rng.choice(mine)
del blocks[p]
lines.append(f'remove {p[0]} {p[1]} {p[2]}')
elif op < 0.72:
jukeboxes = [p for p in mine if blocks[p].startswith('jukebox')]
if jukeboxes:
p = rng.choice(jukeboxes)
record = blocks[p] == 'jukebox'
blocks[p] = 'jukebox_record' if record else 'jukebox'
lines.append(f'record {p[0]} {p[1]} {p[2]} {int(record)}')
elif op < 0.78 and loaded:
cx, cz = rng.choice(sorted(loaded))
loaded.discard((cx, cz))
for p in [p for p in blocks if (p[0] >> 4, p[2] >> 4) == (cx, cz)]:
del blocks[p]
lines.append(f'unload {cx} {cz}')
elif op < 0.85:
load()
else:
lines.append('tick')
lines.append('tick')
return lines
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--scenarios', type=int, default=3000)
parser.add_argument('--seed', type=int, default=0x7469636b)
args = parser.parse_args()
lock, jars, libraries = base.verify_artifacts()
build = lab.LOCAL / 'rust-block-entity-tickers-parity'
build.mkdir(parents=True, exist_ok=True)
rng = random.Random(args.seed)
lines = [line for _ in range(args.scenarios) for line in scenario(rng)]
inputs = build / 'inputs.txt'
inputs.write_text('\n'.join(lines) + '\n')
classes = build / 'classes'
classes.mkdir(exist_ok=True)
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
def run(command, name):
with (build / (name + '.log')).open('w') as log:
result = subprocess.run(list(map(str, command)), cwd=lab.ROOT, timeout=3600, stdout=log,
stderr=subprocess.STDOUT)
if result.returncode:
print((build / (name + '.log')).read_text()[-4000:])
sys.exit(f'{name} failed')
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-nowarn', '-cp', dependencies,
'-d', classes, *(PARITY / f'{n}.java' for n in ('ParityData', 'VanillaBlockEntityTickerOracle'))],
'compile-java')
run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies,
'dev.horizonlab.parity.VanillaBlockEntityTickerOracle', inputs, build / 'vanilla.txt'], 'vanilla')
run(['cargo', 'run', '--quiet', '--profile', 'fast', '-p', 'horizon-client', '--example',
'client_probe', '--', 'block_entity_tickers_fixture', inputs, build / 'rust.txt'], 'rust')
matched = base.compare(build / 'vanilla.txt', build / 'rust.txt')
ticks = sum(1 for line in lines if line == 'tick')
listed = sum(len(line.split()) - 1 for line in (build / 'vanilla.txt').read_text().splitlines())
print(f'block entity tickers: {args.scenarios} scenarios, {matched} of {ticks} ticks match vanilla '
f'({listed} tickers listed)')
if __name__ == '__main__':
main()