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        // Restore count for creative mode (infinite materials)
108        if player.has_infinite_materials() {
109            context.inv.with_item(|item| {
110                if item.count < original_count {
111                    item.count = original_count;
112                }
113            });
114        }
115
116        return result;
117    }
118
119    InteractionResult::Pass
120}
121
122/// Handles using an item (general usage like right-clicking air).
123///
124/// This implements logic similar to `ServerPlayerGameMode.useItem()`.
125pub fn use_item(player: &Player, world: &Arc<World>, hand: InteractionHand) -> InteractionResult {
126    // Spectator mode: can only open menus
127    if player.game_mode() == GameType::Spectator {
128        return InteractionResult::Pass;
129    }
130
131    let inventory_access = InventoryAccess::new(player.inventory.clone(), hand);
132    let (is_empty, original_count, item_ref, stack_before_use) =
133        inventory_access.with_item(|item| (item.is_empty(), item.count, item.item, item.clone()));
134
135    if !is_empty {
136        if player.is_item_on_cooldown(&stack_before_use) {
137            return InteractionResult::Pass;
138        }
139
140        let mut context =
141            crate::behavior::UseItemContext::new(player, hand, world, player.inventory.clone());
142
143        // Get behavior registries
144        let item_behaviors = &*ITEM_BEHAVIORS;
145        let item_behavior = item_behaviors.get_behavior(item_ref);
146
147        let result = item_behavior.use_item(&mut context);
148
149        // Restore count for creative mode (infinite materials)
150        if player.has_infinite_materials() {
151            context.inv.with_item(|item| {
152                if item.count < original_count {
153                    item.count = original_count;
154                }
155            });
156        }
157
158        if result.should_apply_item_use_side_effects() {
159            player.apply_item_use_cooldown(&stack_before_use);
160        }
161
162        return result;
163    }
164
165    InteractionResult::Pass
166}
167
168impl Player {
169    /// Handles the use of an item.
170    pub fn handle_use_item(&self, packet: SUseItem) {
171        if !self.has_client_loaded() {
172            return;
173        }
174
175        log::debug!(
176            "Player {} used {:?} (sequence: {}, yaw: {}, pitch: {})",
177            self.gameprofile.name,
178            packet.hand,
179            packet.sequence,
180            packet.y_rot,
181            packet.x_rot
182        );
183
184        self.ack_block_changes_up_to(packet.sequence);
185
186        let item_stack_is_empty = {
187            let inventory = self.inventory.lock();
188            inventory.get_item_in_hand(packet.hand).is_empty()
189        };
190        if item_stack_is_empty {
191            return;
192        }
193
194        let target_yaw = wrap_degrees(packet.y_rot);
195        let target_pitch = wrap_degrees(packet.x_rot);
196        if self.rotation() != (target_yaw, target_pitch) {
197            self.set_rotation((target_yaw, target_pitch));
198        }
199
200        let world = self.get_world();
201        let result = use_item(self, &world, packet.hand);
202
203        if result.should_swing_server() {
204            self.swing(packet.hand, true);
205        }
206
207        self.broadcast_inventory_changes();
208    }
209}