From d2cd07f1ac7ce2205366f564b6482fd4bf0c7668 Mon Sep 17 00:00:00 2001 From: MSedgeMC <151820093+MSedgeMC@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:48:34 +0000 Subject: [PATCH 1/2] fix: starting an item use posts item_interact_start LivingEntity.startUsingItem ends with this.useItem.causeUseVibration(this, GameEvent.ITEM_INTERACT_START), and ItemStack.causeUseVibration posts only when the stack's use effects ask for interaction vibrations - the same gate and the same position stopUsingItem already used for ITEM_INTERACT_FINISH. Horizon posted only the finish event, so a sculk sensor beside a player drawing a bow never clicked and an allay never heard the use start. use_vibration takes the event now, and start_using_item calls it where vanilla posts it: after the use is set up, before the kinetic weapon's map (which this change leaves alone). --- crates/horizon-client/src/game.rs | 13 +++++++------ crates/horizon-client/src/game/item_use.rs | 7 +++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index da0caeda04..2936d4973d 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -7475,12 +7475,13 @@ impl Game<'_> { /// game event from the player, at its feet. pub(super) fn stop_using_item(&mut self, stack: &ItemStack) { self.using = None; - self.use_vibration(stack); + self.use_vibration(stack, "minecraft:item_interact_finish"); } - /// `ItemStack.causeUseVibration` for `stack` used by the player: an - /// `ITEM_INTERACT_FINISH` game event from the player, at its feet, when the - /// stack's use effects ask for interaction vibrations (the default). - pub(super) fn use_vibration(&mut self, stack: &ItemStack) { + /// `ItemStack.causeUseVibration` for `stack` used by the player: the + /// `event` game event (`ITEM_INTERACT_START` when the use begins, + /// `ITEM_INTERACT_FINISH` when it ends) from the player, at its feet, when + /// the stack's use effects ask for interaction vibrations (the default). + pub(super) fn use_vibration(&mut self, stack: &ItemStack, event: &'static str) { if !self .items() .get(&stack.item) @@ -7492,7 +7493,7 @@ impl Game<'_> { return; }; let ctx = self.player_context(None); - self.post_game_event("minecraft:item_interact_finish", [p.x, p.y, p.z], ctx); + self.post_game_event(event, [p.x, p.y, p.z], ctx); } /// `ServerPlayerGameMode.tick` while a player digs: level event 2020 every /// fourth tick since the START (the hit sound), else 2019 (the crack only), diff --git a/crates/horizon-client/src/game/item_use.rs b/crates/horizon-client/src/game/item_use.rs index d08f723553..89bdbb9f73 100644 --- a/crates/horizon-client/src/game/item_use.rs +++ b/crates/horizon-client/src/game/item_use.rs @@ -32,6 +32,9 @@ impl Game<'_> { self.using = Some((self.held_slot(), 0)); self.use_duration = duration; self.use_ticks_set = ticks_set; + // `LivingEntity.startUsingItem`: `causeUseVibration(ITEM_INTERACT_START)` + // when the use effects of the stack ask for interaction vibrations. + self.use_vibration(&stack, "minecraft:item_interact_start"); // A new charge hears the crossbow's loading sounds again // (`game/crossbow.rs`). self.crossbow_sounds = [false; 2]; @@ -100,7 +103,7 @@ impl Game<'_> { // a potion is drunk (no last tick of its own: a mod's tick ran it out). if self.finish_drinking(slot, stack) { self.use_item_finished(stack); - self.use_vibration(stack); + self.use_vibration(stack, "minecraft:item_interact_finish"); return; } self.using = None; @@ -113,7 +116,7 @@ impl Game<'_> { } self.use_item_finished(stack); // `LivingEntity.completeUsingItem` ends with `stopUsingItem`. - self.use_vibration(stack); + self.use_vibration(stack, "minecraft:item_interact_finish"); } /// `LivingEntityUseItemEvent.Finish` after `finishUsingItem` of `stack` /// (no ticks left): the hand holds what a mod made the result. From 9fb4fde63b4f16931e359622ab6aca111adee57e Mon Sep 17 00:00:00 2001 From: MSedgeMC <151820093+MSedgeMC@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:48:34 +0000 Subject: [PATCH 2/2] test: a use start is an item_interact_start vibration A windowless game from the prepared resources, a bow in hand: the two events the use posts are read back from the game's own event recorder, at the player's feet (10, 64, -4). A spear, whose use effects set interact_vibrations false, posts neither. --- crates/horizon-client/src/game/item_use.rs | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/horizon-client/src/game/item_use.rs b/crates/horizon-client/src/game/item_use.rs index 89bdbb9f73..3e9a479f64 100644 --- a/crates/horizon-client/src/game/item_use.rs +++ b/crates/horizon-client/src/game/item_use.rs @@ -452,3 +452,87 @@ mod tests { } } } + +#[cfg(test)] +mod interact_vibration_tests { + use super::*; + + /// Runs `f` on a windowless game built from the prepared resources with + /// `held` in the main hand and returns the game events it posted; + /// `None` without the resources. + fn with_game(held: &str, f: impl FnOnce(&mut Game<'_>)) -> Option> { + use crate::game::{Audio, Options, Progress, Settings, load_resources}; + let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES")?); + let settings = Settings::default(); + let loaded = load_resources( + root.clone(), + None, + &root, + &settings, + std::time::Instant::now(), + false, + true, + &Progress::new(), + &mut |_, _| unreachable!(), + ) + .ok()?; + let mut options = Options::parse(std::iter::empty()).unwrap().unwrap(); + options.resources = root.clone(); + options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer; + let mut game = Game::new( + &loaded.resources, + loaded.pack, + loaded.gui.unwrap(), + options, + settings, + Audio::silent(&root), + None, + ) + .unwrap(); + game.player = Some( + horizon_client::motion::PlayerMotion::new( + game.motion, + horizon_core::geometry::Vec3::new(10.5, 64.0, -3.25), + ) + .unwrap(), + ); + *game.held_mut() = Some(ItemStack::new(held, 1)); + crate::game::dispense_fixture::start_recording(); + f(&mut game); + Some(crate::game::dispense_fixture::stop_recording()) + } + + /// `LivingEntity.startUsingItem` ends with + /// `this.useItem.causeUseVibration(this, GameEvent.ITEM_INTERACT_START)`, + /// and `ItemStack.causeUseVibration` posts only when the stack's use effects + /// ask for interaction vibrations (the default). `stopUsingItem` posts the + /// finish the same way, which Horizon already did, so a use is a pair of + /// vibrations and an item that turns them off is neither. + #[test] + #[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"] + fn a_use_start_is_an_item_interact_start_vibration() { + let bow = || ItemStack::new("minecraft:bow", 1); + let Some(lines) = with_game("minecraft:bow", |game| { + assert!(game.start_using_item(), "the bow's use starts"); + game.stop_using_item(&bow()); + }) else { + return; + }; + assert_eq!( + lines, + vec![ + "G 10,64,-4 minecraft:item_interact_start".to_owned(), + "G 10,64,-4 minecraft:item_interact_finish".to_owned(), + ] + ); + // A spear's use effects set `interact_vibrations` false: no vibration. + let spear = || ItemStack::new("minecraft:iron_spear", 1); + let Some(lines) = with_game("minecraft:iron_spear", |game| { + assert!(game.start_using_item(), "the spear's use starts"); + game.stop_using_item(&spear()); + }) else { + return; + }; + assert!(lines.is_empty(), "{lines:?}"); + } +}