diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index 490e4a9737..b242206dde 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -7532,12 +7532,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) @@ -7549,7 +7550,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..3e9a479f64 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. @@ -449,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:?}"); + } +}