fix: Bedrock block breaking feedback (#2829)

This commit is contained in:
ZlordHUN
2026-08-08 16:14:10 +02:00
committed by GitHub
parent e5047bd4c9
commit 50fe9f7cd7
5 changed files with 300 additions and 84 deletions

View File

@@ -14,6 +14,7 @@ pub struct CLevelEvent {
#[repr(i32)]
pub enum LevelEvent {
// There are hundreds of these, adding only what we need for now
ParticlesDestroyBlock = 2001,
BlockStartBreak = 3600,
BlockStopBreak = 3601,
BlockUpdateBreak = 3602,

View File

@@ -105,7 +105,7 @@ use crate::plugin::player::player_permission_check::PlayerPermissionCheckEvent;
use crate::plugin::player::player_teleport::PlayerTeleportEvent;
use crate::plugin::server::packet::PacketSentEvent;
use crate::server::Server;
use crate::world::World;
use crate::world::{BlockBreakingProgress, World};
use bytes::Bytes;
use super::breath::BreathManager;
@@ -475,6 +475,8 @@ pub struct Player {
pub stats: Mutex<statistics::Statistics>,
/// The current stage of block destruction of the block the player is breaking.
pub current_block_destroy_stage: AtomicI32,
/// The per-tick block destruction progress last sent to Bedrock clients.
pub current_block_breaking_speed: AtomicU32,
/// Indicates if the player is currently mining a block.
pub mining: AtomicBool,
pub start_mining_time: AtomicI32,
@@ -689,6 +691,7 @@ impl Player {
// TODO: Load this from previous instance
hunger_manager: HungerManager::default(),
current_block_destroy_stage: AtomicI32::new(-1),
current_block_breaking_speed: AtomicU32::new(0),
enchantment_seed: AtomicI32::new(rand::random()),
open_container: AtomicCell::new(None),
open_container_pos: AtomicCell::new(None),
@@ -2092,25 +2095,65 @@ impl Player {
}
if self.mining.load(Ordering::Relaxed) {
let pos = self.mining_pos.lock().await;
let pos = *self.mining_pos.lock().await;
let world = self.world();
let state = world.get_block_state(&pos);
// Is the block broken?
if state.is_air() {
world
.set_block_breaking(&self.living_entity.entity, *pos, -1)
.set_block_breaking(
&self.living_entity.entity,
pos,
BlockBreakingProgress::Stop,
)
.await;
self.current_block_destroy_stage
.store(-1, Ordering::Relaxed);
self.mining.store(false, Ordering::Relaxed);
} else {
self.continue_mining(
*pos,
&world,
state,
self.start_mining_time.load(Ordering::Relaxed),
)
.await;
let finished = self
.continue_mining(
pos,
&world,
state,
self.start_mining_time.load(Ordering::Relaxed),
)
.await;
if finished && matches!(self.client.as_ref(), ClientPlatform::Bedrock(_)) {
self.mining.store(false, Ordering::Relaxed);
self.current_block_destroy_stage
.store(-1, Ordering::Relaxed);
world
.set_block_breaking(
&self.living_entity.entity,
pos,
BlockBreakingProgress::Stop,
)
.await;
let block = Block::from_state_id(state.id);
let can_harvest = self.can_harvest(state, block).await;
let flags = if can_harvest {
pumpkin_world::world::BlockFlags::NOTIFY_NEIGHBORS
} else {
pumpkin_world::world::BlockFlags::SKIP_DROPS
| pumpkin_world::world::BlockFlags::NOTIFY_NEIGHBORS
};
if world
.break_block(&pos, Some(self.clone()), flags)
.await
.is_some()
{
server
.block_registry
.broken(&world, block, self, &pos, server, state)
.await;
self.apply_tool_damage_for_block_break(state).await;
if can_harvest {
self.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
}
}
}
}
}
self.last_attacked_ticks.fetch_add(1, Ordering::Relaxed);
@@ -2156,18 +2199,31 @@ impl Player {
world: &World,
state: &BlockState,
starting_time: i32,
) {
) -> bool {
let time = self.tick_counter.load(Ordering::Relaxed) - starting_time;
let speed = block::calc_block_breaking(self, state, Block::from_state_id(state.id)).await
* (time + 1) as f32;
let progress = (speed * 10.0) as i32;
if progress != self.current_block_destroy_stage.load(Ordering::Relaxed) {
let speed = block::calc_block_breaking(self, state, Block::from_state_id(state.id)).await;
let total_progress = speed * (time + 1) as f32;
let stage = (total_progress * 10.0) as i32;
let stage = stage.min(9);
let old_speed = self
.current_block_breaking_speed
.swap(speed.to_bits(), Ordering::Relaxed);
let speed_changed = old_speed != speed.to_bits();
if stage != self.current_block_destroy_stage.load(Ordering::Relaxed) || speed_changed {
world
.set_block_breaking(&self.living_entity.entity, location, progress)
.set_block_breaking(
&self.living_entity.entity,
location,
BlockBreakingProgress::Update {
stage,
speed: speed_changed.then_some(speed),
},
)
.await;
self.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
.store(stage, Ordering::Relaxed);
}
total_progress >= 1.0
}
pub async fn jump(&self) {

View File

@@ -20,7 +20,7 @@ use pumpkin_protocol::{
bedrock::{
client::{
chunk_radius_update::CChunkRadiusUpdate, container_open::CContainerOpen,
player_hotbar::CPlayerHotbar,
player_hotbar::CPlayerHotbar, update_block::CUpdateBlock,
},
server::{
animate::{AnimateAction, SAnimate},
@@ -61,11 +61,13 @@ use crate::{
player_toggle_flight_event::PlayerToggleFlightEvent,
},
server::{Server, seasonal_events},
world::chunker::{self},
world::{BlockBreakingProgress, chunker},
};
use pumpkin_data::BlockDirection;
use tracing::{debug, info};
const MIN_PREDICTED_BREAK_PROGRESS: f32 = 0.65;
fn descriptor_to_stack(desc: &NetworkItemDescriptor, is_creative: bool) -> ItemStack {
if desc.id.0 == 0 || desc.stack_size == 0 {
ItemStack::EMPTY.clone()
@@ -216,6 +218,10 @@ impl BedrockClient {
return;
}
let entity = player.get_entity();
let on_ground = packet.input_data.get(InputData::VerticalCollision as usize)
&& packet.delta.y < 0.0
&& !entity.has_vehicle().await;
entity.on_ground.store(on_ground, Ordering::Relaxed);
let new_pos = packet
.position
@@ -234,7 +240,6 @@ impl BedrockClient {
if pos_changed || rot_changed {
let world = player.world();
let on_ground = entity.on_ground.load(std::sync::atomic::Ordering::Relaxed);
if pos_changed {
player.get_entity().set_pos(new_pos);
@@ -1128,13 +1133,50 @@ impl BedrockClient {
}
}
} else {
player.mining.store(true, Ordering::Relaxed);
*player.mining_pos.lock().await = location;
let progress = (speed * 10.0) as i32;
world.set_block_breaking(entity, location, progress).await;
player
.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
let mut mining_pos = player.mining_pos.lock().await;
let starts_breaking =
!player.mining.load(Ordering::Relaxed) || *mining_pos != location;
let progress = if starts_breaking {
player.start_mining_time.store(
player.tick_counter.load(Ordering::Relaxed),
Ordering::Relaxed,
);
player.mining.store(true, Ordering::Relaxed);
*mining_pos = location;
(speed * 10.0) as i32
} else {
player.current_block_destroy_stage.load(Ordering::Relaxed)
};
drop(mining_pos);
let old_speed = player
.current_block_breaking_speed
.swap(speed.to_bits(), Ordering::Relaxed);
if starts_breaking {
world
.set_block_breaking(
entity,
location,
BlockBreakingProgress::Start {
stage: progress,
speed,
},
)
.await;
player
.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
} else if old_speed != speed.to_bits() {
world
.set_block_breaking(
entity,
location,
BlockBreakingProgress::Update {
stage: progress,
speed: Some(speed),
},
)
.await;
}
}
}
}
@@ -1147,33 +1189,61 @@ impl BedrockClient {
let entity = &player.get_entity();
let world = entity.world.load_full();
player.mining.store(false, Ordering::Relaxed);
world.set_block_breaking(entity, location, -1).await;
let (block, state) = world.get_block_and_state(&location);
if player.gamemode.load() != GameMode::Creative {
let block_drop = player.can_harvest(state, block).await;
let new_state = world
.break_block(
&location,
Some(player.clone()),
if block_drop {
BlockFlags::NOTIFY_NEIGHBORS
} else {
BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_NEIGHBORS
},
)
.await;
if new_state.is_some() {
server
.block_registry
.broken(&world, block, player, &location, server, state)
if player.gamemode.load() != GameMode::Creative && !state.is_air() {
let speed = crate::block::calc_block_breaking(player, state, block).await;
let elapsed = player.tick_counter.load(Ordering::Relaxed)
- player.start_mining_time.load(Ordering::Relaxed)
+ 1;
let same_block = *player.mining_pos.lock().await == location;
if player.mining.load(Ordering::Relaxed)
&& same_block
&& speed * elapsed as f32 >= MIN_PREDICTED_BREAK_PROGRESS
{
player.mining.store(false, Ordering::Relaxed);
player
.current_block_destroy_stage
.store(-1, Ordering::Relaxed);
world
.set_block_breaking(entity, location, BlockBreakingProgress::Stop)
.await;
player.apply_tool_damage_for_block_break(state).await;
if block_drop {
player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
let can_harvest = player.can_harvest(state, block).await;
let flags = if can_harvest {
BlockFlags::NOTIFY_NEIGHBORS
} else {
BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_NEIGHBORS
};
if world
.break_block(&location, Some(player.clone()), flags)
.await
.is_some()
{
server
.block_registry
.broken(&world, block, player, &location, server, state)
.await;
player.apply_tool_damage_for_block_break(state).await;
if can_harvest {
player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
}
}
} else {
let runtime_id = pumpkin_data::BlockState::to_be_network_id(state.id);
self.enqueue_packet(&CUpdateBlock::new(location, runtime_id as u32))
.await;
world
.set_block_breaking(
entity,
location,
BlockBreakingProgress::Update {
stage: player
.current_block_destroy_stage
.load(Ordering::Relaxed),
speed: Some(speed),
},
)
.await;
}
}
}
@@ -1187,7 +1257,9 @@ impl BedrockClient {
let world = entity.world.load();
player.mining.store(false, Ordering::Relaxed);
world.set_block_breaking(entity, location, -1).await;
world
.set_block_breaking(entity, location, BlockBreakingProgress::Stop)
.await;
}
PlayerAction::DropItem => {
player.drop_held_item(false).await;

View File

@@ -38,7 +38,7 @@ use crate::block::entities::jigsaw_block::JigsawBlockEntity;
use crate::block::entities::sign::SignBlockEntity;
use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent;
use crate::server::{Server, seasonal_events};
use crate::world::{World, chunker};
use crate::world::{BlockBreakingProgress, World, chunker};
use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties};
use pumpkin_data::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl,
@@ -2052,7 +2052,19 @@ impl JavaClient {
player.mining.store(true, Ordering::Relaxed);
*player.mining_pos.lock().await = position;
let progress = (speed * 10.0) as i32;
world.set_block_breaking(entity, position, progress).await;
player
.current_block_breaking_speed
.store(speed.to_bits(), Ordering::Relaxed);
world
.set_block_breaking(
entity,
position,
BlockBreakingProgress::Start {
stage: progress,
speed,
},
)
.await;
player
.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
@@ -2074,7 +2086,11 @@ impl JavaClient {
entity
.world
.load()
.set_block_breaking(entity, player_action.position, -1)
.set_block_breaking(
entity,
player_action.position,
BlockBreakingProgress::Stop,
)
.await;
self.update_sequence(player, player_action.sequence.0);
}
@@ -2095,7 +2111,9 @@ impl JavaClient {
let world = entity.world.load_full();
player.mining.store(false, Ordering::Relaxed);
world.set_block_breaking(entity, location, -1).await;
world
.set_block_breaking(entity, location, BlockBreakingProgress::Stop)
.await;
let (block, state) = world.get_block_and_state(&location);
let block_drop = player.gamemode.load() != GameMode::Creative

View File

@@ -230,6 +230,13 @@ pub struct World {
pub block_entities: DashMap<Vector2<i32>, FxHashMap<BlockPos, Arc<dyn BlockEntity>>>,
}
#[derive(Clone, Copy)]
pub(crate) enum BlockBreakingProgress {
Start { stage: i32, speed: f32 },
Update { stage: i32, speed: Option<f32> },
Stop,
}
impl PartialEq for World {
fn eq(&self, other: &Self) -> bool {
self.uuid == other.uuid
@@ -4328,33 +4335,61 @@ impl World {
}
}
pub async fn set_block_breaking(&self, from: &Entity, location: BlockPos, progress: i32) {
pub(crate) async fn set_block_breaking(
&self,
from: &Entity,
location: BlockPos,
progress: BlockBreakingProgress,
) {
let chunk_pos = location.chunk_position(); // pumpkin's BlockPos already has this method
let je_packet = CSetBlockDestroyStage::new(from.entity_id.into(), location, progress as i8);
let (event_id, data) = match progress {
-1 => (LevelEvent::BlockStopBreak, 0),
0 => (LevelEvent::BlockStartBreak, 0),
_ => (LevelEvent::BlockUpdateBreak, progress),
};
let be_packet = CLevelEvent {
event_id: VarInt(event_id as i32),
position: Vector3::new(
location.0.x as f32,
location.0.y as f32,
location.0.z as f32,
let (stage, bedrock_event) = match progress {
BlockBreakingProgress::Start { stage, speed } => (
stage,
Some((
LevelEvent::BlockStartBreak,
bedrock_block_breaking_rate(speed),
)),
),
data: VarInt(data),
BlockBreakingProgress::Update { stage, speed } => (
stage,
speed.map(|speed| {
(
LevelEvent::BlockUpdateBreak,
bedrock_block_breaking_rate(speed),
)
}),
),
BlockBreakingProgress::Stop => (-1, Some((LevelEvent::BlockStopBreak, 0))),
};
let je_packet = CSetBlockDestroyStage::new(from.entity_id.into(), location, stage as i8);
self.broadcast_to_chunk_except_editioned(
chunk_pos,
&[from.entity_uuid],
&je_packet,
&be_packet,
)
.await;
if let Some((event_id, data)) = bedrock_event {
let be_packet = CLevelEvent {
event_id: VarInt(event_id as i32),
position: Vector3::new(
location.0.x as f32,
location.0.y as f32,
location.0.z as f32,
),
data: VarInt(data),
};
if let Some(player) = self.get_player_by_uuid(from.entity_uuid)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref()
{
client.enqueue_packet(&be_packet).await;
}
self.broadcast_to_chunk_except_editioned(
chunk_pos,
&[from.entity_uuid],
&je_packet,
&be_packet,
)
.await;
} else {
self.broadcast_to_chunk_except(chunk_pos, &[from.entity_uuid], &je_packet);
}
}
/// Sets a block and returns the old block id
@@ -4648,22 +4683,40 @@ impl World {
});
if Block::from_state_id(broken_state_id) != &Block::FIRE {
let particles_packet = CWorldEvent::new(
let je_particles_packet = CWorldEvent::new(
WorldEvent::ParticlesDestroyBlock as i32,
*position,
broken_state_id.as_u16().into(),
false,
);
let be_particles_packet = CLevelEvent {
event_id: VarInt(LevelEvent::ParticlesDestroyBlock as i32),
position: Vector3::new(
position.0.x as f32,
position.0.y as f32,
position.0.z as f32,
),
data: VarInt(BlockState::to_be_network_id(broken_state_id).into()),
};
let chunk_pos = position.chunk_position();
match &cause {
Some(player) => {
self.broadcast_to_chunk_except(
if let ClientPlatform::Bedrock(client) = player.client.as_ref() {
client.enqueue_packet(&be_particles_packet).await;
}
self.broadcast_to_chunk_except_editioned(
chunk_pos,
&[player.get_entity().entity_uuid],
&particles_packet,
);
&je_particles_packet,
&be_particles_packet,
)
.await;
}
None => self.broadcast_to_chunk(chunk_pos, &particles_packet),
None => self.broadcast_to_chunk_editioned_sync(
chunk_pos,
&je_particles_packet,
&be_particles_packet,
),
}
}
if !flags.contains(BlockFlags::SKIP_DROPS) {
@@ -5574,6 +5627,10 @@ impl BlockAccessor for World {
}
}
fn bedrock_block_breaking_rate(speed: f32) -> i32 {
(speed.clamp(0.0, 1.0) * f32::from(u16::MAX)) as i32
}
pub struct WorldPortal(pub Arc<World>);
// Pure Beauty :cap:
@@ -5650,3 +5707,15 @@ impl WorldPortalExt for WorldPortal {
});
}
}
#[cfg(test)]
mod tests {
use super::bedrock_block_breaking_rate;
#[test]
fn bedrock_block_breaking_rate_uses_progress_per_tick() {
assert_eq!(bedrock_block_breaking_rate(0.0), 0);
assert_eq!(bedrock_block_breaking_rate(1.0 / 30.0), 2_184);
assert_eq!(bedrock_block_breaking_rate(1.0), 65_535);
}
}