Skip to main content

steel_core/player/game_mode/
item_interaction.rs

1use super::{
2    Arc, BLOCK_BEHAVIORS, BlockHitResult, Entity, GameType, ITEM_BEHAVIORS, InteractionHand,
3    InteractionResult, InventoryAccess, LivingEntity, Player, REGISTRY, SUseItem, UseOnContext,
4    World, adventure_mode, wrap_degrees,
5};
6use steel_registry::data_components::vanilla_components::USE_REMAINDER;
7use steel_registry::stat::vanilla_stat_types;
8
9/// Handles using an item on a block.
10///
11/// This implements the logic from Java's `ServerPlayerGameMode.useItemOn()`.
12///
13/// # Flow
14/// 1. Spectator mode: Only allow opening menus (currently returns Pass)
15/// 2. Check if block interaction should be suppressed (sneaking + holding items)
16/// 3. If not suppressed: Call block's `use_item_on` method
17/// 4. If block returns `TryEmptyHandInteraction` and main hand: Call block's `use_without_item`
18/// 5. If item not empty: Call item behavior's `use_on` for placement
19/// 6. Handle creative mode infinite materials
20pub fn use_item_on(
21    player: &Arc<Player>,
22    world: &Arc<World>,
23    hand: InteractionHand,
24    hit_result: &BlockHitResult,
25) -> InteractionResult {
26    let pos = hit_result.block_pos;
27    let state = world.get_block_state(pos);
28
29    // Spectator mode: can only open menus
30    // TODO: Implement menu providers for blocks like chests
31    if player.game_mode() == GameType::Spectator {
32        return InteractionResult::Pass;
33    }
34
35    // Check if block interaction should be suppressed (sneaking + holding items in either hand)
36    let have_something = {
37        let inv = player.inventory.lock();
38        !inv.get_item_in_hand(InteractionHand::MainHand).is_empty()
39            || !inv.get_item_in_hand(InteractionHand::OffHand).is_empty()
40    };
41
42    let suppress_block_use = player.is_secondary_use_active() && have_something;
43
44    // Get behavior registries
45    let block_behaviors = &*BLOCK_BEHAVIORS;
46    let item_behaviors = &*ITEM_BEHAVIORS;
47
48    // Try block interaction first (if not suppressed).
49    // No inventory lock held — block behaviors may need inventory access (e.g. opening chests).
50    if !suppress_block_use {
51        let Some(block) = REGISTRY.blocks.by_state_id(state) else {
52            return InteractionResult::Pass;
53        };
54        let behavior = block_behaviors.get_behavior(block);
55
56        let mut inventory_access = InventoryAccess::new(Arc::clone(&player.inventory), hand);
57
58        let block_result = behavior.use_item_on(
59            state,
60            world,
61            pos,
62            player,
63            hand,
64            hit_result,
65            &mut inventory_access,
66        );
67
68        if block_result.consumes_action() {
69            return block_result;
70        }
71
72        if matches!(block_result, InteractionResult::TryEmptyHandInteraction)
73            && hand == InteractionHand::MainHand
74        {
75            let empty_result = behavior.use_without_item(
76                state,
77                world,
78                pos,
79                player,
80                hit_result,
81                &mut inventory_access,
82            );
83
84            if empty_result.consumes_action() {
85                return empty_result;
86            }
87        }
88    }
89
90    let inventory_access = InventoryAccess::new(Arc::clone(&player.inventory), hand);
91    let (is_empty, original_count, item_ref, stack_before_use) =
92        inventory_access.with_item(|item| (item.is_empty(), item.count, item.item, item.clone()));
93
94    if !is_empty {
95        if player.is_item_on_cooldown(&stack_before_use) {
96            return InteractionResult::Pass;
97        }
98        if !player.abilities.lock().may_build
99            && !adventure_mode::can_place_on(&stack_before_use, world, pos)
100        {
101            return InteractionResult::Pass;
102        }
103
104        let mut context = UseOnContext::new(
105            player,
106            hand,
107            hit_result.clone(),
108            world,
109            Arc::clone(&player.inventory),
110        );
111        let item_behavior = item_behaviors.get_behavior(item_ref);
112        let result = item_behavior.use_on(&mut context);
113
114        if result.should_apply_item_use_side_effects() {
115            player.award_stat(&vanilla_stat_types::ITEM_USED, item_ref);
116
117            let extra = stack_before_use.get(USE_REMAINDER).and_then(|remainder| {
118                context.inv.with_item(|item| {
119                    remainder.convert_into_remainder(
120                        item,
121                        stack_before_use.count(),
122                        player.has_infinite_materials(),
123                    )
124                })
125            });
126
127            if let Some(extra) = extra {
128                player.handle_extra_items_created_on_use(extra);
129            }
130
131            player.apply_item_use_cooldown(&stack_before_use);
132        }
133
134        // Restored in both directions: `use_on` can also grow the held stack when
135        // its result merges back into the slot it came from.
136        if player.has_infinite_materials() {
137            context.inv.with_item(|item| item.count = original_count);
138        }
139
140        return result;
141    }
142
143    InteractionResult::Pass
144}
145
146#[cfg(test)]
147#[path = "item_interaction/use_on_tests.rs"]
148mod use_on_tests;
149
150/// Handles using an item (general usage like right-clicking air).
151///
152/// This implements logic similar to `ServerPlayerGameMode.useItem()`.
153pub fn use_item(
154    player: &Arc<Player>,
155    world: &Arc<World>,
156    hand: InteractionHand,
157) -> InteractionResult {
158    // Spectator mode: can only open menus
159    if player.game_mode() == GameType::Spectator {
160        return InteractionResult::Pass;
161    }
162
163    let inventory_access = InventoryAccess::new(Arc::clone(&player.inventory), hand);
164    let (is_empty, item_ref, stack_before_use) =
165        inventory_access.with_item(|item| (item.is_empty(), item.item, item.clone()));
166
167    if !is_empty {
168        if player.is_item_on_cooldown(&stack_before_use) {
169            return InteractionResult::Pass;
170        }
171
172        let mut context = crate::behavior::UseItemContext::new(
173            player,
174            hand,
175            world,
176            Arc::clone(&player.inventory),
177        );
178
179        // Get behavior registries
180        let item_behaviors = &*ITEM_BEHAVIORS;
181        let item_behavior = item_behaviors.get_behavior(item_ref);
182        let is_instantly_used =
183            item_behavior.get_use_duration(&stack_before_use, player.as_ref()) <= 0;
184
185        let result = item_behavior.use_item(&mut context);
186
187        if is_instantly_used && result.should_apply_item_use_side_effects() {
188            player.apply_item_use_cooldown(&stack_before_use);
189        }
190
191        return result;
192    }
193
194    InteractionResult::Pass
195}
196
197impl Player {
198    /// Handles the use of an item.
199    pub fn handle_use_item(self: &Arc<Self>, packet: SUseItem) {
200        if !self.has_client_loaded() {
201            return;
202        }
203
204        self.reset_last_action_time();
205
206        log::debug!(
207            "Player {} used {:?} (sequence: {}, yaw: {}, pitch: {})",
208            self.gameprofile.name(),
209            packet.hand,
210            packet.sequence,
211            packet.y_rot,
212            packet.x_rot
213        );
214
215        self.ack_block_changes_up_to(packet.sequence);
216
217        let item_stack_is_empty = {
218            let inventory = self.inventory.lock();
219            inventory.get_item_in_hand(packet.hand).is_empty()
220        };
221        if item_stack_is_empty {
222            return;
223        }
224
225        let current_rotation = self.rotation();
226        // Vanilla entity setters discard each non-finite rotation component independently.
227        let target_component = |value: f32, current: f32| {
228            if value.is_finite() {
229                wrap_degrees(value)
230            } else {
231                current
232            }
233        };
234        let target_rotation = (
235            target_component(packet.y_rot, current_rotation.0),
236            target_component(packet.x_rot, current_rotation.1),
237        );
238        if target_rotation != current_rotation {
239            self.set_rotation(target_rotation);
240        }
241
242        let world = self.get_world();
243        let animation = self.interact_animation(packet.hand);
244        let result = use_item(self, &world, packet.hand);
245        self.swing_after_interaction(packet.hand, animation, result);
246
247        self.broadcast_inventory_changes();
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use std::sync::Arc;
254
255    use super::use_item;
256    use crate::behavior::{InteractionResult, init_behaviors};
257    use crate::entity::Entity as _;
258    use crate::player::connection::NetworkConnection as _;
259    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
260    use glam::DVec3;
261    use steel_protocol::packets::game::SUseItem;
262    use steel_registry::{
263        RegistryHolderSet,
264        blocks::properties::Direction,
265        data_component_predicate::DataComponentMatchers,
266        data_components::{
267            AdventureModePredicate, BlockPredicate, vanilla_components::CAN_PLACE_ON,
268        },
269        item_stack::ItemStack,
270        items::item::BlockHitResult,
271        vanilla_blocks, vanilla_items,
272    };
273    use steel_utils::{
274        BlockPos, ChunkPos,
275        types::{GameType, InteractionHand, UpdateFlags},
276    };
277
278    use super::use_item_on;
279
280    #[test]
281    fn use_item_discards_non_finite_rotation_components() {
282        let world_fixture = fresh_test_world("use_item_non_finite_rotation");
283        let world = &world_fixture.world;
284        init_behaviors();
285        let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
286        player.set_client_loaded(true);
287        player
288            .inventory
289            .lock()
290            .set_selected_item(ItemStack::new(&vanilla_items::STICK));
291        player.set_rotation((10.0, 20.0));
292
293        for (sequence, y_rot, x_rot, expected) in [
294            (1, f32::NAN, 30.0, (10.0, 30.0)),
295            (2, 40.0, f32::INFINITY, (40.0, 30.0)),
296            (3, f32::NEG_INFINITY, f32::NAN, (40.0, 30.0)),
297        ] {
298            player.handle_use_item(SUseItem {
299                hand: InteractionHand::MainHand,
300                sequence,
301                y_rot,
302                x_rot,
303            });
304            assert_eq!(player.rotation(), expected);
305        }
306        assert!(!player.connection.closed());
307    }
308
309    #[test]
310    fn adventure_mode_requires_can_place_on_before_item_use() {
311        let world_fixture = fresh_test_world("adventure_mode_can_place_on");
312        let world = &world_fixture.world;
313        init_behaviors();
314        let pos = BlockPos::new(1, 64, 0);
315        insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
316        assert!(world.set_block(
317            pos,
318            vanilla_blocks::STONE.default_state(),
319            UpdateFlags::UPDATE_ALL,
320        ));
321
322        let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
323        player.restore_game_modes(GameType::Adventure, None);
324        player
325            .abilities
326            .lock()
327            .update_for_game_mode(GameType::Adventure);
328
329        let hit_result = BlockHitResult {
330            location: DVec3::new(1.5, 64.5, 0.5),
331            direction: Direction::Up,
332            block_pos: pos,
333            miss: false,
334            inside: false,
335            world_border_hit: false,
336        };
337
338        player
339            .inventory
340            .lock()
341            .set_selected_item(ItemStack::new(&vanilla_items::DIRT));
342        assert!(
343            !use_item_on(&player, world, InteractionHand::MainHand, &hit_result).consumes_action()
344        );
345        assert_eq!(
346            world.get_block_state(pos.above()),
347            vanilla_blocks::AIR.default_state()
348        );
349        assert_eq!(player.inventory.lock().get_selected_item().count(), 1);
350
351        let predicate = BlockPredicate::new(
352            Some(RegistryHolderSet::Direct(vec![&vanilla_blocks::STONE])),
353            None,
354            None,
355            DataComponentMatchers::ANY,
356        );
357        let can_place_on =
358            AdventureModePredicate::new(vec![predicate]).expect("one block predicate is valid");
359        let mut dirt = ItemStack::new(&vanilla_items::DIRT);
360        dirt.set(CAN_PLACE_ON, can_place_on);
361        player.inventory.lock().set_selected_item(dirt);
362
363        assert!(
364            use_item_on(&player, world, InteractionHand::MainHand, &hit_result).consumes_action()
365        );
366        assert_eq!(
367            world.get_block_state(pos.above()),
368            vanilla_blocks::DIRT.default_state()
369        );
370        assert_eq!(player.inventory.lock().get_selected_item().count(), 0);
371    }
372
373    /// A player at full hunger cannot start eating a normal food item —
374    /// vanilla `Consumable.canConsume` fails and returns `Fail` without
375    /// starting active use.
376    #[test]
377    fn use_item_refuses_normal_food_at_full_hunger() {
378        let world_fixture = fresh_test_world("use_item_full_hunger_normal_food");
379        let world = &world_fixture.world;
380        init_behaviors();
381        let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
382        player.set_client_loaded(true);
383        player
384            .inventory
385            .lock()
386            .set_selected_item(ItemStack::new(&vanilla_items::APPLE));
387
388        let result = use_item(&player, world, InteractionHand::MainHand);
389
390        assert_eq!(result, InteractionResult::Fail);
391        assert_eq!(player.active_item_use_hand(), None);
392    }
393
394    /// An always-edible food item (e.g. golden apple) can still be eaten at
395    /// full hunger, matching vanilla `FoodProperties.canAlwaysEat`.
396    #[test]
397    fn use_item_allows_always_edible_food_at_full_hunger() {
398        let world_fixture = fresh_test_world("use_item_full_hunger_always_edible_food");
399        let world = &world_fixture.world;
400        init_behaviors();
401        let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
402        player.set_client_loaded(true);
403        player
404            .inventory
405            .lock()
406            .set_selected_item(ItemStack::new(&vanilla_items::GOLDEN_APPLE));
407
408        let result = use_item(&player, world, InteractionHand::MainHand);
409
410        assert_eq!(result, InteractionResult::Consume);
411        assert_eq!(
412            player.active_item_use_hand(),
413            Some(InteractionHand::MainHand)
414        );
415    }
416}