steel_core/player/game_mode/
block_interaction.rs1use super::{block_breaking::BlockBreakAction, *};
2
3impl Player {
4 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 pub fn swing(&self, hand: InteractionHand, update_self: bool) {
25 LivingEntity::swing(self, hand, update_self);
26 }
27
28 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 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 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 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 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 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
317fn 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}