Files
horizon/tools/test_blockstate_model_parity.py
T
Ludovic MarieandClaude Opus 5.5 9edcbc2559 Give parity harness javac runs the oracle source path
Oracles reference shared helper oracles (heightmap, level shape...) that
javac could not find when only the entry files were listed. Also let
chunkgen_bench list unsupported features (HORIZON_LIST_UNSUPPORTED=1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 23:33:12 +02:00

84 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Compare blockstate resource decoding with the pinned client loader's codec."""
import json
import os
from pathlib import Path
import platform
import random
import subprocess
import zipfile
try:
from . import lab, test_rust_parity as base, test_cuboid_model_parity as decoding
except ImportError:
import lab, test_rust_parity as base, test_cuboid_model_parity as decoding
def corpus():
model={'model':'block/stone'}
def root(value): return {'variants':{'':value}}
for value in [None,{},[],False,0,'x',{'variants':{}},{'multipart':[]},{'variants':None},{'variants':None,'multipart':[{'apply':model}]},root(model),root([model]),root([])]:yield json.dumps(value)
values=[None,True,False,0,1,-1,90,90.9,-90,270,360,450,4294967296,4294967386,1e100,'90','true','',[],{},[model]]
for key in ['model','x','y','z','uvlock','weight']:
for value in values:
variant=dict(model);variant[key]=value
yield json.dumps(root(variant));yield json.dumps(root([variant]))
for weights in [[2147483647],[2147483647,1],[2147483646,1],[0,1],[-1,2],[1,1],[1.9,2.9]]:
yield json.dumps(root([dict(model,weight=w) for w in weights]))
for name in ['block/stone',':block/stone',':','minecraft:','a:b:c','A:a','a:é','../a']:
yield json.dumps(root({'model':name}))
for x in [-360,-90,0,90,180,270,360,450]:
for y in [0,90,180,270]:
for z in [0,90,180,270]:yield json.dumps(root(dict(model,x=x,y=y,z=z,uvlock=True)))
terms=[None,True,False,0,1,1.9,-1,4294967297,'true','false','!true','true|false','!true|false','!!true','!','', '|','true|','|true','a||b',' a ',[],{},['true']]
conditions=[{},None,True,[],{'OR':[]},{'AND':[]},{'OR':[{'a':'b'}]},{'AND':[{'a':'b'},{'c':'d'}]},{'OR':'true'},{'OR':[],'AND':[]},{'OR':[],'a':'b'}]
for key in ['a','OR','AND','']:
conditions.extend({key:t} for t in terms)
for condition in conditions:
for apply in [model,[model],[],None]:yield json.dumps({'multipart':[{'when':condition,'apply':apply}]})
for raw in ['{} trailing','{} {}','\ufeff'+json.dumps(root(model)), '{"variants":{"b":{"model":"b"},"a":{"model":"a"},"b":{"model":"c"}}}', '{"variants":{"":{"model":"a"}},"multipart":[{"apply":{"model":"b"}}]}']:
yield raw
rng=random.Random(0x424c4f434b)
for _ in range(500):
selectors=[]
for _ in range(rng.randrange(1,8)):
condition={rng.choice(['a','b','c']):rng.choice(['x','y','!x','x|y'])}
if rng.randrange(2):condition={rng.choice(['OR','AND']):[condition,{'a':'x'}]}
variants=[dict(model,weight=rng.randrange(1,10),x=rng.randrange(-4,5)*90,y=rng.randrange(4)*90,z=rng.randrange(4)*90,uvlock=bool(rng.randrange(2))) for _ in range(rng.randrange(1,6))]
selectors.append({'when':condition,'apply':variants if rng.randrange(2) else variants[0]})
yield json.dumps({'multipart':selectors})
for number in ['1844674407370955161601','18446744073709551616.1','1e10000','1e9999','1' * 1024]:
yield '{"variants":{"":{"model":"block/stone","x":'+number+'}}}'
yield '{"variants":{"":{"model":"block/stone"}},"unknown":'+number+'}'
def main():
lock,jars,libraries=base.verify_artifacts()
build=lab.LOCAL/'rust-blockstate-model-parity';build.mkdir(parents=True,exist_ok=True)
report_path=build/'report.json';report_path.unlink(missing_ok=True)
oracle=lab.ROOT/'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockStateModelOracle.java';helper=oracle.with_name('VanillaCuboidModelOracle.java');crate=lab.ROOT/'crates/horizon-assets'
sources=[oracle,helper,Path(__file__),lab.ROOT/'tools/test_cuboid_model_parity.py',lab.ROOT/'tools/lab.py',lab.ROOT/'tools/test_rust_parity.py',lab.ROOT/'Cargo.toml',lab.ROOT/'Cargo.lock',crate/'Cargo.toml',*sorted(crate.rglob('*.rs'))]
tested=base.source_hashes(sources)
with zipfile.ZipFile(jars['client']) as archive:
names=sorted(n for n in archive.namelist() if n.startswith('assets/minecraft/blockstates/') and n.endswith('.json'))
rows=[(n,archive.read(n).decode()) for n in names]
synthetic=list(corpus());rows.extend((f'synthetic/{i}',raw) for i,raw in enumerate(synthetic))
inputs=build/'inputs.jsonl';inputs.write_text(''.join(json.dumps(row)+'\n' for row in rows));fixtures=base.source_hashes([inputs]);classes=build/'classes';classes.mkdir(exist_ok=True)
def run(command,name):
with (build/(name+'.log')).open('w') as log:subprocess.run(list(map(str,command)),cwd=lab.ROOT,check=True,timeout=3600,stdout=log,stderr=subprocess.STDOUT)
dependencies=os.pathsep.join(map(str,[jars['client'],*libraries]))
run(['javac','--release',lock['minecraft']['java_major'],'-proc:none','-sourcepath',str(lab.ROOT/'mods/horizon/src/test/java'),'-cp',dependencies,'-d',classes,oracle,helper],'compile-java')
run(['cargo','build','--locked','-p','horizon-assets','--example','blockstate_model_fixture'],'compile-rust')
metadata=json.loads(subprocess.check_output(['cargo','metadata','--locked','--no-deps','--format-version','1'],cwd=lab.ROOT));binary=Path(metadata['target_directory'])/'debug/examples'/('blockstate_model_fixture.exe' if os.name=='nt' else 'blockstate_model_fixture')
run([binary,inputs,build/'rust.jsonl'],'rust');run(['java','-Xmx512M','-cp',str(classes)+os.pathsep+dependencies,'dev.horizonlab.parity.VanillaBlockStateModelOracle',inputs,build/'client.jsonl'],'client')
rejected=decoding.canonicalize(build/'client.jsonl');decoding.canonicalize(build/'rust.jsonl')
native=[json.loads(line) for line in (build/'client.jsonl').read_text().splitlines()];rust=[json.loads(line) for line in (build/'rust.jsonl').read_text().splitlines()]
for left,right in zip(native,rust,strict=True):
if left!=right:raise AssertionError('Blockstate model mismatch: '+left['name'])
matched=base.compare(build/'client.jsonl',build/'rust.jsonl');assert all('error' not in row for row in native[:len(names)]),'Bundled blockstate rejected'
base.ensure_sources_unchanged(sources,tested);base.ensure_sources_unchanged([inputs],fixtures)
report={'schema':1,'minecraft':lock['minecraft']['version'],'manifest_sha1':lock['minecraft']['manifest_sha1'],'platform':platform.platform(),'java':subprocess.check_output(['java','--version'],text=True).splitlines()[0],'rustc':subprocess.check_output(['rustc','--version'],text=True).strip(),'resource_blockstates':len(names),'synthetic_definitions':len(synthetic),'rejected':rejected,'sources_sha256':tested,'fixtures_sha256':fixtures,'rust_output_sha256':lab.digest(build/'rust.jsonl','sha256'),'references':[{'side':'client','jar_sha256':lab.digest(jars['client'],'sha256'),'output_sha256':lab.digest(build/'client.jsonl','sha256'),'matched_output_lines':matched}],'scope':'Actual StrictJsonParser and BlockStateModelDispatcher codec used by the pinned resource loader. Every bundled blockstate JSON plus variants, XYZ quarter turns, UV-lock flags, weighted entries, source order, multipart conditions and malformed synthetic cases. No registry binding, property selection, random draw selection, transformed baking, rendered pixels or gameplay parity claim.'}
lab.write_json(report_path,report);print(f'Native blockstate models: {matched:,} definitions match ({rejected:,} rejected); {report_path}',flush=True)
if __name__=='__main__':main()