From dec4b523f38579710050ca74df4e59c0a6d090dd Mon Sep 17 00:00:00 2001 From: Florian ERNST <30944236+TheMrZZ@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:42:57 +0100 Subject: [PATCH] Use neutral source for ominous item warning Co-Authored-By: Codex --- .../src/game/ominous_item_spawner.rs | 6 +- .../ominous_item_spawner/warning_tests.rs | 112 ++++++++++ docs/parity/ominous-warning-sound-source.md | 111 ++++++++++ .../horizonlab/parity/LocatorBarClient.java | 19 ++ tools/test_ominous_warning_sound_parity.py | 194 ++++++++++++++++++ 5 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 crates/horizon-client/src/game/ominous_item_spawner/warning_tests.rs create mode 100644 docs/parity/ominous-warning-sound-source.md create mode 100644 tools/test_ominous_warning_sound_parity.py diff --git a/crates/horizon-client/src/game/ominous_item_spawner.rs b/crates/horizon-client/src/game/ominous_item_spawner.rs index 3bc915dea4..121cfa8f67 100644 --- a/crates/horizon-client/src/game/ominous_item_spawner.rs +++ b/crates/horizon-client/src/game/ominous_item_spawner.rs @@ -114,7 +114,8 @@ impl Game<'_> { let at = block(s.position); let center = at.map(|v| f64::from(v) + 0.5); if s.tick_count == s.spawn_item_after - ABOUT_TO_SPAWN { - self.audio.play( + self.audio.play_on( + 6, // SoundSource.NEUTRAL, as OminousItemSpawner.tickServer selects. "minecraft:block.trial_spawner.about_to_spawn_item", Some(center), 1.0, @@ -237,3 +238,6 @@ impl Game<'_> { } } } + +#[cfg(test)] +mod warning_tests; diff --git a/crates/horizon-client/src/game/ominous_item_spawner/warning_tests.rs b/crates/horizon-client/src/game/ominous_item_spawner/warning_tests.rs new file mode 100644 index 0000000000..afbe8d54b8 --- /dev/null +++ b/crates/horizon-client/src/game/ominous_item_spawner/warning_tests.rs @@ -0,0 +1,112 @@ +use horizon_core::geometry::Vec3; + +#[test] +#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"] +fn warning_dispatch_uses_neutral_at_tick_four() { + use crate::game::{Audio, Game, Options, Progress, Settings, load_resources}; + use horizon_client::motion::PlayerMotion; + let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap()); + let settings = Settings::default(); + let loaded = load_resources( + root.clone(), + None, + &root, + &settings, + std::time::Instant::now(), + false, + true, + &Progress::new(), + &mut |_, _| unreachable!(), + ) + .unwrap(); + let mut options = Options::parse(std::iter::empty()).unwrap().unwrap(); + options.resources = root.clone(); + options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer; + let mut game = Game::new( + &loaded.resources, + loaded.pack, + loaded.gui.unwrap(), + options, + settings, + Audio::silent(&root), + None, + ) + .unwrap(); + let scratch = std::env::temp_dir().join(format!( + "horizon-ominous-sound-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&scratch).unwrap(); + game.world_root = scratch.clone(); + game.player = Some(PlayerMotion::new(game.motion, Vec3::new(4.5, 100.0, 4.5)).unwrap()); + + const WARNING: &str = "minecraft:block.trial_spawner.about_to_spawn_item"; + let center = [6.5, 102.5, 4.5]; + game.audio.capture = Some(Vec::new()); + game.audio + .play("minecraft:block.stone.break", Some(center), 1.0, 1.0); + game.audio.play_on( + 6, + "minecraft:block.note_block.pling", + Some(center), + 1.0, + 1.0, + ); + assert_eq!( + game.audio + .capture + .as_ref() + .unwrap() + .iter() + .map(|s| s.source) + .collect::>(), + [4, 6] + ); + game.audio.capture.as_mut().unwrap().clear(); + let (tag, _) = horizon_world::nbt::parse_snbt(r#"{id:"minecraft:ominous_item_spawner",UUID:[I;128,0,0,1],Pos:[6.25d,102.75d,4.875d],item:{id:"minecraft:diamond",count:1},spawn_item_after_ticks:40L}"#).unwrap(); + game.load_ominous_item_spawner(tag); + assert_eq!(game.ominous_spawners.len(), 1); + let s = &game.ominous_spawners[0]; + assert_eq!(s.uuid, [128, 0, 0, 1]); + assert_eq!(s.position, [6.25, 102.75, 4.875]); + assert_eq!(&*s.item.item, "minecraft:diamond"); + assert_eq!(s.item.count, 1); + assert_eq!(s.spawn_item_after, 40); + assert_eq!(s.tick_count, 0); + for tick in 1..=5 { + game.tick_ominous_item_spawners(); + assert_eq!(game.ominous_spawners[0].tick_count, tick); + let warning: Vec<_> = game + .audio + .capture + .as_ref() + .unwrap() + .iter() + .filter(|s| s.event == WARNING) + .collect(); + assert_eq!(warning.len(), if tick >= 4 { 1 } else { 0 }); + if tick == 4 { + let sound = warning[0]; + assert_eq!(sound.position, center); + assert_eq!(sound.volume.to_bits(), 1.0f32.to_bits()); + assert_eq!(sound.pitch.to_bits(), 1.0f32.to_bits()); + assert_eq!( + sound.source, 6, + "OminousItemSpawner.tickServer explicitly selects NEUTRAL" + ); + } + } + game.audio + .play("minecraft:block.stone.break", Some(center), 1.0, 1.0); + assert_eq!( + game.audio.capture.as_ref().unwrap().last().unwrap().source, + 4, + "the explicit warning source must not change later event-family inference" + ); + drop(game); + std::fs::remove_dir_all(scratch).unwrap(); +} diff --git a/docs/parity/ominous-warning-sound-source.md b/docs/parity/ominous-warning-sound-source.md new file mode 100644 index 0000000000..b42db4177b --- /dev/null +++ b/docs/parity/ominous-warning-sound-source.md @@ -0,0 +1,111 @@ +# Ominous item spawner warning source (H128) + +Official pinned Java26.3 OminousItemSpawner.tickServer chooses SoundSource.NEUTRAL +explicitly for block.trial_spawner.about_to_spawn_item. At a loaded delay40, its +comparison tickCount == spawnItemAfterTicks -36 warns at tick4. The BlockPos overload +uses the block center and volume/pitch1. Own bytecode and port ledger were reviewed. + +Native used Audio::play, whose settings::sound_source infers a category from the event +family; this particular block.trial_spawner name resolves to BLOCKS4. Audio::play does +not globally default to blocks. Only this warning call changes to play_on(6,...). +Global inference, other audio, levelEvent3021, H065 extra local sounds, particles, +items, delay/expiry and rendering remain unchanged. + +## Frozen actual Game/Java route + +Base5849fa33b306beae3f218d03694f4e17497d59c4. The existing dedicated-server and +LocatorBarClient helpers run a fresh pinned Java server and actual headless Game server +against copies of a Java-created flat world. A small opt-in sounds decoder uses the +existing native packet codec and records typed ClientboundSoundPacket fields plus the +complete packet payload. It adds no packet framework and is disabled by default. + +Actual nearby Alice is identified by her offline UUID, type, Health20, mode1 and +Pos[4.5,100,4.5]. The actual loaded spawner has UUID[I;128,0,0,1], type, +Pos[6.25,102.75,4.875], item diamond1 and spawn_item_after_ticks40L. Positive actor, +player, loaded chunk, stone floor and air controls run at every observation boundary. +An independent /playsound neutral note-block control proves the observation route and +neutral source value before the actual spawner warning; it does not manufacture a warning. + +The declared program advances actual world deltas0/1/3/4/4/5, with frozen-world command +receipts before steps and after each observation. Both absolute game clocks and exact +relative deltas are retained. There is no warning at0/1/3, exactly one at4, none during +the immediate same-tick repeat or at5. It is not a wall-wait or expiry/drop trial. + +| Field | Fresh Java | Untouched Game | +|---|---|---| +| Event | minecraft:block.trial_spawner.about_to_spawn_item | same | +| Center | [6.5,102.5,4.5] | same | +| Volume/pitch | 1.0/1.0; bits1065353216 each | same | +| Warning boundary | actual world tick4 | same | +| Category | NEUTRAL6 | **BLOCKS4** | + +The strict contract names stage, event, source ordinal/name, position, volume/pitch and +both float-bit values. These fields are compared exactly, without tolerance or repaired +labels. The expected two records are the neutral control and the actual warning; empty +windows and duplicate rejection are enforced too. All sound seeds and payload bytes, +other sound records, full chats and entity bodies remain raw diagnostics. No random-seed, +full-packet-byte, broad delivery/packet-family or audible playback parity is claimed. + +Available wall/monotonic stage receipt timestamps and server logs remain unchanged. +The actual sound emission timestamp is unknown and recorded as null, not invented from +a receipt time. Absolute startup clocks are diagnostic, not normalized into equality. + +The first Java draft observed the correct sound but rejected successful block/load guards +because its checker incorrectly expected the entity count reply for every guard. That +capture/program/log is retained under .local/h128/draft1 and java-draft1 as a rejected +prerequisite, not a qualified native baseline. Exact per-kind guards and an explicit +pre-step receipt barrier were declared and freshly Java-validated before the real pair. + +The reachable Game regression loads a real spawner through its production NBT route, +checks identity/item/delay, and calls the actual warning-dispatch tick from0 through5. +It verifies no early warning, block center and exact float bits, one warning at4 and no +duplicate at5. Independent inferred-block and explicit-neutral controls precede the old +intended4-versus6 assertion; a later block sound checks source-override restoration. +This unit is a Game dispatch test; the dedicated-server pair supplies the real world ticks. + +Replay with prepared resources and isolated test homes/cwd as required by AGENTS.md: + +```sh +python3 tools/test_ominous_warning_sound_parity.py --binary /path/to/game --reference-only --work-dir .local/h128/reference +python3 tools/test_ominous_warning_sound_parity.py --binary /path/to/game --reference .local/h128/reference/reference.json --work-dir .local/h128/replay +HORIZON_TEST_RESOURCES=/absolute/resources/26.3 cargo test -p horizon-client --lib warning_dispatch_uses_neutral_at_tick_four -- --ignored --nocapture +``` + +Complete raw sounds/payloads, all transcripts, actor/body guards, stage timestamps, clocks, +worlds, bytecode, declarations, logs and immutable binaries stay under .local/h128. +No Mojang files or prepared resources are committed. + +## Declared hashes + +| Artifact | SHA-256 | +|---|---| +| Program before paired runs | `d539ab028d1b6f5fbf65ecbdf93cfd85ac33a0cc9b9fd33b29436d77c7c0b5c4` | +| Fresh strict Java sound reference | `cc293f8619552459f0a7f1b1fc27a4c7769bf281ab25d7321ff997f9d9508d25` | + +## Paired result + +Untouched Game passes the independent sound control but the warning category is +BLOCKS4. Fixed Game matches both declared sound records exactly: controlNEUTRAL6 and +warningNEUTRAL6, including timing, center and exact volume/pitch bits. Both paired +programs and independently regenerated strict Java reference files are byte-identical. +All actor/player/geometry/clock admissions pass. Raw seeds/payloads and timestamp +diagnostics remain complete and unnormalized; they are not asserted byte-equal. + +## Local readiness and later source review + +The resource-backed Game regression executes one test and passes fixed, after its +intended old category4-versus6 failure. Required pr_ready --quick ran once on the final +production/test patch: formatting, workspace/headless clippy,1015 Rust tests, doc tests +and public tree pass. Python ran686 tests, with four existing macOS packaging failures +and one skip: the signature mock lacks the ARM64 archive and optional-archive masks0/1/2 +hit unset Bash3.2 arrays. These files are unchanged; no packaging repair is included. +Focused Java/native network captures and the ignored Game regression were explicitly +run. --quick does not claim the broad parity suite. Only evidence docs were finalized +after checks, followed by public-tree and whitespace verification. + +Actual main pulls reached b0a738abc and then +3c7bb44aa12407aff43fe8c9adab51a9b9ebc61c. The warning producer, audio API/inference, +sound packet builder/codec and existing typed-client baseline are unchanged. Other +cosmetics/launcher and profiling changes were reviewed; LAN changes wrap existing calls +with timing and add wire byte counters, without changing this sound's category or order. +The measured pair remains frozen5849, not a combined latest-main runtime claim. diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java b/mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java index 9d6486cba1..fd5523a458 100644 --- a/mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java @@ -103,6 +103,7 @@ import net.minecraft.world.phys.Vec3; * player's lines to OUT. {@code boats on|off} records boat paddle metadata; * {@code hooks on|off} records the entity a fishing bobber hooks, by type; * {@code paddle NAME LEFT RIGHT} sends the two boolean paddle states; + * {@code sounds on|off} records typed sound packets and their complete payloads; * {@code events on|off} records each level event: its id, block and data; * {@code spawns on|off} records each entity other than a player added to the * level, by type and position. @@ -115,6 +116,7 @@ public final class LocatorBarClient { private static volatile boolean boats; private static volatile boolean hooks; private static volatile boolean state; + private static volatile boolean sounds; private static volatile boolean events; private static volatile boolean spawns; private static volatile boolean menus; @@ -202,6 +204,7 @@ public final class LocatorBarClient { Boolean.parseBoolean(sides[0]), Boolean.parseBoolean(sides[1]))); } case "state" -> state = w[1].equals("on"); + case "sounds" -> sounds = w[1].equals("on"); case "events" -> events = w[1].equals("on"); case "spawns" -> spawns = w[1].equals("on"); case "end" -> { @@ -548,6 +551,22 @@ public final class LocatorBarClient { if (tickSteps && p instanceof net.minecraft.network.protocol.game.ClientboundBlockEventPacket e && e.getPos().equals(new net.minecraft.core.BlockPos(0, 80, 0))) record("piston-event " + e.getB0() + " " + e.getB1()); + if (sounds && p instanceof net.minecraft.network.protocol.game.ClientboundSoundPacket sound) { + com.google.gson.JsonObject j = new com.google.gson.JsonObject(); + j.addProperty("event", sound.getSound().value().location().toString()); + j.addProperty("source", sound.getSource().ordinal()); + j.addProperty("source_name", sound.getSource().name()); + com.google.gson.JsonArray at = new com.google.gson.JsonArray(); + at.add(sound.getX()); at.add(sound.getY()); at.add(sound.getZ()); + j.add("position", at); + j.addProperty("volume", sound.getVolume()); + j.addProperty("pitch", sound.getPitch()); + j.addProperty("volume_bits", Float.floatToRawIntBits(sound.getVolume())); + j.addProperty("pitch_bits", Float.floatToRawIntBits(sound.getPitch())); + j.addProperty("seed", sound.getSeed()); + j.addProperty("payload_hex", java.util.HexFormat.of().formatHex(r.payload)); + record("sound-wire " + j); + } if (state) recordState(p); if (menus) recordMenu(p, r.payload); if (hooks && p instanceof net.minecraft.network.protocol.game.ClientboundAddEntityPacket a) { diff --git a/tools/test_ominous_warning_sound_parity.py b/tools/test_ominous_warning_sound_parity.py new file mode 100644 index 0000000000..69f1f6733a --- /dev/null +++ b/tools/test_ominous_warning_sound_parity.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Real dedicated Game/Java spawner warning, through the existing typed network client.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import time + +import test_locator_bar_parity as net +from test_dedicated_server_parity import Server, game_time + +PLAYER = 'Alice' +SPAWNER = '00000080-0000-0000-0000-000000000001' +WARNING = 'minecraft:block.trial_spawner.about_to_spawn_item' +CONTROL = 'minecraft:block.note_block.pling' +STAGES = [('at0', 0), ('at1', 1), ('at3', 3), ('at4', 4), ('repeat4', 4), ('at5', 5)] + + +def words(name): + raw = bytearray(hashlib.md5(('OfflinePlayer:' + name).encode()).digest()) + raw[6] = (raw[6] & 15) | 48 + raw[8] = (raw[8] & 63) | 128 + return [int.from_bytes(raw[i:i+4], 'big', signed=True) for i in range(0, 16, 4)] + + +def program(): + player_uuid = ','.join(map(str, words(PLAYER))) + guards = [ + f'execute if entity @s[type=player,name={PLAYER},nbt={{UUID:[I;{player_uuid}],Health:20f,Pos:[4.5d,100d,4.5d],playerGameType:1}}]', + 'execute if loaded 6 102 4', 'execute if block 4 99 4 stone', + 'execute if block 6 102 4 air', + f'execute if entity @e[type=ominous_item_spawner,nbt={{UUID:[I;128,0,0,1],Pos:[6.25d,102.75d,4.875d],item:{{id:"minecraft:diamond",count:1}},spawn_item_after_ticks:40L}}]', + ] + return {'player': PLAYER, 'stages': STAGES, 'guards': guards, + 'guard_translations': ['commands.execute.conditional.pass_count' if i in (0,4) else 'commands.execute.conditional.pass' for i in range(len(guards))], + 'step_barrier': 'client receipt before every requested world-step batch', + 'summon': 'summon ominous_item_spawner 6.25 102.75 4.875 {UUID:[I;128,0,0,1],item:{id:"minecraft:diamond",count:1},spawn_item_after_ticks:40L}', + 'sound_contract': ['stage', 'event', 'source', 'source_name', 'position', 'volume', 'pitch', 'volume_bits', 'pitch_bits'], + 'raw_diagnostics': 'all sound seeds/payloads, all chat, absolute clocks and available stage timestamps; emission time unknown'} + + +def dump(client, label): + client.tell('dump ' + label) + while (line := client.process.stdout.readline()) != 'dumped ' + label + '\n': + if not line: + raise AssertionError('network client exited before dump') + client.log.write(line) + + +def wait_new(server, start, expression, timeout=60): + until = time.monotonic() + timeout + while time.monotonic() < until: + if any(re.search(expression, s) for s in server.lines[start:]): + return + if server.process.poll() is not None: + break + time.sleep(.02) + raise AssertionError(f'no new {expression}: {server.lines[start:][-8:]}') + + +def generate(command, build): + directory = build / 'generate' + net.server_dir(directory) + server = Server(command, directory, 'generate') + try: + server.wait_for(r'Done \(') + for cmd, ack in [('gamerule spawn_mobs false', 'spawn_mobs'), + ('gamerule advance_time false', 'advance_time'), + ('gamerule respawn_radius 0', 'respawn_radius'), + ('forceload add 0 0 15 15', 'force loaded')]: + start = len(server.lines); server.console(cmd); wait_new(server, start, ack) + # Readiness only, before actors or the timed program. + until = time.monotonic() + 60 + while True: + start = len(server.lines); server.console('execute if loaded 4 100 4'); game_time(server) + if any('Test passed' in s for s in server.lines[start:]): + break + assert time.monotonic() < until, 'generation chunk did not become ready' + time.sleep(.05) + for cmd, ack in [('fill 0 99 0 15 99 15 stone', '256'), + ('setworldspawn 4 100 4', 'world spawn'), ('save-all flush', 'Saved the game')]: + start = len(server.lines); server.console(cmd); wait_new(server, start, ack) + finally: + server.stop() + return directory / 'world' + + +def run(label, command, client_command, build, world, script): + directory = build / label + port = net.server_dir(directory, world) + server = Server(command, directory, label) + raw = build / (label + '-wire.txt') + log = (build / (label + '-client.log')).open('w') + client = None + timeline = [] + def mark(stage, **data): + timeline.append({'stage': stage, 'wall_time': time.time(), 'monotonic': time.monotonic(), **data}) + try: + server.wait_for(r'Done \(') + client = net.Client(client_command, port, raw, log) + for line in ['sounds on', 'chat on', 'events on', 'join ' + PLAYER, 'fly Alice on']: + client.tell(line) + server.wait_for('Alice joined the game', timeout=120) + client.sync([PLAYER]) + def cmd(text, stage): + client.tell('step ' + stage) + client.tell('cmd Alice ' + text) + client.sync([PLAYER]) + mark(stage, command=text) + cmd('tick freeze', 'freeze') + cmd('tp @s 4.5 100 4.5 0 0', 'position') + cmd('playsound ' + CONTROL + ' neutral Alice 4.5 100 4.5 1 1', 'sound-control') + cmd(script['summon'], 'summon') + start_clock = game_time(server) + previous = 0 + for stage, at in STAGES: + client.tell('step ' + stage) + client.sync([PLAYER]) + if at != previous: + server.console(f'tick step {at - previous}') + until = time.monotonic() + 30 + while game_time(server) < start_clock + at: + assert time.monotonic() < until, 'world steps did not finish' + time.sleep(.01) + actual_clock = game_time(server) + assert actual_clock - start_clock == at, (stage, actual_clock, start_clock) + client.sync([PLAYER]) + mark(stage, clock=actual_clock, relative_clock=actual_clock-start_clock, sound_emission_time=None) + for i, guard in enumerate(script['guards']): + cmd(guard, stage + '-guard' + str(i)) + cmd('data get entity ' + SPAWNER, stage + '-body') + cmd('data get entity Alice', stage + '-player-body') + dump(client, stage) + previous = at + finally: + (build / (label + '-stages.json')).write_text(json.dumps(timeline, indent=2) + '\n') + try: + if client: + try: + dump(client, 'failure-or-final') + client.end() + finally: + client.kill() + finally: + server.stop() + log.close() + records = [] + chats = {} + for line in raw.read_text().splitlines(): + who, rest = line.split(' step=', 1) + stage, kind, data = rest.split(' ', 2) + assert who == PLAYER + if kind == 'sound-wire': + value = json.loads(data) + records.append({'stage': stage, **value}) + elif kind == 'chat': + chats.setdefault(stage, []).append(json.loads(data)) + for stage, _ in STAGES: + for i in range(len(script['guards'])): + rows = chats[stage + '-guard' + str(i)] + key = script['guard_translations'][i] + assert any(r.get('translate') == key and (key.endswith('.pass') or r.get('with') == [1]) for r in rows), (stage, i, rows) + assert not any(' error ' in s for s in raw.read_text().splitlines()), 'client error in raw transcript' + (build / (label + '-sounds.json')).write_text(json.dumps(records, indent=2) + '\n') + fields = script['sound_contract'] + selected = [{k: r[k] for k in fields} for r in records if r['event'] in (WARNING, CONTROL)] + expected = [dict(stage='sound-control', event=CONTROL, source=6, source_name='NEUTRAL', position=[4.5,100.0,4.5], volume=1.0, pitch=1.0, volume_bits=1065353216, pitch_bits=1065353216), + dict(stage='at4', event=WARNING, source=6, source_name='NEUTRAL', position=[6.5,102.5,4.5], volume=1.0, pitch=1.0, volume_bits=1065353216, pitch_bits=1065353216)] + # Warning must actually exist with every non-category field correct, even in the old binary. + assert len(selected) == 2 + assert selected[0] == expected[0], ("independent neutral sound control", selected[0]) + for a, e in zip(selected, expected): + assert {k:v for k,v in a.items() if k not in ('source','source_name')} == {k:v for k,v in e.items() if k not in ('source','source_name')}, (a,e) + return selected + + +def main(): + p=argparse.ArgumentParser();p.add_argument('--binary',type=Path,required=True);p.add_argument('--work-dir',type=Path,required=True);p.add_argument('--reference-only',action='store_true');p.add_argument('--reference',type=Path);a=p.parse_args() + build=a.work_dir.resolve();build.mkdir(parents=True,exist_ok=True) + script=program();(build/'program.json').write_text(json.dumps(script,indent=2)+'\n') + name=str(build.relative_to(net.lab.LOCAL));_,_,java,native,client=net.sides(name,a.binary.resolve()) + world=generate(java,build) + reference=run('java',java,client,build,world,script) + (build/'reference.json').write_text(json.dumps(reference,indent=2)+'\n') + assert all(r['source']==6 and r['source_name']=='NEUTRAL' for r in reference) + if a.reference_only:return + if a.reference:assert a.reference.read_bytes()==(build/'reference.json').read_bytes(),'fresh Java reference changed' + actual=run('native',native,client,build,world,script) + report={'expected':reference,'actual':actual,'pass':reference==actual,'scope':script['sound_contract']} + (build/'report.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2)) + if reference!=actual:raise SystemExit(1) + +if __name__=='__main__':main()