diff --git a/crates/pumpkin-protocol/src/bedrock/client/level_event.rs b/crates/pumpkin-protocol/src/bedrock/client/level_event.rs index 7555c27af..8009925b8 100644 --- a/crates/pumpkin-protocol/src/bedrock/client/level_event.rs +++ b/crates/pumpkin-protocol/src/bedrock/client/level_event.rs @@ -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, diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 6e085c792..b626a1997 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -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, /// 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) { diff --git a/crates/pumpkin/src/net/bedrock/play.rs b/crates/pumpkin/src/net/bedrock/play.rs index aaa29f2e1..9a3bfa0a0 100644 --- a/crates/pumpkin/src/net/bedrock/play.rs +++ b/crates/pumpkin/src/net/bedrock/play.rs @@ -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; diff --git a/crates/pumpkin/src/net/java/play.rs b/crates/pumpkin/src/net/java/play.rs index 9f96ed479..8f44e48b7 100644 --- a/crates/pumpkin/src/net/java/play.rs +++ b/crates/pumpkin/src/net/java/play.rs @@ -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 diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index d819d006d..da025412f 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -230,6 +230,13 @@ pub struct World { pub block_entities: DashMap, FxHashMap>>, } +#[derive(Clone, Copy)] +pub(crate) enum BlockBreakingProgress { + Start { stage: i32, speed: f32 }, + Update { stage: i32, speed: Option }, + 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); // 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); + } +}