1use crate::player::player_inventory::Prediction;
4use std::sync::Arc;
5
6use std::borrow::Cow;
7use steel_protocol::packets::game::SoundSource;
8use steel_registry::data_components::Consumable;
9use steel_registry::data_components::vanilla_components::{
10 BLOCKS_ATTACKS, CONSUMABLE, FOOD, KINETIC_WEAPON, USE_REMAINDER,
11};
12
13use steel_registry::data_components::vanilla_components::ITEM_NAME;
14use steel_registry::item_stack::ItemStack;
15use steel_registry::items::ItemRef;
16use steel_registry::stat::vanilla_stat_types;
17use steel_registry::{REGISTRY, RegistryEntry, RegistryExt, sound_events, vanilla_game_events};
18use steel_utils::types::{InteractionHand, SignTextSlot};
19use text_components::TextComponent;
20
21use crate::behavior::items::{DefaultItemBehavior, SpawnEggItem, block_transformer};
22use crate::behavior::{InteractionResult, InventoryTickContext, UseItemContext, UseOnContext};
23use crate::block_entity::entities::{SignBlockEntity, SignText};
24use crate::entity::consume_effect::apply_consume_effect;
25use crate::entity::damage::DamageSource;
26use crate::entity::{Entity, LivingEntity, LivingEntityRef, SharedEntity};
27use crate::player::{Player, player_inventory::EquipmentSwapResult};
28use crate::world::World;
29
30pub use steel_registry::data_components::vanilla_components::ItemUseAnimation;
31
32pub trait SignApplicator: Send + Sync {
34 fn can_apply_to_sign(&self, text: &SignText, _item: &ItemStack, _player: &Player) -> bool {
36 text.has_message()
37 }
38
39 fn try_apply_to_sign(
41 &self,
42 world: &World,
43 sign: &SignBlockEntity,
44 slot: SignTextSlot,
45 item: &ItemStack,
46 player: &Player,
47 ) -> bool;
48}
49
50pub trait ItemBehavior: Send + Sync {
57 #[cfg(feature = "test-framework")]
59 #[must_use]
60 #[expect(clippy::absolute_paths, reason = "easier for features")]
61 fn type_name(&self) -> &'static str {
62 std::any::type_name::<Self>()
63 }
64
65 fn get_name<'a>(&self, stack: &'a ItemStack) -> Cow<'a, TextComponent> {
68 stack
69 .get(ITEM_NAME)
70 .map_or_else(|| Cow::Owned(TextComponent::new()), Cow::Borrowed)
71 }
72
73 fn get_default_instance(&self, item: ItemRef) -> ItemStack {
76 ItemStack::new(item)
77 }
78
79 fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
81 block_transformer::use_on(context)
82 }
83
84 fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
86 let (consume_ticks, can_eat) = context.inv.with_item(|item| {
89 let can_eat = item
90 .get(FOOD)
91 .is_none_or(|food| context.player.can_eat(food.can_always_eat()));
92 (item.get(CONSUMABLE).map(Consumable::consume_ticks), can_eat)
93 });
94 if let Some(consume_ticks) = consume_ticks {
95 if !can_eat {
96 return InteractionResult::Fail;
97 }
98 if consume_ticks > 0 {
104 context.player.start_using_item(context.hand);
105 } else {
106 let stack = context.inv.with_item(|item| item.clone());
107 let result = finish_consuming_stack(&stack, context.world, context.player.as_ref());
108 context.inv.with_item(|item| *item = result);
109 }
110 return InteractionResult::Consume;
111 }
112
113 let Some(equippable) = context.inv.with_item(|item| item.get_equippable().cloned()) else {
114 return InteractionResult::Pass;
115 };
116
117 if !equippable.swappable || !equippable.can_be_equipped_by(context.player.entity_type()) {
118 return InteractionResult::Pass;
119 }
120
121 let slot = equippable.slot;
122 let equipped = context.inv.with_item(|item| item.copy_with_count(1));
123 let result = context.inv.with_inventory(|inventory| {
124 inventory.try_swap_with_equipment_slot(
125 context.hand,
126 slot,
127 context.player.has_infinite_materials(),
128 )
129 });
130
131 match result {
132 EquipmentSwapResult::Success(overflow) => {
133 if !overflow.is_empty() {
134 let _ = context
135 .player
136 .drop_item(overflow, false, Prediction::Predicted);
137 }
138 context
139 .player
140 .on_equip_item(slot, &ItemStack::empty(), &equipped);
141 InteractionResult::Success
142 }
143 EquipmentSwapResult::Fail => InteractionResult::Fail,
144 }
145 }
146
147 fn get_use_animation(&self, stack: &ItemStack) -> ItemUseAnimation {
150 if let Some(consumable) = stack.get(CONSUMABLE) {
151 consumable.animation()
152 } else if stack.has(BLOCKS_ATTACKS) {
153 ItemUseAnimation::Block
154 } else if stack.has(KINETIC_WEAPON) {
155 ItemUseAnimation::Spear
156 } else {
157 ItemUseAnimation::None
158 }
159 }
160
161 fn get_use_duration(&self, stack: &ItemStack, _user: &dyn LivingEntity) -> i32 {
164 if let Some(consumable) = stack.get(CONSUMABLE) {
165 consumable.consume_ticks()
166 } else if stack.has(BLOCKS_ATTACKS) || stack.has(KINETIC_WEAPON) {
167 72000
168 } else {
169 0
170 }
171 }
172
173 fn on_use_tick(
175 &self,
176 _world: &Arc<World>,
177 user: LivingEntityRef<'_>,
178 stack: &mut ItemStack,
179 ticks_remaining: i32,
180 ) {
181 if let Some(consumable) = stack.get(CONSUMABLE)
182 && should_emit_consume_particles_and_sounds(consumable, ticks_remaining)
183 {
184 emit_consume_particles_and_sounds(consumable, user.living());
185 }
186 }
187
188 fn release_using(
192 &self,
193 _stack: &mut ItemStack,
194 _world: &Arc<World>,
195 _user: &dyn LivingEntity,
196 _time_left: i32,
197 ) -> bool {
198 false
199 }
200
201 fn use_on_release(&self, _stack: &ItemStack) -> bool {
203 false
204 }
205
206 fn finish_using(
208 &self,
209 stack: &mut ItemStack,
210 world: &Arc<World>,
211 user: LivingEntityRef<'_>,
212 ) -> ItemStack {
213 finish_consuming_stack(stack, world, user.living())
214 }
215
216 fn interact_living_entity(
219 &self,
220 _stack: &mut ItemStack,
221 _player: &Player,
222 _target: &dyn LivingEntity,
223 _hand: InteractionHand,
224 ) -> InteractionResult {
225 InteractionResult::Pass
226 }
227
228 fn get_item_damage_source(&self, _attacker: &SharedEntity) -> Option<DamageSource> {
231 None
232 }
233
234 fn get_attack_damage_bonus(
236 &self,
237 _attacker: &dyn LivingEntity,
238 _victim: &dyn Entity,
239 _base_damage: f32,
240 _damage_source: &DamageSource,
241 ) -> f32 {
242 0.0
243 }
244
245 fn hurt_enemy(
248 &self,
249 _stack: &mut ItemStack,
250 _target: &dyn LivingEntity,
251 _attacker: &dyn LivingEntity,
252 ) {
253 }
254
255 fn post_hurt_enemy(
258 &self,
259 _stack: &mut ItemStack,
260 _target: &dyn LivingEntity,
261 _attacker: &dyn LivingEntity,
262 ) {
263 }
264
265 fn inventory_tick(&self, _context: &mut InventoryTickContext<'_>) {}
267
268 fn item_damage_per_attack(&self, stack: &ItemStack) -> Option<i32> {
270 stack
271 .get_weapon()
272 .map(|weapon| weapon.item_damage_per_attack)
273 }
274
275 #[must_use]
278 fn can_fit_inside_container_items(&self) -> bool {
279 true
280 }
281
282 fn as_spawn_egg(&self) -> Option<&SpawnEggItem> {
284 None
285 }
286
287 fn as_sign_applicator(&self) -> Option<&dyn SignApplicator> {
289 None
290 }
291}
292
293fn should_emit_consume_particles_and_sounds(consumable: &Consumable, ticks_remaining: i32) -> bool {
294 let consume_ticks = consumable.consume_ticks();
295 let ticks_used = consume_ticks - ticks_remaining;
296 let wait_ticks = (consume_ticks as f32 * 0.21875) as i32;
297 ticks_used > wait_ticks && ticks_remaining % 4 == 0
298}
299
300fn emit_consume_particles_and_sounds(consumable: &Consumable, user: &dyn LivingEntity) {
301 let (volume, pitch) = if consumable.animation() == ItemUseAnimation::Drink {
303 (0.5, 0.9 + rand::random::<f32>() * 0.1)
304 } else {
305 let volume = if rand::random::<bool>() { 0.5 } else { 1.0 };
306 let pitch = 1.0 + 0.2 * (rand::random::<f32>() - rand::random::<f32>());
307 (volume, pitch)
308 };
309 if let Some(sound) = consumable.sound().registry_ref() {
310 user.play_sound(sound, volume, pitch);
311 }
312}
313
314pub(crate) fn finish_consuming_stack(
318 stack: &ItemStack,
319 world: &Arc<World>,
320 user: &dyn LivingEntity,
321) -> ItemStack {
322 let Some(consumable) = stack.get(CONSUMABLE) else {
323 return apply_use_remainder(stack, stack.copy_with_count(stack.count()), user);
324 };
325
326 emit_consume_particles_and_sounds(consumable, user);
327
328 if let Some(food) = stack.get(FOOD) {
329 if let Some(sound) = consumable.sound().registry_ref() {
330 world.play_sound_at(
331 sound,
332 SoundSource::Neutral,
333 user.position(),
334 1.0,
335 1.0 + 0.4 * (rand::random::<f32>() - rand::random::<f32>()),
337 None,
338 );
339 }
340 if let Some(player) = user.as_player() {
341 player
342 .food_data
343 .lock()
344 .add_food(food.nutrition(), food.saturation());
345 world.play_sound_at(
346 &sound_events::ENTITY_PLAYER_BURP,
347 SoundSource::Players,
348 player.position(),
349 0.5,
350 0.9 + rand::random::<f32>() * 0.1,
352 None,
353 );
354 }
355 }
356
357 for effect in consumable.on_consume_effects() {
358 apply_consume_effect(effect, world, user);
359 }
360
361 let event = if consumable.animation() == ItemUseAnimation::Drink {
362 &vanilla_game_events::DRINK
363 } else {
364 &vanilla_game_events::EAT
365 };
366 user.game_event(event);
367 if let Some(player) = user.as_player() {
368 player.award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
369 }
370 let mut used_stack = stack.copy_with_count(stack.count());
375 if !user.has_infinite_materials() {
376 used_stack.shrink(1);
377 }
378
379 apply_use_remainder(stack, used_stack, user)
380}
381
382pub(crate) fn apply_use_remainder(
387 original_stack: &ItemStack,
388 mut used_stack: ItemStack,
389 user: &dyn LivingEntity,
390) -> ItemStack {
391 let Some(remainder) = original_stack.get(USE_REMAINDER) else {
392 return used_stack;
393 };
394 if let Some(extra) = remainder.convert_into_remainder(
395 &mut used_stack,
396 original_stack.count(),
397 user.has_infinite_materials(),
398 ) {
399 user.handle_extra_items_created_on_use(extra);
400 }
401 used_stack
402}
403
404pub struct ItemBehaviorRegistry {
409 behaviors: Vec<Box<dyn ItemBehavior>>,
410}
411
412impl ItemBehaviorRegistry {
413 #[must_use]
417 pub fn new() -> Self {
418 let item_count = REGISTRY.items.len();
419 let behaviors = (0..item_count)
420 .map(|_| Box::new(DefaultItemBehavior) as Box<dyn ItemBehavior>)
421 .collect();
422
423 Self { behaviors }
424 }
425
426 pub fn set_behavior(&mut self, item: ItemRef, behavior: Box<dyn ItemBehavior>) {
428 let id = item.id();
429 self.behaviors[id] = behavior;
430 }
431
432 #[must_use]
434 pub fn get_behavior(&self, item: ItemRef) -> &dyn ItemBehavior {
435 let id = item.id();
436 self.behaviors[id].as_ref()
437 }
438
439 #[must_use]
442 pub fn default_instance(&self, item: ItemRef) -> ItemStack {
443 self.get_behavior(item).get_default_instance(item)
444 }
445
446 #[must_use]
449 pub fn hover_name<'a>(&self, stack: &'a ItemStack) -> Cow<'a, TextComponent> {
450 stack
451 .custom_name()
452 .unwrap_or_else(|| self.get_behavior(stack.item()).get_name(stack))
453 }
454
455 #[cfg(feature = "test-framework")]
457 #[must_use]
458 pub fn get_behaviors(&self) -> &[Box<dyn ItemBehavior>] {
459 &self.behaviors
460 }
461}
462
463impl Default for ItemBehaviorRegistry {
464 fn default() -> Self {
465 Self::new()
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use std::sync::Arc;
472
473 use steel_registry::data_components::{Consumable, vanilla_components};
474 use steel_registry::item_stack::ItemStack;
475 use steel_registry::stat::vanilla_stat_types;
476 use steel_registry::{init_vanilla_registry, vanilla_entities, vanilla_items, vanilla_potions};
477 use steel_utils::types::InteractionHand;
478 use steel_utils::{ChunkPos, Downcast as _, WorldAabb};
479
480 use super::finish_consuming_stack;
481 use crate::behavior::{ITEM_BEHAVIORS, InteractionResult, UseItemContext, init_behaviors};
482 use crate::entity::entities::ItemEntity;
483 use crate::inventory::container::Container as _;
484 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
485
486 #[test]
487 fn potion_items_default_to_their_vanilla_base_potion() {
488 init_vanilla_registry();
489 init_behaviors();
490
491 for item in [
492 &vanilla_items::POTION,
493 &vanilla_items::SPLASH_POTION,
494 &vanilla_items::LINGERING_POTION,
495 ] {
496 let stack = ITEM_BEHAVIORS.default_instance(item);
497 assert!(stack.is(item));
498 assert_eq!(stack.count(), 1);
499 let contents = stack
500 .get(vanilla_components::POTION_CONTENTS)
501 .expect("potion default instance carries potion contents");
502 assert!(contents.is(&vanilla_potions::WATER));
503 }
504
505 let arrow = ITEM_BEHAVIORS.default_instance(&vanilla_items::TIPPED_ARROW);
506 let contents = arrow
507 .get(vanilla_components::POTION_CONTENTS)
508 .expect("tipped arrow default instance carries potion contents");
509 assert!(contents.is(&vanilla_potions::POISON));
510 }
511
512 #[test]
513 fn items_without_an_override_default_to_a_plain_stack() {
514 init_vanilla_registry();
515 init_behaviors();
516
517 let stack = ITEM_BEHAVIORS.default_instance(&vanilla_items::STONE);
518 assert_eq!(stack, ItemStack::new(&vanilla_items::STONE));
519 }
520
521 #[test]
526 fn honey_bottle_stack_keeps_remaining_bottles_and_hands_off_the_remainder() {
527 init_vanilla_registry();
528 init_behaviors();
529 let world_fixture = fresh_test_world("finish_consuming_honey_bottle_stack");
530 let world = &world_fixture.world;
531 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
532 let player = TestPlayerBuilder::new(Arc::clone(world), "Test", 1).build();
533 player.set_client_loaded(true);
534
535 {
539 let mut inventory = player.inventory.lock();
540 for slot in 0..36 {
541 inventory.set_item(slot, ItemStack::with_count(&vanilla_items::STONE, 64));
542 }
543 }
544
545 let stack = ItemStack::with_count(&vanilla_items::HONEY_BOTTLE, 5);
546 let result = finish_consuming_stack(&stack, world, player.as_ref());
547
548 assert!(result.is(&vanilla_items::HONEY_BOTTLE));
549 assert_eq!(result.count(), 4);
550
551 let dropped = world.get_entities_in_aabb_matching(
552 &WorldAabb::new(-2.0, -1.0, -2.0, 2.0, 3.0, 2.0),
553 |entity| entity.entity_type() == &vanilla_entities::ITEM,
554 );
555 assert_eq!(dropped.len(), 1);
556 let Some(item) = dropped[0].as_ref().downcast_ref::<ItemEntity>() else {
557 panic!("dropped entity should retain its concrete item type");
558 };
559 assert!(item.get_item().is(&vanilla_items::GLASS_BOTTLE));
560 }
561
562 #[test]
570 fn instant_consumable_with_use_remainder_finishes_without_deadlocking() {
571 init_vanilla_registry();
572 init_behaviors();
573 let world_fixture = fresh_test_world("instant_consumable_no_deadlock");
574 let world = &world_fixture.world;
575 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
576 let player = TestPlayerBuilder::new(Arc::clone(world), "Test", 1).build();
577 player.set_client_loaded(true);
578
579 let mut stack = ItemStack::with_count(&vanilla_items::HONEY_BOTTLE, 2);
580 let existing = stack
581 .get(vanilla_components::CONSUMABLE)
582 .expect("honey bottle is consumable")
583 .clone();
584 let instant_consumable = Consumable::new(
585 0.0,
586 existing.animation(),
587 existing.sound().clone(),
588 existing.has_consume_particles(),
589 existing.on_consume_effects().to_vec(),
590 )
591 .expect("valid consumable");
592 stack.set(vanilla_components::CONSUMABLE, instant_consumable);
593
594 {
595 let mut inventory = player.inventory.lock();
596 inventory.set_item_in_hand(InteractionHand::MainHand, stack);
597 }
598
599 let behavior = ITEM_BEHAVIORS.get_behavior(&vanilla_items::HONEY_BOTTLE);
600 let mut context = UseItemContext::new(
601 &player,
602 InteractionHand::MainHand,
603 world,
604 Arc::clone(&player.inventory),
605 );
606
607 let result = behavior.use_item(&mut context);
608
609 assert_eq!(result, InteractionResult::Consume);
610 let remaining = {
611 let inventory = player.inventory.lock();
612 inventory
613 .get_item_in_hand(InteractionHand::MainHand)
614 .clone()
615 };
616 assert!(remaining.is(&vanilla_items::HONEY_BOTTLE));
617 assert_eq!(remaining.count(), 1);
618
619 let glass_bottle_count: i32 = {
620 let inventory = player.inventory.lock();
621 inventory
622 .items()
623 .iter()
624 .filter(|item| item.is(&vanilla_items::GLASS_BOTTLE))
625 .map(ItemStack::count)
626 .sum()
627 };
628 assert_eq!(
629 glass_bottle_count, 1,
630 "USE_REMAINDER glass bottle should land in the player's inventory"
631 );
632 }
633
634 #[test]
638 fn eating_food_applies_its_nutrition_and_saturation() {
639 init_vanilla_registry();
640 let world_fixture = fresh_test_world("finish_consuming_food_applies_nutrition");
641 let world = &world_fixture.world;
642 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
643 let player = TestPlayerBuilder::new(Arc::clone(world), "Test", 1).build();
644 player.set_client_loaded(true);
645 {
646 let mut food = player.food_data.lock();
647 food.food_level = 10;
648 food.saturation_level = 0.0;
649 }
650
651 let stack = ItemStack::new(&vanilla_items::APPLE);
652 let _ = finish_consuming_stack(&stack, world, player.as_ref());
653
654 let food = player.food_data.lock();
655 assert_eq!(food.food_level, 14);
657 assert!((food.saturation_level - 2.4).abs() < f32::EPSILON);
658 }
659
660 #[test]
663 fn consuming_an_item_awards_the_item_used_stat() {
664 init_vanilla_registry();
665 let world_fixture = fresh_test_world("finish_consuming_awards_item_used_stat");
666 let world = &world_fixture.world;
667 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
668 let player = TestPlayerBuilder::new(Arc::clone(world), "Test", 1).build();
669 player.set_client_loaded(true);
670
671 let stack = ItemStack::new(&vanilla_items::APPLE);
672 let _ = finish_consuming_stack(&stack, world, player.as_ref());
673
674 let apple_used = vanilla_stat_types::ITEM_USED.get(&vanilla_items::APPLE);
675 assert_eq!(
676 player
677 .stats()
678 .into_iter()
679 .find(|(stat, _)| *stat == apple_used)
680 .map(|(_, count)| count),
681 Some(1)
682 );
683 }
684}