874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
357 lines
21 KiB
Python
357 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""Differential sculk harness: sensors, calibrated sensors, shriekers, catalysts and wardens.
|
|
|
|
The Java oracle (VanillaSculkOracle) runs each scenario in a real, headless, flat vanilla server
|
|
level: it builds the blocks, places the player (with a warning level) and entities, and ticks the
|
|
level in ServerLevel.tick's order while scripted game events and block changes happen at given
|
|
ticks. Horizon runs the same scenarios through its game code (`HORIZON_SCULK_FIXTURE`, the
|
|
same server phases, with synthetic player and entity state). Both print, per tick, what changed at the watched blocks (state,
|
|
vibration, selector, last frequency, warning level, charge cursors), sounds, level events, the
|
|
player's warden warning level and darkness, new entities, and the level random. The harness
|
|
reports each scenario's first differing tick.
|
|
|
|
python3 tools/test_sculk_parity.py [--only NAME ...] [--count N] [--rust-only] [--java-only] [--report-only]
|
|
"""
|
|
import argparse
|
|
import os
|
|
import random
|
|
import subprocess
|
|
|
|
try:
|
|
from . import lab, oracle_harness, test_rust_parity as base, test_model_baking_parity as baking
|
|
except ImportError:
|
|
import lab
|
|
import oracle_harness
|
|
import test_rust_parity as base
|
|
import test_model_baking_parity as baking
|
|
|
|
FLOOR = -60 # top of the flat preset's grass layer + 1
|
|
SENSOR = 'minecraft:sculk_sensor'
|
|
CALIBRATED = 'minecraft:calibrated_sculk_sensor'
|
|
SHRIEKER = 'minecraft:sculk_shrieker'
|
|
CATALYST = 'minecraft:sculk_catalyst'
|
|
|
|
# Game events that sculk listens to (and some that it does not), by frequency.
|
|
EVENTS = ['step', 'swim', 'flap', 'projectile_land', 'hit_ground', 'splash', 'item_interact_finish', 'projectile_shoot',
|
|
'instrument_play', 'entity_action', 'elytra_glide', 'unequip', 'entity_dismount', 'equip', 'entity_interact',
|
|
'shear', 'entity_mount', 'entity_damage', 'drink', 'eat', 'container_close', 'block_close', 'block_deactivate',
|
|
'block_detach', 'container_open', 'block_open', 'block_activate', 'block_attach', 'prime_fuse', 'note_block_play',
|
|
'block_change', 'block_destroy', 'fluid_pickup', 'block_place', 'fluid_place', 'entity_place', 'lightning_strike',
|
|
'teleport', 'entity_die', 'explode', 'item_interact_start', 'jukebox_play', 'shriek', 'resonate_3', 'resonate_11']
|
|
|
|
|
|
def handwritten():
|
|
s = []
|
|
|
|
def add(name, ticks, lines, opts=''):
|
|
s.append((name, ticks, opts, lines))
|
|
|
|
sensor = f'block 0 {FLOOR} 0 {SENSOR}'
|
|
watch = f'watch 0 {FLOOR} 0'
|
|
add('sensor_step', 60, [sensor, watch, 'player 3.5 -60 0.5', 'at 2 event minecraft:step 3.5 -60 0.5 player'])
|
|
add('sensor_out_of_range', 30, [sensor, watch, 'player 9.5 -60 0.5', 'at 2 event minecraft:step 9.5 -60 0.5 player'])
|
|
add('sensor_edge_of_range', 40, [sensor, watch, 'player 8.5 -60 0.5', 'at 2 event minecraft:step 8.5 -60 0.5 player'])
|
|
add('sensor_wool', 40, [sensor, watch, f'fill 2 {FLOOR} -3 2 {FLOOR + 3} 3 minecraft:white_wool',
|
|
'player 4.5 -60 0.5', 'at 2 event minecraft:block_place 4.5 -60 0.5 player'])
|
|
add('sensor_sneak', 60, [sensor, watch, 'player 3.5 -60 0.5 sneak', 'at 2 event minecraft:step 3.5 -60 0.5 player',
|
|
'at 3 event minecraft:entity_damage 3.5 -60 0.5 player'])
|
|
add('sensor_spectator', 40, [sensor, watch, 'player 3.5 -60 0.5 spectator', 'at 2 event minecraft:step 3.5 -60 0.5 player'])
|
|
add('sensor_wool_dampens', 40, [sensor, watch, 'player 3.5 -60 0.5',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player minecraft:white_wool'])
|
|
add('sensor_own_place', 40, [sensor, watch, 'at 2 event minecraft:block_place 0.5 -59.5 0.5 none',
|
|
'at 6 event minecraft:block_change 0.5 -59.5 0.5 none'])
|
|
add('sensor_selector', 60, [sensor, watch, 'player 3.5 -60 0.5',
|
|
'at 2 event minecraft:step 5.5 -60 0.5 player',
|
|
'at 2 event minecraft:explode 5.5 -60 0.5 none',
|
|
'at 2 event minecraft:eat 2.5 -60 0.5 player',
|
|
'at 2 event minecraft:block_place 6.5 -60 0.5 none',
|
|
'at 3 event minecraft:block_destroy 1.5 -60 0.5 none'])
|
|
add('sensor_cooldown', 120, [sensor, watch, 'player 3.5 -60 0.5'] +
|
|
[f'at {t} event minecraft:block_change 4.5 -60 {0.5 + (t % 5)} player' for t in range(2, 100, 3)])
|
|
add('sensor_redstone', 60, [sensor, watch, f'block 1 {FLOOR} 0 minecraft:redstone_wire', f'block 2 {FLOOR} 0 minecraft:redstone_wire',
|
|
f'block 0 {FLOOR} 1 minecraft:comparator[facing=north]', f'block 0 {FLOOR} 2 minecraft:redstone_wire',
|
|
f'watch 1 {FLOOR} 0', f'watch 2 {FLOOR} 0', f'watch 0 {FLOOR} 1', f'watch 0 {FLOOR} 2',
|
|
'player 5.5 -60 3.5', 'at 2 event minecraft:container_open 5.5 -60 3.5 player'])
|
|
add('sensor_resonance', 80, [sensor, watch, f'block 1 {FLOOR} 0 minecraft:amethyst_block',
|
|
f'block 4 {FLOOR} 0 {CALIBRATED}[facing=north]', f'watch 4 {FLOOR} 0',
|
|
f'block 6 {FLOOR} 0 {SENSOR}', f'watch 6 {FLOOR} 0',
|
|
'player -3.5 -60 0.5', 'at 2 event minecraft:block_place -3.5 -60 0.5 player'])
|
|
for power in (0, 3, 7, 13):
|
|
cal = f'block 0 {FLOOR} 0 {CALIBRATED}[facing=north]'
|
|
lines = [cal, watch, 'player 3.5 -60 0.5']
|
|
if power:
|
|
# A redstone wire behind (south of a north-facing sensor) carries the input power.
|
|
lines += [f'block 0 {FLOOR} 1 minecraft:redstone_wire[power={power},north=side,south=side]', f'watch 0 {FLOOR} 1']
|
|
lines += [f'at {2 + 4 * i} event minecraft:{e} 3.5 -60 {0.5 + i} player' for i, e in
|
|
enumerate(['block_change', 'eat', 'step', 'block_destroy', 'container_open', 'entity_die', 'drink', 'block_place'])]
|
|
add(f'calibrated_power_{power}', 90, lines)
|
|
shrieker = f'block 0 {FLOOR} 0 {SHRIEKER}'
|
|
# Shriekers hear only a sensor's tendrils clicking: a sensor beside it hears the player.
|
|
near = [f'block 2 {FLOOR} 0 {SENSOR}', f'watch 2 {FLOOR} 0']
|
|
for warn in (0, 1, 2, 3):
|
|
add(f'shrieker_warn_{warn}', 120, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, f'player 3.5 -60 0.5 warn={warn}',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player'])
|
|
add('shrieker_no_summon', 120, [shrieker, watch, *near, 'player 3.5 -60 0.5 warn=2', 'at 2 event minecraft:step 3.5 -60 0.5 player'])
|
|
add('shrieker_peaceful', 120, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5 warn=1',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player'], 'difficulty=peaceful')
|
|
add('shrieker_rule_off', 120, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5 warn=1',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player'], 'nowardens')
|
|
add('shrieker_unheard', 60, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 none', 'at 4 event minecraft:block_change 3.5 -60 0.5 player',
|
|
'at 6 event minecraft:eat 3.5 -60 0.5 player'])
|
|
add('shrieker_cooldown', 600, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5'] +
|
|
[f'at {t} event minecraft:step 3.5 -60 0.5 player' for t in range(2, 600, 37)])
|
|
add('shrieker_removed_while_shrieking', 60, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5 warn=2',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player', f'at 20 set 0 {FLOOR} 0 minecraft:air'])
|
|
add('shrieker_summons', 200, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5 warn=3',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player'])
|
|
add('shrieker_creative', 60, [f'block 0 {FLOOR} 0 {SHRIEKER}[can_summon=true]', watch, *near, 'player 3.5 -60 0.5 creative warn=2',
|
|
'at 2 event minecraft:step 3.5 -60 0.5 player'])
|
|
# Catalysts: a mob killed by the player nearby charges the catalyst, which blooms and spreads sculk.
|
|
region = f'region -9 {FLOOR - 3} -9 9 {FLOOR + 3} 9'
|
|
# Mobs without loot (items are another harness's work).
|
|
for name, mob, floor in [('catalyst_silverfish', 'silverfish', 'minecraft:sculk'), ('catalyst_endermite', 'endermite', 'minecraft:stone'),
|
|
('catalyst_axolotl_grass', 'axolotl', None), ('catalyst_blaze', 'blaze', 'minecraft:deepslate')]:
|
|
lines = [f'block 0 {FLOOR} 0 {CATALYST}', watch, region, 'player 5.5 -60 5.5', f'mob minecraft:{mob} 2.5 -60 0.5 0 7 noai',
|
|
'at 3 kill m0']
|
|
if floor:
|
|
lines.insert(0, f'fill -6 {FLOOR - 1} -6 6 {FLOOR - 1} 6 {floor}')
|
|
add(name, 200, lines)
|
|
add('catalyst_far', 60, [f'block 0 {FLOOR} 0 {CATALYST}', watch, region, 'player 5.5 -60 5.5',
|
|
'mob minecraft:silverfish 9.5 -60 0.5 0 7 noai', 'at 3 kill m0'])
|
|
add('catalyst_two', 200, [f'block 0 {FLOOR} 0 {CATALYST}', watch, f'block 4 {FLOOR} 0 {CATALYST}', f'watch 4 {FLOOR} 0', region,
|
|
f'fill -6 {FLOOR - 1} -6 6 {FLOOR - 1} 6 minecraft:sculk', 'player 5.5 -60 5.5',
|
|
'mob minecraft:endermite 3.5 -60 0.5 0 7 noai', 'mob minecraft:silverfish -1.5 -60 2.5 0 8 noai',
|
|
'at 3 kill m0', 'at 4 kill m1'])
|
|
add('catalyst_unkilled', 60, [f'block 0 {FLOOR} 0 {CATALYST}', watch, region, 'player 5.5 -60 5.5',
|
|
'mob minecraft:silverfish 2.5 -60 0.5 0 7 noai', 'at 3 kill m0 3'])
|
|
return s
|
|
|
|
|
|
def random_scenarios(count, seed):
|
|
rng = random.Random(seed)
|
|
out = []
|
|
sculk = [SENSOR, SENSOR, SENSOR, CALIBRATED, CALIBRATED, SHRIEKER, SHRIEKER]
|
|
others = ['minecraft:white_wool', 'minecraft:red_wool', 'minecraft:amethyst_block', 'minecraft:stone', 'minecraft:redstone_wire',
|
|
'minecraft:redstone_block', 'minecraft:white_carpet', 'minecraft:glass']
|
|
facings = ['north', 'south', 'east', 'west']
|
|
for n in range(count):
|
|
lines = []
|
|
cells = {}
|
|
for _ in range(rng.randrange(1, 5)):
|
|
x, y, z = rng.randrange(-4, 5), FLOOR + rng.randrange(0, 3), rng.randrange(-4, 5)
|
|
kind = rng.choice(sculk)
|
|
if kind == CALIBRATED:
|
|
kind += f'[facing={rng.choice(facings)}]'
|
|
elif kind == SHRIEKER and rng.random() < 0.6:
|
|
kind += '[can_summon=true]'
|
|
cells[(x, y, z)] = kind
|
|
for _ in range(rng.randrange(0, 10)):
|
|
x, y, z = rng.randrange(-5, 6), FLOOR + rng.randrange(0, 3), rng.randrange(-5, 6)
|
|
cells.setdefault((x, y, z), rng.choice(others))
|
|
for (x, y, z), state in sorted(cells.items()):
|
|
# Nothing floats: blocks above the floor stand on stone.
|
|
for below in range(FLOOR, y):
|
|
cells.setdefault((x, below, z), 'minecraft:stone')
|
|
# Wire and carpet stand on a full block, or they would pop (items are another harness's work).
|
|
solid = {'minecraft:' + n for n in ('stone', 'white_wool', 'red_wool', 'amethyst_block', 'redstone_block', 'glass')}
|
|
for (x, y, z), state in sorted(cells.items(), key=lambda c: c[0][1]):
|
|
if ('redstone_wire' in state or 'carpet' in state) and y > FLOOR and cells[(x, y - 1, z)] not in solid:
|
|
cells[(x, y, z)] = 'minecraft:stone'
|
|
for (x, y, z), state in sorted(cells.items(), key=lambda c: (c[0][1], c[0][0], c[0][2])):
|
|
lines.append(f'block {x} {y} {z} {state}')
|
|
if any(k in state for k in ('sculk', 'redstone_wire')):
|
|
lines.append(f'watch {x} {y} {z}')
|
|
px, pz = rng.randrange(-7, 8) + 0.5, rng.randrange(-7, 8) + 0.5
|
|
mode = rng.choice(['', '', '', '', ' creative', ' sneak'])
|
|
warn = rng.choice([0, 0, 1, 2, 3, 3])
|
|
lines.append(f'player {px} {FLOOR + 3} {pz}{mode} warn={warn}')
|
|
ticks = rng.choice([60, 100, 160])
|
|
for _ in range(rng.randrange(1, 12)):
|
|
t = rng.randrange(1, ticks - 20)
|
|
ev = rng.choice(EVENTS)
|
|
x, y, z = rng.uniform(-9, 9), FLOOR + rng.uniform(-1, 4), rng.uniform(-9, 9)
|
|
source = rng.choice(['player', 'player', 'player', 'none'])
|
|
if source == 'player':
|
|
x, y, z = px, FLOOR + 3, pz
|
|
lines.append(f'at {t} event minecraft:{ev} {x!r} {y!r} {z!r} {source}')
|
|
# A block nothing stands on is replaced.
|
|
free = [c for c in cells if (c[0], c[1] + 1, c[2]) not in cells]
|
|
if free and rng.random() < 0.3:
|
|
(x, y, z) = rng.choice(free)
|
|
lines.append(f'at {rng.randrange(1, ticks - 5)} set {x} {y} {z} {rng.choice(["minecraft:air", "minecraft:stone"])}')
|
|
opts = rng.choice(['', '', '', 'difficulty=peaceful', 'nowardens', f'levelseed={rng.randrange(1 << 30)}'])
|
|
out.append((f'random_{n}', ticks, opts, lines))
|
|
return out
|
|
|
|
|
|
def catalyst_scenarios(count, seed):
|
|
"""Catalysts over random terrain charged by kills: blooms, cursors, veins, growths."""
|
|
rng = random.Random(seed * 7919 + 13)
|
|
out = []
|
|
ground = ['minecraft:stone', 'minecraft:dirt', 'minecraft:deepslate', 'minecraft:sculk', 'minecraft:grass_block',
|
|
'minecraft:moss_block', 'minecraft:tuff', 'minecraft:cobblestone', 'minecraft:sand']
|
|
extras = ['minecraft:stone', 'minecraft:glass', 'minecraft:oak_planks', 'minecraft:water', 'minecraft:sculk_vein[down=true]',
|
|
'minecraft:sculk', 'minecraft:sculk_catalyst', 'minecraft:white_wool', 'minecraft:oak_slab']
|
|
# Mobs without loot (item drops are another harness's work).
|
|
mobs = ['silverfish', 'endermite', 'axolotl', 'silverfish', 'vex']
|
|
for n in range(count):
|
|
lines = []
|
|
for x in range(-7, 8):
|
|
for z in range(-7, 8):
|
|
if rng.random() < 0.7:
|
|
lines.append(f'block {x} {FLOOR - 1} {z} {rng.choice(ground)}')
|
|
cells = {}
|
|
for _ in range(rng.randrange(0, 14)):
|
|
x, y, z = rng.randrange(-6, 7), FLOOR + rng.randrange(0, 2), rng.randrange(-6, 7)
|
|
cells[(x, y, z)] = rng.choice(extras)
|
|
for (x, y, z), state in sorted(cells.items(), key=lambda c: c[0][1]):
|
|
if y > FLOOR and (x, FLOOR, z) not in cells:
|
|
lines.append(f'block {x} {FLOOR} {z} minecraft:stone')
|
|
lines.append(f'block {x} {y} {z} {state}')
|
|
catalysts = []
|
|
for _ in range(rng.choice([1, 1, 1, 2, 3])):
|
|
x, z = rng.randrange(-4, 5), rng.randrange(-4, 5)
|
|
catalysts.append((x, z))
|
|
lines.append(f'block {x} {FLOOR} {z} {CATALYST}')
|
|
lines.append(f'watch {x} {FLOOR} {z}')
|
|
lines.append(f'region -9 {FLOOR - 3} -9 9 {FLOOR + 3} 9')
|
|
lines.append(f'player {rng.randrange(-8, 9) + 0.5} {FLOOR + 4} {rng.randrange(-8, 9) + 0.5}')
|
|
ticks = rng.choice([120, 200, 300])
|
|
kills = rng.randrange(1, 12)
|
|
for i in range(kills):
|
|
cx, cz = rng.choice(catalysts)
|
|
x, z = cx + rng.randrange(-6, 7) + 0.5, cz + rng.randrange(-6, 7) + 0.5
|
|
lines.append(f'mob minecraft:{rng.choice(mobs)} {x} {FLOOR + 2} {z} 0 {rng.randrange(1000)} noai')
|
|
lines.append(f'at {rng.randrange(1, ticks // 2)} kill m{i}')
|
|
opts = f'levelseed={rng.randrange(1 << 30)}'
|
|
out.append((f'catalyst_random_{n}', ticks, opts, lines))
|
|
return out
|
|
|
|
|
|
def scenarios(count, seed):
|
|
return handwritten() + random_scenarios(count, seed) + catalyst_scenarios(count // 3, seed)
|
|
|
|
|
|
def write_scenarios(path, chosen):
|
|
lines = []
|
|
for i, (name, ticks, opts, body) in chosen:
|
|
# One origin for all: the oracle resets the area between scenarios.
|
|
ox, oz = 512, 0
|
|
lines.append(f'scenario {name} ticks={ticks} {opts}'.rstrip())
|
|
lines.append(f'origin {ox} {oz}')
|
|
for line in body:
|
|
f = line.split()
|
|
if f[0] == 'player':
|
|
f[1] = repr(float(f[1]) + ox)
|
|
f[3] = repr(float(f[3]) + oz)
|
|
elif f[0] == 'mob':
|
|
f[2] = repr(float(f[2]) + ox)
|
|
f[4] = repr(float(f[4]) + oz)
|
|
elif f[0] == 'region':
|
|
for k in (1, 4):
|
|
f[k] = str(int(f[k]) + ox)
|
|
for k in (3, 6):
|
|
f[k] = str(int(f[k]) + oz)
|
|
elif f[0] in ('block', 'watch'):
|
|
f[1] = str(int(f[1]) + ox)
|
|
f[3] = str(int(f[3]) + oz)
|
|
elif f[0] == 'fill':
|
|
for k in (1, 4):
|
|
f[k] = str(int(f[k]) + ox)
|
|
for k in (3, 6):
|
|
f[k] = str(int(f[k]) + oz)
|
|
elif f[0] == 'at' and f[2] == 'event':
|
|
f[4] = repr(float(f[4]) + ox)
|
|
f[6] = repr(float(f[6]) + oz)
|
|
elif f[0] == 'at' and f[2] == 'set':
|
|
f[3] = str(int(f[3]) + ox)
|
|
f[5] = str(int(f[5]) + oz)
|
|
lines.append(' '.join(f))
|
|
lines.append('end')
|
|
path.write_text('\n'.join(lines) + '\n')
|
|
|
|
|
|
def parse(path):
|
|
"""{scenario: {tick: [lines]}}"""
|
|
result, cur = {}, None
|
|
for line in path.read_text().splitlines():
|
|
if line.startswith('== '):
|
|
cur = result.setdefault(line[3:], {})
|
|
continue
|
|
t, _, rest = line.partition(' ')
|
|
if rest.startswith('trace '):
|
|
# SCULK_TRACE=1: the level random's callers, for reading only.
|
|
continue
|
|
cur.setdefault(int(t), []).append(rest)
|
|
return result
|
|
|
|
|
|
def compare(vanilla, rust):
|
|
report, clean = [], 0
|
|
for name, v in vanilla.items():
|
|
r = rust.get(name)
|
|
if r is None:
|
|
report.append(f'{name}: missing from Horizon')
|
|
continue
|
|
bad = None
|
|
for t in sorted(set(v) | set(r)):
|
|
a, b = v.get(t, []), r.get(t, [])
|
|
if sorted(a) != sorted(b):
|
|
bad = t
|
|
report.append(f'{name}: first difference at tick {t}')
|
|
for line in a:
|
|
if line not in b:
|
|
report.append(f' vanilla {line}')
|
|
for line in b:
|
|
if line not in a:
|
|
report.append(f' horizon {line}')
|
|
break
|
|
if bad is None:
|
|
clean += 1
|
|
report.append(f'{name}: all {len(v)} ticks match')
|
|
return report, clean
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('--only', nargs='*')
|
|
parser.add_argument('--count', type=int, default=300, help='random scenarios besides the handwritten ones')
|
|
parser.add_argument('--seed', type=int, default=1)
|
|
parser.add_argument('--java-only', action='store_true')
|
|
parser.add_argument('--rust-only', action='store_true', help='reuse the last vanilla output')
|
|
parser.add_argument('--report-only', action='store_true', help='print differences without failing')
|
|
args = parser.parse_args()
|
|
build = lab.LOCAL / 'sculk-parity'
|
|
build.mkdir(parents=True, exist_ok=True)
|
|
chosen = [(i, s) for i, s in enumerate(scenarios(args.count, args.seed)) if not args.only or any(o in s[0] for o in args.only)]
|
|
inputs = build / 'scenarios.txt'
|
|
write_scenarios(inputs, chosen)
|
|
lock, jars, _ = base.verify_artifacts()
|
|
resources = lab.LOCAL / 'horizon-client' / lock['minecraft']['version']
|
|
if not args.rust_only:
|
|
classes, dependencies, _ = oracle_harness.compile_oracle(build, 'VanillaSculkOracle', jars, baking.client_libraries(),
|
|
lock['minecraft']['java_major'])
|
|
oracle_harness.run_logged(['java', '-Xmx3g', '-Djava.awt.headless=true', '-cp', str(classes) + os.pathsep + dependencies,
|
|
'dev.horizonlab.parity.VanillaSculkOracle', inputs, build / 'vanilla.txt'],
|
|
build, 'vanilla', timeout=3600)
|
|
if not args.java_only:
|
|
oracle_harness.run_logged(['cargo', 'build', '--locked', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'],
|
|
build, 'compile-rust')
|
|
oracle_harness.run_logged([lab.cargo_target() / 'fast/horizon-client', '--resources', resources], build, 'horizon',
|
|
env=lab.game_env(HORIZON_SCULK_FIXTURE=f'{inputs},{build / "horizon.txt"}'))
|
|
if args.java_only:
|
|
return
|
|
names = {s[0] for _, s in chosen}
|
|
vanilla = {k: v for k, v in parse(build / 'vanilla.txt').items() if k in names}
|
|
report, clean = compare(vanilla, parse(build / 'horizon.txt'))
|
|
(build / 'report.txt').write_text('\n'.join(report) + '\n')
|
|
print('\n'.join(line for line in report if not line.endswith('ticks match')))
|
|
print(f'{clean}/{len(vanilla)} scenarios match on every tick')
|
|
if not args.report_only and clean != len(vanilla):
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|