874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
94 lines
7.7 KiB
Python
94 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare animation metadata, frame selection and tick state with the pinned client."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import random
|
|
import struct
|
|
import subprocess
|
|
import zipfile
|
|
try:
|
|
from . import lab, test_rust_parity as base, test_model_baking_parity as baking, test_cuboid_model_parity as decoding
|
|
except ImportError:
|
|
import lab, test_rust_parity as base, test_model_baking_parity as baking, test_cuboid_model_parity as decoding
|
|
|
|
|
|
def corpus():
|
|
def row(name, metadata, width=16, height=64, ticks=128):
|
|
return dict(name=name,json=json.dumps(metadata),width=width,height=height,ticks=ticks)
|
|
yield row('default', {})
|
|
for key in ['width','height','frametime','interpolate','frames']:
|
|
for i,value in enumerate([None,True,False,0,1,-1,1.9,-0.9,4294967297,2147483648,1e100,'1','true',{},[],[0,1], [None], [{'index':1,'time':2}]]):
|
|
yield row(f'field-{key}-{i}',{key:value})
|
|
for key in ['index','time']:
|
|
for i,value in enumerate([None,True,False,0,1,-1,1.9,-0.9,4294967297,2147483648,1e100,'1',{},[],[1]]):
|
|
yield row(f'frame-{key}-{i}',{'frames':[0,dict(index=1,**{key:value})]}) if key=='time' else row(f'frame-{key}-{i}',{'frames':[0,{'index':value}]})
|
|
for i,frames in enumerate([[],[3],[99],[1,99],[2,2],[0,1,0],[3,2,1,0],[3,99,1,2],[{},0],[{'index':2,'time':None},0],[0,{'index':1,'time':2147483647}]]):
|
|
for interpolate in [False,True]: yield row(f'sequence-{i}-{interpolate}',{'frames':frames,'interpolate':interpolate})
|
|
for width,height in [(16,16),(16,48),(48,16),(32,48),(15,32),(1,4),(64,64)]:
|
|
for w,h in [(None,None),(8,None),(None,8),(8,8),(16,32),(32,16),(1,1),(65,65)]:
|
|
yield row(f'dim-{width}-{height}-{w}-{h}',{'width':w,'height':h},width,height,16)
|
|
rng=random.Random(0x535052495445)
|
|
for i in range(512):
|
|
frames=[{'index':rng.randrange(7),'time':rng.choice([1,2,3,7,19,31,2147483647])} for _ in range(rng.randrange(12))]
|
|
yield row(f'random-{i}',{'frames':frames,'frametime':rng.randrange(1,20),'interpolate':bool(rng.randrange(2))},ticks=512)
|
|
for i,raw in enumerate(['null','[]','true','1','"x"','{"width":1e9999}','{"frametime":1e-9999}','{"width":4294967312.9}','{"frames":[0.9,1.9,2.9]}']):
|
|
item=row(f'raw-{i}',{}); item['json']=raw;yield item
|
|
|
|
numbers=['-0.0','4294967296','4294967297','4294967295','9223372036854775809',
|
|
'-9223372036854775809','1e9999','1e10000','1e-9999','1e-10000',
|
|
'1e2147483647','1e2147483648','1.0e10000','1.0e10001',
|
|
'1'*1023,'1'*1024,'1'*10000,'1'*10001,
|
|
*('1'+'0'*(n-2)+'1' for n in [20,64,65,66,100,1023,1024,10000,10001]),
|
|
'1844674407370955161601','-1844674407370955161601',
|
|
'18446744073709551616.1','18446744073709551616e-18']
|
|
for i,number in enumerate(numbers):
|
|
for key in ['width','frametime','interpolate']:
|
|
item=row(f'number-{key}-{i}',{});item['json']='{"'+key+'":'+number+'}';yield item
|
|
item=row(f'number-index-{i}',{});item['json']='{"frames":[0,'+number+']}';yield item
|
|
|
|
|
|
def main():
|
|
lock,jars,_=base.verify_artifacts()
|
|
build=lab.LOCAL/'rust-sprite-animation-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/VanillaSpriteAnimationOracle.java'
|
|
helper=oracle.with_name('VanillaCuboidModelOracle.java');crate=lab.ROOT/'crates/horizon-assets'
|
|
sources=[oracle,helper,Path(__file__),lab.ROOT/'tools/test_model_baking_parity.py',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)
|
|
rows=[]
|
|
with zipfile.ZipFile(jars['client']) as archive:
|
|
for name in sorted(archive.namelist()):
|
|
if not name.startswith('assets/minecraft/textures/') or not name.endswith('.png.mcmeta'):continue
|
|
metadata=json.loads(archive.read(name))
|
|
if 'animation' not in metadata:continue
|
|
width,height=struct.unpack('>II',archive.read(name.removesuffix('.mcmeta'))[16:24])
|
|
rows.append(dict(name=name,json=json.dumps(metadata['animation']),width=width,height=height,ticks=512))
|
|
resources=len(rows);rows.extend(corpus())
|
|
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'],*baking.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','sprite_animation_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'/('sprite_animation_fixture.exe' if os.name=='nt' else 'sprite_animation_fixture')
|
|
run([binary,inputs,build/'rust.jsonl'],'rust')
|
|
run(['java','-Xmx512M','-cp',str(classes)+os.pathsep+dependencies,'dev.horizonlab.parity.VanillaSpriteAnimationOracle',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('Sprite animation mismatch: '+left['name'])
|
|
matched=base.compare(build/'client.jsonl',build/'rust.jsonl')
|
|
assert all('error' not in row for row in native[:resources]), 'Bundled resource was rejected'
|
|
states=sum(len(row['value']['animation']['samples'])+len(row['value']['animation']['edges']) for row in native if row.get('value',{}).get('animation'))
|
|
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_animations':resources,'native_tick_states':states,'fixtures':len(rows),'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 AnimationMetadataSection JsonOps codec, frame-size calculation, SpriteContents sequence construction and AnimationState ticks. All bundled animated texture metadata plus synthetic numeric, null, invalid, repeated and out-of-image frame cases. Valid image grids only for SpriteContents construction. No resource-loader fallback, image decoding, GPU interpolation pixels, atlas packing or gameplay parity claim.'}
|
|
lab.write_json(report_path,report);print(f'Native sprite animation: {matched:,} fixtures match ({rejected:,} rejected); {report_path}',flush=True)
|
|
|
|
|
|
if __name__=='__main__':main()
|