mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix: Bedrock block breaking feedback (#2829)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user