874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
229 lines
12 KiB
Python
229 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare vaults (VaultBlockEntity.Server, VaultState and VaultBlock.useItemOn) with vanilla.
|
|
|
|
Java (VanillaNaturalSpawnOracle, mode=vault) loads a VaultBlockEntity from
|
|
each case's NBT on the oracle's stand-in ServerLevel, on a vault block in the
|
|
case's state, and runs `VaultBlockEntity.Server.tick` tick by tick with the
|
|
game time advancing; before a tick, players move and use stacks on the vault
|
|
through `VaultBlock.useItemOn`. Rust (natural_spawn_fixture, mode=vault) loads
|
|
`vault::Vault` from the same NBT and does the same. Both log, tick by tick,
|
|
the block's state changes, the level events, the sounds (with their pitch),
|
|
each ejected item (type, count, position and velocity bits), every shared data
|
|
update (display item and connected players), each use (accepted or not and
|
|
the stack left), and at the end the saved server and shared data, the saved
|
|
config's fields and the next draw of the level random, which pins every draw
|
|
in between (the display item rolls, the ejection velocities).
|
|
|
|
Configs are the default, the ominous vaults' and inline ones from the edge
|
|
values up (activation past deactivation, keys of two, no key); saved data has
|
|
rewarded players, a paused state, items left to eject and display items.
|
|
Players stand in and out of the activation and deactivation ranges in every
|
|
game mode, walk in and out, and use trial keys, ominous keys, too few keys and
|
|
other items, more than once and after being rewarded.
|
|
"""
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import random
|
|
import subprocess
|
|
try:
|
|
from . import lab, test_rust_parity as base, test_natural_spawn_parity as natural
|
|
except ImportError:
|
|
import lab
|
|
import test_rust_parity as base
|
|
import test_natural_spawn_parity as natural
|
|
|
|
box = natural.box
|
|
STATES = ['inactive', 'active', 'unlocking', 'ejecting']
|
|
TABLES = ['minecraft:chests/trial_chambers/reward', 'minecraft:chests/trial_chambers/reward_ominous',
|
|
'minecraft:chests/trial_chambers/reward_rare', 'minecraft:chests/trial_chambers/reward_common',
|
|
'minecraft:spawners/trial_chamber/consumables', 'minecraft:chests/simple_dungeon', 'minecraft:empty']
|
|
ITEMS = ['minecraft:trial_key', 'minecraft:ominous_trial_key', 'minecraft:diamond', 'minecraft:emerald',
|
|
'minecraft:golden_apple', 'minecraft:arrow', 'minecraft:heavy_core']
|
|
|
|
|
|
def uuid(rng):
|
|
return '[I;' + ','.join(str(rng.randrange(-2**31, 2**31)) for _ in range(4)) + ']'
|
|
|
|
|
|
def player_uuid(i):
|
|
return f'[I;0,0,0,{i + 1}]'
|
|
|
|
|
|
def stack(rng):
|
|
item = rng.choice(ITEMS)
|
|
return f'{{id:"{item}",count:{rng.choice([1, 1, 1, 2, 3, 64])}}}'
|
|
|
|
|
|
def config(rng):
|
|
r = rng.random()
|
|
if r < 0.35:
|
|
return None
|
|
if r < 0.6:
|
|
# The ominous vaults of the trial chambers.
|
|
return '{key_item:{count:1,id:"minecraft:ominous_trial_key"},loot_table:"minecraft:chests/trial_chambers/reward_ominous"}'
|
|
fields = []
|
|
if rng.random() < 0.5:
|
|
fields.append(f'loot_table:"{rng.choice(TABLES)}"')
|
|
if rng.random() < 0.4:
|
|
fields.append(f'activation_range:{rng.choice([0.0, 1.0, 2.5, 4.0, 6.0])}d')
|
|
if rng.random() < 0.4:
|
|
fields.append(f'deactivation_range:{rng.choice([0.5, 3.0, 4.5, 8.0])}d')
|
|
if rng.random() < 0.85:
|
|
key = rng.choice(['minecraft:trial_key', 'minecraft:trial_key', 'minecraft:ominous_trial_key', 'minecraft:diamond'])
|
|
fields.append(f'key_item:{{count:{rng.choice([1, 1, 1, 2])},id:"{key}"}}')
|
|
if rng.random() < 0.3:
|
|
fields.append(f'override_loot_table_to_display:"{rng.choice(TABLES)}"')
|
|
return '{' + ','.join(fields) + '}'
|
|
|
|
|
|
def vault_snbt(rng, now, count):
|
|
fields = []
|
|
c = config(rng)
|
|
if c is not None:
|
|
fields.append(f'config:{c}')
|
|
server = []
|
|
if count and rng.random() < 0.25:
|
|
rewarded = rng.sample(range(count), rng.randrange(1, count + 1))
|
|
server.append('rewarded_players:[' + ','.join(player_uuid(i) for i in rewarded) + ']')
|
|
elif rng.random() < 0.1:
|
|
server.append(f'rewarded_players:[{uuid(rng)}]')
|
|
if rng.random() < 0.3:
|
|
server.append(f'state_updating_resumes_at:{now + rng.choice([-20, 0, 5, 19, 40])}L')
|
|
if rng.random() < 0.15:
|
|
server.append('items_to_eject:[' + ','.join(stack(rng) for _ in range(rng.randrange(1, 4))) + ']')
|
|
if rng.random() < 0.1:
|
|
server.append(f'total_ejections_needed:{rng.choice([0, 1, 3])}')
|
|
if server or rng.random() < 0.3:
|
|
fields.append('server_data:{' + ','.join(server) + '}')
|
|
shared = []
|
|
if rng.random() < 0.25:
|
|
shared.append(f'display_item:{stack(rng)}')
|
|
if count and rng.random() < 0.2:
|
|
shared.append('connected_players:[' + ','.join(player_uuid(i) for i in range(rng.randrange(1, count + 1))) + ']')
|
|
if rng.random() < 0.1:
|
|
shared.append(f'connected_particles_range:{rng.choice([2.0, 4.5, 9.0])}d')
|
|
if shared:
|
|
fields.append('shared_data:{' + ','.join(shared) + '}')
|
|
return '{' + ','.join(fields) + '}'
|
|
|
|
|
|
def spot(rng, vpos):
|
|
"""A player position near the vault, around its activation (4) and deactivation (4.5) ranges."""
|
|
d = rng.choice([0.5, 1.5, 2.5, 3.5, 4.2, 4.6, 5.5, 8.0])
|
|
dx = rng.uniform(-d, d)
|
|
dz = rng.choice([-1, 1]) * max(0.0, d * d - dx * dx) ** 0.5
|
|
return vpos[0] + 0.5 + dx, vpos[1] + rng.choice([0, 0, 0, 1, -1, 3]), vpos[2] + 0.5 + dz
|
|
|
|
|
|
def corpus():
|
|
rng = random.Random(0x5641554c)
|
|
cases = []
|
|
for k in range(3000):
|
|
w = natural.World(rng)
|
|
w.kv['dim'] = w.dim = 'minecraft:overworld'
|
|
vpos = (rng.randrange(-8, 9), rng.randrange(-40, 20), rng.randrange(-8, 9))
|
|
w.block(box(vpos[0] - 10, vpos[1] - 1, vpos[2] - 10, vpos[0] + 10, vpos[1] - 1, vpos[2] + 10), 'minecraft:tuff_bricks')
|
|
count = rng.choice([0, 1, 1, 1, 2, 2, 3])
|
|
modes = []
|
|
for _ in range(count):
|
|
mode = rng.choice(['survival'] * 6 + ['creative', 'adventure', 'spectator'])
|
|
modes.append(mode)
|
|
x, y, z = spot(rng, vpos)
|
|
w.players.append(f'{x:.3f},{y:.3f},{z:.3f}/{mode}')
|
|
natural.common(w, rng, rng.randrange(-2**47, 2**47))
|
|
for key in ('unloaded', 'respawn', 'spawners', 'rules'):
|
|
w.kv.pop(key, None)
|
|
t0 = rng.choice([0, 1, 15, 19, 20, 1000, rng.randrange(0, 10**6), rng.randrange(10**9, 10**12)])
|
|
ticks = rng.choice([1, 2, 20, 21, 60, 100, 200, 400])
|
|
w.kv['spos'] = f'{vpos[0]},{vpos[1]},{vpos[2]}'
|
|
w.kv['state'] = rng.choices(STATES, [5, 5, 1, 1])[0]
|
|
w.kv['ominous'] = int(rng.random() < 0.3)
|
|
w.kv['snbt'] = base64.b64encode(vault_snbt(rng, t0, count).encode()).decode()
|
|
w.kv['t0'] = t0
|
|
w.kv['ticks'] = ticks
|
|
if count:
|
|
uses = []
|
|
for _ in range(rng.choice([0, 1, 1, 2, 3, 6])):
|
|
item = rng.choice(['minecraft:trial_key'] * 4 + ['minecraft:ominous_trial_key'] * 2 + ITEMS)
|
|
uses.append(f'{rng.randrange(ticks)}/{rng.randrange(count)}/{item}/{rng.choice([1, 1, 1, 2, 5])}')
|
|
if uses:
|
|
w.kv['use'] = ';'.join(uses)
|
|
moves = []
|
|
for _ in range(rng.choice([0, 0, 1, 2, 4])):
|
|
x, y, z = spot(rng, vpos)
|
|
moves.append(f'{rng.randrange(ticks)}/{rng.randrange(count)}/{x:.3f},{y:.3f},{z:.3f}')
|
|
if moves:
|
|
w.kv['move'] = ';'.join(moves)
|
|
cases.append(f'vault/{k} mode=vault {w.tokens()}')
|
|
return cases
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--prepare', action='store_true')
|
|
args = parser.parse_args()
|
|
build = lab.LOCAL / 'rust-vault-parity'
|
|
build.mkdir(parents=True, exist_ok=True)
|
|
if args.prepare:
|
|
lab.prepare()
|
|
lock, jars, libraries = base.verify_artifacts()
|
|
resources = lab.LOCAL / 'horizon-client/26.3'
|
|
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaNaturalSpawnOracle.java'
|
|
sources = [oracle, Path(__file__), lab.ROOT / 'crates/horizon-client/examples/probe/natural_spawn_fixture.rs',
|
|
lab.ROOT / 'crates/horizon-client/src/vault.rs',
|
|
lab.ROOT / 'crates/horizon-client/src/chunk/block_entity.rs',
|
|
lab.ROOT / 'crates/horizon-client-core/src/identifier.rs',
|
|
lab.ROOT / 'crates/horizon-client-core/src/nbt_value.rs']
|
|
tested = base.source_hashes(sources)
|
|
inputs = build / 'inputs.txt'
|
|
cases = corpus()
|
|
inputs.write_text(''.join(line + '\n' for line in cases), encoding='utf-8')
|
|
classes = build / 'classes'
|
|
classes.mkdir(exist_ok=True)
|
|
deps = os.pathsep.join(map(str, libraries))
|
|
|
|
def run(command, name):
|
|
with (build / (name + '.log')).open('w', encoding='utf-8') as log:
|
|
base.run(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT)
|
|
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-sourcepath', str(lab.ROOT / 'mods/horizon/src/test/java'),
|
|
'-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
|
|
run(['java', '-Xmx3G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]),
|
|
'dev.horizonlab.parity.VanillaNaturalSpawnOracle', inputs, build / 'server.txt'], 'server')
|
|
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--example', 'client_probe'], 'compile-rust')
|
|
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
|
|
binary = Path(metadata['target_directory']) / 'fast/examples/client_probe'
|
|
run([binary, 'natural_spawn_fixture', resources, inputs, build / 'rust.txt'], 'rust')
|
|
vanilla = (build / 'server.txt').read_text().splitlines()
|
|
errors = [l for l in vanilla if ' error ' in l]
|
|
if errors:
|
|
raise SystemExit(f'oracle errors ({len(errors)}): {errors[:5]}')
|
|
rust = (build / 'rust.txt').read_text().splitlines()
|
|
bad = [(a, b) for a, b in zip(vanilla, rust) if a != b]
|
|
if bad or len(vanilla) != len(rust):
|
|
report = '\n'.join(f' vanilla {a[:2000]}\n rust {b[:2000]}' for a, b in bad[:12])
|
|
raise SystemExit(f'{len(bad)} of {len(vanilla)} cases differ (rust {len(rust)} lines):\n{report}')
|
|
base.ensure_sources_unchanged(sources, tested)
|
|
count = lambda text: sum(line.count(text) for line in vanilla)
|
|
stats = {'uses_on_active': count(' use ok '), 'refused': count('reject_rewarded_player'), 'wrong_keys': count('insert_item_fail'),
|
|
'items': count(' item minecraft:'), 'states': count(' state '), 'updates': count(' update ')}
|
|
for state in STATES:
|
|
stats[f'reached_{state}'] = sum(f' state {state} ' in line for line in vanilla)
|
|
lab.write_json(build / 'report.json', {
|
|
'schema': 1, 'minecraft': lock['minecraft']['version'], 'platform': platform.platform(), 'cases': len(cases),
|
|
**stats, 'sources_sha256': tested,
|
|
'scope': 'VaultBlockEntity: load and save (VaultConfig with its validation, VaultServerData, VaultSharedData), '
|
|
'Server.tick (display item cycling from the loot table, VaultState.tickAndGetNext for every state with '
|
|
'connected players in range, ejection with spawnItem and its pitch, onTransition sounds and level '
|
|
'events, shared data updates), VaultBlock.useItemOn and Server.tryInsertKey (key checks, rewarded '
|
|
'players, fail sounds and their buffer, reward roll with the random sequence, unlocking, creative '
|
|
'players keep the key) with every level random draw.'})
|
|
print(f'{len(cases):,} vault cases match vanilla (' + ', '.join(f'{v:,} {k}' for k, v in stats.items()) + ')')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|