Calculate block breaking speed

Also broadcast current held item. The block progress is already correctly displayed and calculated for tools. But for some reason the destruction does not work, looking at vanilla code i have no idea what i'm doing wrong
This commit is contained in:
Alexander Medvedev 2025-02-15 22:47:14 +01:00
parent 94dc37a275
commit 5a77b21d22
15 changed files with 28167 additions and 61 deletions

File diff suppressed because it is too large Load Diff

View File

@ -115,6 +115,11 @@ impl PlayerInventory {
self.items[self.selected as usize + 36 - 9].as_ref()
}
pub async fn get_mining_speed(&self, block_name: &str) -> f32 {
self.held_item()
.map_or_else(|| 1.0, |e| e.get_speed(block_name))
}
pub fn held_item_mut(&mut self) -> &mut Option<ItemStack> {
debug_assert!((0..9).contains(&self.selected));
&mut self.items[self.selected as usize + 36 - 9]

View File

@ -12,7 +12,7 @@ use bytes::{Buf, BufMut};
mod deserializer;
use thiserror::Error;
pub mod packet;
mod serializer;
pub mod serializer;
#[derive(Debug, Error)]
pub enum ReadingError {

View File

@ -11,11 +11,11 @@ use crate::VarInt;
pub struct CSetBlockDestroyStage {
entity_id: VarInt,
location: BlockPos,
destroy_stage: u8,
destroy_stage: i8,
}
impl CSetBlockDestroyStage {
pub fn new(entity_id: VarInt, location: BlockPos, destroy_stage: u8) -> Self {
pub fn new(entity_id: VarInt, location: BlockPos, destroy_stage: i8) -> Self {
Self {
entity_id,
location,

View File

@ -53,6 +53,7 @@ mod set_border_warning_distance;
mod set_container_content;
mod set_container_property;
mod set_container_slot;
mod set_equipment;
mod set_experience;
mod set_health;
mod set_held_item;
@ -129,6 +130,7 @@ pub use set_border_warning_distance::*;
pub use set_container_content::*;
pub use set_container_property::*;
pub use set_container_slot::*;
pub use set_equipment::*;
pub use set_experience::*;
pub use set_health::*;
pub use set_held_item::*;

View File

@ -0,0 +1,58 @@
use crate::bytebuf::{serializer::Serializer, ByteBufMut};
use bytes::BytesMut;
use pumpkin_data::packet::clientbound::PLAY_SET_EQUIPMENT;
use pumpkin_macros::client_packet;
use serde::Serialize;
use crate::{
codec::{slot::Slot, var_int::VarInt},
ClientPacket,
};
#[client_packet(PLAY_SET_EQUIPMENT)]
pub struct CSetEquipment {
entity_id: VarInt,
equipment: Vec<(EquipmentSlot, Slot)>,
}
impl CSetEquipment {
pub fn new(entity_id: VarInt, equipment: Vec<(EquipmentSlot, Slot)>) -> Self {
Self {
entity_id,
equipment,
}
}
}
impl ClientPacket for CSetEquipment {
fn write(&self, bytebuf: &mut impl bytes::BufMut) {
bytebuf.put_var_int(&self.entity_id);
for i in 0..self.equipment.len() {
let equipment = &self.equipment[i];
let slot = &equipment.0;
if i != self.equipment.len() - 1 {
bytebuf.put_i8(-128);
} else {
bytebuf.put_i8(*slot as i8);
}
let buf = BytesMut::new();
let mut serializer = Serializer::new(buf);
equipment
.1
.serialize(&mut serializer)
.expect("Could not serialize packet");
bytebuf.put(serializer.output);
}
}
}
#[derive(Clone, Copy)]
pub enum EquipmentSlot {
MainHand,
OffHand,
Feet,
Legs,
Chest,
Head,
Body,
}

View File

@ -33,7 +33,7 @@ pub fn total_to_level_and_points(total_points: i32) -> (i32, i32) {
/// Calculate progress (0.0 to 1.0) from points within a level
pub fn progress_in_level(points: i32, level: i32) -> f32 {
let max_points = points_in_level(level);
#[allow(clippy::cast_precision_loss)]
let progress = (points as f32) / (max_points as f32);
progress.clamp(0.0, 1.0)
}

View File

@ -135,6 +135,7 @@ pub struct State {
pub luminance: u8,
pub burnable: bool,
pub tool_required: bool,
pub hardness: f32,
pub opacity: Option<u32>,
pub replaceable: bool,
pub collision_shapes: Vec<u16>,

View File

@ -28,6 +28,19 @@ impl ItemStack {
Self { item_count, item }
}
pub fn get_speed(&self, block: &str) -> f32 {
if let Some(tool) = self.item.components.tool {
for rule in tool.rules {
if rule.speed.is_none() || !rule.blocks.contains(&block) {
continue;
}
return rule.speed.unwrap();
}
return tool.default_mining_speed.unwrap_or(1.0);
}
1.0
}
pub fn is_correct_for_drops(&self, block: &str) -> bool {
if let Some(tool) = self.item.components.tool {
for rule in tool.rules {

View File

@ -20,16 +20,16 @@ use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::block::registry::Block;
use pumpkin_world::block::registry::{Block, State};
use pumpkin_world::item::ItemStack;
use rand::Rng;
use crate::block::blocks::crafting_table::CraftingTableBlock;
use crate::block::blocks::jukebox::JukeboxBlock;
use crate::block::registry::BlockRegistry;
use crate::entity::item::ItemEntity;
use crate::server::Server;
use crate::world::World;
use crate::{block::blocks::crafting_table::CraftingTableBlock, entity::player::Player};
use std::sync::Arc;
mod blocks;
@ -68,6 +68,22 @@ pub async fn drop_loot(server: &Server, world: &Arc<World>, block: &Block, pos:
item_entity.send_meta_packet().await;
}
pub async fn calc_block_breaking(player: &Player, state: &State, block_name: &str) -> f32 {
let hardness = state.hardness;
#[expect(clippy::float_cmp)]
if hardness == -1.0 {
// unbreakable
return 0.0;
}
let i = if player.can_harvest(state, block_name).await {
30
} else {
100
};
player.get_mining_speed(block_name).await / hardness / i as f32
}
#[must_use]
pub fn default_block_properties_manager() -> Arc<BlockPropertiesManager> {
let mut manager = BlockPropertiesManager::default();

View File

@ -4,8 +4,14 @@ use async_trait::async_trait;
use crossbeam::atomic::AtomicCell;
use pumpkin_data::{damage::DamageType, sound::Sound};
use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::client::play::{CDamageEvent, CEntityStatus, MetaDataType, Metadata};
use pumpkin_protocol::{
client::play::{
CDamageEvent, CEntityStatus, CSetEquipment, EquipmentSlot, MetaDataType, Metadata,
},
codec::slot::Slot,
};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
use super::{Entity, EntityId, NBTStorage};
@ -49,6 +55,19 @@ impl LivingEntity {
}
}
pub async fn send_equipment_changes(&self, equipment: Vec<(EquipmentSlot, ItemStack)>) {
let equipment: Vec<(EquipmentSlot, Slot)> = equipment
.iter()
.map(|(slot, stack)| (*slot, Slot::from(stack)))
.collect();
self.entity
.world
.read()
.await
.broadcast_packet_all(&CSetEquipment::new(self.entity_id().into(), equipment))
.await;
}
pub fn set_pos(&self, position: Vector3<f64>) {
self.last_pos.store(self.entity.pos.load());
self.entity.set_pos(position);

View File

@ -2,7 +2,7 @@ use pumpkin_world::block::registry::State;
use std::{
num::NonZeroU8,
sync::{
atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, AtomicU8, Ordering},
atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, Ordering},
Arc,
},
time::{Duration, Instant},
@ -23,10 +23,10 @@ use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::{
bytebuf::packet::Packet,
client::play::{
CActionBar, CCombatDeath, CDisguisedChatMessage, CEntityStatus, CGameEvent, CHurtAnimation,
CKeepAlive, CParticle, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate,
CPlayerPosition, CRespawn, CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage,
CTitleText, CUnloadChunk, GameEvent, MetaDataType, PlayerAction,
CAcknowledgeBlockChange, CActionBar, CCombatDeath, CDisguisedChatMessage, CEntityStatus,
CGameEvent, CHurtAnimation, CKeepAlive, CParticle, CPlayDisconnect, CPlayerAbilities,
CPlayerInfoUpdate, CPlayerPosition, CRespawn, CSetExperience, CSetHealth, CSubtitle,
CSystemChatMessage, CTitleText, CUnloadChunk, GameEvent, MetaDataType, PlayerAction,
},
server::play::{
SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd,
@ -70,6 +70,7 @@ use super::{
Entity, EntityBase, EntityId, NBTStorage,
};
use crate::{
block,
command::{client_suggestions, dispatcher::CommandDispatcher},
data::op_data::OPERATOR_CONFIG,
net::{Client, PlayerConfig},
@ -102,7 +103,6 @@ pub struct Player {
pub open_container: AtomicCell<Option<u64>>,
/// The item currently being held by the player.
pub carried_item: AtomicCell<Option<ItemStack>>,
/// send `send_abilities_update` when changed
/// The player's abilities and special powers.
///
@ -110,9 +110,14 @@ pub struct Player {
///
/// **Note:** When the `abilities` field is updated, the server should send a `send_abilities_update` packet to the client to notify them of the changes.
pub abilities: Mutex<Abilities>,
/// The current stage of the block the player is breaking.
pub current_block_destroy_stage: AtomicU8,
pub current_block_destroy_stage: AtomicI32,
/// Indicates if the player is currently mining a block.
pub mining: AtomicBool,
pub start_mining_time: AtomicI32,
pub tick_counter: AtomicI32,
pub packet_sequence: AtomicI32,
pub mining_pos: Mutex<BlockPos>,
/// A counter for teleport IDs used to track pending teleports.
pub teleport_id_count: AtomicI32,
/// The pending teleport information, including the teleport ID and target location.
@ -189,10 +194,15 @@ impl Player {
awaiting_teleport: Mutex::new(None),
// TODO: Load this from previous instance
hunger_manager: HungerManager::default(),
current_block_destroy_stage: AtomicU8::new(0),
current_block_destroy_stage: AtomicI32::new(-1),
open_container: AtomicCell::new(None),
tick_counter: AtomicI32::new(0),
packet_sequence: AtomicI32::new(-1),
start_mining_time: AtomicI32::new(0),
carried_item: AtomicCell::new(None),
teleport_id_count: AtomicI32::new(0),
mining: AtomicBool::new(false),
mining_pos: Mutex::new(BlockPos(Vector3::new(0, 0, 0))),
abilities: Mutex::new(Abilities::default()),
gamemode: AtomicCell::new(gamemode),
// We want this to be an impossible watched section so that `player_chunker::update_position`
@ -443,7 +453,39 @@ impl Player {
{
return;
}
let now = Instant::now();
if self.packet_sequence.load(Ordering::Relaxed) > -1 {
self.client
.send_packet(&CAcknowledgeBlockChange::new(
self.packet_sequence.swap(-1, Ordering::Relaxed).into(),
))
.await;
}
self.tick_counter.fetch_add(1, Ordering::Relaxed);
if self.mining.load(Ordering::Relaxed) {
let pos = self.mining_pos.lock().await;
let world = self.world().await;
let block = world.get_block(&pos).await.unwrap();
let state = world.get_block_state(&pos).await.unwrap();
// Is block broken ?
if state.air {
world.set_block_breaking(self.entity_id(), *pos, -1).await;
self.current_block_destroy_stage
.store(-1, Ordering::Relaxed);
self.mining.store(false, Ordering::Relaxed);
} else {
self.continue_mining(
*pos,
&world,
state,
&block.name,
self.start_mining_time.load(Ordering::Relaxed),
)
.await;
}
}
self.last_attacked_ticks
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
@ -452,6 +494,8 @@ impl Player {
// timeout/keep alive handling
self.tick_client_load_timeout();
let now = Instant::now();
if now.duration_since(self.last_keep_alive_time.load()) >= Duration::from_secs(15) {
// We never got a response from our last keep alive we send
if self
@ -472,6 +516,26 @@ impl Player {
}
}
async fn continue_mining(
&self,
location: BlockPos,
world: &World,
state: &State,
block_name: &str,
starting_time: i32,
) {
let time = self.tick_counter.load(Ordering::Relaxed) - starting_time;
let speed = block::calc_block_breaking(self, state, block_name).await * (time + 1) as f32;
let progress = (speed * 10.0) as i32;
if progress != self.current_block_destroy_stage.load(Ordering::Relaxed) {
world
.set_block_breaking(self.entity_id(), location, progress)
.await;
self.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
}
}
pub async fn jump(&self) {
if self.living_entity.entity.sprinting.load(Ordering::Relaxed) {
self.add_exhaustion(0.2).await;
@ -508,7 +572,6 @@ impl Player {
}
pub fn get_attack_cooldown_progress(&self, base_time: f64, attack_speed: f64) -> f64 {
#[allow(clippy::cast_precision_loss)]
let x = f64::from(
self.last_attacked_ticks
.load(std::sync::atomic::Ordering::Acquire),
@ -849,7 +912,6 @@ impl Player {
))
.await;
#[allow(clippy::cast_precision_loss)]
self.client
.send_packet(&CGameEvent::new(
GameEvent::ChangeGameMode,
@ -885,6 +947,20 @@ impl Player {
.map_or_else(|| false, |e| e.is_correct_for_drops(block_name))
}
pub async fn get_mining_speed(&self, block_name: &str) -> f32 {
let mut speed = self
.inventory
.lock()
.await
.get_mining_speed(block_name)
.await;
// TODO: Handle effects
if !self.living_entity.entity.on_ground.load(Ordering::Relaxed) {
speed /= 5.0;
}
speed
}
pub async fn send_message(
&self,
message: &TextComponent,
@ -929,7 +1005,6 @@ impl Player {
}
/// Sets the player's experience level and updates the client
#[allow(clippy::cast_precision_loss)]
pub async fn set_experience(&self, level: i32, progress: f32, points: i32) {
self.experience_level.store(level, Ordering::Relaxed);
self.experience_progress.store(progress.clamp(0.0, 1.0));
@ -945,7 +1020,6 @@ impl Player {
}
/// Sets the player's experience level directly
#[allow(clippy::cast_precision_loss)]
pub async fn set_experience_level(&self, new_level: i32, keep_progress: bool) {
let progress = self.experience_progress.load();
let mut points = self.experience_points.load(Ordering::Relaxed);
@ -974,7 +1048,6 @@ impl Player {
}
/// Set the player's experience points directly, Returns true if successful.
#[allow(clippy::cast_precision_loss)]
pub async fn set_experience_points(&self, new_points: i32) -> bool {
let current_points = self.experience_points.load(Ordering::Relaxed);

View File

@ -20,6 +20,7 @@
#![deny(clippy::print_stderr)]
// REMOVE SOME WHEN RELEASE
#![expect(clippy::cargo_common_metadata)]
#![expect(clippy::cast_precision_loss)]
#![expect(clippy::multiple_crate_versions)]
#![expect(clippy::single_call_fn)]
#![expect(clippy::cast_sign_loss)]

View File

@ -1,6 +1,7 @@
use std::num::NonZeroU8;
use std::sync::Arc;
use crate::block;
use crate::block::properties::Direction;
use crate::block::registry::BlockActionResult;
use crate::entity::mob;
@ -22,16 +23,15 @@ use pumpkin_inventory::player::PlayerInventory;
use pumpkin_inventory::InventoryError;
use pumpkin_macros::block_entity;
use pumpkin_protocol::client::play::{
CBlockEntityData, COpenSignEditor, CSetContainerSlot, CSetHeldItem,
CBlockEntityData, COpenSignEditor, CSetContainerSlot, CSetHeldItem, EquipmentSlot,
};
use pumpkin_protocol::codec::slot::Slot;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::server::play::{SCookieResponse as SPCookieResponse, SUpdateSign};
use pumpkin_protocol::{
client::play::{
Animation, CAcknowledgeBlockChange, CCommandSuggestions, CEntityAnimation, CHeadRot,
CPingResponse, CPlayerChatMessage, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot,
FilterType,
Animation, CCommandSuggestions, CEntityAnimation, CHeadRot, CPingResponse,
CPlayerChatMessage, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, FilterType,
},
server::play::{
Action, ActionType, SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay,
@ -481,6 +481,10 @@ impl Player {
// Update held item
inventory.set_selected(dest_slot as u32);
let empty = &ItemStack::new(0, Item::AIR);
let stack = inventory.held_item().unwrap_or(empty);
let equipment = vec![(EquipmentSlot::MainHand, *stack)];
self.living_entity.send_equipment_changes(equipment).await;
self.client
.send_packet(&CSetHeldItem::new(dest_slot as i8))
.await;
@ -801,6 +805,7 @@ impl Player {
}
}
#[expect(clippy::too_many_lines)]
pub async fn handle_player_action(
self: Arc<Self>,
player_action: SPlayerAction,
@ -820,14 +825,16 @@ impl Player {
);
return;
}
// TODO: do validation
// TODO: Config
if self.gamemode.load() == GameMode::Creative {
let location = player_action.location;
// Block break & block break sound
let entity = &self.living_entity.entity;
let world = &entity.world.read().await;
let block = world.get_block(&location).await;
let state = world.get_block_state(&location).await.unwrap();
// TODO: do validation
// TODO: Config
if self.gamemode.load() == GameMode::Creative {
// Block break & block break sound
world
.break_block(server, &location, Some(self.clone()), false)
@ -838,10 +845,38 @@ impl Player {
.broken(block, &self, location, server)
.await;
}
return;
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
self.start_mining_time.store(
self.tick_counter.load(std::sync::atomic::Ordering::Relaxed),
std::sync::atomic::Ordering::Relaxed,
);
if let Ok(block) = block {
if !state.air {
let speed = block::calc_block_breaking(&self, state, &block.name).await;
// Instant break
if speed >= 1.0 {
world
.break_block(server, &location, Some(self.clone()), true)
.await;
server
.block_registry
.broken(block, &self, location, server)
.await;
} else {
self.mining
.store(true, std::sync::atomic::Ordering::Relaxed);
*self.mining_pos.lock().await = location;
let progress = (speed * 10.0) as i32;
world
.set_block_breaking(self.entity_id(), location, progress)
.await;
self.current_block_destroy_stage
.store(progress, std::sync::atomic::Ordering::Relaxed);
}
}
}
self.update_sequence(player_action.sequence.0);
}
Status::CancelledDigging => {
if !self.can_interact_with_block_at(&player_action.location, 1.0) {
@ -852,13 +887,17 @@ impl Player {
);
return;
}
self.current_block_destroy_stage
.store(0, std::sync::atomic::Ordering::Relaxed);
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
self.mining
.store(false, std::sync::atomic::Ordering::Relaxed);
let entity = &self.living_entity.entity;
let world = &entity.world.read().await;
world
.set_block_breaking(self.entity_id(), player_action.location, -1)
.await;
self.update_sequence(player_action.sequence.0);
}
Status::FinishedDigging => {
dbg!("fuff");
// TODO: do validation
let location = player_action.location;
if !self.can_interact_with_block_at(&location, 1.0) {
@ -872,6 +911,11 @@ impl Player {
// Block break & block break sound
let entity = &self.living_entity.entity;
let world = &entity.world.read().await;
self.mining
.store(false, std::sync::atomic::Ordering::Relaxed);
world
.set_block_breaking(self.entity_id(), location, -1)
.await;
let block = world.get_block(&location).await;
let state = world.get_block_state(&location).await;
if let Ok(block) = block {
@ -887,9 +931,7 @@ impl Player {
.broken(block, &self, location, server)
.await;
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
.await;
self.update_sequence(player_action.sequence.0);
}
Status::DropItemStack | Status::DropItem => {
self.drop_item(server).await;
@ -918,6 +960,18 @@ impl Player {
}
}
pub fn update_sequence(&self, sequence: i32) {
if sequence < 0 {
log::error!("Expected packet sequence >= 0");
}
self.packet_sequence.store(
self.packet_sequence
.load(std::sync::atomic::Ordering::Relaxed)
.max(sequence),
std::sync::atomic::Ordering::Relaxed,
);
}
pub async fn handle_player_abilities(&self, player_abilities: SPlayerAbilities) {
let mut abilities = self.abilities.lock().await;
@ -943,6 +997,7 @@ impl Player {
if !self.has_client_loaded() {
return Ok(());
}
self.update_sequence(use_item_on.sequence.0);
let location = use_item_on.location;
let mut should_try_decrement = false;
@ -986,9 +1041,6 @@ impl Player {
.on_interact(block, block_state, &ItemStack::new(0, Item::AIR))
.await;
world.set_block_state(&location, new_state).await;
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
}
return Ok(());
};
@ -1008,9 +1060,6 @@ impl Player {
.on_interact(block, block_state, &stack)
.await;
world.set_block_state(&location, new_state).await;
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence.clone()))
.await;
match action_result {
BlockActionResult::Continue => {}
BlockActionResult::Consume => {
@ -1093,7 +1142,12 @@ impl Player {
self.kick(TextComponent::text("Invalid held slot")).await;
return;
}
self.inventory().lock().await.set_selected(slot as u32);
let mut inv = self.inventory().lock().await;
inv.set_selected(slot as u32);
let empty = &ItemStack::new(0, Item::AIR);
let stack = inv.held_item().unwrap_or(empty);
let equipment = vec![(EquipmentSlot::MainHand, *stack)];
self.living_entity.send_equipment_changes(equipment).await;
}
pub async fn handle_set_creative_slot(
@ -1253,9 +1307,6 @@ impl Player {
// check block under the world
if location.0.y + face.to_offset().y < WORLD_LOWEST_Y.into() {
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
return Err(BlockPlacingError::BlockOutOfWorld.into());
}
@ -1270,17 +1321,11 @@ impl Player {
true,
)
.await;
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
return Err(BlockPlacingError::BlockOutOfWorld.into());
}
match self.gamemode.load() {
GameMode::Spectator | GameMode::Adventure => {
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
return Err(BlockPlacingError::InvalidGamemode.into());
}
_ => {}
@ -1356,10 +1401,6 @@ impl Player {
.on_placed(&block, self, final_block_pos, server)
.await;
self.client
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence))
.await;
self.send_sign_packet(block, final_block_pos, face).await;
// Block was placed successfully, decrement inventory
return Ok(true);

View File

@ -28,7 +28,9 @@ use pumpkin_data::{
world::WorldEvent,
};
use pumpkin_macros::send_cancellable;
use pumpkin_protocol::client::play::{CBlockUpdate, CDisguisedChatMessage, CRespawn, CWorldEvent};
use pumpkin_protocol::client::play::{
CBlockUpdate, CDisguisedChatMessage, CRespawn, CSetBlockDestroyStage, CWorldEvent,
};
use pumpkin_protocol::{client::play::CLevelEvent, codec::identifier::Identifier};
use pumpkin_protocol::{
client::play::{
@ -985,6 +987,15 @@ impl World {
.await;
}
pub async fn set_block_breaking(&self, entity_id: EntityId, location: BlockPos, progress: i32) {
self.broadcast_packet_all(&CSetBlockDestroyStage::new(
entity_id.into(),
location,
progress as i8,
))
.await;
}
/// Sets a block
pub async fn set_block_state(&self, position: &BlockPos, block_state_id: u16) -> u16 {
let (chunk_coordinate, relative_coordinates) = position.chunk_and_chunk_relative_position();