mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
10 files changed
+423
-52
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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@`."
|
||||
}
|
||||
Reference in new issue
Block a user