mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
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:
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()
|
||||
|
||||
+52
-11
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in new issue
Block a user