Skip to main content

steel_core/player/game_mode/
block_interaction.rs

1use super::{block_breaking::BlockBreakAction, *};
2
3impl Player {
4    /// Sends block update packets for a position and its neighbor.
5    /// Optionally also sends an update for an additional placement position
6    /// (useful for items like buckets that place blocks at different positions).
7    fn send_block_updates(&self, pos: BlockPos, direction: Direction) {
8        let world = self.get_world();
9        let state = world.get_block_state(pos);
10        self.send_packet(CBlockUpdate {
11            pos,
12            block_state: state,
13        });
14
15        let neighbor_pos = direction.relative(pos);
16        let neighbor_state = world.get_block_state(neighbor_pos);
17        self.send_packet(CBlockUpdate {
18            pos: neighbor_pos,
19            block_state: neighbor_state,
20        });
21    }
22
23    /// Handles the use of an item on a block.
24    ///
25    /// Implements the logic from Java's `ServerGamePacketListenerImpl.handleUseItemOn()`.
26    pub fn handle_use_item_on(self: &Arc<Self>, packet: SUseItemOn) {
27        if !self.has_client_loaded() {
28            return;
29        }
30
31        self.ack_block_changes_up_to(packet.sequence);
32
33        let pos = packet.block_hit.block_pos;
34        let direction = packet.block_hit.direction;
35
36        if !self.is_within_block_interaction_range(pos) {
37            self.send_block_updates(pos, direction);
38            return;
39        }
40
41        let center_x = f64::from(pos.x()) + 0.5;
42        let center_y = f64::from(pos.y()) + 0.5;
43        let center_z = f64::from(pos.z()) + 0.5;
44        let location = &packet.block_hit.location;
45        let limit = 1.000_000_1;
46        let location_is_valid = (location.x - center_x).abs() < limit
47            && (location.y - center_y).abs() < limit
48            && (location.z - center_z).abs() < limit;
49
50        if !location_is_valid {
51            log::warn!(
52                "Rejecting UseItemOnPacket from {}: location {:?} too far from block {:?}",
53                self.gameprofile.name(),
54                location,
55                pos
56            );
57            self.send_block_updates(pos, direction);
58            return;
59        }
60
61        self.reset_last_action_time();
62
63        let world = self.get_world();
64
65        if pos.y() >= world.max_build_height() {
66            self.send_build_limit_too_high_message(world.get_max_y());
67            self.send_block_updates(pos, direction);
68            return;
69        }
70        if self.is_awaiting_teleport() {
71            self.send_block_updates(pos, direction);
72            return;
73        }
74
75        if !world.may_interact(self, pos) {
76            self.send_block_updates(pos, direction);
77            return;
78        }
79
80        let animation = self.interact_animation(packet.hand);
81        let result = use_item_on(self, &world, packet.hand, &packet.block_hit);
82        self.swing_after_interaction(packet.hand, animation, result);
83
84        self.send_block_updates(pos, direction);
85        self.broadcast_inventory_changes();
86    }
87
88    /// Handles a player action packet (block breaking, item dropping, etc.).
89    pub fn handle_player_action(self: &Arc<Self>, packet: SPlayerAction) {
90        if !self.has_client_loaded() {
91            return;
92        }
93
94        self.reset_last_action_time();
95
96        let world = self.get_world();
97        match packet.action {
98            PlayerAction::StartDestroyBlock => {
99                self.block_breaking.lock().handle_block_break_action(
100                    self,
101                    &world,
102                    packet.pos,
103                    BlockBreakAction::Start,
104                    packet.direction,
105                );
106                self.ack_block_changes_up_to(packet.sequence);
107            }
108            PlayerAction::ChangeDestroyDirection => {
109                self.block_breaking.lock().handle_block_break_action(
110                    self,
111                    &world,
112                    packet.pos,
113                    BlockBreakAction::ChangeDirection,
114                    packet.direction,
115                );
116                self.ack_block_changes_up_to(packet.sequence);
117            }
118            PlayerAction::StopDestroyBlock => {
119                self.block_breaking.lock().handle_block_break_action(
120                    self,
121                    &world,
122                    packet.pos,
123                    BlockBreakAction::Stop,
124                    packet.direction,
125                );
126                self.ack_block_changes_up_to(packet.sequence);
127            }
128            PlayerAction::AbortDestroyBlock => {
129                self.block_breaking.lock().handle_block_break_action(
130                    self,
131                    &world,
132                    packet.pos,
133                    BlockBreakAction::Abort,
134                    packet.direction,
135                );
136                self.ack_block_changes_up_to(packet.sequence);
137            }
138            PlayerAction::DropAllItems => {
139                self.drop_from_selected(true);
140            }
141            PlayerAction::DropItem => {
142                self.drop_from_selected(false);
143            }
144            PlayerAction::ReleaseUseItem => {
145                let player: SharedEntity = Arc::<Player>::clone(self);
146                self.release_using_item(&player);
147            }
148            PlayerAction::SwapItemWithOffhand => {
149                if self.game_mode() == GameType::Spectator {
150                    return;
151                }
152
153                let changed = self.inventory.lock().swap_hands();
154                self.stop_using_item();
155                if changed {
156                    self.broadcast_inventory_changes();
157                }
158            }
159            PlayerAction::Stab => {
160                if self.game_mode() == GameType::Spectator {
161                    return;
162                }
163
164                let main_hand_item = {
165                    let inventory = self.inventory.lock();
166                    let stack = inventory.get_item_in_hand(InteractionHand::MainHand);
167                    stack.copy_with_count(stack.count())
168                };
169                if self.cannot_attack_with_item(&main_hand_item, 5) {
170                    return;
171                }
172                if let Some(piercing_weapon) = main_hand_item.get_piercing_weapon() {
173                    self.piercing_attack(&main_hand_item, piercing_weapon);
174                }
175            }
176        }
177    }
178
179    /// Handles the pick block action (middle click on a block).
180    ///
181    /// # Panics
182    ///
183    /// Panics if the behavior registry has not been initialized.
184    pub fn handle_pick_item_from_block(&self, packet: SPickItemFromBlock) {
185        if !self.is_within_block_interaction_range(packet.pos) {
186            return;
187        }
188
189        let world = self.get_world();
190
191        let state = world.get_block_state(packet.pos);
192        if state.is_air() {
193            return;
194        }
195
196        let block = state.get_block();
197        let block_behaviors = &*BLOCK_BEHAVIORS;
198        let behavior = block_behaviors.get_behavior(block);
199
200        let block_entity = world.get_block_entity(packet.pos);
201
202        let include_data = self.has_infinite_materials() && packet.include_data;
203
204        let Some(item_stack) =
205            behavior.get_clone_item_stack(block, state, block_entity, include_data)
206        else {
207            return;
208        };
209
210        if item_stack.is_empty() {
211            return;
212        }
213
214        // TODO: If include_data, copy the block entity data into the picked item stack.
215
216        let mut inventory = self.inventory.lock();
217
218        match inventory.find_slot_matching_item_with_same_components(&item_stack) {
219            Some(slot_with_item) => {
220                if PlayerInventory::is_hotbar_slot(slot_with_item) {
221                    inventory.set_selected_slot(slot_with_item);
222                } else {
223                    let slot = inventory.get_suitable_hotbar_slot();
224
225                    inventory.set_selected_slot(slot);
226                    inventory.pick_slot(slot_with_item);
227                }
228            }
229            None => {
230                if self.has_infinite_materials() {
231                    let slot = inventory.get_suitable_hotbar_slot();
232
233                    inventory.set_selected_slot(slot);
234                    inventory.add_and_pick_item(item_stack);
235                } else {
236                    return;
237                }
238            }
239        }
240
241        self.send_packet(CSetHeldSlot {
242            slot: i32::from(inventory.get_selected_slot()),
243        });
244
245        drop(inventory);
246        self.inventory_menu
247            .lock()
248            .behavior_mut()
249            .broadcast_changes(&self.connection);
250    }
251
252    /// Handles a sign update packet from the client.
253    pub fn handle_sign_update(&self, packet: SSignUpdate) {
254        if !self.is_within_block_interaction_range(packet.pos) {
255            return;
256        }
257
258        self.reset_last_action_time();
259
260        let world = self.get_world();
261
262        let Some(block_entity) = world.get_block_entity(packet.pos) else {
263            return;
264        };
265
266        let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
267            return;
268        };
269
270        if sign.is_waxed() {
271            return;
272        }
273
274        if sign.get_player_who_may_edit() != Some(self.gameprofile.id()) {
275            log::warn!(
276                "Player {} tried to edit sign they're not allowed to edit",
277                self.gameprofile.name()
278            );
279            return;
280        }
281
282        let mut text = sign.get_text(packet.slot);
283        for (i, line) in packet.lines.iter().enumerate() {
284            if i < 4 {
285                let stripped = strip_formatting_codes(line);
286                text.set_message(i, TextComponent::plain(stripped));
287            }
288        }
289
290        sign.set_text(text, packet.slot);
291        sign.set_player_who_may_edit(None);
292        sign.set_changed();
293
294        let update_tag = sign.get_update_tag();
295        let block_entity_type = sign.get_type();
296        let pos = packet.pos;
297
298        if let Some(nbt) = update_tag {
299            world.broadcast_block_entity_update(pos, block_entity_type, nbt);
300        }
301    }
302
303    /// Opens the sign editor for the player.
304    ///
305    /// # Arguments
306    /// * `pos` - Position of the sign block
307    /// * `slot` - Which side of the sign to edit
308    pub fn open_sign_editor(&self, pos: BlockPos, slot: SignTextSlot) {
309        let world = self.get_world();
310
311        if let Some(block_entity) = world.get_block_entity(pos)
312            && let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>()
313        {
314            sign.set_player_who_may_edit(Some(self.gameprofile.id()));
315        }
316
317        let state = world.get_block_state(pos);
318        self.send_packet(CBlockUpdate {
319            pos,
320            block_state: state,
321        });
322
323        self.send_packet(COpenSignEditor { pos, slot });
324    }
325}
326
327/// Strips Minecraft formatting codes (§ followed by a character) from a string.
328///
329/// This is equivalent to vanilla's `ChatFormatting.stripFormatting()`.
330fn strip_formatting_codes(text: &str) -> String {
331    let mut result = String::with_capacity(text.len());
332    let mut chars = text.chars();
333
334    while let Some(c) = chars.next() {
335        if c == '§' {
336            chars.next();
337        } else {
338            result.push(c);
339        }
340    }
341
342    result
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::behavior::init_behaviors;
349    use crate::player::connection::NetworkConnection as _;
350    use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
351    use steel_registry::vanilla_items;
352    use steel_utils::ChunkPos;
353
354    #[test]
355    fn use_item_on_rejects_non_finite_hit_locations() {
356        let world_fixture = fresh_test_world("use_item_on_non_finite_hit_location");
357        let world = &world_fixture.world;
358        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
359        init_behaviors();
360        let player = TestPlayerBuilder::new(Arc::clone(world), "TestPlayer", 1).build();
361        player.set_client_loaded(true);
362        player
363            .inventory
364            .lock()
365            .set_selected_item(ItemStack::new(&vanilla_items::FIREWORK_ROCKET));
366
367        for (sequence, location) in [
368            (1, DVec3::new(f64::NAN, 0.5, 0.5)),
369            (2, DVec3::new(0.5, f64::INFINITY, 0.5)),
370            (3, DVec3::new(0.5, 0.5, f64::NEG_INFINITY)),
371        ] {
372            player.handle_use_item_on(SUseItemOn {
373                hand: InteractionHand::MainHand,
374                block_hit: BlockHitResult {
375                    location,
376                    direction: Direction::Up,
377                    block_pos: BlockPos::ZERO,
378                    miss: false,
379                    inside: false,
380                    world_border_hit: false,
381                },
382                sequence,
383            });
384        }
385
386        let inventory = player.inventory.lock();
387        let held_item = inventory.get_item_in_hand(InteractionHand::MainHand);
388        assert!(held_item.is(&vanilla_items::FIREWORK_ROCKET));
389        assert_eq!(held_item.count(), 1);
390        assert!(!player.connection.closed());
391    }
392}