#!/usr/bin/env python3 """Compare Horizon's command execution with vanilla's real command implementations. Java (VanillaCommandExecOracle) starts a stand-in integrated server: a real MinecraftServer with a generated world, Commands.CommandSelection.INTEGRATED and one single-player owner named Player. Rust runs the same script through Horizon's integrated server with a connected headless client (HORIZON_COMMAND_SCRIPT). Both run each command as the player through Commands.performPrefixedCommand and record every system or chat message the player receives (text and resolved style: colour, bold, italic, underlined, strikethrough, obfuscated) together with the results the command reported to the player's CommandResultCallback. World, entity and item changes are observed through probe commands whose feedback reports them (execute if, clear with a zero count, data get, scoreboard queries...). Messages that depend on time or a random UUID are compared after masking. """ import argparse import json import os from pathlib import Path import platform import re import shutil import subprocess import sys try: from . import lab, test_rust_parity as base from .integration_env import run_game from . import command_exec_corpus as corpus_module except ImportError: import lab from integration_env import run_game import test_rust_parity as base import command_exec_corpus as corpus_module SEED = 1 def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--prepare', action='store_true') parser.add_argument('--limit', type=int, default=40, help='print at most this many mismatches') parser.add_argument('--allow-gaps', action='store_true', help='report mismatches without failing') parser.add_argument('--only', help='run only corpus groups whose name contains this') args = parser.parse_args() build = lab.LOCAL / 'command-exec-parity' build.mkdir(parents=True, exist_ok=True) report_path = build / 'report.json' report_path.unlink(missing_ok=True) if args.prepare: lab.prepare() lock, jars, libraries = base.verify_artifacts() oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCommandExecOracle.java' classes = build / 'classes' classes.mkdir(exist_ok=True) dependency_path = os.pathsep.join(map(str, libraries)) def run(command, name, **kw): with (build / (name + '.log')).open('w', encoding='utf-8') as log: runner = run_game if Path(command[0]).name == 'horizon-client' else subprocess.run result = runner(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT, **kw) if result.returncode: tail = (build / (name + '.log')).read_text(errors='replace')[-3000:] sys.exit(f'{name} failed ({result.returncode}):\n{tail}') groups = corpus_module.groups() if args.only: groups = [g for g in groups if args.only in g[0] or g[0] == 'setup'] script = [] for _, lines in groups: script.extend(lines) script_path = build / 'script.jsonl' script_path.write_text(''.join(json.dumps(op, ensure_ascii=False) + '\n' for op in script), encoding='utf-8') run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp', str(jars['server']) + os.pathsep + dependency_path, '-d', classes, oracle], 'compile-java') vanilla_dir = build / 'vanilla' shutil.rmtree(vanilla_dir, ignore_errors=True) vanilla_dir.mkdir() run(['java', '-Xmx3G', '-Duser.timezone=UTC', '-cp', os.pathsep.join([str(classes), str(jars['server']), dependency_path]), 'dev.horizonlab.parity.VanillaCommandExecOracle', vanilla_dir / 'universe', script_path, build / 'vanilla.jsonl', SEED], 'java', cwd=vanilla_dir) run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'], 'compile-rust', cwd=lab.ROOT) metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True)) binary = Path(metadata['target_directory']) / 'fast' / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client') rust_dir = build / 'rust' shutil.rmtree(rust_dir, ignore_errors=True) rust_dir.mkdir() env = lab.game_env(HORIZON_SERVER_THREAD=1, HORIZON_PLAYER_NAME="Player", HORIZON_COMMAND_SCRIPT=script_path, HORIZON_COMMAND_OUTPUT=build / 'rust.jsonl') run([binary, '--resources', lab.RESOURCES, '--world', 'harness', '--seed', SEED, '--radius', 4], 'rust', cwd=rust_dir, env=env) vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').open(encoding='utf-8')] rust = [json.loads(l) for l in (build / 'rust.jsonl').open(encoding='utf-8')] if len(vanilla) != len(rust): sys.exit(f'record count differs: vanilla {len(vanilla)} rust {len(rust)}') commands_by_group = [] index = 0 for name, lines in groups: n = sum(1 for op in lines if (op['op'] == 'cmd' and op.get('record', True)) or (op['op'] == 'tick' and op.get('record'))) commands_by_group.append((name, index, index + n)) index += n mismatches = [] for v, r in zip(vanilla, rust): assert v['cmd'] == r['cmd'], (v['cmd'], r['cmd']) kind = compare(v, r) if kind: mismatches.append({'kind': kind, 'cmd': v['cmd'], 'vanilla': v, 'rust': r}) (build / 'mismatches.jsonl').write_text(''.join(json.dumps(m, ensure_ascii=False) + '\n' for m in mismatches), encoding='utf-8') summary = [] bad = {m['cmd'] for m in mismatches} for name, lo, hi in commands_by_group: failed = sum(1 for rec in vanilla[lo:hi] if rec['cmd'] in bad) summary.append({'group': name, 'commands': hi - lo, 'mismatches': failed}) print(f'{name}: {hi - lo - failed}/{hi - lo} commands match', flush=True) for m in mismatches[:args.limit]: print(m['kind'], repr(m['cmd'])) print(' vanilla', render(m['vanilla'])[:400]) print(' rust ', render(m['rust'])[:400]) total = len(vanilla) print(f'total: {total - len(mismatches)}/{total} commands match', flush=True) report = {'schema': 1, 'minecraft': lock['minecraft']['version'], 'platform': platform.platform(), 'seed': SEED, 'commands': total, 'mismatches': len(mismatches), 'groups': summary, 'scope': 'Feedback and failure messages (text and resolved style), chat lines, and CommandResultCallback results of commands run by the single-player owner of an integrated server; world, entity and item effects through probe commands.'} lab.write_json(report_path, report) if mismatches and not args.allow_gaps: sys.exit(f'{len(mismatches)} command execution mismatches; see {build}') print(f'Command execution parity report: {report_path}') MASKS = [ (re.compile(r'debug-trace-\d{4}-\d\d-\d\d_\d\d\.\d\d\.\d\d\.txt'), 'debug-trace-