mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Synchronize zombie infection and cure sounds
Use the vanilla infection/conversion level events and the client cure-start entity event. Respect silent mobs, draw cure volume before pitch on each client and avoid duplicate captured sound packets. Fixes #8892 Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
acd184c1d9
commit
850c43dedb
9 files changed
+385
-24
No files matched your search
@@ -108,6 +108,7 @@ mod vehicle_container_fixture;
|
||||
mod weather_fixture;
|
||||
mod world_border_render_fixture;
|
||||
mod world_options_fixture;
|
||||
mod zombie_cure_sound_probe;
|
||||
|
||||
probes! {
|
||||
ambient_fixture,
|
||||
@@ -142,6 +143,7 @@ probes! {
|
||||
environment_fixture,
|
||||
finalize_spawn_fixture,
|
||||
fire_starter_sound_probe,
|
||||
zombie_cure_sound_probe,
|
||||
firework_tooltip_fixture,
|
||||
first_person_probe,
|
||||
frame_limiter_probe,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#![forbid(unsafe_code)]
|
||||
//! Replays tools/test_zombie_cure_sound_parity.py without prepared resources.
|
||||
use horizon_client::{
|
||||
Result,
|
||||
mobs::{Anim, Brain, Flags, Mob, MobEvent, MobType, Part, Species, zombie_villager},
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::Vec3,
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
fn kind(eye_height: f64) -> Arc<MobType> {
|
||||
Arc::new(MobType {
|
||||
id: "minecraft:zombie_villager".into(),
|
||||
species: Species::ZombieVillager,
|
||||
width: 0.6,
|
||||
height: 1.95,
|
||||
eye_height,
|
||||
category: "monster".into(),
|
||||
max_health: 20.0,
|
||||
speed: 0.23,
|
||||
flying_speed: 0.4,
|
||||
attack_damage: 3.0,
|
||||
attack_knockback: 0.0,
|
||||
follow_range: 35.0,
|
||||
step_height: 0.6,
|
||||
armor: 2.0,
|
||||
armor_toughness: 0.0,
|
||||
knockback_resistance: 0.0,
|
||||
jump_strength: 0.42,
|
||||
attributes: Arc::default(),
|
||||
gravity: 0.08,
|
||||
safe_fall_distance: 3.0,
|
||||
fall_damage_multiplier: 1.0,
|
||||
water_efficiency: 0.0,
|
||||
water_slowdown: 0.8,
|
||||
air_speed: 0.02,
|
||||
baby_scale: 0.5,
|
||||
head_rot_speed: 10.0,
|
||||
max_head_x_rot: 40.0,
|
||||
max_head_y_rot: 75.0,
|
||||
ambient_interval: 120,
|
||||
flags: Flags::default(),
|
||||
model: Arc::new(Part {
|
||||
name: "root".into(),
|
||||
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
polygons: Vec::new(),
|
||||
children: Vec::new(),
|
||||
}),
|
||||
layers: Default::default(),
|
||||
texture: String::new(),
|
||||
brain: Brain::Passive,
|
||||
anim: Anim::Zombie,
|
||||
sounds: [""; 3],
|
||||
step_sound: None,
|
||||
food: Arc::default(),
|
||||
tempt: Arc::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for line in BufReader::new(File::open(&args[1])?).lines() {
|
||||
let line = line?;
|
||||
let p: Vec<&str> = line.split_whitespace().collect();
|
||||
let mut m = Mob::new(
|
||||
1,
|
||||
kind(p[5].parse()?),
|
||||
Vec3::new(p[2].parse()?, p[3].parse()?, p[4].parse()?),
|
||||
0.0,
|
||||
);
|
||||
m.random = LegacyRandom::new(p[0].parse()?);
|
||||
m.silent = p[1] == "1";
|
||||
if let Some(MobEvent::Sound(name, at, volume, pitch)) = zombie_villager::cure_sound(&mut m)
|
||||
{
|
||||
write!(
|
||||
out,
|
||||
"minecraft:{name} {:016x} {:016x} {:016x} {:08x} {:08x}",
|
||||
at[0].to_bits(),
|
||||
at[1].to_bits(),
|
||||
at[2].to_bits(),
|
||||
volume.to_bits(),
|
||||
pitch.to_bits()
|
||||
)?;
|
||||
} else {
|
||||
write!(out, "silent")?;
|
||||
}
|
||||
writeln!(out, " next {}", m.random.next_long())?;
|
||||
}
|
||||
out.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -5400,6 +5400,8 @@ impl Game<'_> {
|
||||
// LevelEvent.SOUND_ZOMBIE_WOODEN_DOOR (attack, break).
|
||||
1019 => Some(("minecraft:entity.zombie.attack_wooden_door", 2.0)),
|
||||
1021 => Some(("minecraft:entity.zombie.break_wooden_door", 2.0)),
|
||||
1026 => Some(("minecraft:entity.zombie.infect", 2.0)),
|
||||
1027 => Some(("minecraft:entity.zombie_villager.converted", 2.0)),
|
||||
1030 => Some(("minecraft:block.anvil.use", 1.0)),
|
||||
1029 => Some(("minecraft:block.anvil.destroy", 1.0)),
|
||||
1031 => Some(("minecraft:block.anvil.land", 0.3)),
|
||||
@@ -5430,6 +5432,10 @@ impl Game<'_> {
|
||||
1029 | 1030 | 1042 | 1043 | 1044 | 1053 | 1054 => {
|
||||
self.world.random.next_float() * 0.1 + 0.9
|
||||
}
|
||||
1026 | 1027 => {
|
||||
let r = self.particles.level_random();
|
||||
(r.next_float() - r.next_float()) * 0.2 + 1.0
|
||||
}
|
||||
1019 | 1021 => {
|
||||
(self.world.random.next_float() - self.world.random.next_float())
|
||||
* 0.2
|
||||
@@ -5439,7 +5445,17 @@ impl Game<'_> {
|
||||
};
|
||||
if matches!(
|
||||
id,
|
||||
1019 | 1021 | 1029 | 1030 | 1045 | 1052 | 1053 | 1054 | 1501 | 1503
|
||||
1019 | 1021
|
||||
| 1026
|
||||
| 1027
|
||||
| 1029
|
||||
| 1030
|
||||
| 1045
|
||||
| 1052
|
||||
| 1053
|
||||
| 1054
|
||||
| 1501
|
||||
| 1503
|
||||
) {
|
||||
// Clients play these sounds from their level events;
|
||||
// capturing it also sends a duplicate sound packet.
|
||||
|
||||
@@ -2646,6 +2646,8 @@ impl Game<'_> {
|
||||
2015 | 1501
|
||||
| 1503
|
||||
| 1505
|
||||
| 1026
|
||||
| 1027
|
||||
| 1029
|
||||
| 1030
|
||||
| 1045
|
||||
@@ -2689,6 +2691,10 @@ impl Game<'_> {
|
||||
if self.audio.capture.is_some() {
|
||||
self.lan_entity_events.push((id, event, at));
|
||||
}
|
||||
// Event 16: zombie villagers play their cure sound on each client.
|
||||
if event == 16 && self.options.execution_role.is_client() {
|
||||
self.zombie_villager_cure_sound(id as u32);
|
||||
}
|
||||
// Event 63: a sniffer's digging sound loop starts (ClientPacketListener.handleEntityEvent 63).
|
||||
if event == 63 && self.options.execution_role.is_client() {
|
||||
self.start_sniffer_digging_sound(id as u32);
|
||||
|
||||
@@ -4015,6 +4015,10 @@ impl Game<'_> {
|
||||
if p.event == 21 {
|
||||
self.start_guardian_sound(p.id as u32);
|
||||
}
|
||||
// ZombieVillager.handleEntityEvent 16: the cure starts.
|
||||
if p.event == 16 {
|
||||
self.zombie_villager_cure_sound(p.id as u32);
|
||||
}
|
||||
// Witch.handleEntityEvent 15: the magic puffs over its hat.
|
||||
if p.event == 15 {
|
||||
self.witch_magic_particles(p.id as u32);
|
||||
|
||||
@@ -34,6 +34,17 @@ fn infects(difficulty: Difficulty, coin: impl FnOnce() -> bool) -> bool {
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// `ZombieVillager.handleEntityEvent(16)`: only the client plays the cure sound.
|
||||
pub(super) fn zombie_villager_cure_sound(&mut self, id: u32) {
|
||||
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id)
|
||||
&& let Some(horizon_client::mobs::MobEvent::Sound(name, at, volume, pitch)) =
|
||||
zombie_villager::cure_sound(m)
|
||||
{
|
||||
self.audio
|
||||
.play_unshared(&format!("minecraft:{name}"), Some(at), volume, pitch);
|
||||
}
|
||||
}
|
||||
|
||||
/// `Zombie.killedEntity`: a villager killed by a zombie may rise as a
|
||||
/// zombie villager keeping its profession, trades and gossip.
|
||||
pub(super) fn zombie_killed(&mut self, killer: u32, victim: u32) {
|
||||
@@ -72,14 +83,18 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
convert_to_zombie_villager(&old, z);
|
||||
let killer_at = self.mobs[k].position;
|
||||
let pitch = (self.world.random.next_float() - self.world.random.next_float()) * 0.2 + 1.0;
|
||||
self.audio.play(
|
||||
"minecraft:entity.zombie.infect",
|
||||
Some([killer_at.x, killer_at.y, killer_at.z]),
|
||||
2.0,
|
||||
pitch,
|
||||
);
|
||||
if !self.mobs[k].silent {
|
||||
let at = self.mobs[k].position;
|
||||
self.level_event_at(
|
||||
1026,
|
||||
[
|
||||
at.x.floor() as i32,
|
||||
at.y.floor() as i32,
|
||||
at.z.floor() as i32,
|
||||
],
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `ZombieVillager.mobInteract`: a golden apple on a weakened zombie
|
||||
@@ -150,13 +165,17 @@ impl Game<'_> {
|
||||
g.trigger_cured_zombie_villager(&zombie_view, &villager_view)
|
||||
});
|
||||
}
|
||||
let pitch = (self.world.random.next_float() - self.world.random.next_float()) * 0.2 + 1.0;
|
||||
self.audio.play(
|
||||
"minecraft:entity.zombie_villager.converted",
|
||||
Some(at),
|
||||
2.0,
|
||||
pitch,
|
||||
);
|
||||
if !old.silent {
|
||||
self.level_event_at(
|
||||
1027,
|
||||
[
|
||||
old.position.x.floor() as i32,
|
||||
old.position.y.floor() as i32,
|
||||
old.position.z.floor() as i32,
|
||||
],
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `ServerLevel.getPlayerByUUID`: the player with `uuid` in this level.
|
||||
|
||||
@@ -24,6 +24,20 @@ pub struct Cure {
|
||||
pub fn cure(m: &Mob) -> Option<&Cure> {
|
||||
villager::ext(m).and_then(|e| e.cure.as_ref())
|
||||
}
|
||||
/// `ZombieVillager.handleEntityEvent(16)`: the client's cure sound.
|
||||
pub fn cure_sound(m: &mut Mob) -> Option<MobEvent> {
|
||||
if m.species() != Species::ZombieVillager || m.silent {
|
||||
return None;
|
||||
}
|
||||
let volume = 1.0 + m.random.next_float();
|
||||
let pitch = m.random.next_float() * 0.7 + 0.3;
|
||||
Some(MobEvent::Sound(
|
||||
"entity.zombie_villager.cure",
|
||||
[m.position.x, m.eye_y(), m.position.z],
|
||||
volume,
|
||||
pitch,
|
||||
))
|
||||
}
|
||||
/// `ZombieVillager.DATA_CONVERTING_ID` on a client: only `is_converting`
|
||||
/// (the cure's shaking) reads the cure, so its time does not matter.
|
||||
pub fn set_converting(m: &mut Mob, on: bool) {
|
||||
@@ -53,14 +67,11 @@ pub fn start_converting(
|
||||
m.extra.effects.retain(|e| e.0 != "minecraft:weakness");
|
||||
// Strength amplifier: min(difficulty id - 1, 0), always 0 with monsters.
|
||||
m.add_effect("minecraft:strength", time, 0);
|
||||
let pitch = m.random.next_float() * 0.7 + 0.3;
|
||||
let volume = 1.0 + m.random.next_float();
|
||||
events.push(MobEvent::Sound(
|
||||
"entity.zombie_villager.cure",
|
||||
[m.position.x, m.eye_y(), m.position.z],
|
||||
volume,
|
||||
pitch,
|
||||
));
|
||||
events.push(MobEvent::EntityEvent {
|
||||
mob: m.id,
|
||||
event: 16,
|
||||
at: [m.position.x, m.position.y, m.position.z],
|
||||
});
|
||||
}
|
||||
|
||||
/// `getConversionProgress`: 1 per tick, plus a chance per nearby iron bars or
|
||||
@@ -151,6 +162,18 @@ pub fn read_nbt(m: &mut Mob, tag: &Tag) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use horizon_core::{geometry::Vec3, random::LegacyRandom};
|
||||
|
||||
fn zombie_villager() -> Mob {
|
||||
let mut kind = super::super::sensor_scan_tests::kind();
|
||||
let data = std::sync::Arc::make_mut(&mut kind);
|
||||
data.id = "minecraft:zombie_villager".into();
|
||||
data.species = Species::ZombieVillager;
|
||||
data.eye_height = 1.5;
|
||||
let mut m = Mob::new(42, kind, Vec3::new(1.25, 2.0, 3.75), 0.0);
|
||||
m.random = LegacyRandom::new(0);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cure_round_trips_through_nbt() {
|
||||
@@ -270,4 +293,60 @@ mod tests {
|
||||
assert_eq!(map.get(&"ConversionTime".into()), Some(&Tag::Int(-1)));
|
||||
assert_eq!(parse_cure(&Tag::Compound(map)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starting_a_cure_broadcasts_even_when_silent_without_drawing_sound_randomness() {
|
||||
for silent in [false, true] {
|
||||
let mut m = zombie_villager();
|
||||
m.silent = silent;
|
||||
m.add_effect("minecraft:weakness", 100, 0);
|
||||
let mut random = m.random.clone();
|
||||
let starter = Some([1, 2, 3, 4]);
|
||||
let mut events = Vec::new();
|
||||
|
||||
start_converting(&mut m, starter, 4000, &mut events);
|
||||
|
||||
assert_eq!(
|
||||
cure(&m),
|
||||
Some(&Cure {
|
||||
time: 4000,
|
||||
starter
|
||||
})
|
||||
);
|
||||
assert!(!m.has_effect("minecraft:weakness"));
|
||||
assert!(m.extra.effects.contains(&("minecraft:strength", 4000, 0)));
|
||||
assert_eq!(events.len(), 1);
|
||||
let MobEvent::EntityEvent { mob, event, at } = events[0] else {
|
||||
panic!("Starting a cure must broadcast an entity event");
|
||||
};
|
||||
assert_eq!((mob, event, at), (42, 16, [1.25, 2.0, 3.75]));
|
||||
assert_eq!(m.random.next_long(), random.next_long());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cure_sound_draws_volume_before_pitch_at_eye_height() {
|
||||
let mut m = zombie_villager();
|
||||
let Some(MobEvent::Sound(name, at, volume, pitch)) = cure_sound(&mut m) else {
|
||||
panic!("A non-silent zombie villager must play the cure sound");
|
||||
};
|
||||
assert_eq!(name, "entity.zombie_villager.cure");
|
||||
assert_eq!(at, [1.25, 3.5, 3.75]);
|
||||
// The first two java.util.Random(0) floats, volume before pitch.
|
||||
assert_eq!(volume.to_bits(), 0x3fdd_905a);
|
||||
assert_eq!(pitch.to_bits(), 0x3f61_cb52);
|
||||
assert_eq!(m.random.next_float().to_bits(), 0x3e76_4f2c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn silent_or_other_mobs_do_not_play_the_cure_sound_or_draw_randomness() {
|
||||
let mut silent = zombie_villager();
|
||||
silent.silent = true;
|
||||
let other = Mob::new(43, super::super::sensor_scan_tests::kind(), Vec3::ZERO, 0.0);
|
||||
for mut m in [silent, other] {
|
||||
let mut random = m.random.clone();
|
||||
assert!(cure_sound(&mut m).is_none());
|
||||
assert_eq!(m.random.next_long(), random.next_long());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.monster.zombie.ZombieVillager;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Records the real ZombieVillager.handleEntityEvent(16), without an audio engine. */
|
||||
public final class VanillaZombieCureSoundOracle {
|
||||
static Field field(Class<?> owner, String name) throws Exception {
|
||||
var f = owner.getDeclaredField(name); f.setAccessible(true); return f;
|
||||
}
|
||||
static String d(double value) { return String.format("%016x", Double.doubleToRawLongBits(value)); }
|
||||
static final class World extends ClientLevel {
|
||||
String sound;
|
||||
World() { super(null, null, null, null, 0, 0, null, false, 0L, 0); }
|
||||
@Override public void playLocalSound(double x, double y, double z, SoundEvent event,
|
||||
SoundSource source, float volume, float pitch, boolean delay) {
|
||||
if (sound != null) throw new AssertionError("More than one cure sound");
|
||||
if (delay) throw new AssertionError("Unexpected delayed cure sound");
|
||||
sound = BuiltInRegistries.SOUND_EVENT.getKey(event) + " " + d(x) + " " + d(y) + " " + d(z)
|
||||
+ " " + String.format("%08x %08x", Float.floatToRawIntBits(volume), Float.floatToRawIntBits(pitch));
|
||||
}
|
||||
}
|
||||
static final class Actor extends ZombieVillager {
|
||||
boolean silent;
|
||||
Actor() { super(null, null); }
|
||||
@Override public boolean isSilent() { return silent; }
|
||||
@Override public SoundSource getSoundSource() { return SoundSource.HOSTILE; }
|
||||
}
|
||||
public static void main(String[] args) throws Exception {
|
||||
net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap();
|
||||
var u = Class.forName("sun.misc.Unsafe"); var unsafe = field(u, "theUnsafe").get(null);
|
||||
var allocate = u.getMethod("allocateInstance", Class.class);
|
||||
var world = (World) allocate.invoke(unsafe, World.class);
|
||||
var actor = (Actor) allocate.invoke(unsafe, Actor.class);
|
||||
field(Entity.class, "level").set(actor, world);
|
||||
var position = field(Entity.class, "position");
|
||||
var eye = field(Entity.class, "eyeHeight");
|
||||
var random = field(Entity.class, "random");
|
||||
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1])))) {
|
||||
for (String line : Files.readAllLines(Path.of(args[0]))) {
|
||||
var p = line.split(" ");
|
||||
var rng = RandomSource.create(Long.parseLong(p[0]));
|
||||
actor.silent = p[1].equals("1");
|
||||
position.set(actor, new Vec3(Double.parseDouble(p[2]), Double.parseDouble(p[3]), Double.parseDouble(p[4])));
|
||||
eye.setFloat(actor, Float.parseFloat(p[5]));
|
||||
random.set(actor, rng);
|
||||
world.sound = null;
|
||||
actor.handleEntityEvent((byte) 16);
|
||||
out.println((world.sound == null ? "silent" : world.sound) + " next " + rng.nextLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare ZombieVillager.handleEntityEvent(16)'s cure sound with vanilla 26.3.
|
||||
|
||||
The real method runs on a recording ClientLevel, without an audio device or
|
||||
prepared game resources. Compare sound names, coordinates, volume/pitch bits,
|
||||
and the entity random afterwards, including silent mobs.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import random
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
from . import lab, oracle_harness as harness, test_rust_parity as base
|
||||
except ImportError:
|
||||
import lab, oracle_harness as harness, test_rust_parity as base
|
||||
|
||||
|
||||
def main():
|
||||
lock, directory = lab.config()
|
||||
manifest, client = lab.import_client(lock, directory)
|
||||
# This recording oracle uses Java classes, without loading audio or graphics natives.
|
||||
native_paths = {library['downloads']['artifact']['path'] for library in manifest['libraries']
|
||||
if library['name'].split(':')[-1].startswith('natives-')}
|
||||
libraries = [lab.import_library(a, directory / 'libraries/client')
|
||||
for a in lab.client_library_artifacts(manifest) if a['path'] not in native_paths]
|
||||
build = lab.LOCAL / 'rust-zombie-cure-sound-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report = build / 'report.json'
|
||||
report.unlink(missing_ok=True)
|
||||
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaZombieCureSoundOracle.java'
|
||||
probe = lab.ROOT / 'crates/horizon-client/examples/probe/zombie_cure_sound_probe.rs'
|
||||
sources = [Path(__file__), oracle, probe, lab.ROOT / 'tools/lab.py',
|
||||
lab.ROOT / 'tools/oracle_harness.py', lab.ROOT / 'tools/test_rust_parity.py',
|
||||
lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock',
|
||||
lab.ROOT / 'crates/horizon-client-lib/Cargo.toml',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs/zombie_villager.rs']
|
||||
tested = base.source_hashes(sources)
|
||||
rng = random.Random(0x43555245)
|
||||
seeds = [0, 1, -1, 42, -(2**63), 2**63 - 1] + [rng.randrange(-2**63, 2**63) for _ in range(128)]
|
||||
rows = [f'{seed} {int(silent)} {x} {y} {z} {eye}'
|
||||
for seed in seeds for silent in (False, True)
|
||||
for x, y, z, eye in [(0, 0, 0, 1.5), (1.25, 2, -3.75, 1.25),
|
||||
(-10.5, -64, 20.5, 0.75)]]
|
||||
inputs = build / 'inputs.txt'
|
||||
inputs.write_text('\n'.join(rows) + '\n')
|
||||
input_hash = lab.digest(inputs, 'sha256')
|
||||
classes, dependencies, _ = harness.compile_oracle(build, 'VanillaZombieCureSoundOracle',
|
||||
{'client': client}, libraries, lock['minecraft']['java_major'])
|
||||
command = harness.build_probe(build, 'zombie_cure_sound_probe')
|
||||
if os.name == 'nt':
|
||||
command[0] = command[0].with_suffix('.exe')
|
||||
harness.run_logged([*command, inputs, build / 'rust.txt'], build, 'rust')
|
||||
harness.java_run(build, classes, dependencies, 'VanillaZombieCureSoundOracle',
|
||||
[inputs, build / 'client.txt'], 'client')
|
||||
matched = harness.compare(build / 'client.txt', build / 'rust.txt', len(rows))
|
||||
base.ensure_sources_unchanged(sources, tested)
|
||||
if lab.digest(inputs, 'sha256') != input_hash:
|
||||
raise AssertionError('Input fixtures changed')
|
||||
lab.write_json(report, dict(schema=1, minecraft=lock['minecraft']['version'],
|
||||
platform=platform.platform(), java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],
|
||||
rustc=subprocess.check_output(['rustc', '--version'], text=True).strip(),
|
||||
sources_sha256=tested, input_sha256=input_hash, client_jar_sha256=lab.digest(client, 'sha256'),
|
||||
matched_output_lines=matched, output_sha256=lab.digest(build / 'rust.txt', 'sha256'),
|
||||
scope='Real ZombieVillager.handleEntityEvent(16): silent suppression, sound name, eye-position coordinates, '
|
||||
'volume/pitch float bits and entity RNG tail. Server infection/conversion triggers and network delivery '
|
||||
'are not exercised here; audio engine resolution is not exercised.'))
|
||||
print(f'{matched} zombie cure sound cases match the pinned client: {report}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user