steel_core/player/game_mode/
item_interaction.rs1use super::{
2 Arc, BLOCK_BEHAVIORS, BlockHitResult, Entity, GameType, ITEM_BEHAVIORS, InteractionHand,
3 InteractionResult, InventoryAccess, Player, REGISTRY, SUseItem, UseOnContext, World,
4 wrap_degrees,
5};
6
7pub 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 if player.game_mode() == GameType::Spectator {
30 return InteractionResult::Pass;
31 }
32
33 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 let block_behaviors = &*BLOCK_BEHAVIORS;
44 let item_behaviors = &*ITEM_BEHAVIORS;
45
46 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 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
119pub fn use_item(player: &Player, world: &Arc<World>, hand: InteractionHand) -> InteractionResult {
123 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 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 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 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 #[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 #[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}