1use 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
30pub trait ItemBehavior: Send + Sync {
37 #[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 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 fn use_on(&self, _context: &mut UseOnContext) -> InteractionResult {
54 InteractionResult::Pass
55 }
56
57 fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
59 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 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 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 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 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 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 fn use_on_release(&self, _stack: &ItemStack) -> bool {
168 false
169 }
170
171 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 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 fn get_item_damage_source(&self, _attacker: &dyn LivingEntity) -> Option<DamageSource> {
194 None
195 }
196
197 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 fn hurt_enemy(
210 &self,
211 _stack: &mut ItemStack,
212 _target: &dyn LivingEntity,
213 _attacker: &dyn LivingEntity,
214 ) {
215 }
216
217 fn post_hurt_enemy(
219 &self,
220 _stack: &mut ItemStack,
221 _target: &dyn LivingEntity,
222 _attacker: &dyn LivingEntity,
223 ) {
224 }
225
226 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 #[must_use]
236 fn can_fit_inside_container_items(&self) -> bool {
237 true
238 }
239
240 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
253fn emit_consume_particles_and_sounds(consumable: &Consumable, user: &dyn LivingEntity) {
255 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
268pub(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 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 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 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
336pub(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
361pub struct ItemBehaviorRegistry {
366 behaviors: Vec<Box<dyn ItemBehavior>>,
367}
368
369impl ItemBehaviorRegistry {
370 #[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 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 #[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 #[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 #[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 #[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 {
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 #[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 #[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 assert_eq!(food.food_level, 14);
566 assert!((food.saturation_level - 2.4).abs() < f32::EPSILON);
567 }
568
569 #[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}