1use std::sync::Arc;
7
8use steel_protocol::packets::game::CBlockUpdate;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::data_components::AdventureModePredicate;
11use steel_registry::data_components::vanilla_components::CAN_BREAK;
12use steel_registry::vanilla_attributes;
13use steel_registry::{
14 REGISTRY, blocks::properties::Direction, item_stack::ItemStack, vanilla_blocks,
15 vanilla_game_events,
16};
17use steel_utils::{
18 BlockPos, BlockStateId,
19 nbt::compare_nbt_compounds,
20 types::{GameType, InteractionHand, UpdateFlags},
21};
22
23use crate::behavior::{BLOCK_BEHAVIORS, BlockLootContext};
24use crate::entity::{Entity, LivingEntity};
25use crate::fluid::fluid_state_to_block;
26use crate::player::Player;
27use crate::player::food_data::food_constants;
28use crate::world::{ConditionalBlockSetResult, World, game_event::GameEventContext};
29
30impl Player {
31 pub(super) fn block_action_restricted(&self, world: &World, pos: BlockPos) -> bool {
33 let game_mode = self.game_mode();
34 if !matches!(game_mode, GameType::Adventure | GameType::Spectator) {
35 return false;
36 }
37 if game_mode == GameType::Spectator {
38 return true;
39 }
40 if self.abilities.lock().may_build {
41 return false;
42 }
43
44 let can_break = {
48 let inventory = self.inventory.lock();
49 let item = inventory.get_selected_item();
50 if item.is_empty() {
51 return true;
52 }
53 item.get(CAN_BREAK).cloned()
54 };
55 let Some(can_break) = can_break else {
56 return true;
57 };
58 !Self::can_break_block_in_adventure_mode(&can_break, world, pos)
59 }
60
61 fn can_break_block_in_adventure_mode(
62 predicate: &AdventureModePredicate,
63 world: &World,
64 pos: BlockPos,
65 ) -> bool {
66 let state = world.get_block_state(pos);
67 predicate.predicates().iter().any(|predicate| {
70 if !predicate.matches_state(state) {
71 return false;
72 }
73 let Some(expected_nbt) = predicate.nbt() else {
74 return true;
75 };
76 let Some(block_entity) = world.get_block_entity(pos) else {
77 return false;
78 };
79 let actual_nbt = block_entity.save_with_full_metadata();
80 compare_nbt_compounds(expected_nbt.tag(), &actual_nbt, true)
81 })
82 }
83}
84
85pub struct BlockBreakingManager {
89 is_destroying_block: bool,
91 destroy_progress_start: u64,
93 destroy_pos: BlockPos,
95 game_ticks: u64,
97 has_delayed_destroy: bool,
99 delayed_destroy_pos: BlockPos,
101 delayed_tick_start: u64,
103 last_sent_state: i32,
105}
106
107impl Default for BlockBreakingManager {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl BlockBreakingManager {
114 #[must_use]
116 pub const fn new() -> Self {
117 Self {
118 is_destroying_block: false,
119 destroy_progress_start: 0,
120 destroy_pos: BlockPos::new(0, 0, 0),
121 game_ticks: 0,
122 has_delayed_destroy: false,
123 delayed_destroy_pos: BlockPos::new(0, 0, 0),
124 delayed_tick_start: 0,
125 last_sent_state: -1,
126 }
127 }
128
129 pub fn tick(&mut self, player: &Player, world: &Arc<World>) {
133 self.game_ticks += 1;
134
135 if self.has_delayed_destroy {
136 let state = world.get_block_state(self.delayed_destroy_pos);
137 if is_air(state) {
138 self.has_delayed_destroy = false;
139 } else {
140 let progress = self.increment_destroy_progress(
141 player,
142 world,
143 state,
144 self.delayed_destroy_pos,
145 self.delayed_tick_start,
146 );
147 if progress >= 1.0 {
148 self.has_delayed_destroy = false;
149 self.destroy_block(player, world, self.delayed_destroy_pos);
150 }
151 }
152 } else if self.is_destroying_block {
153 let state = world.get_block_state(self.destroy_pos);
154 if is_air(state) {
155 world.broadcast_block_destruction(player.id(), self.destroy_pos, -1);
157 self.last_sent_state = -1;
158 self.is_destroying_block = false;
159 } else {
160 self.increment_destroy_progress(
161 player,
162 world,
163 state,
164 self.destroy_pos,
165 self.destroy_progress_start,
166 );
167 }
168 }
169 }
170
171 fn increment_destroy_progress(
173 &mut self,
174 player: &Player,
175 world: &Arc<World>,
176 block_state: BlockStateId,
177 pos: BlockPos,
178 destroy_start_tick: u64,
179 ) -> f32 {
180 let ticks_spent = self.game_ticks.saturating_sub(destroy_start_tick);
181 let destroy_speed = get_destroy_progress(player, block_state);
182 let progress = destroy_speed * (ticks_spent + 1) as f32;
183 let state = (progress * 10.0) as i32;
184
185 if state != self.last_sent_state {
186 world.broadcast_block_destruction(player.id(), pos, state);
187 self.last_sent_state = state;
188 }
189
190 progress
191 }
192
193 pub fn handle_block_break_action(
198 &mut self,
199 player: &Player,
200 world: &Arc<World>,
201 pos: BlockPos,
202 action: BlockBreakAction,
203 _direction: Direction,
204 ) {
205 if !player.is_within_block_interaction_range(pos) {
207 return;
208 }
209
210 if pos.y() >= world.max_build_height() {
212 player.send_packet(CBlockUpdate {
213 pos,
214 block_state: world.get_block_state(pos),
215 });
216 return;
217 }
218
219 match action {
220 BlockBreakAction::Start => {
221 if !world.may_interact(player, pos) {
223 player.send_packet(CBlockUpdate {
224 pos,
225 block_state: world.get_block_state(pos),
226 });
227 return;
228 }
229
230 if player.game_mode() == GameType::Creative {
232 self.destroy_and_ack(player, world, pos);
233 return;
234 }
235
236 if player.block_action_restricted(world, pos) {
237 player.send_packet(CBlockUpdate {
238 pos,
239 block_state: world.get_block_state(pos),
240 });
241 return;
242 }
243
244 self.destroy_progress_start = self.game_ticks;
245 let block_state = world.get_block_state(pos);
246
247 if !is_air(block_state) {
248 BLOCK_BEHAVIORS
250 .get_behavior(block_state.get_block())
251 .attack(block_state, world, pos, player);
252
253 let progress = get_destroy_progress(player, block_state);
254
255 if progress >= 1.0 {
256 self.destroy_and_ack(player, world, pos);
258 } else {
259 if self.is_destroying_block {
261 player.send_packet(CBlockUpdate {
263 pos: self.destroy_pos,
264 block_state: world.get_block_state(self.destroy_pos),
265 });
266 }
267
268 self.is_destroying_block = true;
269 self.destroy_pos = pos;
270 let state = (progress * 10.0) as i32;
271 world.broadcast_block_destruction(player.id(), pos, state);
272 self.last_sent_state = state;
273 }
274 }
275 }
276
277 BlockBreakAction::Stop => {
278 if pos == self.destroy_pos {
279 let ticks_spent = self.game_ticks.saturating_sub(self.destroy_progress_start);
280 let block_state = world.get_block_state(pos);
281
282 if !is_air(block_state) {
283 let destroy_speed = get_destroy_progress(player, block_state);
284 let progress = destroy_speed * (ticks_spent + 1) as f32;
285
286 if progress >= 0.7 {
287 self.is_destroying_block = false;
289 world.broadcast_block_destruction(player.id(), pos, -1);
290 self.destroy_and_ack(player, world, pos);
291 return;
292 }
293
294 if !self.has_delayed_destroy {
295 self.is_destroying_block = false;
297 self.has_delayed_destroy = true;
298 self.delayed_destroy_pos = pos;
299 self.delayed_tick_start = self.destroy_progress_start;
300 }
301 }
302 }
303 }
304
305 BlockBreakAction::Abort => {
306 self.is_destroying_block = false;
307
308 if self.destroy_pos != pos {
309 log::warn!(
310 "Mismatch in destroy block pos: {:?} vs {:?}",
311 self.destroy_pos,
312 pos
313 );
314 world.broadcast_block_destruction(player.id(), self.destroy_pos, -1);
315 }
316
317 world.broadcast_block_destruction(player.id(), pos, -1);
318 }
319 }
320 }
321
322 fn destroy_and_ack(&mut self, player: &Player, world: &Arc<World>, pos: BlockPos) {
324 if !self.destroy_block(player, world, pos) {
325 player.send_packet(CBlockUpdate {
327 pos,
328 block_state: world.get_block_state(pos),
329 });
330 }
331 }
332
333 #[expect(
337 clippy::unused_self,
338 reason = "method belongs logically to BlockBreakingManager and will use self when additional state is added"
339 )]
340 fn destroy_block(&self, player: &Player, world: &Arc<World>, pos: BlockPos) -> bool {
341 let state = world.get_block_state(pos);
342
343 let Some(_block) = REGISTRY.blocks.by_state_id(state) else {
348 return false;
349 };
350
351 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
355 let adjusted_state = behavior.player_will_destroy(state, world, pos, player);
356 world.game_event(
357 &vanilla_game_events::BLOCK_DESTROY,
358 pos,
359 &GameEventContext::new(Some(player), Some(adjusted_state)),
360 );
361 let state_after_player_will_destroy = world.get_block_state(pos);
362
363 let replacement = fluid_state_to_block(state.get_fluid_state());
366 let removed_by_player_break = !state_after_player_will_destroy.is_air()
369 && world.set_block_if_unchanged(
370 pos,
371 state_after_player_will_destroy,
372 replacement,
373 UpdateFlags::UPDATE_ALL,
374 ) == ConditionalBlockSetResult::Changed;
375 let changed_by_player_will_destroy = state_after_player_will_destroy != state;
376 let changed = changed_by_player_will_destroy || removed_by_player_break;
377
378 if removed_by_player_break {
379 behavior.destroy(adjusted_state, world, pos);
380
381 let block = REGISTRY.blocks.by_state_id(adjusted_state);
384 let is_fire = block.is_some_and(|b| {
385 b.key == vanilla_blocks::FIRE.key || b.key == vanilla_blocks::SOUL_FIRE.key
386 });
387 if !is_fire {
388 world.destroy_block_effect(pos, u32::from(adjusted_state.0), Some(player.id()));
389 }
390
391 let (has_correct_tool, destroyed_with) = {
394 let inv = player.inventory.lock();
395 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
396 (
397 main_hand.is_correct_tool_for_drops(adjusted_state)
398 || !requires_correct_tool(adjusted_state),
399 main_hand.copy_with_count(main_hand.count),
400 )
401 };
402
403 let block_destroy_time = REGISTRY
406 .blocks
407 .by_state_id(adjusted_state)
408 .map_or(0.0, |b| b.config.destroy_time);
409
410 if block_destroy_time != 0.0 {
411 let mut inv = player.inventory.lock();
412 let damage_per_block = inv.get_selected_item().get_tool_damage_per_block();
413
414 if damage_per_block > 0 {
415 let has_infinite_materials = player.has_infinite_materials();
418 let broke = inv.with_selected_item_mut(|main_hand| {
419 main_hand.hurt_and_break(damage_per_block, has_infinite_materials)
420 });
421 if broke {
422 log::debug!("Tool broke while mining block at {pos:?}");
424 }
425 }
426 }
427
428 player.cause_food_exhaustion(food_constants::EXHAUSTION_MINE);
429
430 let game_mode = player.game_mode();
432 if game_mode != GameType::Spectator
433 && game_mode != GameType::Creative
434 && has_correct_tool
435 {
436 drop_block_loot(player, world, pos, adjusted_state, &destroyed_with);
437 let block_entity = world.get_block_entity(pos);
438 behavior.player_destroy(
439 world,
440 player,
441 pos,
442 adjusted_state,
443 block_entity.as_ref(),
444 &destroyed_with,
445 );
446 }
447 }
448
449 changed
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
455pub enum BlockBreakAction {
456 Start,
458 Stop,
460 Abort,
462}
463
464fn is_air(state: BlockStateId) -> bool {
466 let Some(block) = REGISTRY.blocks.by_state_id(state) else {
467 return true;
468 };
469 block.config.is_air
470}
471
472fn requires_correct_tool(state: BlockStateId) -> bool {
474 let Some(block) = REGISTRY.blocks.by_state_id(state) else {
475 return false;
476 };
477 block.config.requires_correct_tool_for_drops
478}
479
480fn get_destroy_progress(player: &Player, block_state: BlockStateId) -> f32 {
487 let Some(block) = REGISTRY.blocks.by_state_id(block_state) else {
488 return 0.0;
489 };
490
491 let destroy_time = block.config.destroy_time;
492
493 if player.game_mode() == GameType::Creative {
495 return 1.0;
496 }
497
498 if destroy_time < 0.0 {
500 return 0.0;
501 }
502
503 if destroy_time == 0.0 {
505 return 1.0;
506 }
507
508 let mining_speed = {
510 let inv = player.inventory.lock();
511 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
512 main_hand.get_destroy_speed(block_state)
513 };
514
515 let has_correct_tool = {
517 let inv = player.inventory.lock();
518 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
519 main_hand.is_correct_tool_for_drops(block_state)
520 };
521
522 let speed = mining_speed;
524
525 let divisor = if has_correct_tool || !block.config.requires_correct_tool_for_drops {
532 30.0
533 } else {
534 100.0
535 };
536
537 speed / destroy_time / divisor
538}
539
540fn drop_block_loot(
542 player: &Player,
543 world: &Arc<World>,
544 pos: BlockPos,
545 state: BlockStateId,
546 tool: &ItemStack,
547) {
548 let luck = player
549 .attributes()
550 .lock()
551 .get_value(vanilla_attributes::LUCK)
552 .unwrap_or(0.0) as f32;
553
554 let drops = BlockLootContext::new(world, pos)
555 .with_luck(luck)
556 .with_tool(tool)
557 .get_drops(state);
558
559 for item in drops {
561 if !item.is_empty() {
562 player.get_world().pop_resource(pos, item);
563 }
564 }
565
566 BLOCK_BEHAVIORS
567 .get_behavior(state.get_block())
568 .spawn_after_break(state, world, pos, tool, true);
569}