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, Player, REGISTRY, SUseItem, UseOnContext, World,
4    wrap_degrees,
5};
6
7/// Handles using an item on a block.
8///
9/// This implements the logic from Java's `ServerPlayerGameMode.useItemOn()`.
10///
11/// # Flow
12/// 1. Spectator mode: Only allow opening menus (currently returns Pass)
13/// 2. Check if block interaction should be suppressed (sneaking + holding items)
14/// 3. If not suppressed: Call block's `use_item_on` method
15/// 4. If block returns `TryEmptyHandInteraction` and main hand: Call block's `use_without_item`
16/// 5. If item not empty: Call item behavior's `use_on` for placement
17/// 6. Handle creative mode infinite materials
18pub fn use_item_on(
19    player: &Player,
20    world: &Arc<World>,
21    hand: InteractionHand,
22    hit_result: &BlockHitResult,
23) -> InteractionResult {
24    let pos = hit_result.block_pos;
25    let state = world.get_block_state(pos);
26
27    // Spectator mode: can only open menus
28    // TODO: Implement menu providers for blocks like chests
29    if player.game_mode() == GameType::Spectator {
30        return InteractionResult::Pass;
31    }
32
33    // Check if block interaction should be suppressed (sneaking + holding items in either hand)
34    let have_something = {
35        let inv = player.inventory.lock();
36        !inv.get_item_in_hand(InteractionHand::MainHand).is_empty()
37            || !inv.get_item_in_hand(InteractionHand::OffHand).is_empty()
38    };
39
40    let suppress_block_use = player.is_secondary_use_active() && have_something;
41
42    // Get behavior registries
43    let block_behaviors = &*BLOCK_BEHAVIORS;
44    let item_behaviors = &*ITEM_BEHAVIORS;
45
46    // Try block interaction first (if not suppressed).
47    // No inventory lock held — block behaviors may need inventory access (e.g. opening chests).
48    if !suppress_block_use {
49        let Some(block) = REGISTRY.blocks.by_state_id(state) else {
50            return InteractionResult::Pass;
51        };
52        let behavior = block_behaviors.get_behavior(block);
53
54        let mut inventory_access = InventoryAccess::new(player.inventory.clone(), hand);
55
56        let block_result = behavior.use_item_on(
57            state,
58            world,
59            pos,
60            player,
61            hand,
62            hit_result,
63            &mut inventory_access,
64        );
65
66        if block_result.consumes_action() {
67            return block_result;
68        }
69
70        if matches!(block_result, InteractionResult::TryEmptyHandInteraction)
71            && hand == InteractionHand::MainHand
72        {
73            let empty_result = behavior.use_without_item(
74                state,
75                world,
76                pos,
77                player,
78                hit_result,
79                &mut inventory_access,
80            );
81
82            if empty_result.consumes_action() {
83                return empty_result;
84            }
85        }
86    }
87
88    let inventory_access = InventoryAccess::new(player.inventory.clone(), hand);
89    let (is_empty, original_count, item_ref, stack_before_use) =
90        inventory_access.with_item(|item| (item.is_empty(), item.count, item.item, item.clone()));
91
92    if !is_empty {
93        if player.is_item_on_cooldown(&stack_before_use) {
94            return InteractionResult::Pass;
95        }
96
97        let mut context = UseOnContext::new(
98            player,
99            hand,
100            hit_result.clone(),
101            world,
102            player.inventory.clone(),
103        );
104        let item_behavior = item_behaviors.get_behavior(item_ref);
105        let result = item_behavior.use_on(&mut context);
106
107        // Restored in both directions: `use_on` can also grow the held stack when
108        // its result merges back into the slot it came from.
109        if player.has_infinite_materials() {
110            context.inv.with_item(|item| item.count = original_count);
111        }
112
113        return result;
114    }
115
116    InteractionResult::Pass
117}
118
119/// Handles using an item (general usage like right-clicking air).
120///
121/// This implements logic similar to `ServerPlayerGameMode.useItem()`.
122pub fn use_item(player: &Player, world: &Arc<World>, hand: InteractionHand) -> InteractionResult {
123    // Spectator mode: can only open menus
124    if player.game_mode() == GameType::Spectator {
125        return InteractionResult::Pass;
126    }
127
128    let inventory_access = InventoryAccess::new(player.inventory.clone(), hand);
129    let (is_empty, item_ref, stack_before_use) =
130        inventory_access.with_item(|item| (item.is_empty(), item.item, item.clone()));
131
132    if !is_empty {
133        if player.is_item_on_cooldown(&stack_before_use) {
134            return InteractionResult::Pass;
135        }
136
137        let mut context =
138            crate::behavior::UseItemContext::new(player, hand, world, player.inventory.clone());
139
140        // Get behavior registries
141        let item_behaviors = &*ITEM_BEHAVIORS;
142        let item_behavior = item_behaviors.get_behavior(item_ref);
143        let is_instantly_used = item_behavior.get_use_duration(&stack_before_use, player) <= 0;
144
145        let result = item_behavior.use_item(&mut context);
146
147        if is_instantly_used && result.should_apply_item_use_side_effects() {
148            player.apply_item_use_cooldown(&stack_before_use);
149        }
150
151        return result;
152    }
153
154    InteractionResult::Pass
155}
156
157impl Player {
158    /// Handles the use of an item.
159    pub fn handle_use_item(&self, packet: SUseItem) {
160        if !self.has_client_loaded() {
161            return;
162        }
163
164        self.reset_last_action_time();
165
166        log::debug!(
167            "Player {} used {:?} (sequence: {}, yaw: {}, pitch: {})",
168            self.gameprofile.name,
169            packet.hand,
170            packet.sequence,
171            packet.y_rot,
172            packet.x_rot
173        );
174
175        self.ack_block_changes_up_to(packet.sequence);
176
177        let item_stack_is_empty = {
178            let inventory = self.inventory.lock();
179            inventory.get_item_in_hand(packet.hand).is_empty()
180        };
181        if item_stack_is_empty {
182            return;
183        }
184
185        let current_rotation = self.rotation();
186        // Vanilla entity setters discard each non-finite rotation component independently.
187        let target_component = |value: f32, current: f32| {
188            if value.is_finite() {
189                wrap_degrees(value)
190            } else {
191                current
192            }
193        };
194        let target_rotation = (
195            target_component(packet.y_rot, current_rotation.0),
196            target_component(packet.x_rot, current_rotation.1),
197        );
198        if target_rotation != current_rotation {
199            self.set_rotation(target_rotation);
200        }
201
202        let world = self.get_world();
203        let result = use_item(self, &world, packet.hand);
204
205        if result.should_swing_server() {
206            self.swing(packet.hand, true);
207        }
208
209        self.broadcast_inventory_changes();
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::use_item;
216    use crate::behavior::{InteractionResult, init_behaviors};
217    use crate::entity::Entity as _;
218    use crate::player::connection::NetworkConnection as _;
219    use crate::test_support::{TestPlayerBuilder, fresh_test_world};
220    use steel_protocol::packets::game::SUseItem;
221    use steel_registry::{item_stack::ItemStack, vanilla_items};
222    use steel_utils::types::InteractionHand;
223
224    #[test]
225    fn use_item_discards_non_finite_rotation_components() {
226        let world = fresh_test_world("use_item_non_finite_rotation");
227        init_behaviors();
228        let player = TestPlayerBuilder::new(world, "TestPlayer", 1).build();
229        player.set_client_loaded(true);
230        player
231            .inventory
232            .lock()
233            .set_selected_item(ItemStack::new(&vanilla_items::STICK));
234        player.set_rotation((10.0, 20.0));
235
236        for (sequence, y_rot, x_rot, expected) in [
237            (1, f32::NAN, 30.0, (10.0, 30.0)),
238            (2, 40.0, f32::INFINITY, (40.0, 30.0)),
239            (3, f32::NEG_INFINITY, f32::NAN, (40.0, 30.0)),
240        ] {
241            player.handle_use_item(SUseItem {
242                hand: InteractionHand::MainHand,
243                sequence,
244                y_rot,
245                x_rot,
246            });
247            assert_eq!(player.rotation(), expected);
248        }
249        assert!(!player.connection.closed());
250    }
251
252    /// A player at full hunger cannot start eating a normal food item —
253    /// vanilla `Consumable.canConsume` fails and returns `Fail` without
254    /// starting active use.
255    #[test]
256    fn use_item_refuses_normal_food_at_full_hunger() {
257        let world = fresh_test_world("use_item_full_hunger_normal_food");
258        init_behaviors();
259        let player = TestPlayerBuilder::new(world.clone(), "TestPlayer", 1).build();
260        player.set_client_loaded(true);
261        player
262            .inventory
263            .lock()
264            .set_selected_item(ItemStack::new(&vanilla_items::APPLE));
265
266        let result = use_item(&player, &world, InteractionHand::MainHand);
267
268        assert_eq!(result, InteractionResult::Fail);
269        assert_eq!(player.active_item_use_hand(), None);
270    }
271
272    /// An always-edible food item (e.g. golden apple) can still be eaten at
273    /// full hunger, matching vanilla `FoodProperties.canAlwaysEat`.
274    #[test]
275    fn use_item_allows_always_edible_food_at_full_hunger() {
276        let world = fresh_test_world("use_item_full_hunger_always_edible_food");
277        init_behaviors();
278        let player = TestPlayerBuilder::new(world.clone(), "TestPlayer", 1).build();
279        player.set_client_loaded(true);
280        player
281            .inventory
282            .lock()
283            .set_selected_item(ItemStack::new(&vanilla_items::GOLDEN_APPLE));
284
285        let result = use_item(&player, &world, InteractionHand::MainHand);
286
287        assert_eq!(result, InteractionResult::Consume);
288        assert_eq!(
289            player.active_item_use_hand(),
290            Some(InteractionHand::MainHand)
291        );
292    }
293}