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 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 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 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 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 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 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
327fn 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}