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
52        if (location.x - center_x).abs() >= limit
53            || (location.y - center_y).abs() >= limit
54            || (location.z - center_z).abs() >= limit
55        {
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        let world = self.get_world();
67
68        if pos.y() >= world.max_build_height() {
69            // TODO: Send "build.tooHigh" message to player
70            self.send_block_updates(pos, direction);
71            return;
72        }
73
74        if self.is_awaiting_teleport() {
75            self.send_block_updates(pos, direction);
76            return;
77        }
78
79        if !world.may_interact(self, pos) {
80            self.send_block_updates(pos, direction);
81            return;
82        }
83
84        let result = use_item_on(self, &world, packet.hand, &packet.block_hit);
85
86        if result.should_swing_server() {
87            self.swing(packet.hand, true);
88        }
89
90        self.send_block_updates(pos, direction);
91        self.broadcast_inventory_changes();
92    }
93
94    /// Handles a player action packet (block breaking, item dropping, etc.).
95    pub fn handle_player_action(&self, packet: SPlayerAction) {
96        if !self.has_client_loaded() {
97            return;
98        }
99
100        let world = self.get_world();
101        match packet.action {
102            PlayerAction::StartDestroyBlock => {
103                self.block_breaking.lock().handle_block_break_action(
104                    self,
105                    &world,
106                    packet.pos,
107                    BlockBreakAction::Start,
108                    packet.direction,
109                );
110                self.ack_block_changes_up_to(packet.sequence);
111            }
112            PlayerAction::StopDestroyBlock => {
113                self.block_breaking.lock().handle_block_break_action(
114                    self,
115                    &world,
116                    packet.pos,
117                    BlockBreakAction::Stop,
118                    packet.direction,
119                );
120                self.ack_block_changes_up_to(packet.sequence);
121            }
122            PlayerAction::AbortDestroyBlock => {
123                self.block_breaking.lock().handle_block_break_action(
124                    self,
125                    &world,
126                    packet.pos,
127                    BlockBreakAction::Abort,
128                    packet.direction,
129                );
130                self.ack_block_changes_up_to(packet.sequence);
131            }
132            PlayerAction::DropAllItems => {
133                self.drop_from_selected(true);
134            }
135            PlayerAction::DropItem => {
136                self.drop_from_selected(false);
137            }
138            PlayerAction::ReleaseUseItem => {
139                // TODO: Implement release use item (releasing bow, etc.)
140                log::debug!("Player {} released use item", self.gameprofile.name);
141            }
142            PlayerAction::SwapItemWithOffhand => {
143                if self.game_mode() == GameType::Spectator {
144                    return;
145                }
146
147                let changed = self.inventory.lock().swap_hands();
148                if changed {
149                    self.broadcast_inventory_changes();
150                }
151                // TODO: Stop active item use once the using-item foundation exists.
152            }
153            PlayerAction::Stab => {
154                if self.game_mode() == GameType::Spectator {
155                    return;
156                }
157
158                let main_hand_item = {
159                    let inventory = self.inventory.lock();
160                    let stack = inventory.get_item_in_hand(InteractionHand::MainHand);
161                    stack.copy_with_count(stack.count())
162                };
163                if self.cannot_attack_with_item(&main_hand_item, 5) {
164                    return;
165                }
166                if let Some(piercing_weapon) = main_hand_item.get_piercing_weapon() {
167                    self.piercing_attack(&main_hand_item, piercing_weapon);
168                }
169            }
170        }
171    }
172
173    /// Handles the pick block action (middle click on a block).
174    ///
175    /// # Panics
176    ///
177    /// Panics if the behavior registry has not been initialized.
178    pub fn handle_pick_item_from_block(&self, packet: SPickItemFromBlock) {
179        if !self.is_within_block_interaction_range(packet.pos) {
180            return;
181        }
182
183        let state = self.get_world().get_block_state(packet.pos);
184        if state.is_air() {
185            return;
186        }
187
188        let block = state.get_block();
189        let block_behaviors = &*BLOCK_BEHAVIORS;
190        let behavior = block_behaviors.get_behavior(block);
191
192        let include_data = self.has_infinite_materials() && packet.include_data;
193
194        let Some(item_stack) = behavior.get_clone_item_stack(block, state, include_data) else {
195            return;
196        };
197
198        if item_stack.is_empty() {
199            return;
200        }
201
202        // TODO: If include_data, add block entity NBT data to the item stack
203        // This requires block entity support which isn't implemented yet
204
205        let mut inventory = self.inventory.lock();
206
207        let slot_with_item = inventory.find_slot_matching_item(&item_stack);
208
209        if slot_with_item != -1 {
210            if PlayerInventory::is_hotbar_slot(slot_with_item as usize) {
211                inventory.set_selected_slot(slot_with_item as u8);
212            } else {
213                inventory.pick_slot(slot_with_item);
214            }
215        } else if self.has_infinite_materials() {
216            inventory.add_and_pick_item(item_stack);
217        } else {
218            return;
219        }
220
221        self.send_packet(CSetHeldSlot {
222            slot: i32::from(inventory.get_selected_slot()),
223        });
224
225        drop(inventory);
226        self.inventory_menu
227            .lock()
228            .behavior_mut()
229            .broadcast_changes(&self.connection);
230    }
231
232    /// Handles a sign update packet from the client.
233    pub fn handle_sign_update(&self, packet: SSignUpdate) {
234        if !self.is_within_block_interaction_range(packet.pos) {
235            return;
236        }
237
238        let world = self.get_world();
239
240        let Some(block_entity) = world.get_block_entity(packet.pos) else {
241            return;
242        };
243
244        let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>() else {
245            return;
246        };
247
248        if sign.is_waxed() {
249            return;
250        }
251
252        if sign.get_player_who_may_edit() != Some(self.gameprofile.id) {
253            log::warn!(
254                "Player {} tried to edit sign they're not allowed to edit",
255                self.gameprofile.name
256            );
257            return;
258        }
259
260        let mut text = sign.get_text(packet.is_front_text);
261        for (i, line) in packet.lines.iter().enumerate() {
262            if i < 4 {
263                let stripped = strip_formatting_codes(line);
264                text.set_message(i, TextComponent::plain(stripped));
265            }
266        }
267
268        sign.set_text(text, packet.is_front_text);
269        sign.set_player_who_may_edit(None);
270        sign.set_changed();
271
272        let update_tag = sign.get_update_tag();
273        let block_entity_type = sign.get_type();
274        let pos = packet.pos;
275
276        if let Some(nbt) = update_tag {
277            world.broadcast_block_entity_update(pos, block_entity_type, nbt);
278        }
279    }
280
281    /// Opens the sign editor for the player.
282    ///
283    /// # Arguments
284    /// * `pos` - Position of the sign block
285    /// * `is_front_text` - Whether to edit front (true) or back (false) text
286    pub fn open_sign_editor(&self, pos: BlockPos, is_front_text: bool) {
287        let world = self.get_world();
288
289        if let Some(block_entity) = world.get_block_entity(pos)
290            && let Some(sign) = block_entity.downcast_ref::<SignBlockEntity>()
291        {
292            sign.set_player_who_may_edit(Some(self.gameprofile.id));
293        }
294
295        let state = world.get_block_state(pos);
296        self.send_packet(CBlockUpdate {
297            pos,
298            block_state: state,
299        });
300
301        self.send_packet(COpenSignEditor { pos, is_front_text });
302    }
303}
304
305/// Strips Minecraft formatting codes (§ followed by a character) from a string.
306///
307/// This is equivalent to vanilla's `ChatFormatting.stripFormatting()`.
308fn strip_formatting_codes(text: &str) -> String {
309    let mut result = String::with_capacity(text.len());
310    let mut chars = text.chars().peekable();
311
312    while let Some(c) = chars.next() {
313        if c == '§' {
314            chars.next();
315        } else {
316            result.push(c);
317        }
318    }
319
320    result
321}