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, CAN_PLACE_ON};
12use steel_registry::equipment::EquipmentSlot;
13use steel_registry::stat::vanilla_stat_types;
14use steel_registry::vanilla_attributes;
15use steel_registry::{
16 REGISTRY, blocks::properties::Direction, item_stack::ItemStack, vanilla_blocks,
17 vanilla_game_events,
18};
19use steel_utils::{
20 BlockPos, BlockStateId,
21 nbt::compare_nbt_compounds,
22 types::{GameType, InteractionHand, UpdateFlags},
23};
24
25use crate::behavior::{BLOCK_BEHAVIORS, BlockLootContext};
26use crate::block_entity::BlockEntity;
27use crate::entity::{Entity, LivingEntity};
28use crate::fluid::fluid_state_to_block;
29use crate::player::Player;
30use crate::player::food_data::food_constants;
31use crate::world::{ConditionalBlockSetResult, World, game_event::GameEventContext};
32
33impl Player {
34 pub(crate) fn may_use_item_at(
36 &self,
37 pos: BlockPos,
38 direction: Direction,
39 item_stack: &ItemStack,
40 ) -> bool {
41 if self.abilities.lock().may_build {
42 return true;
43 }
44
45 let Some(can_place_on) = item_stack.get(CAN_PLACE_ON) else {
46 return false;
47 };
48 let target = pos.relative(direction.opposite());
49 let world = self.get_world();
50 Self::matches_adventure_mode_predicate(can_place_on, &world, target)
51 }
52
53 pub(super) fn block_action_restricted(&self, world: &World, pos: BlockPos) -> bool {
55 let game_mode = self.game_mode();
56 if !matches!(game_mode, GameType::Adventure | GameType::Spectator) {
57 return false;
58 }
59 if game_mode == GameType::Spectator {
60 return true;
61 }
62 if self.abilities.lock().may_build {
63 return false;
64 }
65
66 let can_break = {
70 let inventory = self.inventory.lock();
71 let item = inventory.get_selected_item();
72 if item.is_empty() {
73 return true;
74 }
75 item.get(CAN_BREAK).cloned()
76 };
77 let Some(can_break) = can_break else {
78 return true;
79 };
80 !Self::matches_adventure_mode_predicate(&can_break, world, pos)
81 }
82
83 fn matches_adventure_mode_predicate(
84 predicate: &AdventureModePredicate,
85 world: &World,
86 pos: BlockPos,
87 ) -> bool {
88 let state = world.get_block_state(pos);
89 predicate.predicates().iter().any(|predicate| {
92 if !predicate.matches_state(state) {
93 return false;
94 }
95 let Some(expected_nbt) = predicate.nbt() else {
96 return true;
97 };
98 let Some(block_entity) = world.get_block_entity(pos) else {
99 return false;
100 };
101 let actual_nbt = block_entity.save_with_full_metadata();
102 compare_nbt_compounds(expected_nbt.tag(), &actual_nbt, true)
103 })
104 }
105}
106
107pub struct BlockBreakingManager {
111 is_destroying_block: bool,
113 destroy_progress_start: u64,
115 destroy_pos: BlockPos,
117 game_ticks: u64,
119 has_delayed_destroy: bool,
121 delayed_destroy_pos: BlockPos,
123 delayed_tick_start: u64,
125 last_sent_state: i32,
127}
128
129impl Default for BlockBreakingManager {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl BlockBreakingManager {
136 #[must_use]
138 pub const fn new() -> Self {
139 Self {
140 is_destroying_block: false,
141 destroy_progress_start: 0,
142 destroy_pos: BlockPos::new(0, 0, 0),
143 game_ticks: 0,
144 has_delayed_destroy: false,
145 delayed_destroy_pos: BlockPos::new(0, 0, 0),
146 delayed_tick_start: 0,
147 last_sent_state: -1,
148 }
149 }
150
151 pub fn tick(&mut self, player: &Player, world: &Arc<World>) {
155 self.game_ticks += 1;
156
157 if self.has_delayed_destroy {
158 let state = world.get_block_state(self.delayed_destroy_pos);
159 if is_air(state) {
160 self.has_delayed_destroy = false;
161 } else {
162 let progress = self.increment_destroy_progress(
163 player,
164 world,
165 state,
166 self.delayed_destroy_pos,
167 self.delayed_tick_start,
168 );
169 if progress >= 1.0 {
170 self.has_delayed_destroy = false;
171 self.destroy_block(player, world, self.delayed_destroy_pos);
172 }
173 }
174 } else if self.is_destroying_block {
175 let state = world.get_block_state(self.destroy_pos);
176 if is_air(state) {
177 world.broadcast_block_destruction(player.id(), self.destroy_pos, -1);
179 self.last_sent_state = -1;
180 self.is_destroying_block = false;
181 } else {
182 self.increment_destroy_progress(
183 player,
184 world,
185 state,
186 self.destroy_pos,
187 self.destroy_progress_start,
188 );
189 }
190 }
191 }
192
193 fn increment_destroy_progress(
195 &mut self,
196 player: &Player,
197 world: &Arc<World>,
198 block_state: BlockStateId,
199 pos: BlockPos,
200 destroy_start_tick: u64,
201 ) -> f32 {
202 let ticks_spent = self.game_ticks.saturating_sub(destroy_start_tick);
203 let destroy_speed = get_destroy_progress(player, block_state);
204 let progress = destroy_speed * (ticks_spent + 1) as f32;
205 let state = (progress * 10.0) as i32;
206
207 if state != self.last_sent_state {
208 world.broadcast_block_destruction(player.id(), pos, state);
209 self.last_sent_state = state;
210 }
211
212 progress
213 }
214
215 pub fn handle_block_break_action(
220 &mut self,
221 player: &Player,
222 world: &Arc<World>,
223 pos: BlockPos,
224 action: BlockBreakAction,
225 _direction: Direction,
226 ) {
227 if !player.is_within_block_interaction_range(pos) {
229 return;
230 }
231
232 if pos.y() >= world.max_build_height() {
234 player.send_packet(CBlockUpdate {
235 pos,
236 block_state: world.get_block_state(pos),
237 });
238 return;
239 }
240
241 match action {
242 BlockBreakAction::Start => {
243 if !world.may_interact(player, pos) {
245 player.send_packet(CBlockUpdate {
246 pos,
247 block_state: world.get_block_state(pos),
248 });
249 return;
250 }
251
252 if player.game_mode() == GameType::Creative {
254 self.destroy_and_ack(player, world, pos);
255 return;
256 }
257
258 if player.block_action_restricted(world, pos) {
259 player.send_packet(CBlockUpdate {
260 pos,
261 block_state: world.get_block_state(pos),
262 });
263 return;
264 }
265
266 self.destroy_progress_start = self.game_ticks;
267 let block_state = world.get_block_state(pos);
268
269 if !is_air(block_state) {
270 BLOCK_BEHAVIORS
272 .get_behavior(block_state.get_block())
273 .attack(block_state, world, pos, player);
274
275 let progress = get_destroy_progress(player, block_state);
276
277 if progress >= 1.0 {
278 self.destroy_and_ack(player, world, pos);
280 } else {
281 if self.is_destroying_block {
283 player.send_packet(CBlockUpdate {
285 pos: self.destroy_pos,
286 block_state: world.get_block_state(self.destroy_pos),
287 });
288 }
289
290 self.is_destroying_block = true;
291 self.destroy_pos = pos;
292 let state = (progress * 10.0) as i32;
293 world.broadcast_block_destruction(player.id(), pos, state);
294 self.last_sent_state = state;
295 }
296 }
297 }
298
299 BlockBreakAction::Stop => {
300 if pos == self.destroy_pos {
301 let ticks_spent = self.game_ticks.saturating_sub(self.destroy_progress_start);
302 let block_state = world.get_block_state(pos);
303
304 if !is_air(block_state) {
305 let destroy_speed = get_destroy_progress(player, block_state);
306 let progress = destroy_speed * (ticks_spent + 1) as f32;
307
308 if progress >= 0.7 {
309 self.is_destroying_block = false;
311 world.broadcast_block_destruction(player.id(), pos, -1);
312 self.destroy_and_ack(player, world, pos);
313 return;
314 }
315
316 if !self.has_delayed_destroy {
317 self.is_destroying_block = false;
319 self.has_delayed_destroy = true;
320 self.delayed_destroy_pos = pos;
321 self.delayed_tick_start = self.destroy_progress_start;
322 }
323 }
324 }
325 }
326
327 BlockBreakAction::Abort => {
328 self.is_destroying_block = false;
329
330 if self.destroy_pos != pos {
331 log::warn!(
332 "Mismatch in destroy block pos: {:?} vs {:?}",
333 self.destroy_pos,
334 pos
335 );
336 world.broadcast_block_destruction(player.id(), self.destroy_pos, -1);
337 }
338
339 world.broadcast_block_destruction(player.id(), pos, -1);
340 }
341 }
342 }
343
344 fn destroy_and_ack(&mut self, player: &Player, world: &Arc<World>, pos: BlockPos) {
346 if !self.destroy_block(player, world, pos) {
347 player.send_packet(CBlockUpdate {
349 pos,
350 block_state: world.get_block_state(pos),
351 });
352 }
353 }
354
355 #[expect(
359 clippy::unused_self,
360 reason = "method belongs logically to BlockBreakingManager and will use self when additional state is added"
361 )]
362 fn destroy_block(&self, player: &Player, world: &Arc<World>, pos: BlockPos) -> bool {
363 let state = world.get_block_state(pos);
364
365 if player.get_abilities().instabuild && !held_item_can_destroy_in_creative(player) {
366 return false;
367 }
368
369 let Some(_block) = REGISTRY.blocks.by_state_id(state) else {
374 return false;
375 };
376
377 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
381 let adjusted_state = behavior.player_will_destroy(state, world, pos, player);
382 world.game_event(
383 &vanilla_game_events::BLOCK_DESTROY,
384 pos,
385 &GameEventContext::new(Some(player), Some(adjusted_state)),
386 );
387 let state_after_player_will_destroy = world.get_block_state(pos);
388
389 let replacement = fluid_state_to_block(state.get_fluid_state());
392
393 let block_entity = world.get_block_entity(pos);
396
397 let removed_by_player_break = !state_after_player_will_destroy.is_air()
400 && world.set_block_if_unchanged(
401 pos,
402 state_after_player_will_destroy,
403 replacement,
404 UpdateFlags::UPDATE_ALL,
405 ) == ConditionalBlockSetResult::Changed;
406 let changed_by_player_will_destroy = state_after_player_will_destroy != state;
407 let changed = changed_by_player_will_destroy || removed_by_player_break;
408
409 if removed_by_player_break {
410 behavior.destroy(adjusted_state, world, pos);
411
412 let block = REGISTRY.blocks.by_state_id(adjusted_state);
415 let is_fire = block.is_some_and(|b| {
416 b.key == vanilla_blocks::FIRE.key || b.key == vanilla_blocks::SOUL_FIRE.key
417 });
418 if !is_fire {
419 world.destroy_block_effect(pos, u32::from(adjusted_state.0), Some(player.id()));
420 }
421
422 let (has_correct_tool, destroyed_with) = {
425 let inv = player.inventory.lock();
426 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
427 (
428 main_hand.is_correct_tool_for_drops(adjusted_state)
429 || !requires_correct_tool(adjusted_state),
430 main_hand.copy_with_count(main_hand.count),
431 )
432 };
433
434 let block_destroy_time = REGISTRY
437 .blocks
438 .by_state_id(adjusted_state)
439 .map_or(0.0, |b| b.config.destroy_time);
440
441 if block_destroy_time != 0.0 {
442 let broke = {
443 let mut inv = player.inventory.lock();
444 let damage_per_block = inv.get_selected_item().get_tool_damage_per_block();
445
446 if damage_per_block > 0 {
447 let has_infinite_materials = player.has_infinite_materials();
450 inv.with_selected_item_mut(|main_hand| {
451 let item = main_hand.item;
452 main_hand
453 .hurt_and_break(damage_per_block, has_infinite_materials)
454 .then_some(item)
455 })
456 } else {
457 None
458 }
459 };
460
461 if let Some(item) = broke {
462 player.on_equipped_item_broken(item, EquipmentSlot::MainHand);
463 log::debug!("Tool broke while mining block at {pos:?}");
464 }
465 }
466
467 let game_mode = player.game_mode();
469 if game_mode != GameType::Spectator
470 && game_mode != GameType::Creative
471 && has_correct_tool
472 {
473 player.award_stat(&vanilla_stat_types::BLOCK_MINED, state.get_block());
474 player.cause_food_exhaustion(food_constants::EXHAUSTION_MINE);
475
476 drop_block_loot(
477 player,
478 world,
479 pos,
480 adjusted_state,
481 &destroyed_with,
482 block_entity.as_deref(),
483 );
484 behavior.player_destroy(
485 world,
486 player,
487 pos,
488 adjusted_state,
489 block_entity.as_ref(),
490 &destroyed_with,
491 );
492 }
493 }
494
495 changed
496 }
497}
498
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum BlockBreakAction {
502 Start,
504 Stop,
506 Abort,
508}
509
510fn held_item_can_destroy_in_creative(player: &Player) -> bool {
511 let inventory = player.inventory.lock();
512 let main_hand = inventory.get_item_in_hand(InteractionHand::MainHand);
513 main_hand.can_destroy_blocks_in_creative()
514}
515
516fn is_air(state: BlockStateId) -> bool {
518 let Some(block) = REGISTRY.blocks.by_state_id(state) else {
519 return true;
520 };
521 block.config.is_air
522}
523
524fn requires_correct_tool(state: BlockStateId) -> bool {
526 let Some(block) = REGISTRY.blocks.by_state_id(state) else {
527 return false;
528 };
529 block.config.requires_correct_tool_for_drops
530}
531
532fn get_destroy_progress(player: &Player, block_state: BlockStateId) -> f32 {
539 let Some(block) = REGISTRY.blocks.by_state_id(block_state) else {
540 return 0.0;
541 };
542
543 let destroy_time = block.config.destroy_time;
544
545 if player.game_mode() == GameType::Creative {
547 return 1.0;
548 }
549
550 if destroy_time < 0.0 {
552 return 0.0;
553 }
554
555 if destroy_time == 0.0 {
557 return 1.0;
558 }
559
560 let mining_speed = {
562 let inv = player.inventory.lock();
563 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
564 main_hand.get_destroy_speed(block_state)
565 };
566
567 let has_correct_tool = {
569 let inv = player.inventory.lock();
570 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
571 main_hand.is_correct_tool_for_drops(block_state)
572 };
573
574 let speed = mining_speed;
576
577 let divisor = if has_correct_tool || !block.config.requires_correct_tool_for_drops {
584 30.0
585 } else {
586 100.0
587 };
588
589 speed / destroy_time / divisor
590}
591
592fn drop_block_loot(
594 player: &Player,
595 world: &Arc<World>,
596 pos: BlockPos,
597 state: BlockStateId,
598 tool: &ItemStack,
599 block_entity: Option<&dyn BlockEntity>,
600) {
601 let luck = player
602 .attributes()
603 .lock()
604 .get_value(vanilla_attributes::LUCK)
605 .unwrap_or(0.0) as f32;
606
607 let drops = BlockLootContext::new(world, pos)
608 .with_luck(luck)
609 .with_tool(tool)
610 .with_block_entity(block_entity)
611 .get_drops(state);
612
613 for item in drops {
615 if !item.is_empty() {
616 player.get_world().pop_resource(pos, item);
617 }
618 }
619
620 BLOCK_BEHAVIORS
621 .get_behavior(state.get_block())
622 .spawn_after_break(state, world, pos, tool, true);
623}