mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Classify the verified hostile sound families as hostile
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
69fab597a6
commit
6303017b03
4 files changed
+471
-1
No files matched your search
@@ -18,7 +18,7 @@ pub fn set_source_volumes(volumes: [f32; 11]) {
|
||||
pub fn sound_source(event: &str) -> usize {
|
||||
let e = event.trim_start_matches("minecraft:");
|
||||
let index = |name: &str| SOUND_SOURCES.iter().position(|n| *n == name).unwrap_or(0);
|
||||
const HOSTILE: [&str; 25] = [
|
||||
const HOSTILE: [&str; 34] = [
|
||||
"zombie",
|
||||
"skeleton",
|
||||
"creeper",
|
||||
@@ -44,6 +44,16 @@ pub fn sound_source(event: &str) -> usize {
|
||||
"silverfish",
|
||||
"zombified_piglin",
|
||||
"wither_skeleton",
|
||||
"bogged",
|
||||
"breeze",
|
||||
"endermite",
|
||||
"parched",
|
||||
"warden",
|
||||
"zombie_villager",
|
||||
"vex",
|
||||
"wither",
|
||||
// Monster's common hurt/death (e.g. Giant) and other shared sounds.
|
||||
"hostile",
|
||||
];
|
||||
if e == "block.potent_sulfur.noxious_gas" {
|
||||
// PotentSulfurBlock.animateTick, its only caller, plays it as AMBIENT.
|
||||
@@ -856,6 +866,57 @@ mod tests {
|
||||
horizon_narrator::Status::Off
|
||||
);
|
||||
}
|
||||
/// `Monster.getSoundSource`: the verified families and inherited common sounds.
|
||||
#[test]
|
||||
fn hostile_families_keep_their_sound_category() {
|
||||
for (event, source) in [
|
||||
("entity.zombie.hurt", 5),
|
||||
("entity.cow.hurt", 6),
|
||||
("block.stone.break", 4),
|
||||
("entity.player.hurt", 7),
|
||||
("entity.arrow.hit", 7),
|
||||
("music_disc.13", 2),
|
||||
("item.goat_horn.sound.0", 2),
|
||||
("weather.rain", 3),
|
||||
("ambient.cave", 8),
|
||||
] {
|
||||
assert_eq!(sound_source(event), source, "{event}");
|
||||
}
|
||||
// Explicit sources still take precedence, including H331's arrow route.
|
||||
let mut audio = crate::audio::Audio::silent(Path::new("/nonexistent/h068-audio"));
|
||||
audio.capture = Some(Vec::new());
|
||||
audio.play_on(6, "minecraft:entity.arrow.hit", Some([1.0; 3]), 1.0, 1.0);
|
||||
audio.play_on(4, "minecraft:entity.warden.hurt", Some([1.0; 3]), 1.0, 1.0);
|
||||
assert_eq!(
|
||||
audio
|
||||
.capture
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|s| s.source)
|
||||
.collect::<Vec<_>>(),
|
||||
[6, 4]
|
||||
);
|
||||
for family in [
|
||||
"bogged",
|
||||
"breeze",
|
||||
"endermite",
|
||||
"parched",
|
||||
"warden",
|
||||
"zombie_villager",
|
||||
"vex",
|
||||
"wither",
|
||||
"hostile",
|
||||
] {
|
||||
for suffix in ["hurt", "death"] {
|
||||
for prefix in ["", "minecraft:"] {
|
||||
let event = format!("{prefix}entity.{family}.{suffix}");
|
||||
assert_eq!(sound_source(&event), 5, "{event}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_d_skin_layers_move_from_options_txt_to_the_extras_file() {
|
||||
// #8091 wrote the switch into options.txt; a player who had it on keeps
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Verified hostile sound families
|
||||
|
||||
H068 adds only bogged, breeze, endermite, parched, warden, zombie_villager, vex,
|
||||
wither and the shared `entity.hostile` family to inferred HOSTILE5. Pinned26.3
|
||||
`Monster.getSoundSource` returns HOSTILE; the eight verified types inherit that
|
||||
source. `Monster.getHurtSound/getDeathSound` supply the common pair used by Giant
|
||||
(merged#7834). Existing25 names, other branches, normalization, explicit sources,
|
||||
volumes, names, timing and RNG calls are unchanged. No all-hostile census claim.
|
||||
|
||||
## Frozen live category contract
|
||||
|
||||
Base `69fab597a6f0e8a6ce524d38d1b73e599e343034`. Fresh Java26.3 generated the small
|
||||
loaded platform/roof world; both actual dedicated Game runs serve independent
|
||||
copies. One real connected Alice has actual UUID/type/Health20/creative/Pos guards.
|
||||
Twelve distinct fixed-UUID actors have typed species/Health/NoAI/NoGravity and
|
||||
loaded floor/air guards. Eleven fresh hurt cases set Health6, require accepted
|
||||
result/success1 with fresh sentinels, and independently check live Health5. One
|
||||
fresh Giant takes a lethal hit; raw snapshots independently show Health0.
|
||||
|
||||
Eight assigned hostile types, Giant, zombie and cow supply the hurt events;
|
||||
Giant supplies common death. Each hit is followed by exactly one real world step
|
||||
to drain native events, for12 total. No hit is retried and no extra world step is
|
||||
used for sound readiness. Receipt barriers wait only for the actual packet. This
|
||||
does not claim identical Java/native emission timing. Body/position snapshots
|
||||
remain raw; post-hit trajectories are not compared.
|
||||
|
||||
Predeclared16-row contract: stage, event name, source ordinal and sound position.
|
||||
Actor/control admissions remain required on both sides. Zombie5 and cow6 are
|
||||
live controls; explicit block4/player7/record2 and Warden-neutral6 override are
|
||||
controls. Control volume/pitch1 bits are exact. Random hurt pitch/volume must be
|
||||
finite/positive; no exact random pitch, seed or full packet-family comparison.
|
||||
No Breeze ambient output is required (H069 remains independent).
|
||||
|
||||
The existing typed SoundPacket switch is reused unchanged from own#10266 in
|
||||
`LocatorBarClient`: vanilla's protocol decoder reads both servers and retains
|
||||
all positional sound payload bytes, names, source names, volume/pitch bits,
|
||||
seeds and arrival order. Nothing is reconstructed from a desired category. All
|
||||
recorded sound rows stay raw, including any extra ambient sound; only the
|
||||
predeclared event contract is canonical. No new observer/protocol implementation.
|
||||
H128 production and H331 arrow production/decoder are not imported or changed.
|
||||
Keep the identical test-only receipt switch once when combining#10266, while
|
||||
preserving both drivers.
|
||||
|
||||
Program JSON SHA-256 (declared before fresh Java/baseline):
|
||||
`fc1d6ae79de25bf520ed8e511b9e20a43f1bb4012b6fbcfee7cb0b8ba526c39f`.
|
||||
|
||||
```sh
|
||||
python3 tools/test_hostile_sound_categories_parity.py --binary /path/to/old-game --reference-only --work-dir /tmp/h068-java
|
||||
python3 tools/test_hostile_sound_categories_parity.py --binary /path/to/old-game --reference-dir /tmp/h068-java --work-dir /tmp/h068-old
|
||||
python3 tools/test_hostile_sound_categories_parity.py --binary /path/to/fixed-game --reference-dir /tmp/h068-java --work-dir /tmp/h068-fixed
|
||||
```
|
||||
|
||||
Each output directory is fresh; references are immutable. The owned untouched
|
||||
69fab executable was reused from the completed H041 baseline after source/hash
|
||||
verification, not from a historical capture. SHA-256:
|
||||
`440bc67f89ce05a1691798cde40a5f9c05bfb61699cb124a132803672433b3bd`.
|
||||
The H068 captures themselves are fresh. Fixed Game is independently built from
|
||||
the same frozen base plus this classifier change.
|
||||
|
||||
## Unit and evidence limits
|
||||
|
||||
The resource-free classifier regression first verifies existing zombie/cow,
|
||||
block/player/record/horn/weather/ambient categories and actual `Audio::play_on`
|
||||
overrides (arrow6 and Warden4). It then checks hurt/death for all nine additions,
|
||||
with and without the namespace. Old fails at bogged6 versus5; fixed passes. It
|
||||
preserves the existing arrow inference branch and checks override precedence;
|
||||
no live arrow-route parity or H331 fix is claimed.
|
||||
|
||||
All program/reference hashes, raw typed sound packets/payloads/seeds/pitch/order,
|
||||
chat, complete actor bodies, command sent/receipt times, actual clocks, logs,
|
||||
source/header review and immutable executable evidence remain under `.local/h068/`.
|
||||
Command/stage timestamps are real UTC observations. Packet receipt and sound
|
||||
emission timestamps are unavailable and explicitly null; original reused build
|
||||
start is null. Available build/check elapsed times are retained, not inferred.
|
||||
Verified inactive owned executable archives retain original paths, modes and
|
||||
hashes; no shared dependency tree or other owner's artifact was removed.
|
||||
|
||||
No Breeze ambient routing, mob damage policy, AI/physics, exact random stream,
|
||||
full sound-event/packet family, client rendering, mixer or reopen closure claim.
|
||||
|
||||
## Results
|
||||
|
||||
Fresh Java16/16; untouched current Game **6/16 → fixed16/16**. All actor/action,
|
||||
position, override and twelve-tick admissions pass on both Game binaries.
|
||||
|
||||
| Stage / event | Java | Old Game | Fixed Game |
|
||||
| --- | --- | --- | --- |
|
||||
| `control_block` / `minecraft:block.note_block.harp` | 4 | 4 | 4 |
|
||||
| `control_player` / `minecraft:entity.player.hurt` | 7 | 7 | 7 |
|
||||
| `control_record` / `minecraft:music_disc.13` | 2 | 2 | 2 |
|
||||
| `control_override` / `minecraft:entity.warden.hurt` | 6 | 6 | 6 |
|
||||
| `bogged` / `minecraft:entity.bogged.hurt` | 5 | 6 | 5 |
|
||||
| `breeze` / `minecraft:entity.breeze.hurt` | 5 | 6 | 5 |
|
||||
| `endermite` / `minecraft:entity.endermite.hurt` | 5 | 6 | 5 |
|
||||
| `parched` / `minecraft:entity.parched.hurt` | 5 | 6 | 5 |
|
||||
| `warden` / `minecraft:entity.warden.hurt` | 5 | 6 | 5 |
|
||||
| `zombie_villager` / `minecraft:entity.zombie_villager.hurt` | 5 | 6 | 5 |
|
||||
| `vex` / `minecraft:entity.vex.hurt` | 5 | 6 | 5 |
|
||||
| `wither` / `minecraft:entity.wither.hurt` | 5 | 6 | 5 |
|
||||
| `giant` / `minecraft:entity.hostile.hurt` | 5 | 6 | 5 |
|
||||
| `zombie` / `minecraft:entity.zombie.hurt` | 5 | 5 | 5 |
|
||||
| `cow` / `minecraft:entity.cow.hurt` | 6 | 6 | 6 |
|
||||
| `giant_death` / `minecraft:entity.hostile.death` | 5 | 6 | 5 |
|
||||
|
||||
Only the ten affected category ordinals change in the canonical contract; no
|
||||
category mask or error normalization. Raw seeds/pitch/packet encodings/order
|
||||
are preserved separately and are not claimed byte-equal across independent runs.
|
||||
|
||||
Immutable fresh Java reference SHA-256:
|
||||
`f043ae5906dd9e9fabaa9ddd23255ceb0299e0bbe5b86e14fd80bf85b02a98c9`. Program, observer and reference hashes are verified
|
||||
unchanged across old/fixed captures. Giant death raw snapshots show Health0 on
|
||||
both sides. No source modification was needed outside the family table.
|
||||
|
||||
## Local checks and source checkpoint
|
||||
|
||||
Final production/harness:1517 renderer tests, formatting, workspace/server
|
||||
Clippy, doc tests and public-tree checks pass. Python696 has four existing
|
||||
macOS tooling failures (ARM installer-fixture assets and three Bash3.2 optional
|
||||
archive cases), one skip. Narrow installer diagnostic retained; no packaging fix.
|
||||
|
||||
Later main1532c9c15 adds tick tracking/post-AI gates, LAN caching and player
|
||||
metadata. Classifier/audio dispatch are unchanged; nearby NoAI hurt cases have
|
||||
no pickup/search inputs affected by those post-passes. This is source review,
|
||||
not a latest-base capture. Proof remains frozen69fab. Subsequent edits only add
|
||||
evidence notes; final public-tree/whitespace checks pass.
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live hurt/death sound categories, through the existing typed network capture.
|
||||
|
||||
Seeds, pitch, payloads, packet order, chat, bodies and real clocks stay raw.
|
||||
Only the predeclared event/category/position contract is compared across sides.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
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'
|
||||
FAMILIES = ['bogged', 'breeze', 'endermite', 'parched', 'warden', 'zombie_villager', 'vex', 'wither', 'giant', 'zombie', 'cow']
|
||||
CONTROLS = [
|
||||
('block', 'minecraft:block.note_block.harp', 'block', 4),
|
||||
('player', 'minecraft:entity.player.hurt', 'player', 7),
|
||||
('record', 'minecraft:music_disc.13', 'record', 2),
|
||||
('override', 'minecraft:entity.warden.hurt', 'neutral', 6),
|
||||
]
|
||||
|
||||
|
||||
def now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
|
||||
|
||||
def uuid_words(name):
|
||||
raw = bytearray(hashlib.md5(('OfflinePlayer:' + name).encode()).digest())
|
||||
raw[6] = (raw[6] & 15) | 48
|
||||
raw[8] = (raw[8] & 63) | 128
|
||||
return ','.join(str(int.from_bytes(raw[i:i+4], 'big', signed=True)) for i in range(0, 16, 4))
|
||||
|
||||
|
||||
def actor(index):
|
||||
return f'00000044-0000-0000-0000-{index:012x}'
|
||||
|
||||
|
||||
def program():
|
||||
expected = [dict(stage='control_' + label, event=event, source=source, position=[4.5, 100.0, 4.5])
|
||||
for label, event, _, source in CONTROLS]
|
||||
for kind in FAMILIES:
|
||||
family = 'hostile' if kind == 'giant' else kind
|
||||
expected.append(dict(stage=kind, event=f'minecraft:entity.{family}.hurt', source=6 if kind == 'cow' else 5,
|
||||
position=[8.5, 100.0, 8.5]))
|
||||
expected.append(dict(stage='giant_death', event='minecraft:entity.hostile.death', source=5, position=[8.5, 100.0, 8.5]))
|
||||
return dict(player=PLAYER, families=FAMILIES, controls=CONTROLS, expected=expected,
|
||||
contract=['stage', 'event', 'source', 'position'], world_ticks=12,
|
||||
damage='1 minecraft:out_of_world (fresh Health6→5); fresh giant death uses100',
|
||||
diagnostic_fields=['source_name', 'volume', 'pitch', 'volume_bits', 'pitch_bits', 'seed', 'payload_hex', 'receipt_order'],
|
||||
sound_emission_time=None, packet_receipt_time=None,
|
||||
scope='Positional sound category and admitted live actor/action; no exact random pitch/seed/trajectory or packet-family claim')
|
||||
|
||||
|
||||
def dump(client, label):
|
||||
client.tell('dump ' + label)
|
||||
while (line := client.process.stdout.readline()) != 'dumped ' + label + '\n':
|
||||
if not line:
|
||||
raise AssertionError('client exited before dump')
|
||||
client.log.write(line)
|
||||
|
||||
|
||||
def raw_sounds(path):
|
||||
out = []
|
||||
if path.exists():
|
||||
for order, line in enumerate(path.read_text().splitlines()):
|
||||
who, rest = line.split(' step=', 1)
|
||||
stage, kind, data = rest.split(' ', 2)
|
||||
assert who == PLAYER
|
||||
if kind == 'sound-wire':
|
||||
out.append(dict(stage=stage, receipt_order=order, **json.loads(data)))
|
||||
return out
|
||||
|
||||
|
||||
class Commands:
|
||||
def __init__(self, server, path):
|
||||
self.server, self.path, self.serial, self.rows = server, path, 0, []
|
||||
|
||||
def reply(self, command):
|
||||
self.serial += 1
|
||||
start = len(self.server.lines)
|
||||
row = dict(command=command, sent_at=now(), receipt_at=None, lines=[])
|
||||
self.rows.append(row)
|
||||
self.server.console(command)
|
||||
self.server.console(f'data modify storage h068:barrier value set value {self.serial}')
|
||||
self.server.console('data get storage h068:barrier value')
|
||||
try:
|
||||
self.server.wait_for(rf'h068:barrier.*contents: {self.serial}$', timeout=90)
|
||||
row['receipt_at'] = now()
|
||||
return '\n'.join(self.server.lines[start:])
|
||||
finally:
|
||||
row['lines'] = self.server.lines[start:]
|
||||
self.path.write_text(json.dumps(self.rows, indent=2) + '\n')
|
||||
|
||||
def passed(self, command):
|
||||
text = self.reply(command)
|
||||
assert 'Test passed' in text, (command, text)
|
||||
|
||||
|
||||
def generate(java, build):
|
||||
directory = build / 'generate'
|
||||
net.server_dir(directory)
|
||||
server = Server(java, directory, 'generate')
|
||||
commands = Commands(server, build / 'generate-commands.json')
|
||||
try:
|
||||
server.wait_for(r'Done \(')
|
||||
for rule in ['spawn_mobs', 'advance_time', 'mob_griefing']:
|
||||
commands.reply(f'gamerule {rule} false')
|
||||
commands.reply('gamerule respawn_radius 0')
|
||||
commands.reply('forceload add 0 0 15 15')
|
||||
deadline = time.monotonic() + 90
|
||||
while 'Test passed' not in commands.reply('execute if loaded 8 100 8'):
|
||||
assert time.monotonic() < deadline, 'generation readiness'
|
||||
time.sleep(.05)
|
||||
assert '256' in commands.reply('fill 0 99 0 15 99 15 stone')
|
||||
commands.reply('fill 0 100 0 15 115 15 air')
|
||||
assert '256' in commands.reply('fill 0 116 0 15 116 15 stone')
|
||||
commands.reply('setworldspawn 2 100 2')
|
||||
commands.reply('save-all flush')
|
||||
finally:
|
||||
server.stop()
|
||||
return directory / 'world'
|
||||
|
||||
|
||||
def run(label, command, client_command, build, world, spec):
|
||||
directory = build / label
|
||||
port = net.server_dir(directory, world)
|
||||
server = Server(command, directory, label)
|
||||
commands = Commands(server, build / (label + '-commands.json'))
|
||||
path = build / (label + '-wire.txt')
|
||||
client_log = (build / (label + '-client.log')).open('w')
|
||||
client, timeline, checks = None, [], []
|
||||
def mark(stage, **data):
|
||||
timeline.append(dict(stage=stage, recorded_at=now(), monotonic_ns=time.monotonic_ns(), sound_emission_time=None, **data))
|
||||
try:
|
||||
server.wait_for(r'Done \(')
|
||||
commands.reply('tick freeze')
|
||||
commands.reply('scoreboard objectives add h068 dummy')
|
||||
client = net.Client(client_command, port, path, client_log)
|
||||
for line in ['sounds on', 'chat on', 'spawns on', 'join ' + PLAYER, 'fly Alice on']:
|
||||
client.tell(line)
|
||||
server.wait_for('Alice joined the game', timeout=120)
|
||||
client.sync([PLAYER])
|
||||
commands.reply('tp Alice 2.5 100 2.5 0 0')
|
||||
commands.passed('execute if entity @a[name=Alice,nbt={UUID:[I;' + uuid_words(PLAYER) + '],Health:20f,playerGameType:1,Pos:[2.5d,100d,2.5d]}]')
|
||||
commands.reply('data get entity Alice')
|
||||
for y, block in [(99, 'stone'), (100, 'air'), (112, 'air'), (115, 'air'), (116, 'stone')]:
|
||||
commands.passed(f'execute if loaded 8 {y} 8')
|
||||
commands.passed(f'execute if block 8 {y} 8 {block}')
|
||||
start = game_time(server)
|
||||
elapsed = 0
|
||||
for case in spec['expected']:
|
||||
stage, event = case['stage'], case['event']
|
||||
client.tell('step ' + stage); client.sync([PLAYER])
|
||||
mark(stage, game_time=game_time(server), relative_clock=elapsed)
|
||||
if stage.startswith('control_'):
|
||||
_, _, category, _ = next(c for c in CONTROLS if 'control_' + c[0] == stage)
|
||||
commands.reply(f'playsound {event} {category} Alice 4.5 100 4.5 1 1')
|
||||
else:
|
||||
kind = 'giant' if stage == 'giant_death' else stage
|
||||
index = len(FAMILIES) + 1 if stage == 'giant_death' else FAMILIES.index(kind) + 1
|
||||
uid = actor(index)
|
||||
tag = 'h068_' + stage
|
||||
commands.reply(f'summon minecraft:{kind} 8.5 100 8.5 {{UUID:[I;68,0,0,{index}],Tags:["{tag}"],Health:6f,NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Invul:0}}')
|
||||
selector = f'@e[type=minecraft:{kind},tag={tag},nbt={{UUID:[I;68,0,0,{index}],Health:6f,NoAI:1b,NoGravity:1b}},distance=..0]'
|
||||
commands.passed('execute positioned 8.5 100 8.5 if entity ' + selector)
|
||||
commands.reply(f'data get entity {uid}')
|
||||
for key in ['hit_result', 'hit_success']:
|
||||
commands.reply(f'scoreboard players set {key} h068 -99')
|
||||
amount = 100 if stage == 'giant_death' else 1
|
||||
commands.reply(f'execute store result score hit_result h068 store success score hit_success h068 run damage {uid} {amount} minecraft:out_of_world')
|
||||
for key in ['hit_result', 'hit_success']:
|
||||
text = commands.reply(f'scoreboard players get {key} h068')
|
||||
assert f'{key} has 1 [h068]' in text, (key, text)
|
||||
if stage != 'giant_death':
|
||||
commands.passed(f'execute if entity @e[type=minecraft:{kind},tag={tag},nbt={{UUID:[I;68,0,0,{index}],Health:5f}}]')
|
||||
commands.reply(f'data get entity {uid}')
|
||||
commands.reply('tick step 1')
|
||||
elapsed += 1
|
||||
deadline = time.monotonic() + 60
|
||||
while game_time(server) < start + elapsed:
|
||||
assert time.monotonic() < deadline, 'world step stalled'
|
||||
time.sleep(.01)
|
||||
assert game_time(server) == start + elapsed
|
||||
if stage != 'giant_death':
|
||||
commands.passed(f'execute if entity @e[type=minecraft:{kind},tag={tag},nbt={{UUID:[I;68,0,0,{index}],Health:5f,NoAI:1b,NoGravity:1b}}]')
|
||||
commands.reply(f'data get entity {uid}')
|
||||
# Wait for receipt only: never retry damage or add a world tick.
|
||||
deadline = time.monotonic() + 30
|
||||
while True:
|
||||
client.sync([PLAYER]); dump(client, stage)
|
||||
matches = [r for r in raw_sounds(path) if r['stage'] == stage and r['event'] == event]
|
||||
if matches:
|
||||
break
|
||||
assert time.monotonic() < deadline, (stage, 'no expected hurt/control sound')
|
||||
time.sleep(.02)
|
||||
assert len(matches) == 1, (stage, matches)
|
||||
sound = matches[0]
|
||||
assert sound['position'] == case['position'], (stage, sound)
|
||||
assert math.isfinite(sound['volume']) and sound['volume'] > 0
|
||||
assert math.isfinite(sound['pitch']) and sound['pitch'] > 0
|
||||
if stage.startswith('control_'):
|
||||
assert (sound['volume_bits'], sound['pitch_bits']) == (1065353216, 1065353216)
|
||||
checks.append(stage)
|
||||
mark(stage + '-received', game_time=game_time(server), relative_clock=elapsed)
|
||||
assert game_time(server) - start == spec['world_ticks']
|
||||
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(); client_log.close()
|
||||
(build / (label + '-sounds.json')).write_text(json.dumps(raw_sounds(path), indent=2) + '\n')
|
||||
records = raw_sounds(path)
|
||||
names = {c['event'] for c in spec['expected']}
|
||||
contract = [{k: r[k] for k in spec['contract']} for r in records if r['event'] in names]
|
||||
assert len(contract) == len(spec['expected']), (label, contract)
|
||||
return dict(contract=contract, admission=checks, total_world_ticks=elapsed,
|
||||
recorded_at=now(), sound_emission_time=None, packet_receipt_time=None)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary', type=Path)
|
||||
parser.add_argument('--work-dir', type=Path)
|
||||
parser.add_argument('--reference-only', action='store_true')
|
||||
parser.add_argument('--reference-dir', type=Path)
|
||||
args = parser.parse_args()
|
||||
build = (args.work_dir or net.lab.LOCAL / 'hostile-sound-categories' / f'run-{time.time_ns()}').resolve()
|
||||
build.mkdir(parents=True, exist_ok=False)
|
||||
spec = program(); (build / 'program.json').write_text(json.dumps(spec, indent=2) + '\n')
|
||||
name = str(build.relative_to(net.lab.LOCAL))
|
||||
_, _, java, native, client = net.sides(name, args.binary.resolve() if args.binary else None)
|
||||
reference = args.reference_dir.resolve() if args.reference_dir else build
|
||||
if not args.reference_dir:
|
||||
world = generate(java, build)
|
||||
data = run('java', java, client, build, world, spec)
|
||||
assert data['contract'] == spec['expected'], data
|
||||
(build / 'reference.json').write_text(json.dumps(data, indent=2) + '\n')
|
||||
if args.reference_only:
|
||||
return
|
||||
expected_bytes = (reference / 'reference.json').read_bytes()
|
||||
expected = json.loads(expected_bytes)
|
||||
assert expected['contract'] == spec['expected']
|
||||
actual = run('native', native, client, build, reference / 'generate/world', spec)
|
||||
assert expected_bytes == (reference / 'reference.json').read_bytes()
|
||||
report = dict(expected=expected, actual=actual, pass_=expected['contract'] == actual['contract'],
|
||||
reference_sha256=hashlib.sha256(expected_bytes).hexdigest())
|
||||
(build / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print('Sound category contract:', sum(a == b for a, b in zip(expected['contract'], actual['contract'])), '/', len(spec['expected']))
|
||||
if not report['pass_']:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user