874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
173 lines
8.1 KiB
Python
173 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare Horizon's subtitle overlay with the vanilla 26.3 client's SubtitleOverlay.
|
|
|
|
The Java oracle (VanillaSubtitleOracle) runs vanilla's own SubtitleOverlay with a
|
|
stand-in client: sounds the sound engine reports to its listeners (subtitles from
|
|
sounds.json, unknown keys and sounds without one; positions; ranges, infinite for
|
|
relative and unattenuated sounds; looping) at controlled times of Util.getMillis,
|
|
and frames that render it for a listener transform (position, forward and up
|
|
vectors), the subtitle option turned on or off (it starts and stops listening as
|
|
it renders), the notification display time, the text background opacity and its
|
|
"chat only" setting, and a GUI size. Each frame records the fills and texts in
|
|
order with the pose: which subtitles show, their order, merging of repeats,
|
|
expiry, the fade by age (looping sounds stay bright longer), the direction
|
|
arrows and the box width. `examples/probe/subtitle_fixture` replays the cases through
|
|
`horizon_client::subtitles`; the frames must agree exactly.
|
|
|
|
test_subtitle_parity.py [--count N] [--detail N]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import math
|
|
import os
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from . import lab, test_rust_parity as base, test_model_baking_parity as baking
|
|
except ImportError:
|
|
import lab
|
|
import test_rust_parity as base
|
|
import test_model_baking_parity as baking
|
|
|
|
PARITY = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity'
|
|
|
|
|
|
def subtitle_keys(resources):
|
|
sounds = json.loads((resources / 'assets/minecraft/sounds.json').read_text())
|
|
keys = sorted({v['subtitle'] for v in sounds.values() if isinstance(v, dict) and 'subtitle' in v})
|
|
return keys
|
|
|
|
|
|
def unit(rng):
|
|
while True:
|
|
v = [rng.uniform(-1, 1) for _ in range(3)]
|
|
n = math.sqrt(sum(x * x for x in v))
|
|
if n > 0.1:
|
|
return [x / n for x in v]
|
|
|
|
|
|
def listener(rng, pos):
|
|
yaw, pitch = rng.uniform(-180, 180), rng.uniform(-90, 90)
|
|
y, p = math.radians(yaw), math.radians(pitch)
|
|
forward = [-math.sin(y) * math.cos(p), -math.sin(p), math.cos(y) * math.cos(p)]
|
|
up = [-math.sin(y) * -math.sin(p), math.cos(p), math.cos(y) * -math.sin(p)]
|
|
if rng.random() < 0.2:
|
|
forward, up = rng.choice([([0.0, 0.0, -1.0], [0.0, 1.0, 0.0]), ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
|
|
([0.0, -1.0, 0.0], [0.0, 0.0, 1.0])])
|
|
return {'pos': pos, 'forward': forward, 'up': up}
|
|
|
|
|
|
def cases(count, keys):
|
|
rng = random.Random(0x5355425449544c45)
|
|
out = []
|
|
for n in range(count):
|
|
pool = rng.sample(keys, rng.choice([1, 3, 8, 20])) + ['subtitles.horizon.unknown']
|
|
t = rng.randint(0, 10_000_000)
|
|
pos = [rng.uniform(-100, 100), rng.uniform(-60, 300), rng.uniform(-100, 100)]
|
|
here = listener(rng, pos)
|
|
mult = rng.choice([0.5, 1.0, 1.0, 1.5, 2.0, 3.5, 10.0])
|
|
show = rng.random() < 0.9
|
|
opacity, chat_only = rng.choice([0.5, 0.0, 1.0, 0.25]), rng.random() < 0.6
|
|
size = rng.choice([(427, 240), (320, 240), (480, 270), (961, 501)])
|
|
ops = [{'op': 'time', 'ms': t, 'sub': rng.randint(0, 999999)}]
|
|
locations = [[pos[i] + rng.uniform(-30, 30) for i in range(3)] for _ in range(6)]
|
|
|
|
def frame():
|
|
nonlocal here
|
|
if rng.random() < 0.3:
|
|
here = listener(rng, [p + rng.uniform(-2, 2) for p in here['pos']])
|
|
return {'op': 'frame', **here, 'show': show, 'mult': mult, 'opacity': opacity, 'chat_only': chat_only,
|
|
'w': size[0], 'h': size[1]}
|
|
ops.append(frame())
|
|
for _ in range(rng.choice([5, 20, 60])):
|
|
r = rng.random()
|
|
if r < 0.45:
|
|
at = rng.choice(locations) if rng.random() < 0.6 else [here['pos'][i] + rng.uniform(-20, 20) for i in range(3)]
|
|
if rng.random() < 0.05:
|
|
at = list(here['pos'])
|
|
key = rng.choice(pool) if rng.random() < 0.9 else None
|
|
rng_range = rng.choice([16.0, 16.0, 8.0, 48.0, 2.5, 100.0, None])
|
|
ops.append({'op': 'play', 'key': key, 'at': at, 'range': rng_range, 'looping': rng.random() < 0.15})
|
|
elif r < 0.75:
|
|
t += rng.choice([0, 1, 16, 50, 499, 500, 501, 1000, 2999, 3000, 3001, rng.randint(0, 8000)])
|
|
ops.append({'op': 'time', 'ms': t, 'sub': rng.randint(0, 999999)})
|
|
else:
|
|
if rng.random() < 0.08:
|
|
show = not show
|
|
if rng.random() < 0.05:
|
|
mult = rng.choice([0.5, 1.0, 2.0, 10.0])
|
|
ops.append(frame())
|
|
ops.append(frame())
|
|
out.append({'case': n, 'ops': ops})
|
|
return out
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--count', type=int, default=1500)
|
|
parser.add_argument('--detail', type=int, default=3)
|
|
args = parser.parse_args()
|
|
lock, jars, _ = base.verify_artifacts()
|
|
libraries = baking.client_libraries()
|
|
resources = lab.RESOURCES
|
|
build = lab.LOCAL / 'subtitle-parity'
|
|
build.mkdir(parents=True, exist_ok=True)
|
|
classes = build / 'classes'
|
|
classes.mkdir(exist_ok=True)
|
|
dependencies = os.pathsep.join(map(str, [jars['client'], *libraries]))
|
|
|
|
def run(command, name):
|
|
with (build / (name + '.log')).open('w') as log:
|
|
result = subprocess.run(list(map(str, command)), cwd=lab.ROOT, timeout=3600, stdout=log, stderr=subprocess.STDOUT)
|
|
if result.returncode:
|
|
print((build / (name + '.log')).read_text()[-4000:])
|
|
sys.exit(f'{name} failed')
|
|
|
|
chosen = cases(args.count, subtitle_keys(resources))
|
|
inputs = build / 'inputs.jsonl'
|
|
inputs.write_text(''.join(json.dumps(c) + '\n' for c in chosen))
|
|
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-nowarn', '-cp', dependencies, '-d', classes,
|
|
*(PARITY / f'{n}.java' for n in ('ParityFont', 'VanillaSubtitleOracle'))], 'compile-java')
|
|
run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaSubtitleOracle',
|
|
inputs, build / 'vanilla.jsonl'], 'vanilla')
|
|
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.exe' if os.name=='nt' else 'client_probe')
|
|
run([binary, 'subtitle_fixture',resources, inputs, build / 'rust.jsonl'], 'rust')
|
|
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text(encoding='utf-8').splitlines()]
|
|
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text(encoding='utf-8').splitlines()]
|
|
assert len(vanilla) == len(rust) == len(chosen)
|
|
errors = [v for v in vanilla if 'error' in v]
|
|
bad, frames, drawn = [], 0, 0
|
|
for c, v, r in zip(chosen, vanilla, rust):
|
|
if 'error' in v:
|
|
continue
|
|
frames += len(v['frames'])
|
|
drawn += sum(1 for f in v['frames'] if f)
|
|
for i, (a, b) in enumerate(zip(v['frames'], r['frames'])):
|
|
if a != b:
|
|
bad.append((c, i, a, b))
|
|
break
|
|
else:
|
|
if len(v['frames']) != len(r['frames']):
|
|
bad.append((c, len(r['frames']), v['frames'][len(r['frames']):][:1], []))
|
|
for v in errors[:3]:
|
|
print('ORACLE ERROR', v['case'], v['error'])
|
|
for c, i, a, b in bad[:args.detail]:
|
|
first = next((j for j, (x, y) in enumerate(zip(a, b)) if x != y), min(len(a), len(b)))
|
|
print(f'MISMATCH case {c["case"]} frame {i} at draw {first} ({len(a)} vanilla, {len(b)} Horizon draws)')
|
|
print(' vanilla', json.dumps(a[first:first + 2], ensure_ascii=False))
|
|
print(' horizon', json.dumps(b[first:first + 2], ensure_ascii=False))
|
|
print(f'{len(chosen) - len(bad) - len(errors)}/{len(chosen)} subtitle cases match '
|
|
f'({frames} frames, {drawn} with subtitles); {len(errors)} oracle errors')
|
|
if bad or errors:
|
|
sys.exit(1)
|
|
print('subtitle parity ok')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|