6d7f11feb2
lab.prepare imported every client library in the version JSON, ignoring its OS rules, so a Linux install failed on the macOS-only java-objc-bridge the official launcher never downloads there. The rule evaluation moves from runtime.py into lab.py and one selector, client_library_artifacts, now serves preparation, the runtime and the model baking harness: OS rules as the launcher applies them, and the natives for this architecture (classifier suffixes, plain natives being the x64 build only when an ARM variant is listed). Required libraries are still checksummed and a missing one still errors. Mojang republished 26.3.json twice (702fe591, then 11f1aefe, the current one) with only assetIndex changed: non-English translations, same jars and libraries. The lock pins the current file and accepts both earlier ones; preparation takes the launcher's copy so it matches the asset index installed with it, and an unknown republish errors clearly. Fixes #41 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
8.2 KiB
Python
116 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare cuboid quad positions, UVs and element matrices with the pinned client."""
|
|
import argparse
|
|
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, test_model_resolution_parity as resolution
|
|
except ImportError:
|
|
import lab, test_rust_parity as base, test_cuboid_model_parity as decoding, test_model_resolution_parity as resolution
|
|
|
|
|
|
def corpus():
|
|
yield from resolution.corpus()
|
|
directions = ['down', 'up', 'north', 'south', 'west', 'east']
|
|
models = {}
|
|
def add(rotation, start=None, end=None, explicit=False):
|
|
index = len(models)
|
|
faces = {d: {'texture': '#all', 'cullface': d, 'rotation': 90 * (index % 4),
|
|
'tintindex': index % 3 - 1} for d in directions}
|
|
if explicit:
|
|
for face in faces.values(): face['uv'] = [-0.0, 3.25, 15.5, -1.0]
|
|
element = {'from': start or [0, 0, 0], 'to': end or [16, 16, 16], 'faces': faces,
|
|
'light_emission': index % 16}
|
|
if rotation is not None: element['rotation'] = rotation
|
|
if index % 3 == 0: element['shade_direction_override'] = directions[index % 6]
|
|
models[f'horizon:geometry_{index}'] = {'textures': {'all': 'block/stone'}, 'elements': [element]}
|
|
for bounds in [([0,0,0],[16,16,16]), ([16,16,16],[0,0,0]), ([0,8,0],[16,8,16]),
|
|
([0,0,0],[0,0,0]), ([-16,-16,-16],[32,32,32]), ([-0.0,0,0],[16,16,16])]:
|
|
for explicit in [False, True]: add(None, *bounds, explicit)
|
|
for angle in [-720,-360,-180,-90,-63,-45,-22.5,-0.0,0,22.5,45,63,90,180,270,360,540,720]:
|
|
for axis in ['x','y','z']:
|
|
for rescale in [False,True]:
|
|
add({'origin':[8,8,8], 'axis':axis, 'angle':angle, 'rescale':rescale}, explicit=True)
|
|
for x in [0,22.5,90,180]:
|
|
for y in [0,45,90,270]:
|
|
for z in [-90,0,45,180]:
|
|
for rescale in [False,True]:
|
|
add({'origin':[8,3,-4], 'x':x, 'y':y, 'z':z, 'rescale':rescale})
|
|
rng=random.Random(0x42414b494e47)
|
|
for _ in range(1200):
|
|
rotation={'origin':[rng.uniform(-64,64) for _ in range(3)], 'rescale':bool(rng.randrange(2))}
|
|
if rng.randrange(2): rotation.update(axis=rng.choice(['x','y','z']), angle=rng.uniform(-3600,3600))
|
|
else: rotation.update(zip(['x','y','z'],[rng.uniform(-3600,3600) for _ in range(3)]))
|
|
add(rotation, [rng.uniform(-16,32) for _ in range(3)], [rng.uniform(-16,32) for _ in range(3)], bool(rng.randrange(2)))
|
|
row=resolution.bundle(models)
|
|
for token in ['1e100','-1e100','1e-100','-1e-100']:
|
|
name='horizon:extreme_'+str(len(row['models']))
|
|
raw=json.dumps({'elements':[{'from':[0,0,0],'to':[16,16,16],'faces':{'up':{'texture':'#all'}},'rotation':{'origin':[8,8,8],'axis':'x','angle':'TOKEN','rescale':True}}]})
|
|
row['models'][name]=raw.replace('"TOKEN"',token); row['roots'].append(name)
|
|
yield row
|
|
|
|
|
|
def client_libraries():
|
|
_, directory=lab.config()
|
|
manifest=json.loads((directory/'manifest.json').read_text())
|
|
libraries=[]
|
|
for artifact in lab.client_library_artifacts(manifest):
|
|
relative=Path(artifact['path'])
|
|
if relative.is_absolute() or '..' in relative.parts: raise ValueError('Unsafe client library path')
|
|
path=directory/'libraries/client'/relative
|
|
if lab.digest(path,'sha1')!=artifact['sha1']: raise ValueError('Client dependency checksum changed: '+str(relative))
|
|
libraries.append(path)
|
|
return libraries
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--prepare', action='store_true'); args = parser.parse_args()
|
|
if args.prepare: lab.prepare()
|
|
lock, jars, libraries = base.verify_artifacts()
|
|
build = lab.LOCAL / 'rust-model-baking-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/VanillaModelBakingOracle.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/test_model_resolution_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_sources = base.source_hashes(sources)
|
|
with zipfile.ZipFile(jars['client']) as archive:
|
|
names = sorted(name for name in archive.namelist() if name.startswith('assets/minecraft/models/block/') and name.endswith('.json'))
|
|
models = {'minecraft:' + name.removeprefix('assets/minecraft/models/').removesuffix('.json'): archive.read(name).decode() for name in names}
|
|
rows = [{'models': models, 'roots': list(models) + ['minecraft:builtin/missing', 'horizon:missing'],
|
|
'queries': ['particle', '#particle', 'all', '#all', 'missingno', 'unknown', '', '#']}, *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'], *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', 'model_baking_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' / ('model_baking_fixture.exe' if os.name == 'nt' else 'model_baking_fixture')
|
|
run([binary, inputs, build / 'rust.jsonl'], 'rust')
|
|
run(['java', '-Xmx512M', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaModelBakingOracle', inputs, build / 'client.jsonl'], 'client')
|
|
rejected = decoding.canonicalize(build / 'client.jsonl'); decoding.canonicalize(build / 'rust.jsonl')
|
|
matched = base.compare(build / 'client.jsonl', build / 'rust.jsonl')
|
|
base.ensure_sources_unchanged(sources, tested_sources); 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_models': len(names), 'graph_fixtures': len(rows), 'rejected_roots': rejected,
|
|
'sources_sha256': tested_sources, '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 client FaceBakery geometry overload with a real unit-atlas sprite and default JOML math options. Identity blockstate transform, all bundled cuboid geometry and synthetic rotations, rescaling, default/explicit UVs, winding, degenerate and reversed bounds, inherited models and missing materials. Positions, UVs and element matrix coefficients compare by float bits. This does not cover blockstate transforms/UV lock, atlas packing, transparency classification, lighting, GPU rendering or gameplay.'}
|
|
lab.write_json(report_path, report); print(f'Client cuboid baking: {matched:,} roots match ({rejected:,} rejected); {report_path}', flush=True)
|
|
|
|
|
|
if __name__ == '__main__': main()
|