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