1use std::sync::Arc;
7
8use steel_protocol::packets::game::CBlockUpdate;
9use steel_registry::attribute::AttributeRef;
10use steel_registry::blocks::block_state_ext::BlockStateExt;
11use steel_registry::equipment::EquipmentSlot;
12use steel_registry::stat::vanilla_stat_types;
13use steel_registry::{
14 REGISTRY, blocks::properties::Direction, item_stack::ItemStack, level_events, vanilla_blocks,
15 vanilla_game_events,
16};
17use steel_registry::{vanilla_attributes, vanilla_mob_effects};
18use steel_utils::{
19 BlockPos, BlockStateId,
20 types::{GameType, InteractionHand, UpdateFlags},
21};
22
23use crate::behavior::{BLOCK_BEHAVIORS, BlockLootContext};
24use crate::block_entity::BlockEntity;
25use crate::entity::{ActiveMobEffect, Entity, LivingEntity};
26use crate::fluid::fluid_state_to_block;
27use crate::player::Player;
28use crate::player::food_data::food_constants;
29use crate::world::{ConditionalBlockSetResult, World, game_event::GameEventContext};
30
31use super::adventure_mode;
32
33impl Player {
34 fn get_destroy_speed(&self, state: BlockStateId) -> f32 {
37 let mut speed = {
38 let inv = self.inventory.lock();
39 inv.get_item_in_hand(InteractionHand::MainHand)
40 .get_destroy_speed(state)
41 };
42
43 let attributes = self.attributes().lock();
44 let attribute_value = |attribute: AttributeRef| {
45 attributes
46 .get_value(attribute)
47 .unwrap_or(attribute.default_value) as f32
48 };
49
50 if speed > 1.0 {
54 speed += attribute_value(vanilla_attributes::MINING_EFFICIENCY);
55 }
56
57 let haste = self.mob_effect(vanilla_mob_effects::HASTE);
58 let conduit_power = self.mob_effect(vanilla_mob_effects::CONDUIT_POWER);
59 let dig_speed_amplifier = haste
60 .iter()
61 .chain(conduit_power.iter())
62 .map(ActiveMobEffect::amplifier)
63 .max();
64 if let Some(amplifier) = dig_speed_amplifier {
65 speed *= 1.0 + (amplifier + 1) as f32 * 0.2;
66 }
67
68 if let Some(mining_fatigue) = self.mob_effect(vanilla_mob_effects::MINING_FATIGUE) {
69 speed *= 0.3_f32.powi(mining_fatigue.amplifier() + 1);
70 }
71
72 speed *= attribute_value(vanilla_attributes::BLOCK_BREAK_SPEED);
73 if self.is_eye_in_water() {
74 speed *= attribute_value(vanilla_attributes::SUBMERGED_MINING_SPEED);
75 }
76
77 if !self.on_ground() {
78 speed /= 5.0;
79 }
80
81 speed
82 }
83
84 pub(super) fn block_action_restricted(&self, world: &World, pos: BlockPos) -> bool {
86 let game_mode = self.game_mode();
87 if !matches!(game_mode, GameType::Adventure | GameType::Spectator) {
88 return false;
89 }
90 if game_mode == GameType::Spectator {
91 return true;
92 }
93 if self.abilities.lock().may_build {
94 return false;
95 }
96
97 let item = {
98 let inventory = self.inventory.lock();
99 let item = inventory.get_selected_item();
100 if item.is_empty() {
101 return true;
102 }
103 item.clone()
104 };
105 !adventure_mode::can_break(&item, world, pos)
106 }
107}
108
109pub struct BlockBreakingManager {
113 is_destroying_block: bool,
115 destroy_progress_start: u64,
117 destroy_pos: BlockPos,
119 destroy_direction: Direction,
121 game_ticks: u64,
123 has_delayed_destroy: bool,
125 delayed_destroy_pos: BlockPos,
127 delayed_tick_start: u64,
129 last_sent_state: i32,
131}
132
133impl Default for BlockBreakingManager {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl BlockBreakingManager {
140 #[must_use]
142 pub const fn new() -> Self {
143 Self {
144 is_destroying_block: false,
145 destroy_progress_start: 0,
146 destroy_pos: BlockPos::new(0, 0, 0),
147 destroy_direction: Direction::Down,
148 game_ticks: 0,
149 has_delayed_destroy: false,
150 delayed_destroy_pos: BlockPos::new(0, 0, 0),
151 delayed_tick_start: 0,
152 last_sent_state: -1,
153 }
154 }
155
156 pub fn tick(&mut self, player: &Player, world: &Arc<World>) {
160 self.game_ticks += 1;
161
162 if self.has_delayed_destroy {
163 let state = world.get_block_state(self.delayed_destroy_pos);
164 if is_air(state) {
165 self.has_delayed_destroy = false;
166 } else {
167 let progress = self.increment_destroy_progress(
168 player,
169 world,
170 state,
171 self.delayed_destroy_pos,
172 self.delayed_tick_start,
173 );
174 if progress >= 1.0 {
175 self.has_delayed_destroy = false;
176 self.destroy_block(player, world, self.delayed_destroy_pos);
177 }
178 }
179 } else if self.is_destroying_block {
180 let state = world.get_block_state(self.destroy_pos);
181 if is_air(state) {
182 world.broadcast_block_destruction(player.id(), self.destroy_pos, -1);
184 self.last_sent_state = -1;
185 self.is_destroying_block = false;
186 } else {
187 let ticks_spent_destroying =
188 self.game_ticks.saturating_sub(self.destroy_progress_start);
189 let event = if ticks_spent_destroying.is_multiple_of(4) {
190 level_events::PARTICLES_AND_SOUND_DESTROY_PROGRESS
191 } else {
192 level_events::PARTICLES_DESTROY_PROGRESS
193 };
194
195 self.increment_destroy_progress(
196 player,
197 world,
198 state,
199 self.destroy_pos,
200 self.destroy_progress_start,
201 );
202 world.level_event(
203 event,
204 self.destroy_pos,
205 self.destroy_direction.get_3d_data_value(),
206 None,
207 );
208 }
209 }
210 }
211
212 fn increment_destroy_progress(
214 &mut self,
215 player: &Player,
216 world: &Arc<World>,
217 block_state: BlockStateId,
218 pos: BlockPos,
219 destroy_start_tick: u64,
220 ) -> f32 {
221 let ticks_spent = self.game_ticks.saturating_sub(destroy_start_tick);
222 let destroy_speed = get_destroy_progress(player, block_state);
223 let progress = destroy_speed * (ticks_spent + 1) as f32;
224 let state = (progress * 10.0) as i32;
225
226 if state != self.last_sent_state {
227 world.broadcast_block_destruction(player.id(), pos, state);
228 self.last_sent_state = state;
229 }
230
231 progress
232 }
233
234 pub fn handle_block_break_action(
239 &mut self,
240 player: &Player,
241 world: &Arc<World>,
242 pos: BlockPos,
243 action: BlockBreakAction,
244 direction: Direction,
245 ) {
246 if !player.is_within_block_interaction_range(pos) {
248 if self.is_destroying_block && action == BlockBreakAction::Abort {
249 self.abort_destroy_block(player, world, pos);
250 }
251 return;
252 }
253
254 if pos.y() >= world.max_build_height() {
256 player.send_packet(CBlockUpdate {
257 pos,
258 block_state: world.get_block_state(pos),
259 });
260 return;
261 }
262
263 match action {
264 BlockBreakAction::ChangeDirection => {
265 self.destroy_direction = direction;
266 }
267
268 BlockBreakAction::Start => {
269 if !world.may_interact(player, pos) {
271 player.send_packet(CBlockUpdate {
272 pos,
273 block_state: world.get_block_state(pos),
274 });
275 return;
276 }
277
278 if player.game_mode() == GameType::Creative {
280 self.destroy_and_ack(player, world, pos);
281 return;
282 }
283
284 if player.block_action_restricted(world, pos) {
285 player.send_packet(CBlockUpdate {
286 pos,
287 block_state: world.get_block_state(pos),
288 });
289 return;
290 }
291
292 self.destroy_progress_start = self.game_ticks;
293 let block_state = world.get_block_state(pos);
294
295 if !is_air(block_state) {
296 BLOCK_BEHAVIORS
298 .get_behavior(block_state.get_block())
299 .attack(block_state, world, pos, player);
300
301 let progress = get_destroy_progress(player, block_state);
302
303 if progress >= 1.0 {
304 self.destroy_and_ack(player, world, pos);
306 } else {
307 if self.is_destroying_block {
309 player.send_packet(CBlockUpdate {
311 pos: self.destroy_pos,
312 block_state: world.get_block_state(self.destroy_pos),
313 });
314 }
315
316 self.is_destroying_block = true;
317 self.destroy_pos = pos;
318 self.destroy_direction = direction;
319 let state = (progress * 10.0) as i32;
320 world.broadcast_block_destruction(player.id(), pos, state);
321 self.last_sent_state = state;
322 }
323 }
324 }
325
326 BlockBreakAction::Stop => {
327 if pos == self.destroy_pos {
328 let ticks_spent = self.game_ticks.saturating_sub(self.destroy_progress_start);
329 let block_state = world.get_block_state(pos);
330
331 if !is_air(block_state) {
332 let destroy_speed = get_destroy_progress(player, block_state);
333 let progress = destroy_speed * (ticks_spent + 1) as f32;
334
335 if progress >= 0.7 {
336 self.is_destroying_block = false;
338 world.broadcast_block_destruction(player.id(), pos, -1);
339 self.destroy_and_ack(player, world, pos);
340 return;
341 }
342
343 if !self.has_delayed_destroy {
344 self.is_destroying_block = false;
346 self.has_delayed_destroy = true;
347 self.delayed_destroy_pos = pos;
348 self.delayed_tick_start = self.destroy_progress_start;
349 }
350 }
351 }
352 }
353
354 BlockBreakAction::Abort => {
355 self.abort_destroy_block(player, world, pos);
356 }
357 }
358 }
359
360 fn abort_destroy_block(&mut self, player: &Player, world: &Arc<World>, pos: BlockPos) {
361 self.is_destroying_block = false;
362
363 if self.destroy_pos != pos {
364 log::warn!(
365 "Mismatch in destroy block pos: {:?} vs {:?}",
366 self.destroy_pos,
367 pos
368 );
369 world.broadcast_block_destruction(player.id(), self.destroy_pos, -1);
370 }
371
372 world.broadcast_block_destruction(player.id(), pos, -1);
373 }
374
375 fn destroy_and_ack(&mut self, player: &Player, world: &Arc<World>, pos: BlockPos) {
377 if !self.destroy_block(player, world, pos) {
378 player.send_packet(CBlockUpdate {
380 pos,
381 block_state: world.get_block_state(pos),
382 });
383 }
384 }
385
386 #[expect(
390 clippy::unused_self,
391 reason = "method belongs logically to BlockBreakingManager and will use self when additional state is added"
392 )]
393 fn destroy_block(&self, player: &Player, world: &Arc<World>, pos: BlockPos) -> bool {
394 let state = world.get_block_state(pos);
395
396 if player.get_abilities().instabuild && !held_item_can_destroy_in_creative(player) {
397 return false;
398 }
399
400 let Some(_block) = REGISTRY.blocks.by_state_id(state) else {
402 return false;
403 };
404
405 if player.block_action_restricted(world, pos) {
408 return false;
409 }
410
411 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
412 let adjusted_state = behavior.player_will_destroy(state, world, pos, player);
413 world.game_event(
414 &vanilla_game_events::BLOCK_DESTROY,
415 pos,
416 &GameEventContext::new(Some(player), Some(adjusted_state)),
417 );
418 let state_after_player_will_destroy = world.get_block_state(pos);
419
420 let replacement = fluid_state_to_block(state.get_fluid_state());
423
424 let block_entity = world.get_block_entity(pos);
427
428 let removed_by_player_break = !state_after_player_will_destroy.is_air()
431 && world.set_block_if_unchanged(
432 pos,
433 state_after_player_will_destroy,
434 replacement,
435 UpdateFlags::UPDATE_ALL,
436 ) == ConditionalBlockSetResult::Changed;
437 let changed_by_player_will_destroy = state_after_player_will_destroy != state;
438 let changed = changed_by_player_will_destroy || removed_by_player_break;
439
440 if removed_by_player_break {
441 behavior.destroy(adjusted_state, world, pos);
442
443 let block = REGISTRY.blocks.by_state_id(adjusted_state);
446 let is_fire = block.is_some_and(|b| {
447 b.key == vanilla_blocks::FIRE.key || b.key == vanilla_blocks::SOUL_FIRE.key
448 });
449 if !is_fire {
450 world.destroy_block_effect(pos, u32::from(adjusted_state.0), Some(player.id()));
451 }
452
453 let (has_correct_tool, destroyed_with) = {
456 let inv = player.inventory.lock();
457 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
458 (
459 main_hand.is_correct_tool_for_drops(adjusted_state)
460 || !requires_correct_tool(adjusted_state),
461 main_hand.copy_with_count(main_hand.count),
462 )
463 };
464
465 let block_destroy_time = REGISTRY
468 .blocks
469 .by_state_id(adjusted_state)
470 .map_or(0.0, |b| b.config.destroy_time);
471
472 if block_destroy_time != 0.0 {
473 let broke = {
474 let mut inv = player.inventory.lock();
475 let damage_per_block = inv.get_selected_item().get_tool_damage_per_block();
476
477 if damage_per_block > 0 {
478 let has_infinite_materials = player.has_infinite_materials();
481 inv.with_selected_item_mut(|main_hand| {
482 let item = main_hand.item;
483 main_hand
484 .hurt_and_break(damage_per_block, has_infinite_materials)
485 .then_some(item)
486 })
487 } else {
488 None
489 }
490 };
491
492 if let Some(item) = broke {
493 player.on_equipped_item_broken(item, EquipmentSlot::MainHand);
494 log::debug!("Tool broke while mining block at {pos:?}");
495 }
496 }
497
498 let game_mode = player.game_mode();
500 if game_mode != GameType::Spectator
501 && game_mode != GameType::Creative
502 && has_correct_tool
503 {
504 player.award_stat(&vanilla_stat_types::BLOCK_MINED, state.get_block());
505 player.cause_food_exhaustion(food_constants::EXHAUSTION_MINE);
506
507 drop_block_loot(
508 player,
509 world,
510 pos,
511 adjusted_state,
512 &destroyed_with,
513 block_entity.as_deref(),
514 );
515 behavior.player_destroy(
516 world,
517 player,
518 pos,
519 adjusted_state,
520 block_entity.as_ref(),
521 &destroyed_with,
522 );
523 }
524 }
525
526 changed
527 }
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
532pub enum BlockBreakAction {
533 Start,
535 ChangeDirection,
537 Stop,
539 Abort,
541}
542
543fn held_item_can_destroy_in_creative(player: &Player) -> bool {
544 let inventory = player.inventory.lock();
545 let main_hand = inventory.get_item_in_hand(InteractionHand::MainHand);
546 main_hand.can_destroy_blocks_in_creative()
547}
548
549fn is_air(state: BlockStateId) -> bool {
551 let Some(block) = REGISTRY.blocks.by_state_id(state) else {
552 return true;
553 };
554 block.config.is_air
555}
556
557fn requires_correct_tool(state: BlockStateId) -> bool {
559 let Some(block) = REGISTRY.blocks.by_state_id(state) else {
560 return false;
561 };
562 block.config.requires_correct_tool_for_drops
563}
564
565fn get_destroy_progress(player: &Player, block_state: BlockStateId) -> f32 {
572 let Some(block) = REGISTRY.blocks.by_state_id(block_state) else {
573 return 0.0;
574 };
575
576 let destroy_time = block.config.destroy_time;
577
578 if player.game_mode() == GameType::Creative {
580 return 1.0;
581 }
582
583 if destroy_time < 0.0 {
585 return 0.0;
586 }
587
588 if destroy_time == 0.0 {
590 return 1.0;
591 }
592
593 let speed = player.get_destroy_speed(block_state);
594
595 let has_correct_tool = {
596 let inv = player.inventory.lock();
597 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
598 main_hand.is_correct_tool_for_drops(block_state)
599 };
600
601 let divisor = if has_correct_tool || !block.config.requires_correct_tool_for_drops {
604 30.0
605 } else {
606 100.0
607 };
608
609 speed / destroy_time / divisor
610}
611
612fn drop_block_loot(
614 player: &Player,
615 world: &Arc<World>,
616 pos: BlockPos,
617 state: BlockStateId,
618 tool: &ItemStack,
619 block_entity: Option<&dyn BlockEntity>,
620) {
621 let luck = player
622 .attributes()
623 .lock()
624 .get_value(vanilla_attributes::LUCK)
625 .unwrap_or(0.0) as f32;
626
627 let drops = BlockLootContext::new(world, pos)
628 .with_luck(luck)
629 .with_tool(tool)
630 .with_block_entity(block_entity)
631 .get_drops(state);
632
633 for item in drops {
635 if !item.is_empty() {
636 player.get_world().pop_resource(pos, item);
637 }
638 }
639
640 BLOCK_BEHAVIORS
641 .get_behavior(state.get_block())
642 .spawn_after_break(state, world, pos, tool, true);
643}