874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
178 lines
10 KiB
Python
178 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare Mob.finalizeSpawn (spawned mob gear, enchantments and state) with vanilla.
|
|
|
|
Java (VanillaFinalizeSpawnOracle) creates each case's mobs on a stand-in
|
|
ServerLevel and runs vanilla's finalizeSpawn with a seeded level random (what
|
|
finalizeSpawn reads through level.getRandom()) and seeded entity randoms; Rust
|
|
(finalize_spawn_fixture) runs horizon_client::mobs::finalize on the same case.
|
|
Cases cover every mob type, every spawn reason, peaceful to hard, game times,
|
|
inhabited times and moon phases (the local difficulty and its special
|
|
multiplier), packs sharing group data, biomes (variants, villager types,
|
|
sheep colours, rabbit and fox types), structures (black cats), roofs (camel
|
|
husk room) and chickens standing by (zombie jockeys).
|
|
|
|
Each entity created in a case, the mobs and the jockeys finalizeSpawn makes,
|
|
is compared: equipment with its enchantments, drop chances, CanPickUpLoot,
|
|
LeftHanded, persistence, age, health, attribute values, effects, variants,
|
|
the memories finalizeSpawn sets and the vehicle it rides; then the next draw
|
|
of every entity random and of the level random, which pins every draw in
|
|
between.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import random
|
|
import subprocess
|
|
try:
|
|
from . import lab, test_rust_parity as base
|
|
except ImportError:
|
|
import lab
|
|
import test_rust_parity as base
|
|
|
|
REASONS = ['NATURAL', 'CHUNK_GENERATION', 'SPAWNER', 'STRUCTURE', 'BREEDING', 'MOB_SUMMONED', 'JOCKEY', 'EVENT',
|
|
'CONVERSION', 'REINFORCEMENT', 'TRIGGERED', 'BUCKET', 'SPAWN_ITEM_USE', 'COMMAND', 'DISPENSER', 'PATROL',
|
|
'TRIAL_SPAWNER', 'LOAD', 'DIMENSION_TRAVEL']
|
|
# Mobs whose finalizeSpawn draws, by how often the corpus picks them.
|
|
GEARED = ['zombie', 'husk', 'drowned', 'zombie_villager', 'zombified_piglin', 'skeleton', 'stray', 'bogged', 'parched',
|
|
'wither_skeleton', 'pillager', 'vindicator', 'piglin', 'piglin_brute', 'vex', 'fox', 'spider', 'illusioner',
|
|
'evoker', 'ravager', 'witch']
|
|
OTHERS = ['pig', 'cow', 'mooshroom', 'sheep', 'chicken', 'rabbit', 'horse', 'donkey', 'mule', 'llama', 'trader_llama',
|
|
'goat', 'panda', 'polar_bear', 'wolf', 'cat', 'ocelot', 'parrot', 'bee', 'turtle', 'armadillo', 'frog',
|
|
'axolotl', 'squid', 'glow_squid', 'cod', 'salmon', 'tropical_fish', 'pufferfish', 'dolphin', 'bat', 'villager',
|
|
'wandering_trader', 'iron_golem', 'snow_golem', 'creeper', 'cave_spider', 'enderman', 'endermite',
|
|
'silverfish', 'slime', 'magma_cube', 'phantom', 'hoglin', 'zoglin', 'ghast', 'blaze', 'strider', 'guardian',
|
|
'elder_guardian', 'shulker', 'camel', 'allay', 'breeze', 'sniffer', 'happy_ghast', 'warden', 'creaking',
|
|
'camel_husk', 'nautilus', 'zombie_nautilus', 'sulfur_cube', 'zombie_horse', 'skeleton_horse', 'tadpole',
|
|
'giant', 'wither']
|
|
BIOMES = ['minecraft:plains', 'minecraft:desert', 'minecraft:snowy_plains', 'minecraft:snowy_taiga', 'minecraft:taiga',
|
|
'minecraft:jungle', 'minecraft:bamboo_jungle', 'minecraft:swamp', 'minecraft:mangrove_swamp',
|
|
'minecraft:savanna', 'minecraft:badlands', 'minecraft:forest', 'minecraft:grove', 'minecraft:old_growth_pine_taiga',
|
|
'minecraft:old_growth_spruce_taiga', 'minecraft:sparse_jungle', 'minecraft:windswept_savanna',
|
|
'minecraft:mushroom_fields', 'minecraft:river', 'minecraft:frozen_river', 'minecraft:ocean',
|
|
'minecraft:warm_ocean', 'minecraft:lukewarm_ocean', 'minecraft:cold_ocean', 'minecraft:frozen_ocean',
|
|
'minecraft:ice_spikes', 'minecraft:cherry_grove', 'minecraft:pale_garden', 'minecraft:dripstone_caves',
|
|
'minecraft:lush_caves', 'minecraft:deep_dark', 'minecraft:nether_wastes', 'minecraft:crimson_forest',
|
|
'minecraft:soul_sand_valley', 'minecraft:the_end', 'minecraft:snowy_slopes', 'minecraft:frozen_peaks',
|
|
'minecraft:wooded_badlands', 'minecraft:beach', 'minecraft:snowy_beach', 'minecraft:meadow',
|
|
'minecraft:sulfur_caves']
|
|
STRUCTURES = ['minecraft:swamp_hut', 'minecraft:village_plains', 'minecraft:pillager_outpost', 'minecraft:fortress',
|
|
'minecraft:trial_chambers']
|
|
FLOORS = ['minecraft:grass_block', 'minecraft:sand', 'minecraft:stone', 'minecraft:snow_block', 'minecraft:netherrack']
|
|
|
|
|
|
def dimension(biome):
|
|
if biome in ('minecraft:nether_wastes', 'minecraft:crimson_forest', 'minecraft:soul_sand_valley'):
|
|
return 'minecraft:the_nether'
|
|
if biome == 'minecraft:the_end':
|
|
return 'minecraft:the_end'
|
|
return 'minecraft:overworld'
|
|
|
|
|
|
def corpus():
|
|
rng = random.Random(0x46494E414C)
|
|
cases = []
|
|
for k in range(int(os.environ.get('FINALIZE_SPAWN_CASES', '24000'))):
|
|
kind = rng.choice(GEARED) if rng.random() < 0.55 else rng.choice(OTHERS)
|
|
reason = rng.choice(['NATURAL'] * 10 + ['CHUNK_GENERATION', 'SPAWNER', 'STRUCTURE', 'COMMAND', 'SPAWN_ITEM_USE',
|
|
'TRIAL_SPAWNER', 'PATROL', 'EVENT', 'JOCKEY'] * 2 + REASONS)
|
|
biome = rng.choice(BIOMES)
|
|
kv = {
|
|
'type': f'minecraft:{kind}',
|
|
'reason': reason,
|
|
'difficulty': rng.choice(['hard', 'hard', 'normal', 'normal', 'easy', 'peaceful']),
|
|
'time': rng.choice([0, 0, 50000, 72000, 400000, 800000, 1512000, 3000000, rng.randrange(0, 4000000)]),
|
|
'inhabited': rng.choice([0, 0, 0, 100000, 1000000, 3600000, 5000000, rng.randrange(0, 4000000)]),
|
|
'phase': rng.randrange(8),
|
|
'seed': rng.randrange(-2**63, 2**63),
|
|
'eseed': rng.randrange(-2**47, 2**47),
|
|
'pack': rng.choice([1, 1, 1, 2, 3, 4, 6]),
|
|
'pos': f'{rng.randrange(-200, 200) + rng.choice([0.5, 0.25, 0.0]):.2f},{rng.randrange(-30, 120)},'
|
|
f'{rng.randrange(-200, 200) + rng.choice([0.5, 0.75, 0.0]):.2f}',
|
|
'biome': biome,
|
|
'dim': dimension(biome),
|
|
'floor': rng.choice(FLOORS),
|
|
}
|
|
if rng.random() < 0.15:
|
|
kv['structure'] = rng.choice(STRUCTURES)
|
|
if rng.random() < 0.2:
|
|
kv['roof'] = rng.choice([1, 2, 3])
|
|
if rng.random() < 0.15:
|
|
kv['chickens'] = rng.choice([1, 2])
|
|
if rng.random() < 0.05:
|
|
kv['monsters'] = 0
|
|
cases.append(f'case/{k} ' + ' '.join(f'{a}={b}' for a, b in kv.items()))
|
|
return cases
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--prepare', action='store_true')
|
|
parser.add_argument('--java-only', action='store_true', help='run the oracle and stop')
|
|
args = parser.parse_args()
|
|
build = lab.LOCAL / 'rust-finalize-spawn-parity'
|
|
build.mkdir(parents=True, exist_ok=True)
|
|
if args.prepare:
|
|
lab.prepare()
|
|
lock, jars, libraries = base.verify_artifacts()
|
|
resources = lab.RESOURCES
|
|
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaFinalizeSpawnOracle.java'
|
|
sources = [oracle, Path(__file__), lab.ROOT / 'crates/horizon-client/examples/probe/finalize_spawn_fixture.rs',
|
|
lab.ROOT / 'crates/horizon-client/src/mobs/finalize.rs']
|
|
tested = base.source_hashes([s for s in sources if s.exists()])
|
|
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):
|
|
path = build / (name + '.log')
|
|
with path.open('w', encoding='utf-8') as log:
|
|
try:
|
|
base.run(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT)
|
|
except subprocess.CalledProcessError:
|
|
log.flush()
|
|
raise SystemExit(f'{name} failed:\n' + path.read_text(errors='replace')[-4000:])
|
|
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.VanillaFinalizeSpawnOracle', inputs, build / 'server.txt'], 'server')
|
|
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]}')
|
|
if args.java_only:
|
|
print(f'{len(vanilla)} oracle lines')
|
|
return
|
|
halloween = vanilla[0].split()[1]
|
|
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, 'finalize_spawn_fixture', resources, inputs, build / 'rust.txt', halloween], 'rust')
|
|
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)} lines differ (rust {len(rust)} lines):\n{report}')
|
|
base.ensure_sources_unchanged([s for s in sources if s.exists()], tested)
|
|
entities = sum(line.count(' | ') for line in vanilla) // 2
|
|
enchanted = sum(line.count('[minecraft:') for line in vanilla)
|
|
lab.write_json(build / 'report.json', {
|
|
'schema': 1, 'minecraft': lock['minecraft']['version'], 'platform': platform.platform(), 'cases': len(cases),
|
|
'entities': entities, 'enchanted_items': enchanted, 'sources_sha256': tested,
|
|
'scope': 'Mob.finalizeSpawn for every mob type and spawn reason: populateDefaultEquipmentSlots, '
|
|
'populateDefaultEquipmentEnchantments through the mob_spawn_equipment and pillager_spawn_crossbow '
|
|
'providers, drop chances, CanPickUpLoot, LeftHanded, the follow range spawn bonus, babies and '
|
|
'group data, jockeys (chicken, spider, strider, zombie horse, camel husk, zombie nautilus), '
|
|
'variants and sound variants, zombie attributes, spider effects, patrol leaders, villager types, '
|
|
'cube sizes, horse and llama attributes and the memories finalizeSpawn sets, with every draw of the '
|
|
'level random and of the entities\' own randoms.'})
|
|
print(f'{len(cases):,} finalizeSpawn cases match vanilla ({entities:,} entities, {enchanted:,} enchanted items)')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|