Skip to main content

steel_core/behavior/
item.rs

1//! Item behavior trait and registry.
2
3use std::sync::Arc;
4
5use std::borrow::Cow;
6use steel_protocol::packets::game::SoundSource;
7use steel_registry::data_components::Consumable;
8use steel_registry::data_components::vanilla_components::{
9    BLOCKS_ATTACKS, CONSUMABLE, FOOD, KINETIC_WEAPON, USE_REMAINDER,
10};
11
12use steel_registry::data_components::vanilla_components::ITEM_NAME;
13use steel_registry::item_stack::ItemStack;
14use steel_registry::items::ItemRef;
15use steel_registry::stat::vanilla_stat_types;
16use steel_registry::{REGISTRY, RegistryEntry, RegistryExt, sound_events, vanilla_game_events};
17use steel_utils::types::InteractionHand;
18use text_components::TextComponent;
19
20use crate::behavior::items::{DefaultItemBehavior, SpawnEggItem};
21use crate::behavior::{InteractionResult, UseItemContext, UseOnContext};
22use crate::entity::consume_effect::apply_consume_effect;
23use crate::entity::damage::DamageSource;
24use crate::entity::{Entity, LivingEntity};
25use crate::player::{Player, player_inventory::EquipmentSwapResult};
26use crate::world::World;
27
28pub use steel_registry::data_components::vanilla_components::ItemUseAnimation;
29
30/// Trait defining the behavior of an item.
31///
32/// This trait handles dynamic/functional aspects of items:
33/// - Use on blocks (placing, interacting)
34/// - Use in air
35/// - etc.
36pub trait ItemBehavior: Send + Sync {
37    /// Returns the Rust type name of the concrete behavior implementation.
38    #[cfg(feature = "flint")]
39    #[must_use]
40    #[expect(clippy::absolute_paths, reason = "easier for features")]
41    fn type_name(&self) -> &'static str {
42        std::any::type_name::<Self>()
43    }
44
45    /// Returns vanilla `Item.getName(stack)`.
46    fn get_name<'a>(&self, stack: &'a ItemStack) -> Cow<'a, TextComponent> {
47        stack
48            .get(ITEM_NAME)
49            .map_or_else(|| Cow::Owned(TextComponent::new()), Cow::Borrowed)
50    }
51
52    /// Called when this item is used on a block.
53    fn use_on(&self, _context: &mut UseOnContext) -> InteractionResult {
54        InteractionResult::Pass
55    }
56
57    /// Called when this item is used (e.g. right click in air).
58    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
59        // TODO: Mirror Item.use for BLOCKS_ATTACKS and KINETIC_WEAPON so
60        // specialized behaviors inherit the complete Vanilla base path.
61        let (consume_ticks, can_eat) = context.inv.with_item(|item| {
62            let can_eat = item
63                .get(FOOD)
64                .is_none_or(|food| context.player.can_eat(food.can_always_eat()));
65            (item.get(CONSUMABLE).map(Consumable::consume_ticks), can_eat)
66        });
67        if let Some(consume_ticks) = consume_ticks {
68            if !can_eat {
69                return InteractionResult::Fail;
70            }
71            // Mirrors vanilla `Consumable.startConsuming`: an item with no
72            // consume duration is consumed immediately instead of entering
73            // the timed-use state — `start_using_item` rejects a duration of
74            // 0 outright, so going through it here would silently never
75            // finish and never run the item's effects.
76            if consume_ticks > 0 {
77                context.player.start_using_item(context.hand);
78            } else {
79                let stack = context.inv.with_item(|item| item.clone());
80                let result = finish_consuming_stack(&stack, context.world, context.player);
81                context.inv.with_item(|item| *item = result);
82            }
83            return InteractionResult::Consume;
84        }
85
86        let Some(equippable) = context.inv.with_item(|item| item.get_equippable().cloned()) else {
87            return InteractionResult::Pass;
88        };
89
90        if !equippable.swappable || !equippable.can_be_equipped_by(context.player.entity_type()) {
91            return InteractionResult::Pass;
92        }
93
94        let slot = equippable.slot;
95        let result = context.inv.with_inventory(|inventory| {
96            inventory.try_swap_with_equipment_slot(
97                context.hand,
98                slot,
99                context.player.has_infinite_materials(),
100            )
101        });
102
103        match result {
104            EquipmentSwapResult::Success(overflow) => {
105                if !overflow.is_empty() {
106                    let _ = context.player.drop_item(overflow, false, false);
107                }
108                InteractionResult::Success
109            }
110            EquipmentSwapResult::Fail => InteractionResult::Fail,
111        }
112    }
113
114    /// Returns vanilla `Item.getUseAnimation`.
115    fn get_use_animation(&self, stack: &ItemStack) -> ItemUseAnimation {
116        if let Some(consumable) = stack.get(CONSUMABLE) {
117            consumable.animation()
118        } else if stack.has(BLOCKS_ATTACKS) {
119            ItemUseAnimation::Block
120        } else if stack.has(KINETIC_WEAPON) {
121            ItemUseAnimation::Spear
122        } else {
123            ItemUseAnimation::None
124        }
125    }
126
127    /// Returns vanilla `Item.getUseDuration`.
128    fn get_use_duration(&self, stack: &ItemStack, _user: &dyn LivingEntity) -> i32 {
129        if let Some(consumable) = stack.get(CONSUMABLE) {
130            consumable.consume_ticks()
131        } else if stack.has(BLOCKS_ATTACKS) || stack.has(KINETIC_WEAPON) {
132            72000
133        } else {
134            0
135        }
136    }
137
138    /// Called every tick while a living entity is actively using this item.
139    fn on_use_tick(
140        &self,
141        _world: &Arc<World>,
142        user: &dyn LivingEntity,
143        stack: &mut ItemStack,
144        ticks_remaining: i32,
145    ) {
146        if let Some(consumable) = stack.get(CONSUMABLE)
147            && should_emit_consume_particles_and_sounds(consumable, ticks_remaining)
148        {
149            emit_consume_particles_and_sounds(consumable, user);
150        }
151    }
152
153    /// Called when active use is released before completion.
154    ///
155    /// Returns whether vanilla should apply the stack's after-use component side effects.
156    fn release_using(
157        &self,
158        _stack: &mut ItemStack,
159        _world: &Arc<World>,
160        _user: &dyn LivingEntity,
161        _time_left: i32,
162    ) -> bool {
163        false
164    }
165
166    /// Returns whether the item acts when the use key is released, rather than the use timer expiring
167    fn use_on_release(&self, _stack: &ItemStack) -> bool {
168        false
169    }
170
171    /// Called when active use reaches its full duration.
172    fn finish_using(
173        &self,
174        stack: &mut ItemStack,
175        world: &Arc<World>,
176        user: &dyn LivingEntity,
177    ) -> ItemStack {
178        finish_consuming_stack(stack, world, user)
179    }
180
181    /// Called by vanilla `ItemStack.interactLivingEntity`.
182    fn interact_living_entity(
183        &self,
184        _stack: &mut ItemStack,
185        _player: &Player,
186        _target: &dyn LivingEntity,
187        _hand: InteractionHand,
188    ) -> InteractionResult {
189        InteractionResult::Pass
190    }
191
192    /// Returns vanilla `Item.getItemDamageSource`.
193    fn get_item_damage_source(&self, _attacker: &dyn LivingEntity) -> Option<DamageSource> {
194        None
195    }
196
197    /// Returns item-specific attack damage added by `Item.getAttackDamageBonus`.
198    fn get_attack_damage_bonus(
199        &self,
200        _attacker: &dyn LivingEntity,
201        _victim: &dyn Entity,
202        _base_damage: f32,
203        _damage_source: &DamageSource,
204    ) -> f32 {
205        0.0
206    }
207
208    /// Called by vanilla `Item.hurtEnemy`.
209    fn hurt_enemy(
210        &self,
211        _stack: &mut ItemStack,
212        _target: &dyn LivingEntity,
213        _attacker: &dyn LivingEntity,
214    ) {
215    }
216
217    /// Called by vanilla `Item.postHurtEnemy`.
218    fn post_hurt_enemy(
219        &self,
220        _stack: &mut ItemStack,
221        _target: &dyn LivingEntity,
222        _attacker: &dyn LivingEntity,
223    ) {
224    }
225
226    /// Returns how much durability this weapon consumes after a successful entity hit.
227    fn item_damage_per_attack(&self, stack: &ItemStack) -> Option<i32> {
228        stack
229            .get_weapon()
230            .map(|weapon| weapon.item_damage_per_attack)
231    }
232
233    /// Whether this item may be stored inside container items such as shulker
234    /// boxes and bundles, which vanilla uses to stop them nesting.
235    #[must_use]
236    fn can_fit_inside_container_items(&self) -> bool {
237        true
238    }
239
240    /// Returns this item behavior as a `SpawnEggItem`.
241    fn as_spawn_egg(&self) -> Option<&SpawnEggItem> {
242        None
243    }
244}
245
246fn should_emit_consume_particles_and_sounds(consumable: &Consumable, ticks_remaining: i32) -> bool {
247    let consume_ticks = consumable.consume_ticks();
248    let ticks_used = consume_ticks - ticks_remaining;
249    let wait_ticks = (consume_ticks as f32 * 0.21875) as i32;
250    ticks_used > wait_ticks && ticks_remaining % 4 == 0
251}
252
253/// Mirrors vanilla `Consumable.emitParticlesAndSounds`
254fn emit_consume_particles_and_sounds(consumable: &Consumable, user: &dyn LivingEntity) {
255    // TODO: spawn item-crumb particles when `has_consume_particles()` is set.
256    let (volume, pitch) = if consumable.animation() == ItemUseAnimation::Drink {
257        (0.5, 0.9 + rand::random::<f32>() * 0.1)
258    } else {
259        let volume = if rand::random::<bool>() { 0.5 } else { 1.0 };
260        let pitch = 1.0 + 0.2 * (rand::random::<f32>() - rand::random::<f32>());
261        (volume, pitch)
262    };
263    if let Some(sound) = consumable.sound().registry_ref() {
264        user.play_sound(sound, volume, pitch);
265    }
266}
267
268/// Applies vanilla `Consumable.onConsume`'s shared tail: runs
269/// `on_consume_effects`, plays the consume sound, then shrinks the stack by
270/// one (creative mode leaves it untouched).
271pub(crate) fn finish_consuming_stack(
272    stack: &ItemStack,
273    world: &Arc<World>,
274    user: &dyn LivingEntity,
275) -> ItemStack {
276    let Some(consumable) = stack.get(CONSUMABLE) else {
277        return apply_use_remainder(stack, stack.copy_with_count(stack.count()), user);
278    };
279
280    emit_consume_particles_and_sounds(consumable, user);
281
282    if let Some(food) = stack.get(FOOD) {
283        if let Some(sound) = consumable.sound().registry_ref() {
284            world.play_sound_at(
285                sound,
286                SoundSource::Neutral,
287                user.position(),
288                1.0,
289                // Java `RandomSource.triangle(1.0, 0.4)`.
290                1.0 + 0.4 * (rand::random::<f32>() - rand::random::<f32>()),
291                None,
292            );
293        }
294        if let Some(player) = user.as_player() {
295            player
296                .food_data
297                .lock()
298                .add_food(food.nutrition(), food.saturation());
299            world.play_sound_at(
300                &sound_events::ENTITY_PLAYER_BURP,
301                SoundSource::Players,
302                player.position(),
303                0.5,
304                // Java `Mth.randomBetween(random, 0.9, 1.0)`.
305                0.9 + rand::random::<f32>() * 0.1,
306                None,
307            );
308        }
309    }
310
311    for effect in consumable.on_consume_effects() {
312        apply_consume_effect(effect, world, user);
313    }
314
315    let event = if consumable.animation() == ItemUseAnimation::Drink {
316        &vanilla_game_events::DRINK
317    } else {
318        &vanilla_game_events::EAT
319    };
320    user.game_event(event);
321    if let Some(player) = user.as_player() {
322        player.award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
323    }
324    // TODO: Spawn item-crumb particles when `has_consume_particles()` is set.
325    // TODO: Trigger CriteriaTriggers.CONSUME_ITEM once the advancement-criteria
326    // foundation exists.
327
328    let mut used_stack = stack.copy_with_count(stack.count());
329    if !user.has_infinite_materials() {
330        used_stack.shrink(1);
331    }
332
333    apply_use_remainder(stack, used_stack, user)
334}
335
336/// Applies vanilla `UseRemainder.convertIntoRemainder`: if the original stack
337/// had a `use_remainder` and was actually consumed, either swap the fully
338/// emptied stack for the remainder, or — for a stack that still has items
339/// left (e.g. one honey bottle out of several)
340pub(crate) fn apply_use_remainder(
341    original_stack: &ItemStack,
342    used_stack: ItemStack,
343    user: &dyn LivingEntity,
344) -> ItemStack {
345    let Some(remainder) = original_stack.get(USE_REMAINDER) else {
346        return used_stack;
347    };
348    if user.has_infinite_materials() || used_stack.count() >= original_stack.count() {
349        return used_stack;
350    }
351
352    let remainder_stack = remainder.convert_into().create();
353    if used_stack.is_empty() {
354        return remainder_stack;
355    }
356
357    user.handle_extra_items_created_on_use(remainder_stack);
358    used_stack
359}
360
361/// Registry for item behaviors.
362///
363/// Created after the main registry is frozen. Block items get `BlockItemBehavior`,
364/// other items get `DefaultItemBehavior`. Custom behaviors can be registered.
365pub struct ItemBehaviorRegistry {
366    behaviors: Vec<Box<dyn ItemBehavior>>,
367}
368
369impl ItemBehaviorRegistry {
370    /// Creates a new behavior registry with default behaviors for all items.
371    ///
372    /// Call `register_item_behaviors()` after this to set up proper behaviors.
373    #[must_use]
374    pub fn new() -> Self {
375        let item_count = REGISTRY.items.len();
376        let behaviors = (0..item_count)
377            .map(|_| Box::new(DefaultItemBehavior) as Box<dyn ItemBehavior>)
378            .collect();
379
380        Self { behaviors }
381    }
382
383    /// Sets a custom behavior for an item.
384    pub fn set_behavior(&mut self, item: ItemRef, behavior: Box<dyn ItemBehavior>) {
385        let id = item.id();
386        self.behaviors[id] = behavior;
387    }
388
389    /// Gets the behavior for an item.
390    #[must_use]
391    pub fn get_behavior(&self, item: ItemRef) -> &dyn ItemBehavior {
392        let id = item.id();
393        self.behaviors[id].as_ref()
394    }
395
396    /// Returns vanilla `ItemStack.getHoverName`, including item-specific
397    /// `Item.getName(stack)` overrides when no custom name is present.
398    #[must_use]
399    pub fn hover_name<'a>(&self, stack: &'a ItemStack) -> Cow<'a, TextComponent> {
400        stack
401            .custom_name()
402            .unwrap_or_else(|| self.get_behavior(stack.item()).get_name(stack))
403    }
404
405    /// Get all behaviors.
406    #[cfg(feature = "flint")]
407    #[must_use]
408    pub fn get_behaviors(&self) -> &[Box<dyn ItemBehavior>] {
409        &self.behaviors
410    }
411}
412
413impl Default for ItemBehaviorRegistry {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use steel_registry::data_components::{Consumable, vanilla_components};
422    use steel_registry::item_stack::ItemStack;
423    use steel_registry::stat::vanilla_stat_types;
424    use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items};
425    use steel_utils::types::InteractionHand;
426    use steel_utils::{ChunkPos, Downcast as _, WorldAabb};
427
428    use super::finish_consuming_stack;
429    use crate::behavior::{ITEM_BEHAVIORS, InteractionResult, UseItemContext, init_behaviors};
430    use crate::entity::entities::ItemEntity;
431    use crate::inventory::container::Container as _;
432    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
433
434    /// Drinking one honey bottle out of a stack must only consume one item:
435    /// the leftover bottles stay in hand, and the empty glass bottle it
436    /// produces is handed off separately rather than replacing the whole
437    /// stack. Mirrors vanilla `UseRemainder.convertIntoRemainder`.
438    #[test]
439    fn honey_bottle_stack_keeps_remaining_bottles_and_hands_off_the_remainder() {
440        init_vanilla_registry();
441        let world = fresh_test_world("finish_consuming_honey_bottle_stack");
442        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
443        let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
444        player.set_client_loaded(true);
445
446        // Fill the inventory so the glass bottle remainder cannot be stored
447        // and must be dropped instead — the scenario the honey-bottle fix
448        // targets.
449        {
450            let mut inventory = player.inventory.lock();
451            for slot in 0..36 {
452                inventory.set_item(slot, ItemStack::with_count(&vanilla_items::STONE, 64));
453            }
454        }
455
456        let stack = ItemStack::with_count(&vanilla_items::HONEY_BOTTLE, 5);
457        let result = finish_consuming_stack(&stack, &world, player.as_ref());
458
459        assert!(result.is(&vanilla_items::HONEY_BOTTLE));
460        assert_eq!(result.count(), 4);
461
462        let dropped = world.get_entities_in_aabb_matching(
463            &WorldAabb::new(-2.0, -1.0, -2.0, 2.0, 3.0, 2.0),
464            |entity| entity.entity_type() == &vanilla_entities::ITEM,
465        );
466        assert_eq!(dropped.len(), 1);
467        let Some(item) = dropped[0].as_ref().downcast_ref::<ItemEntity>() else {
468            panic!("dropped entity should retain its concrete item type");
469        };
470        assert!(item.get_item().is(&vanilla_items::GLASS_BOTTLE));
471    }
472
473    /// Regression test for a deadlock: `use_item` on a `consume_seconds:
474    /// 0.0` item must finish it immediately (mirroring vanilla `Consumable
475    /// .startConsuming`'s `consumeTicks() > 0` branch) *without* holding the
476    /// inventory lock while doing so — `finish_consuming_stack` can recurse
477    /// into the same lock via `USE_REMAINDER` (honey bottle → glass bottle)
478    /// through `handle_extra_items_created_on_use`, which used to deadlock
479    /// when called from inside the lock `use_item` originally held.
480    #[test]
481    fn instant_consumable_with_use_remainder_finishes_without_deadlocking() {
482        init_vanilla_registry();
483        init_behaviors();
484        let world = fresh_test_world("instant_consumable_no_deadlock");
485        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
486        let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
487        player.set_client_loaded(true);
488
489        let mut stack = ItemStack::with_count(&vanilla_items::HONEY_BOTTLE, 2);
490        let existing = stack
491            .get(vanilla_components::CONSUMABLE)
492            .expect("honey bottle is consumable")
493            .clone();
494        let instant_consumable = Consumable::new(
495            0.0,
496            existing.animation(),
497            existing.sound().clone(),
498            existing.has_consume_particles(),
499            existing.on_consume_effects().to_vec(),
500        )
501        .expect("valid consumable");
502        stack.set(vanilla_components::CONSUMABLE, instant_consumable);
503
504        {
505            let mut inventory = player.inventory.lock();
506            inventory.set_item_in_hand(InteractionHand::MainHand, stack);
507        }
508
509        let behavior = ITEM_BEHAVIORS.get_behavior(&vanilla_items::HONEY_BOTTLE);
510        let mut context = UseItemContext::new(
511            &player,
512            InteractionHand::MainHand,
513            &world,
514            player.inventory.clone(),
515        );
516
517        let result = behavior.use_item(&mut context);
518
519        assert_eq!(result, InteractionResult::Consume);
520        let remaining = {
521            let inventory = player.inventory.lock();
522            inventory
523                .get_item_in_hand(InteractionHand::MainHand)
524                .clone()
525        };
526        assert!(remaining.is(&vanilla_items::HONEY_BOTTLE));
527        assert_eq!(remaining.count(), 1);
528
529        let glass_bottle_count: i32 = {
530            let inventory = player.inventory.lock();
531            inventory
532                .items()
533                .iter()
534                .filter(|item| item.is(&vanilla_items::GLASS_BOTTLE))
535                .map(ItemStack::count)
536                .sum()
537        };
538        assert_eq!(
539            glass_bottle_count, 1,
540            "USE_REMAINDER glass bottle should land in the player's inventory"
541        );
542    }
543
544    /// Eating a food item must restore hunger/saturation by the exact
545    /// vanilla amounts from its `minecraft:food` component, applied as-is
546    /// (not recomputed from a modifier).
547    #[test]
548    fn eating_food_applies_its_nutrition_and_saturation() {
549        init_vanilla_registry();
550        let world = fresh_test_world("finish_consuming_food_applies_nutrition");
551        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
552        let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
553        player.set_client_loaded(true);
554        {
555            let mut food = player.food_data.lock();
556            food.food_level = 10;
557            food.saturation_level = 0.0;
558        }
559
560        let stack = ItemStack::new(&vanilla_items::APPLE);
561        let _ = finish_consuming_stack(&stack, &world, player.as_ref());
562
563        let food = player.food_data.lock();
564        // Vanilla apple: nutrition 4, saturation 2.4.
565        assert_eq!(food.food_level, 14);
566        assert!((food.saturation_level - 2.4).abs() < f32::EPSILON);
567    }
568
569    /// Consuming an item must award one count of `Stats.ITEM_USED` for that
570    /// item, matching vanilla `Consumable.onConsume`.
571    #[test]
572    fn consuming_an_item_awards_the_item_used_stat() {
573        init_vanilla_registry();
574        let world = fresh_test_world("finish_consuming_awards_item_used_stat");
575        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
576        let player = TestPlayerBuilder::new(world.clone(), "Test", 1).build();
577        player.set_client_loaded(true);
578
579        let stack = ItemStack::new(&vanilla_items::APPLE);
580        let _ = finish_consuming_stack(&stack, &world, player.as_ref());
581
582        let apple_used = vanilla_stat_types::ITEM_USED.get(&vanilla_items::APPLE);
583        assert_eq!(
584            player
585                .stats()
586                .into_iter()
587                .find(|(stat, _)| *stat == apple_used)
588                .map(|(_, count)| count),
589            Some(1)
590        );
591    }
592}