Merge pull request #5905 from reservedbytes/feat/native-item-use-tick-release

🤖 Agent: verified with main + #5866 + #5905: pr_ready --quick green (1756 tests), 18 item-use and menu harnesses pass on the 26.3 jars.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-01 18:23:34 +01:00
commit f081099e51
10 files changed
+423 -52

No files matched your search

+33 -23
View File
@@ -2411,6 +2411,8 @@ struct Game<'a> {
/// `getUseDuration` when that use started, as a mod left it
/// (`game/item_use.rs`): consumables and mod items count to it.
use_duration: i32,
/// A mod's `Start` or `Tick` set that use's ticks left (`game/item_use.rs`).
use_ticks_set: bool,
/// Camera.oldFovModifier / fovModifier.
/// `Camera` (eye height and field-of-view easing, view placement).
view: horizon_client::camera::Camera,
@@ -6391,8 +6393,17 @@ impl Game<'_> {
self.using = None;
return;
};
// a mod's tick ran the use out: finished now, or frozen until let go
// for an item used on release (CrossbowItem.useOnRelease).
if !self.use_item_ticked(&stack, ticks) {
if &*stack.item != "minecraft:crossbow" {
self.complete_using_item(slot, &stack, &info);
}
return;
}
if self.blocks_attacks(&stack).is_some() {
// ShieldItem: used for 72000 ticks, i.e. until released.
self.item_on_use_tick(&stack, ticks);
self.using = Some((slot, ticks + 1));
return;
}
@@ -6423,13 +6434,11 @@ impl Game<'_> {
return;
}
if &*stack.item == "minecraft:spyglass" {
// SpyglassItem.USE_DURATION, then finishUsingItem.
if ticks + 1 >= 1200 {
self.send_use_item_complete();
self.stop_spyglass();
self.using = None;
} else {
self.using = Some((slot, ticks + 1));
// SpyglassItem.getUseDuration (1200, as a mod left it), then
// completeUsingItem: finishUsingItem lowers it, and the finish hook.
match item_use::count_down(ticks, self.use_duration) {
Some(ticks) => self.using = Some((slot, ticks)),
None => self.complete_using_item(slot, &stack, &info),
}
return;
}
@@ -6461,22 +6470,16 @@ impl Game<'_> {
};
// ItemStack.onUseTick: Consumable.shouldEmitParticlesAndSounds with
// the remaining ticks before this tick counts them down.
// the duration is getUseDuration's when the use started.
// the duration is getUseDuration's when the use started, as a mod's
// Start and Tick left it.
let remaining = self.use_duration - ticks as i32;
let done = item_use::count_down(ticks, self.use_duration).is_none();
let ticks = ticks + 1;
if consumable.should_emit(remaining) {
self.emit_consume(consumable, &stack, 5);
}
if done {
self.using = None;
self.send_use_item_complete();
// ItemStack.finishUsingItem's applyAfterUseComponentSideEffects.
self.apply_use_cooldown(&stack);
self.finish_consuming(&info, consumable, &stack);
self.use_item_finished(&stack);
} else {
self.using = Some((slot, ticks));
self.item_on_use_tick(&stack, ticks);
match item_use::count_down(ticks, self.use_duration) {
Some(ticks) => self.using = Some((slot, ticks)),
None => self.complete_using_item(slot, &stack, &info),
}
}
/// `BundleItem.onUseTick` and `getUseDuration` (200): the first tick and
@@ -6528,14 +6531,20 @@ impl Game<'_> {
}
/// `LivingEntity.releaseUsingItem` (`Item.releaseUsing`) for `stack`,
/// used for `ticks`, its hand acting: a bow or a trident lets go (not
/// when its hand changed), a spyglass stops scoping.
/// when its hand changed), a spyglass stops scoping, and a mod's own
/// release follows. A mod's `Stop` may skip the release.
pub(super) fn release_used_item(&mut self, stack: &ItemStack, ticks: u32, hand_changed: bool) {
let canceled = !hand_changed && !self.use_item_stopped(stack, ticks);
let (release, spyglass) = item_use::release_parts(canceled, hand_changed);
match &*stack.item {
"minecraft:bow" if !hand_changed => self.release_bow(ticks),
"minecraft:trident" if !hand_changed => self.release_trident(ticks),
"minecraft:spyglass" => self.stop_spyglass(),
"minecraft:bow" if release => self.release_bow(self.ticks_held(stack, ticks)),
"minecraft:trident" if release => self.release_trident(self.ticks_held(stack, ticks)),
"minecraft:spyglass" if spyglass => self.stop_spyglass(),
_ => {}
}
if release {
self.mod_release_using(stack, ticks);
}
}
/// `ServerPlayerGameMode.tick` while a player digs: level event 2020 every
/// fourth tick since the START (the hit sound), else 2019 (the crack only),
@@ -17936,6 +17945,7 @@ impl<'a> Game<'a> {
},
using: None,
use_duration: 0,
use_ticks_set: false,
view: Default::default(),
limiter: horizon_client::window::FramerateLimiter::new(0),
frame_clock: Default::default(),
+4 -3
View File
@@ -96,10 +96,11 @@ impl Game<'_> {
}
/// `CrossbowItem.onUseTick` for the charge in progress.
pub(super) fn tick_crossbow(&mut self, slot: usize, ticks: u32, stack: &ItemStack) {
let ticks = ticks + 1;
self.using = Some((slot, ticks));
// getUseDuration - the ticks left, as a mod's Start or Tick left them.
let held = self.ticks_held(stack, ticks);
self.using = Some((slot, ticks + 1));
let charge = Self::charge_ticks(stack).max(1) as f32;
let (before, now) = ((ticks - 1) as f32 / charge, ticks as f32 / charge);
let (before, now) = (held as f32 / charge, (held + 1) as f32 / charge);
let sounds = Self::charging_sounds(stack);
let at = self
.player
+5 -5
View File
@@ -189,12 +189,12 @@ impl Game<'_> {
self.avatar.add_walked(dx, dz);
}
}
/// `SpyglassItem.use`: start scoping.
/// `SpyglassItem.use`: the sound, then start scoping (a mod's `Start`
/// may refuse that, not the sound).
pub(super) fn use_spyglass(&mut self) {
if self.start_using_item() {
let at = self.eye_position();
self.audio.play("minecraft:item.spyglass.use", at, 1.0, 1.0);
}
let at = self.eye_position();
self.audio.play("minecraft:item.spyglass.use", at, 1.0, 1.0);
self.start_using_item();
}
/// `SpyglassItem.releaseUsing` / `finishUsingItem` (after 1200 ticks).
pub(super) fn stop_spyglass(&mut self) {
+1 -1
View File
@@ -273,7 +273,7 @@ impl Game<'_> {
owner.using_this_item = true;
owner.use_duration = duration;
// getUseItemRemainingTicks: the duration the use started with,
// as LivingEntityUseItemEvent.Start left it (`start_using_item`).
// as LivingEntityUseItemEvent.Start and .Tick left it (`game/item_use.rs`).
owner.use_item_remaining_ticks = self.use_duration - ticks as i32;
owner.crossbow_charge_duration = Self::charge_ticks(stack).max(1) as i32;
}
+216 -8
View File
@@ -2,9 +2,14 @@
//! `Item` subclasses don't know: what `Item.getUseDuration` answers, a
//! native mod's own item counted down to its `finishUsingItem`, and the hand
//! pose of a `getUseAnimation` (`native_use_duration`, `native_use_animation`,
//! `native_living_entity_use_item_start`, `native_living_entity_use_item_finish`).
//! `native_living_entity_use_item_start`, `native_living_entity_use_item_finish`),
//! and each tick and the release of a use (`native_living_entity_use_item_tick`,
//! `native_on_use_tick`, `native_living_entity_use_item_stop`, `native_release_using`).
use super::Game;
use horizon_client::{items::ItemStack, spear::Kinetic};
use horizon_client::{
items::{ItemInfo, ItemStack},
spear::Kinetic,
};
use horizon_world::consumable::UseAnimation;
impl Game<'_> {
@@ -17,15 +22,16 @@ impl Game<'_> {
let Some(stack) = self.held().cloned().filter(|_| self.using.is_none()) else {
return false;
};
let mut duration = self.use_duration(&stack);
let mut canceled = false;
let own = self.use_duration(&stack);
let (mut duration, mut canceled) = (own, false);
let hand = self.hand;
self.native_living_entity_use_item_start(&stack, None, hand, &mut duration, &mut canceled);
if canceled {
let Some(ticks_set) = start_of(own, duration, canceled) else {
return false;
}
};
self.using = Some((self.held_slot(), 0));
self.use_duration = duration;
self.use_ticks_set = ticks_set;
true
}
/// `ItemStack.getUseDuration`: a consumable's time, 72000 for what is
@@ -72,14 +78,35 @@ impl Game<'_> {
/// `LivingEntity.updateUsingItem` for a `mod_use` item used `ticks` so far:
/// `completeUsingItem` once its duration ran out.
pub(super) fn tick_mod_use(&mut self, slot: usize, ticks: u32, stack: &ItemStack) {
self.item_on_use_tick(stack, ticks);
if let Some(ticks) = count_down(ticks, self.use_duration) {
self.using = Some((slot, ticks));
return;
}
let Some(info) = self.items().get(&stack.item).cloned() else {
self.using = None;
return;
};
self.complete_using_item(slot, stack, &info);
}
/// `ServerPlayer.completeUsingItem` of `stack` in `slot`: the client is
/// told, `ItemStack.finishUsingItem` (a consumable is eaten, a potion
/// drunk, a spyglass lowered, anything else given back) with its use
/// cooldown, then `LivingEntityUseItemEvent.Finish`.
pub(super) fn complete_using_item(&mut self, slot: usize, stack: &ItemStack, info: &ItemInfo) {
// 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);
return;
}
self.using = None;
self.send_use_item_complete();
// in ItemStack.finishUsingItem, Item's own gives the stack back.
self.apply_use_cooldown(stack);
if let Some(consumable) = &info.consumable {
self.finish_consuming(info, consumable, stack);
} else if &*stack.item == "minecraft:spyglass" {
self.stop_spyglass();
}
self.use_item_finished(stack);
}
/// `LivingEntityUseItemEvent.Finish` after `finishUsingItem` of `stack`
@@ -93,6 +120,56 @@ impl Game<'_> {
*self.held_mut() = result.filter(|s| s.count > 0);
}
}
/// `LivingEntityUseItemEvent.Tick` for `stack` in use `ticks` so far, as
/// `updatingUsingItem` starts: a mod may change the ticks left, which
/// moves the end. False when it ran the use out (`tick_ran_out`): the
/// item's tick and countdown are skipped.
pub(super) fn use_item_ticked(&mut self, stack: &ItemStack, ticks: u32) -> bool {
let before = ticks_left(self.use_duration, ticks);
let (mut remaining, mut canceled) = (before, false);
let hand = self.used_hand().unwrap_or(self.hand);
self.native_living_entity_use_item_tick(stack, None, hand, &mut remaining, &mut canceled);
let (left, set) = tick_left(before, remaining, canceled, self.use_ticks_set);
self.use_ticks_set = set;
self.use_duration = duration_leaving(ticks, left);
left > 0 || !set
}
/// A mod's `Item.onUseTick` of `stack` used `ticks`, while ticks are left.
pub(super) fn item_on_use_tick(&mut self, stack: &ItemStack, ticks: u32) {
let remaining = ticks_left(self.use_duration, ticks);
if remaining > 0 {
self.native_on_use_tick(stack, None, remaining);
}
}
/// How long `stack`, used `ticks`, counts as held: `getUseDuration` less
/// the ticks left (`BowItem.releaseUsing`, `CrossbowItem.onUseTick`,
/// `TridentItem.releaseUsing`), `ticks` unless a mod's Start or Tick
/// moved the ticks left.
pub(super) fn ticks_held(&self, stack: &ItemStack, ticks: u32) -> u32 {
held(
self.use_duration(stack),
ticks_left(self.use_duration, ticks),
)
}
/// `LivingEntityUseItemEvent.Stop` as `releaseUsingItem` lets go of
/// `stack` used `ticks`: false when a mod skips its `releaseUsing`.
pub(super) fn use_item_stopped(&mut self, stack: &ItemStack, ticks: u32) -> bool {
let mut canceled = false;
let remaining = ticks_left(self.use_duration, ticks);
let hand = self.hand;
self.native_living_entity_use_item_stop(stack, None, hand, remaining, &mut canceled);
!canceled
}
/// A mod's `Item.releaseUsing` of `stack` used `ticks`: when it says it
/// was used, `ItemStack.releaseUsing` applies the use cooldown.
pub(super) fn mod_release_using(&mut self, stack: &ItemStack, ticks: u32) {
let mut used = false;
let remaining = ticks_left(self.use_duration, ticks);
self.native_release_using(stack, None, remaining, &mut used);
if used {
self.apply_use_cooldown(stack);
}
}
/// `Player.canEat(canAlwaysEat)` for `stack`, food that is eaten only
/// when hungry, then a mod's answer.
pub(super) fn can_eat(&self, stack: &ItemStack, can_always_eat: bool) -> bool {
@@ -107,6 +184,49 @@ impl Game<'_> {
}
}
/// The ticks left after a mod's `Tick`, a cancel being NeoForge's -1, and
/// whether a mod ever set this use's: once one did, the use goes on only
/// while some are left, else it ran out. A use no mod touched goes on as
/// vanilla's own do (an eye of ender's has none from the start).
fn tick_left(before: i32, remaining: i32, canceled: bool, set: bool) -> (i32, bool) {
let left = if canceled { -1 } else { remaining };
(left, set || canceled || remaining != before)
}
/// What `releaseUsingItem` runs of a release: the item's own and a mod's
/// `releaseUsing` (not when a mod's `Stop` canceled, nor when the hand
/// changed item, which stops the use without one), and the spyglass's
/// sound (not when canceled).
pub(super) fn release_parts(canceled: bool, hand_changed: bool) -> (bool, bool) {
(!canceled && !hand_changed, !canceled)
}
/// `useItemRemaining` of a use of `duration` ticks, `ticks` old.
fn ticks_left(duration: i32, ticks: u32) -> i32 {
duration.saturating_sub_unsigned(ticks)
}
/// `getUseDuration(stack) - remainingTime` of a use of `own` ticks with
/// `remaining` left, none below 0.
fn held(own: i32, remaining: i32) -> u32 {
u32::try_from(i64::from(own) - i64::from(remaining)).unwrap_or(0)
}
/// The duration that leaves `remaining` ticks of a use `ticks` old, as a
/// mod's `Tick` set them.
fn duration_leaving(ticks: u32, remaining: i32) -> i32 {
remaining.saturating_add_unsigned(ticks)
}
/// What a `Start` left of a use of `own` ticks: None refused (a cancel, or
/// a time below 0, as NeoForge's `startUsingItem` returns), else whether a
/// mod set the ticks left to none. Such a use runs out on its first tick
/// (`tick_left`), as NeoForge's `--useItemRemaining <= 0` completes it, while
/// an item's own 0 (an eye of ender) is held until let go, as vanilla's.
fn start_of(own: i32, duration: i32, canceled: bool) -> Option<bool> {
(!canceled && duration >= 0).then_some(duration == 0 && own != 0)
}
/// `Consumable.startConsuming`: a time of 0 or less is no use, the item is
/// consumed at once (`onConsume`).
pub(super) fn consumes_at_once(consume_ticks: i32) -> bool {
@@ -148,7 +268,10 @@ pub(super) fn use_pose(
#[cfg(test)]
mod tests {
use super::{consumes_at_once, count_down, use_pose};
use super::{
consumes_at_once, count_down, duration_leaving, held, release_parts, start_of, tick_left,
ticks_left, use_pose,
};
use horizon_client::first_person::ItemPose;
use horizon_world::consumable::UseAnimation;
@@ -179,6 +302,91 @@ mod tests {
}
}
#[test]
fn a_start_below_0_refuses_the_use_and_a_mods_0_runs_out() {
// a cancel or a time below 0 refuses it.
assert_eq!(start_of(32, 32, true), None);
assert_eq!(start_of(32, -1, false), None);
assert_eq!(start_of(0, -1, false), None);
// a mod's 0 starts it, and its first tick runs it out.
assert_eq!(start_of(32, 0, false), Some(true));
let (left, set) = tick_left(0, 0, false, true);
assert!(!(left > 0 || !set));
// an item's own 0 (an eye of ender) goes on, as a changed time does.
assert_eq!(start_of(0, 0, false), Some(false));
let (left, set) = tick_left(0, 0, false, false);
assert!(left > 0 || !set);
assert_eq!(start_of(32, 64, false), Some(false));
}
#[test]
fn a_tick_moves_the_end_of_a_use() {
// a 32 tick use, 10 ticks in: 22 left.
assert_eq!(ticks_left(32, 10), 22);
// a mod leaves 1 tick: the next tick finishes it.
let duration = duration_leaving(10, 1);
assert_eq!(count_down(10, duration), None);
// or 30 more: it goes on to tick 40.
let duration = duration_leaving(10, 30);
assert_eq!(duration, 40);
assert_eq!(count_down(38, duration), Some(39));
assert_eq!(count_down(39, duration), None);
// left as it was, nothing moves.
assert_eq!(duration_leaving(10, ticks_left(32, 10)), 32);
}
#[test]
fn a_tick_of_no_time_left_runs_the_use_out() {
let goes_on = |(left, set): (i32, bool)| left > 0 || !set;
// untouched, a use goes on, even one of no time (an eye of ender).
assert_eq!(tick_left(20, 20, false, false), (20, false));
assert!(goes_on(tick_left(-3, -3, false, false)));
// a mod's ticks left go on while above 0.
assert_eq!(tick_left(20, 5, false, false), (5, true));
assert!(goes_on(tick_left(20, 5, false, false)));
// a cancel is -1 left, and it or 0 or less set runs the use out.
assert_eq!(tick_left(20, 20, true, false), (-1, true));
assert!(!goes_on(tick_left(20, 0, false, false)));
assert!(!goes_on(tick_left(20, -1, false, false)));
// a use a mod ran out stays out on the next ticks (a crossbow frozen).
assert!(!goes_on(tick_left(-1, -1, false, true)));
// and goes on again if a mod gives it ticks back, as NeoForge's.
assert!(goes_on(tick_left(-1, 10, false, true)));
}
#[test]
fn a_release_runs_what_vanilla_runs() {
// let go: the item's release and the spyglass's sound.
assert_eq!(release_parts(false, false), (true, true));
// a mod's Stop canceled: neither.
assert_eq!(release_parts(true, false), (false, false));
// another item in the hand: no release (Horizon still lowers a spyglass).
assert_eq!(release_parts(false, true), (false, true));
}
#[test]
fn a_moved_end_moves_the_charge() {
// a bow held 10 ticks of 72000: 10, as vanilla's.
assert_eq!(held(72_000, ticks_left(72_000, 10)), 10);
// a mod's Tick leaves 71000 at tick 10: it counts as held 1000.
assert_eq!(
held(72_000, ticks_left(duration_leaving(10, 71_000), 10)),
1000
);
// more ticks left than the item's own time: not held at all.
assert_eq!(held(72_000, 80_000), 0);
}
#[test]
fn a_release_tells_how_long_the_item_was_held() {
// a bow-like item held 25 ticks of 72000: the time left gives the charge back.
let left = ticks_left(72_000, 25);
assert_eq!(left, 71_975);
assert_eq!(72_000 - left, 25);
// released at once.
assert_eq!(ticks_left(72_000, 0), 72_000);
}
#[test]
fn a_food_of_no_time_is_eaten_at_once() {
assert!(consumes_at_once(0));
@@ -43,6 +43,7 @@ pub(super) struct GuestPlay {
destroy_delay: u32,
using: Option<(usize, u32)>,
use_duration: i32,
use_ticks_set: bool,
use_delay: u32,
/// The shield's ItemCooldowns entry: an axe disabled it for this many ticks.
pub(super) shield_cooldown: u32,
@@ -174,6 +175,7 @@ pub(super) struct GuestSwap {
destroy_delay: u32,
using: Option<(usize, u32)>,
use_duration: i32,
use_ticks_set: bool,
use_delay: u32,
pub(super) shield_cooldown: u32,
/// The guest's item cooldowns (`Game::cooldowns` for its turn).
@@ -473,6 +475,7 @@ impl Game<'_> {
let destroy_delay = std::mem::replace(&mut self.destroy_delay, play.destroy_delay);
let using = std::mem::replace(&mut self.using, play.using.take());
let use_duration = std::mem::replace(&mut self.use_duration, play.use_duration);
let use_ticks_set = std::mem::replace(&mut self.use_ticks_set, play.use_ticks_set);
let use_delay = std::mem::replace(&mut self.use_delay, play.use_delay);
let shield_cooldown = std::mem::replace(&mut self.shield_cooldown, play.shield_cooldown);
let cooldowns = std::mem::replace(&mut self.cooldowns, std::mem::take(&mut play.cooldowns));
@@ -587,6 +590,7 @@ impl Game<'_> {
destroy_delay,
using,
use_duration,
use_ticks_set,
use_delay,
shield_cooldown,
cooldowns,
@@ -663,6 +667,7 @@ impl Game<'_> {
destroy_delay,
using,
use_duration,
use_ticks_set,
use_delay,
shield_cooldown,
cooldowns,
@@ -781,6 +786,7 @@ impl Game<'_> {
play.destroy_delay = std::mem::replace(&mut self.destroy_delay, destroy_delay);
play.using = std::mem::replace(&mut self.using, using);
play.use_duration = std::mem::replace(&mut self.use_duration, use_duration);
play.use_ticks_set = std::mem::replace(&mut self.use_ticks_set, use_ticks_set);
play.use_delay = std::mem::replace(&mut self.use_delay, use_delay);
play.shield_cooldown = std::mem::replace(&mut self.shield_cooldown, shield_cooldown);
play.cooldowns = std::mem::replace(&mut self.cooldowns, cooldowns);
+90 -4
View File
@@ -2366,9 +2366,11 @@ impl Game<'_> {
/// `stack` in `hand` (`LivingEntity.startUsingItem`), for `duration`
/// ticks (`native_use_duration`'s answer). `entity` is a mob's
/// `Entity.getId`, None for a player (`native_acting_player` says who);
/// only players fire it so far. Change `duration` (consumables other
/// than potions, and mods' items, count to it; 0 or less lasts until let
/// go), or set `canceled` and the use doesn't start. Both sides, as
/// only players fire it so far. Change `duration` (consumables, potions,
/// spyglasses and mods' items count to it; below 0 refuses the use, and
/// 0 runs it out on its first tick, as `native_living_entity_use_item_tick`
/// running it out, unless the item's own is 0 already), or set
/// `canceled` and the use doesn't start. Both sides, as
/// vanilla's: wherever the world runs (the server, single player
/// in-frame, a LAN host) for every player of the world, and on a
/// server's client for the uses it predicts. Answer the same on both,
@@ -2391,7 +2393,7 @@ impl Game<'_> {
/// `native_living_entity_use_item_start`. `result` arrives with what
/// `finishUsingItem` left in the hand (the bowl of a stew, none for the
/// last apple; a mod's own item is kept) and may be replaced. Fired for
/// consumables, potions and mods' items. Server-side, for every player
/// consumables, potions, spyglasses and mods' items. Server-side, for every player
/// of the world (the helpers read the acting one). API @API:item_use_over_time@.
pub(super) fn native_living_entity_use_item_finish(
&mut self,
@@ -2403,6 +2405,90 @@ impl Game<'_> {
) {
let _ = (stack, entity, hand, duration, result);
}
/// NeoForge's `LivingEntityUseItemEvent.Tick`: each tick `entity` uses
/// `stack` in `hand`, where `LivingEntity.updatingUsingItem` starts,
/// `duration` the ticks left. `entity` is as in
/// `native_living_entity_use_item_start`. Change `duration` to move the
/// end (consumables, potions, spyglasses and mods' items count to it,
/// and so do a client's eating crumbs and first person pose, as the
/// started duration of `native_living_entity_use_item_start` does). A
/// cancel sets it to -1. Once a mod set it, the use goes on
/// only while it is above 0: at 0 or less, by a cancel or set, the
/// item's tick is skipped and the server finishes the use
/// (`completeUsingItem`: `finishUsingItem`, the food eaten,
/// `native_living_entity_use_item_finish`), while an item used on
/// release (a crossbow) stays frozen until let go. NeoForge's javadoc
/// says a cancel stops the use, but its code finishes it; this follows
/// the code. On a server's client a run-out use stays in use, frozen,
/// so letting go still tells the server. Both sides, as vanilla's
/// `updatingUsingItem`: answer the same on both, and change the world
/// only where this `Game` runs the world, not on a server's client (no
/// helper tells the two apart yet; `native_window` is also true
/// in-frame and on an in-frame LAN host). Runs every tick of every use:
/// check the item first. API @API:item_use_tick_release@.
pub(super) fn native_living_entity_use_item_tick(
&mut self,
stack: &ItemStack,
entity: Option<i32>,
hand: Hand,
duration: &mut i32,
canceled: &mut bool,
) {
let _ = (stack, entity, hand, duration, canceled);
}
/// `Item.onUseTick` of `stack`, used by `entity` with `remaining` ticks
/// left, always above 0: what a mod's own item does each tick it is used
/// (cook, charge, drain). It comes where `ItemStack.onUseTick` calls the
/// item, once `native_living_entity_use_item_tick` let the use go on:
/// after a consumable's crumbs, for consumables, shields and mods' items
/// (vanilla's other items do their own, and a kinetic weapon has none on
/// the server). Both sides, as `native_living_entity_use_item_tick`:
/// change the world only where this `Game` runs the world, not on a
/// server's client. Runs every tick of every use: check the item first.
/// API @API:item_use_tick_release@.
pub(super) fn native_on_use_tick(
&mut self,
stack: &ItemStack,
entity: Option<i32>,
remaining: i32,
) {
let _ = (stack, entity, remaining);
}
/// NeoForge's `LivingEntityUseItemEvent.Stop`: `entity` lets go of
/// `stack` in `hand` before its use ran its time
/// (`LivingEntity.releaseUsingItem`: the use button let go, a screen
/// opened), `duration` the ticks left. Set `canceled` to skip the item's
/// release (a bow doesn't shoot, a spyglass makes no sound,
/// `native_release_using` isn't called); the use stops anyway. Not when
/// the hand changed item, which stops the use without a release. Both
/// sides, as `native_living_entity_use_item_tick`. API @API:item_use_tick_release@.
pub(super) fn native_living_entity_use_item_stop(
&mut self,
stack: &ItemStack,
entity: Option<i32>,
hand: Hand,
duration: i32,
canceled: &mut bool,
) {
let _ = (stack, entity, hand, duration, canceled);
}
/// `Item.releaseUsing` of `stack`: `entity` let it go with `time_left`
/// ticks of its `native_use_duration` left, so it was held for
/// `duration - time_left` (a bow-like item shoots by that charge). Set
/// `used` as vanilla's return, and the stack's use cooldown starts
/// (`ItemStack.releaseUsing`). After the vanilla item's own release, once
/// `native_living_entity_use_item_stop` let it through. Both sides, as
/// vanilla's: change the world only where this `Game` runs the world,
/// not on a server's client. API @API:item_use_tick_release@.
pub(super) fn native_release_using(
&mut self,
stack: &ItemStack,
entity: Option<i32>,
time_left: i32,
used: &mut bool,
) {
let _ = (stack, entity, time_left, used);
}
/// A player breaks the block `state` at `pos`, once the game allowed it
/// (the held item can break blocks, the debug stick didn't take the
/// click): where `ServerPlayerGameMode.destroyBlock` starts. Set
+18 -3
View File
@@ -613,19 +613,34 @@ impl Game<'_> {
if &*stack.item != "minecraft:potion" && !ominous {
return false;
}
// the duration the use started with (32, as a mod's Start or Tick left it).
let duration = self.use_duration;
let ticks = ticks + 1;
let remaining = 32u32.saturating_sub(ticks);
let remaining = duration.saturating_sub_unsigned(ticks).max(0);
let eye = self.eye_position().unwrap_or_default();
// Consumable.emitParticlesAndSounds: the drink sound every 4 ticks near the end.
if remaining <= 25 && remaining.is_multiple_of(4) {
if remaining <= 25 && remaining % 4 == 0 {
let pitch = self.world.random.next_float() * 0.1 + 0.9;
self.audio
.play("minecraft:entity.generic.drink", Some(eye), 0.5, pitch);
}
if ticks < 32 {
// ItemStack.onUseTick: the item's own tick after the consumable's.
self.item_on_use_tick(stack, ticks - 1);
if i64::from(ticks) < i64::from(duration) {
self.using = Some((slot, ticks));
return true;
}
self.finish_drinking(slot, stack);
true
}
/// `PotionItem.finishUsingItem` (and `OminousBottleItem`'s) of `stack` in
/// `slot`, the use complete: false for anything else.
pub(super) fn finish_drinking(&mut self, slot: usize, stack: &ItemStack) -> bool {
let ominous = &*stack.item == "minecraft:ominous_bottle";
if &*stack.item != "minecraft:potion" && !ominous {
return false;
}
let eye = self.eye_position().unwrap_or_default();
self.using = None;
self.send_use_item_complete();
self.post_entity_event("minecraft:drink", horizon_client::mobs::Who::Player);
+24 -5
View File
@@ -3484,6 +3484,7 @@ impl Game<'_> {
&& let Some(stack) = self.stack_in(slot).cloned()
{
self.use_duration = self.use_duration(&stack);
self.use_ticks_set = false;
}
}
_ => {}
@@ -7806,6 +7807,13 @@ impl Game<'_> {
if self.mod_use_tick(slot, ticks, !self.right || !in_hand) {
return;
}
// updatingUsingItem runs here too: a use a mod's tick ran out stays
// frozen until let go (RELEASE_USE_ITEM) or the server finishes it.
if let Some(s) = stack.as_ref().filter(|_| in_hand)
&& !self.use_item_ticked(s, ticks)
{
return;
}
// BrushItem.onUseTick: with no block aimed the use ends (the server
// ends it too); a stroke shows its dust. Its sounds are the server's.
if stack
@@ -7840,7 +7848,10 @@ impl Game<'_> {
}
// A drawn bow, a charging crossbow, a raised shield...: held until
// the button lets go.
if stack.as_ref().is_some_and(|s| self.held_until_released(s)) {
if let Some(s) = stack.as_ref().filter(|s| self.held_until_released(s)) {
if in_hand && (self.mod_use(s) || self.blocks_attacks(s).is_some()) {
self.item_on_use_tick(s, ticks);
}
self.using = in_hand.then_some((slot, ticks + 1));
return;
}
@@ -7885,6 +7896,7 @@ impl Game<'_> {
// player's own server.
self.emit_consume(&consumable, &stack, 5);
}
self.item_on_use_tick(&stack, ticks);
self.using = Some((slot, ticks + 1));
}
/// `Player.handleEntityEvent(9)`: `completeUsingItem` on the client. A consumable's last crumbs, and FoodProperties.onConsume,
@@ -8297,14 +8309,21 @@ impl Game<'_> {
direction: 0,
sequence: 0,
}));
// a mod's Stop may skip the item's release, and a mod's own release follows.
let hand = self.used_hand();
if let Some((slot, ticks)) = self.using.take()
&& let Some(hand) = hand
&& self
.stack_in(slot)
.is_some_and(|s| &*s.item == "minecraft:trident")
&& let Some(stack) = self.stack_in(slot).cloned()
{
self.with_hand(hand, |g| g.release_trident(ticks));
self.with_hand(hand, |g| {
if !g.use_item_stopped(&stack, ticks) {
return;
}
if &*stack.item == "minecraft:trident" {
g.release_trident(g.ticks_held(&stack, ticks));
}
g.mod_release_using(&stack, ticks);
});
}
}
/// ClientPacketListener.sendChat / sendCommand, signed with a chat
@@ -0,0 +1,26 @@
{
"hooks": [
{
"name": "living_entity_use_item_tick",
"target": "horizon_client::game::native_hooks::Game<'_>::native_living_entity_use_item_tick",
"neoforge": "LivingEntityUseItemEvent.Tick",
"cancellable": true
},
{
"name": "on_use_tick",
"target": "horizon_client::game::native_hooks::Game<'_>::native_on_use_tick"
},
{
"name": "living_entity_use_item_stop",
"target": "horizon_client::game::native_hooks::Game<'_>::native_living_entity_use_item_stop",
"neoforge": "LivingEntityUseItemEvent.Stop",
"cancellable": true
},
{
"name": "release_using",
"target": "horizon_client::game::native_hooks::Game<'_>::native_release_using"
}
],
"rust_summary": "an item's use tick and release (`native_living_entity_use_item_tick`,\n`native_on_use_tick`, `native_living_entity_use_item_stop`, `native_release_using`).",
"doc_summary": "an item's use tick and release, as vanilla's `Item.onUseTick` and `releaseUsing` and NeoForge's `LivingEntityUseItemEvent.Tick` and `.Stop`. Each tick of a use, `native_living_entity_use_item_tick` may change the ticks left: at 0 or less, or on a cancel, the server finishes the use, as NeoForge's code does, and an item used on release stays frozen until let go. `native_on_use_tick` is what a mod's own item does then (cook, charge). When the player lets go early, `native_living_entity_use_item_stop` may skip the release, and `native_release_using` acts with the ticks left (a bow-like item shoots by its charge); setting `used` starts the item's cooldown. All four run on both sides, as vanilla's do, and take the using entity, None for a player; the tick and stop take the hand too. Change the world only where this game runs the world, not on a server's client. Bundles that use them need `\"hooks\": @API:item_use_tick_release@`."
}