mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10266 from TheMrZZ/fix/ominous-warning-sound-source
Use neutral source for ominous item warning
This commit is contained in:
4 files changed
+422
-1
No files matched your search
@@ -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;
|
||||
@@ -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::<Vec<_>>(),
|
||||
[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();
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user