diff --git a/crates/horizon-client/examples/probe/fire_starter_sound_probe.rs b/crates/horizon-client/examples/probe/fire_starter_sound_probe.rs index 18c1b487a2..7a8772f93d 100644 --- a/crates/horizon-client/examples/probe/fire_starter_sound_probe.rs +++ b/crates/horizon-client/examples/probe/fire_starter_sound_probe.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -//! Runs tools/test_fire_starter_sound_parity.py's level random seeds through -//! the fire charge's use sound (`horizon_client::items::fire_starter_pitch`). +//! Runs tools/test_fire_starter_sound_parity.py's cases through the fire +//! starter's use sound (`horizon_client::items::fire_starter_pitch`). use horizon_client::{ Result, items::{fire_starter_pitch, fire_starter_sound}, @@ -16,12 +16,16 @@ pub fn main() -> Result<()> { let input = BufReader::new(File::open(&args[1])?); let mut out = BufWriter::new(File::create(&args[2])?); for line in input.lines() { - let mut random = LegacyRandom::new(line?.trim().parse()?); - let pitch = fire_starter_pitch(true, &mut random); + let line = line?; + let mut parts = line.trim().split(' '); + let fire_charge = parts.next() == Some("fire_charge"); + parts.next(); // the block the use is made on: the sound does not read it + let mut random = LegacyRandom::new(parts.next().unwrap_or_default().parse()?); + let pitch = fire_starter_pitch(fire_charge, &mut random); writeln!( out, "{} {} {} {}", - fire_starter_sound(true), + fire_starter_sound(fire_charge), 1.0f32.to_bits() as i32, pitch.to_bits() as i32, random.next_long() diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaFireStarterSoundOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaFireStarterSoundOracle.java index 276a0aae32..7781558544 100644 --- a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaFireStarterSoundOracle.java +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaFireStarterSoundOracle.java @@ -5,34 +5,66 @@ import java.lang.reflect.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; +import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.RandomSource; -import net.minecraft.world.item.FireChargeItem; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; /** - * Original oracle for the fire charge's use sound: the real private - * FireChargeItem.playSound (used for both a lit campfire or candle and a - * placed fire) on a level stand-in whose random is seeded from the case, + * Original oracle for a fire starter's use sound: the real + * FireChargeItem.useOn and FlintAndSteelItem.useOn on a campfire, a candle and + * a stone block, on a level stand-in whose random is seeded from the case, * recording the sound, its volume and pitch and the level random's next long * (which shows how many values the sound drew). */ public final class VanillaFireStarterSoundOracle { static final StringBuilder sounds = new StringBuilder(); + static final BlockPos POS = new BlockPos(3, 64, -7); static final class World extends ServerLevel { + BlockState state = Blocks.AIR.defaultBlockState(); World() { super(null, null, null, null, null, null, false, 0L, List.of(), false); } + @Override public BlockState getBlockState(BlockPos p) { + return p.equals(POS) ? state : Blocks.AIR.defaultBlockState(); + } + @Override public boolean setBlock(BlockPos p, BlockState s, int flags) { state = s; return true; } + @Override public boolean setBlockAndUpdate(BlockPos p, BlockState s) { state = s; return true; } + @Override public void gameEvent(Entity entity, Holder event, BlockPos p) {} @Override public void playSeededSound(net.minecraft.world.entity.Entity p, double x, double y, double z, - net.minecraft.core.Holder s, net.minecraft.sounds.SoundSource c, float v, float pitch, long seed) { + Holder s, net.minecraft.sounds.SoundSource c, float v, float pitch, long seed) { sounds.append(s.unwrapKey().orElseThrow().identifier()).append(' ') .append(Float.floatToIntBits(v)).append(' ').append(Float.floatToIntBits(pitch)); } } + /** The player a use is made by: only its level and its empty hand are read. */ + static final class Shell extends LocalPlayer { + Shell() { + super(null, null, null, null, null, Input.EMPTY, false, null, null); + } + @Override public ItemStack getMainHandItem() { return ItemStack.EMPTY; } + @Override public ItemStack getItemBySlot(EquipmentSlot slot) { return ItemStack.EMPTY; } + @Override public boolean hasInfiniteMaterials() { return false; } + } + static void put(Object unsafe, Object target, String name, Object value) throws Exception { Field f = Level.class.getDeclaredField(name); long offset = (long) unsafe.getClass().getMethod("objectFieldOffset", Field.class).invoke(unsafe, f); @@ -46,18 +78,27 @@ public final class VanillaFireStarterSoundOracle { theUnsafe.setAccessible(true); Object unsafe = theUnsafe.get(null); World world = (World) u.getMethod("allocateInstance", Class.class).invoke(unsafe, World.class); - Method play = FireChargeItem.class.getDeclaredMethod("playSound", Level.class, BlockPos.class); - play.setAccessible(true); - var item = (FireChargeItem) Items.FIRE_CHARGE; + Object shell = u.getMethod("allocateInstance", Class.class).invoke(unsafe, Shell.class); + Field levelField = Entity.class.getDeclaredField("level"); + levelField.setAccessible(true); + levelField.set(shell, world); + var hit = new BlockHitResult(new Vec3(3.5, 65.0, -6.5), Direction.UP, POS, false); try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8)); var lines = Files.lines(Path.of(args[0]))) { for (var line : (Iterable) lines::iterator) { - long seed = Long.parseLong(line.trim()); - RandomSource random = RandomSource.create(seed); + var parts = line.trim().split(" "); + boolean fireCharge = parts[0].equals("fire_charge"); + world.state = switch (parts[1]) { + case "campfire" -> Blocks.CAMPFIRE.defaultBlockState().setValue(BlockStateProperties.LIT, false); + case "candle" -> Blocks.CANDLE.defaultBlockState().setValue(BlockStateProperties.LIT, false); + default -> Blocks.STONE.defaultBlockState(); + }; + RandomSource random = RandomSource.create(Long.parseLong(parts[2])); put(unsafe, world, "random", random); put(unsafe, world, "soundSeedGenerator", RandomSource.create(0)); sounds.setLength(0); - play.invoke(item, world, new BlockPos(3, 64, -7)); + var context = new UseOnContext((net.minecraft.world.entity.player.Player) shell, InteractionHand.MAIN_HAND, hit); + (fireCharge ? Items.FIRE_CHARGE : Items.FLINT_AND_STEEL).useOn(context); out.println(sounds + " " + random.nextLong()); } } diff --git a/tools/test_fire_starter_sound_parity.py b/tools/test_fire_starter_sound_parity.py index 6953e768f3..7024d558bf 100644 --- a/tools/test_fire_starter_sound_parity.py +++ b/tools/test_fire_starter_sound_parity.py @@ -25,9 +25,11 @@ def make_cases(build): def main(): - scope = ('The real private FireChargeItem.playSound on a ServerLevel stand-in with a seeded LegacyRandomSource: ' - 'the sound event, volume, pitch ((nextFloat - nextFloat) * 0.2 + 1) and the random afterwards. ' - 'FireChargeItem.useOn\'s choice between lighting and placing fire is not exercised.') + scope = ('Real FireChargeItem.useOn and FlintAndSteelItem.useOn on a ServerLevel stand-in holding a campfire, a ' + 'candle or stone, with a seeded LegacyRandomSource: the sound event, volume, pitch ((nextFloat - nextFloat) ' + '* 0.2 + 1 for the charge, nextFloat * 0.4 + 0.8 for the steel) and the random afterwards. Both items light ' + 'a campfire and a candle and place fire on stone; the block states they write, their game events and the ' + 'item damage are not compared.') harness.differential( 'rust-fire-starter-sound-parity', 'VanillaFireStarterSoundOracle', 'fire_starter_sound_probe', make_cases, scope, sources=[lab.ROOT / 'crates/horizon-items/src/items.rs'])