harness: the oracle calls each item's own useOn

The oracle builds a stand-in level holding the case's block, hands the real
useOn a player with an empty hand and records the sound the level is asked
for; the probe reads the case's item and asks the matching branch for the
sound and the pitch.
This commit is contained in:
MSedgeMC committed 2026-11-02 23:33:46 +00:00
1 parent 73a8f221f9
commit 37d4e42817
3 files changed
+66 -19

No files matched your search

@@ -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()
@@ -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<GameEvent> event, BlockPos p) {}
@Override public void playSeededSound(net.minecraft.world.entity.Entity p, double x, double y, double z,
net.minecraft.core.Holder<net.minecraft.sounds.SoundEvent> s, net.minecraft.sounds.SoundSource c, float v, float pitch, long seed) {
Holder<net.minecraft.sounds.SoundEvent> 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<String>) 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());
}
}
+5 -3
View File
@@ -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'])