mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix(bedrock): synchronize Bedrock chest behavior (#2915)
* Fix Bedrock chest synchronization * Add Bedrock chest lid animations
This commit is contained in:
@@ -154,6 +154,7 @@ pub trait InventoryPlayer: Send + Sync {
|
||||
fn enqueue_inventory_packet<'a>(
|
||||
&'a self,
|
||||
packet: &'a CSetContainerContent,
|
||||
window_type: Option<WindowType>,
|
||||
) -> PlayerFuture<'a, ()>;
|
||||
|
||||
/// Sends a single slot update packet.
|
||||
|
||||
@@ -92,16 +92,19 @@ impl SyncHandler {
|
||||
) {
|
||||
if let Some(player) = self.player.lock().await.as_ref() {
|
||||
player
|
||||
.enqueue_inventory_packet(&CSetContainerContent::new(
|
||||
VarInt(screen_handler.sync_id.into()),
|
||||
VarInt(next_revision as i32),
|
||||
stacks
|
||||
.iter()
|
||||
.map(|stack| ItemStackSerializer::from(stack.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
&ItemStackSerializer::from(cursor_stack.clone()),
|
||||
))
|
||||
.enqueue_inventory_packet(
|
||||
&CSetContainerContent::new(
|
||||
VarInt(screen_handler.sync_id.into()),
|
||||
VarInt(next_revision as i32),
|
||||
stacks
|
||||
.iter()
|
||||
.map(|stack| ItemStackSerializer::from(stack.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
&ItemStackSerializer::from(cursor_stack.clone()),
|
||||
),
|
||||
screen_handler.window_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
for (i, property) in properties.iter().enumerate() {
|
||||
|
||||
44
crates/pumpkin-protocol/src/bedrock/client/block_event.rs
Normal file
44
crates/pumpkin-protocol/src/bedrock/client/block_event.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use pumpkin_macros::packet;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
|
||||
use crate::{codec::var_int::VarInt, serial::PacketWrite};
|
||||
|
||||
/// Updates a client-side block animation, such as a chest lid opening or closing.
|
||||
#[derive(PacketWrite)]
|
||||
#[packet(26)]
|
||||
pub struct CBlockEvent {
|
||||
pub position: BlockPos,
|
||||
pub event_type: VarInt,
|
||||
pub event_data: VarInt,
|
||||
}
|
||||
|
||||
impl CBlockEvent {
|
||||
#[must_use]
|
||||
pub const fn new(position: BlockPos, event_type: i32, event_data: i32) -> Self {
|
||||
Self {
|
||||
position,
|
||||
event_type: VarInt(event_type),
|
||||
event_data: VarInt(event_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
|
||||
use super::*;
|
||||
use crate::{Packet, serial::PacketWrite};
|
||||
|
||||
#[test]
|
||||
fn chest_lid_event_uses_bedrock_wire_format() {
|
||||
assert_eq!(<CBlockEvent as Packet>::PACKET_ID, 26);
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
CBlockEvent::new(BlockPos::new(1, 64, -2), 1, 3)
|
||||
.write(&mut encoded)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded, [2, 128, 1, 3, 2, 6]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::io::{Error, Write};
|
||||
use xxhash_rust::xxh64::xxh64;
|
||||
|
||||
use pumpkin_macros::packet;
|
||||
use pumpkin_nbt::{Nbt, compound::NbtCompound};
|
||||
use pumpkin_world::chunk::{ChunkData, palette::NetworkPalette};
|
||||
|
||||
use crate::{
|
||||
@@ -20,16 +21,26 @@ pub struct CLevelChunk<'a> {
|
||||
// https://gist.github.com/Tomcc/a96af509e275b1af483b25c543cfbf37
|
||||
// https://github.com/Mojang/bedrock-protocol-docs/blob/main/additional_docs/SubChunk%20Request%20System%20v1.18.10.md
|
||||
pub chunk: &'a ChunkData,
|
||||
pub block_actors: &'a [NbtCompound],
|
||||
}
|
||||
|
||||
pub type ChunkBlob = (u64, Vec<u8>);
|
||||
pub type EncodedChunk = (Vec<u8>, Vec<ChunkBlob>);
|
||||
|
||||
fn encode_block_actors(block_actors: &[NbtCompound]) -> Result<Vec<u8>, Error> {
|
||||
let mut encoded = Vec::new();
|
||||
for block_actor in block_actors {
|
||||
encoded.write_all(&Nbt::from(block_actor.clone()).write_bedrock())?;
|
||||
}
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
impl CLevelChunk<'_> {
|
||||
pub fn encode_chunk(
|
||||
chunk: &ChunkData,
|
||||
dimension: i32,
|
||||
cache_enabled: bool,
|
||||
block_actors: &[NbtCompound],
|
||||
) -> Result<EncodedChunk, Error> {
|
||||
let mut writer = Vec::new();
|
||||
|
||||
@@ -115,6 +126,8 @@ impl CLevelChunk<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
let block_actor_bytes = encode_block_actors(block_actors)?;
|
||||
|
||||
if cache_enabled {
|
||||
for subchunk_buf in subchunk_bytes_list {
|
||||
let hash = xxh64(&subchunk_buf, 0);
|
||||
@@ -128,9 +141,16 @@ impl CLevelChunk<'_> {
|
||||
writer.write_all(&hash.to_le_bytes())?;
|
||||
}
|
||||
|
||||
// Chunk data payload when cache_enabled: only border block count byte (0).
|
||||
VarUInt(1).write(&mut writer)?;
|
||||
// Palette data is cached, but the per-chunk border and block actor data is not.
|
||||
VarUInt(u32::try_from(1 + block_actor_bytes.len()).map_err(|_| {
|
||||
Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Bedrock block actor payload exceeds the packet size limit",
|
||||
)
|
||||
})?)
|
||||
.write(&mut writer)?;
|
||||
writer.write_all(&[0])?;
|
||||
writer.write_all(&block_actor_bytes)?;
|
||||
} else {
|
||||
VarUInt(0).write(&mut writer)?;
|
||||
|
||||
@@ -140,6 +160,7 @@ impl CLevelChunk<'_> {
|
||||
}
|
||||
chunk_data.write_all(&biome_buf)?;
|
||||
chunk_data.write_all(&[0])?;
|
||||
chunk_data.write_all(&block_actor_bytes)?;
|
||||
|
||||
VarUInt(chunk_data.len() as u32).write(&mut writer)?;
|
||||
writer.write_all(&chunk_data)?;
|
||||
@@ -151,19 +172,26 @@ impl CLevelChunk<'_> {
|
||||
|
||||
impl PacketWrite for CLevelChunk<'_> {
|
||||
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
|
||||
let (encoded, _) = Self::encode_chunk(self.chunk, self.dimension, self.cache_enabled)?;
|
||||
let (encoded, _) = Self::encode_chunk(
|
||||
self.chunk,
|
||||
self.dimension,
|
||||
self.cache_enabled,
|
||||
self.block_actors,
|
||||
)?;
|
||||
writer.write_all(&encoded)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicBool, AtomicU64},
|
||||
};
|
||||
|
||||
use pumpkin_data::chunk::ChunkStatus;
|
||||
use pumpkin_nbt::{Nbt, compound::NbtCompound, deserializer::NbtReadHelperBedrock};
|
||||
use pumpkin_world::{
|
||||
chunk::{ChunkData, ChunkHeightmaps, ChunkLight, ChunkSections},
|
||||
tick::scheduler::ChunkTickScheduler,
|
||||
@@ -185,9 +213,8 @@ mod tests {
|
||||
panic!("VarUInt is too long");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn biomes_follow_subchunks_without_subchunk_headers() {
|
||||
let chunk = ChunkData {
|
||||
fn empty_chunk() -> ChunkData {
|
||||
ChunkData {
|
||||
section: ChunkSections::new(24, -64),
|
||||
heightmap: Mutex::new(ChunkHeightmaps::default()),
|
||||
x: 0,
|
||||
@@ -201,12 +228,18 @@ mod tests {
|
||||
blending_data: None,
|
||||
dirty: AtomicBool::new(false),
|
||||
inhabited_time: AtomicU64::new(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn biomes_follow_subchunks_without_subchunk_headers() {
|
||||
let chunk = empty_chunk();
|
||||
let mut encoded = Vec::new();
|
||||
CLevelChunk {
|
||||
dimension: 0,
|
||||
cache_enabled: false,
|
||||
chunk: &chunk,
|
||||
block_actors: &[],
|
||||
}
|
||||
.write(&mut encoded)
|
||||
.unwrap();
|
||||
@@ -240,4 +273,32 @@ mod tests {
|
||||
assert_eq!(raw[raw_offset], 0); // Border block count.
|
||||
assert_eq!(raw_offset + 1, raw.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actor_nbt_follows_the_chunk_border_data() {
|
||||
let chunk = empty_chunk();
|
||||
let mut block_actor = NbtCompound::new();
|
||||
block_actor.put_string("id", "Chest".to_string());
|
||||
block_actor.put_int("x", 1);
|
||||
block_actor.put_int("y", 64);
|
||||
block_actor.put_int("z", 2);
|
||||
|
||||
let (encoded, _) = CLevelChunk::encode_chunk(&chunk, 0, true, &[block_actor]).unwrap();
|
||||
let mut offset = 0;
|
||||
for _ in 0..3 {
|
||||
read_var_uint(&encoded, &mut offset);
|
||||
}
|
||||
read_var_uint(&encoded, &mut offset);
|
||||
offset += 2;
|
||||
let blob_count = read_var_uint(&encoded, &mut offset) as usize;
|
||||
offset += blob_count * size_of::<u64>();
|
||||
let raw_len = read_var_uint(&encoded, &mut offset) as usize;
|
||||
let raw = &encoded[offset..offset + raw_len];
|
||||
|
||||
assert_eq!(raw[0], 0);
|
||||
let mut reader = NbtReadHelperBedrock::new(Cursor::new(&raw[1..]));
|
||||
let parsed = Nbt::read(&mut reader).unwrap();
|
||||
assert_eq!(parsed.get_string("id"), Some("Chest"));
|
||||
assert_eq!(parsed.get_int("x"), Some(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod add_item_actor;
|
||||
pub mod add_player;
|
||||
pub mod available_commands;
|
||||
pub mod biome_definition_list;
|
||||
pub mod block_event;
|
||||
pub mod boss_event;
|
||||
pub mod change_dimension;
|
||||
pub mod chunk_radius_update;
|
||||
@@ -60,6 +61,7 @@ pub use add_item_actor::*;
|
||||
pub use add_player::*;
|
||||
pub use available_commands::*;
|
||||
pub use biome_definition_list::*;
|
||||
pub use block_event::*;
|
||||
pub use boss_event::*;
|
||||
pub use change_dimension::*;
|
||||
pub use chunk_radius_update::*;
|
||||
|
||||
@@ -25,7 +25,8 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::block::{
|
||||
BlockFuture, BrokenArgs, EmitsRedstonePowerArgs, GetComparatorOutputArgs, GetRedstonePowerArgs,
|
||||
NormalUseArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PlacedArgs, RandomTickArgs,
|
||||
NormalUseArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PlacedArgs, PlayerPlacedArgs,
|
||||
RandomTickArgs,
|
||||
};
|
||||
use crate::entity::EntityBase;
|
||||
use crate::world::World;
|
||||
@@ -126,6 +127,19 @@ async fn placed_chest_impl<E: BlockEntity + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
fn player_placed_chest_impl(args: &PlayerPlacedArgs<'_>) {
|
||||
let position = pumpkin_util::math::vector3::Vector3::new(
|
||||
args.position.0.x as f64 + 0.5,
|
||||
args.position.0.y as f64 + 0.5,
|
||||
args.position.0.z as f64 + 0.5,
|
||||
);
|
||||
args.world.play_bedrock_level_sound(
|
||||
"place",
|
||||
&position,
|
||||
i32::from(pumpkin_data::BlockState::to_be_network_id(args.state_id)),
|
||||
);
|
||||
}
|
||||
|
||||
async fn get_chest_comparator_output(args: GetComparatorOutputArgs<'_>) -> Option<u8> {
|
||||
let state = args.world.get_block_state_id(args.position);
|
||||
let first_chest = args.world.get_block_entity(args.position);
|
||||
@@ -279,6 +293,10 @@ impl BlockBehaviour for ChestBlock {
|
||||
Box::pin(placed_chest_impl(args, ChestBlockEntity::new))
|
||||
}
|
||||
|
||||
fn player_placed<'a>(&'a self, args: PlayerPlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
||||
Box::pin(async move { player_placed_chest_impl(&args) })
|
||||
}
|
||||
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(normal_use_chest_impl(args))
|
||||
}
|
||||
@@ -315,6 +333,10 @@ impl BlockBehaviour for CopperChestBlock {
|
||||
Box::pin(placed_chest_impl(args, ChestBlockEntity::new))
|
||||
}
|
||||
|
||||
fn player_placed<'a>(&'a self, args: PlayerPlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
||||
Box::pin(async move { player_placed_chest_impl(&args) })
|
||||
}
|
||||
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(normal_use_chest_impl(args))
|
||||
}
|
||||
@@ -511,6 +533,10 @@ impl BlockBehaviour for TrappedChestBlock {
|
||||
Box::pin(placed_chest_impl(args, TrappedChestBlockEntity::new))
|
||||
}
|
||||
|
||||
fn player_placed<'a>(&'a self, args: PlayerPlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
||||
Box::pin(async move { player_placed_chest_impl(&args) })
|
||||
}
|
||||
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(normal_use_chest_impl(args))
|
||||
}
|
||||
|
||||
@@ -372,6 +372,12 @@ macro_rules! impl_chest_helper_methods {
|
||||
0.5,
|
||||
pumpkin_util::random::RandomImpl::next_f32(&mut rng) * 0.1 + 0.9,
|
||||
);
|
||||
let bedrock_sound = match sound {
|
||||
pumpkin_data::sound::Sound::BlockChestOpen => "chest.open",
|
||||
pumpkin_data::sound::Sound::BlockChestClose => "chest.closed",
|
||||
_ => return,
|
||||
};
|
||||
world.play_bedrock_level_sound(bedrock_sound, &position, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5661,6 +5661,7 @@ impl InventoryPlayer for Player {
|
||||
fn enqueue_inventory_packet<'a>(
|
||||
&'a self,
|
||||
packet: &'a CSetContainerContent,
|
||||
window_type: Option<WindowType>,
|
||||
) -> PlayerFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
match self.client.as_ref() {
|
||||
@@ -5699,6 +5700,39 @@ impl InventoryPlayer for Player {
|
||||
storage_item: NetworkItemStackDescriptor::default(),
|
||||
};
|
||||
bedrock.enqueue_packet(&bedrock_packet).await;
|
||||
} else if matches!(
|
||||
window_type,
|
||||
Some(
|
||||
WindowType::Generic9x1
|
||||
| WindowType::Generic9x2
|
||||
| WindowType::Generic9x3
|
||||
| WindowType::Generic9x4
|
||||
| WindowType::Generic9x5
|
||||
| WindowType::Generic9x6
|
||||
| WindowType::Generic3x3
|
||||
)
|
||||
) {
|
||||
// Java container screens append the player's 36 inventory slots to
|
||||
// the container slots. Bedrock synchronizes those two inventories
|
||||
// separately and addresses generic block containers as LevelEntity.
|
||||
let container_slot_count = packet
|
||||
.slot_data
|
||||
.len()
|
||||
.saturating_sub(PlayerInventory::MAIN_SIZE);
|
||||
let slots = packet.slot_data[..container_slot_count]
|
||||
.iter()
|
||||
.map(|stack| NetworkItemStackDescriptor::from(&*stack.0))
|
||||
.collect();
|
||||
let bedrock_packet = CInventoryContent {
|
||||
container_id: VarUInt(window_id),
|
||||
slots,
|
||||
full_container_name: FullContainerName {
|
||||
container_name: ContainerName::LevelEntity,
|
||||
dynamic_id: None,
|
||||
},
|
||||
storage_item: NetworkItemStackDescriptor::default(),
|
||||
};
|
||||
bedrock.enqueue_packet(&bedrock_packet).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,8 +311,9 @@ impl BedrockClient {
|
||||
|
||||
let mut serialize_tasks = Vec::with_capacity(valid_chunks.len());
|
||||
for chunk in valid_chunks {
|
||||
let block_actors = player.world().bedrock_chunk_block_actors(&chunk);
|
||||
serialize_tasks.push(tokio::task::spawn_blocking(move || {
|
||||
CLevelChunk::encode_chunk(&chunk, bedrock_dimension, cache_enabled)
|
||||
CLevelChunk::encode_chunk(&chunk, bedrock_dimension, cache_enabled, &block_actors)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,12 @@ use pumpkin_data::{
|
||||
sound_id_remap::remap_sound_id_for_version,
|
||||
world::{RAW, WorldEvent},
|
||||
};
|
||||
use pumpkin_data::{BlockDirection, BlockState, HorizontalFacingExt, translation};
|
||||
use pumpkin_data::{
|
||||
BlockDirection, BlockState, HorizontalFacingExt,
|
||||
block_properties::{BlockProperties, ChestLikeProperties, ChestType},
|
||||
tag::Taggable,
|
||||
translation,
|
||||
};
|
||||
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
|
||||
use pumpkin_inventory::screen_handler::InventoryPlayer;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
@@ -90,9 +95,11 @@ use pumpkin_protocol::{
|
||||
bedrock::{
|
||||
client::{
|
||||
add_player::CAddPlayer,
|
||||
block_event::CBlockEvent as CBedrockBlockEvent,
|
||||
common::BuildPlatform,
|
||||
creative_content::{CCreativeContent, CreativeCategory, Entry, Group},
|
||||
gamerules_changed::GameRules,
|
||||
level_sound_event::CLevelSoundEvent,
|
||||
player_list::{CPlayerList, PlayerListEntry, Skin},
|
||||
remove_actor::CRemoveActor,
|
||||
start_game::{Experiments, GamePublishSetting, LevelSettings},
|
||||
@@ -166,6 +173,41 @@ type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperti
|
||||
|
||||
const MAX_LIGHT_LEVEL: u8 = 15;
|
||||
|
||||
fn bedrock_chest_block_actor(state_id: BlockStateId, position: BlockPos) -> Option<NbtCompound> {
|
||||
let (block, _) = BlockState::from_id_with_block(state_id);
|
||||
if !block.has_tag(&pumpkin_data::tag::Block::C_CHESTS_WOODEN)
|
||||
&& !block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_COPPER_CHESTS)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// Block actor tags describe the chest itself. Container contents are synchronized
|
||||
// through inventory packets and must not be exposed in chunk data.
|
||||
let mut nbt = NbtCompound::new();
|
||||
nbt.put_string("id", "Chest".to_string());
|
||||
nbt.put_int("x", position.0.x);
|
||||
nbt.put_int("y", position.0.y);
|
||||
nbt.put_int("z", position.0.z);
|
||||
nbt.put_bool("isMovable", true);
|
||||
|
||||
let properties = ChestLikeProperties::from_state_id(state_id, block);
|
||||
if properties.r#type != ChestType::Single {
|
||||
let direction = if properties.r#type == ChestType::Left {
|
||||
properties.facing.rotate_clockwise()
|
||||
} else {
|
||||
properties.facing.rotate_counter_clockwise()
|
||||
};
|
||||
let pair = position.offset(direction.to_offset());
|
||||
nbt.put_int("pairx", pair.0.x);
|
||||
nbt.put_int("pairz", pair.0.z);
|
||||
if properties.r#type == ChestType::Right {
|
||||
nbt.put_bool("pairlead", true);
|
||||
}
|
||||
}
|
||||
|
||||
Some(nbt)
|
||||
}
|
||||
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
impl PumpkinError for GetBlockError {
|
||||
@@ -545,7 +587,7 @@ impl World {
|
||||
continue;
|
||||
}
|
||||
let chunk_pos = event.pos.chunk_position();
|
||||
self.broadcast_to_chunk(
|
||||
self.broadcast_to_chunk_editioned_sync(
|
||||
chunk_pos,
|
||||
&CBlockEvent::new(
|
||||
event.pos,
|
||||
@@ -553,6 +595,7 @@ impl World {
|
||||
event.data,
|
||||
VarInt(block.id.as_u16() as i32),
|
||||
),
|
||||
&CBedrockBlockEvent::new(event.pos, i32::from(event.r#type), i32::from(event.data)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -901,6 +944,34 @@ impl World {
|
||||
self.play_sound_raw(sound as u16, category, position, volume, pitch);
|
||||
}
|
||||
|
||||
/// Plays a Bedrock level sound for players close enough to hear it.
|
||||
pub fn play_bedrock_level_sound(
|
||||
&self,
|
||||
sound_id: &str,
|
||||
position: &Vector3<f64>,
|
||||
extra_data: i32,
|
||||
) {
|
||||
let packet = CLevelSoundEvent {
|
||||
sound_id: sound_id.to_string(),
|
||||
position: Vector3::new(position.x as f32, position.y as f32, position.z as f32),
|
||||
extra_data: VarInt(extra_data),
|
||||
entity_type: String::new(),
|
||||
is_baby_mob: false,
|
||||
is_global: false,
|
||||
actor_unique_id: 0,
|
||||
fire_at_position: None,
|
||||
};
|
||||
let chunk_pos = BlockPos::floored_v(*position).chunk_position();
|
||||
|
||||
for player in self.players.load().iter() {
|
||||
if is_within_view_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1)
|
||||
&& let ClientPlatform::Bedrock(client) = player.client.as_ref()
|
||||
{
|
||||
client.try_enqueue_packet(&packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_sound_expect(
|
||||
&self,
|
||||
player: &Player,
|
||||
@@ -5337,6 +5408,36 @@ impl World {
|
||||
Some(entity)
|
||||
}
|
||||
|
||||
/// Builds Bedrock block actor tags that are not represented by Java block states alone.
|
||||
pub fn bedrock_chunk_block_actors(&self, chunk: &ChunkData) -> Vec<NbtCompound> {
|
||||
let chunk_pos = Vector2::new(chunk.x, chunk.z);
|
||||
let live_positions: FxHashSet<_> = self
|
||||
.block_entities
|
||||
.get(&chunk_pos)
|
||||
.map(|entities| entities.keys().copied().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let pending = chunk
|
||||
.pending_block_entities
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
live_positions
|
||||
.iter()
|
||||
.chain(
|
||||
pending
|
||||
.keys()
|
||||
.filter(|position| !live_positions.contains(position)),
|
||||
)
|
||||
.filter_map(|position| {
|
||||
let relative = position.chunk_relative_position();
|
||||
chunk
|
||||
.section
|
||||
.get_block_absolute_y(relative.x as usize, relative.y, relative.z as usize)
|
||||
.and_then(|state_id| bedrock_chest_block_actor(state_id, *position))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn add_block_entity(&self, block_entity: Arc<dyn BlockEntity>) {
|
||||
let block_pos = block_entity.get_position();
|
||||
let chunk_pos = block_pos.chunk_position();
|
||||
@@ -5864,7 +5965,13 @@ impl WorldPortalExt for WorldPortal {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::bedrock_block_breaking_rate;
|
||||
use pumpkin_data::{
|
||||
Block,
|
||||
block_properties::{BlockProperties, ChestLikeProperties, ChestType, HorizontalFacing},
|
||||
};
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
|
||||
use super::{bedrock_block_breaking_rate, bedrock_chest_block_actor};
|
||||
|
||||
#[test]
|
||||
fn bedrock_block_breaking_rate_uses_progress_per_tick() {
|
||||
@@ -5872,4 +5979,20 @@ mod tests {
|
||||
assert_eq!(bedrock_block_breaking_rate(1.0 / 30.0), 2_184);
|
||||
assert_eq!(bedrock_block_breaking_rate(1.0), 65_535);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bedrock_double_chest_block_actor_identifies_pair_and_lead() {
|
||||
let position = BlockPos::new(5, 64, 7);
|
||||
let properties = ChestLikeProperties {
|
||||
facing: HorizontalFacing::North,
|
||||
r#type: ChestType::Right,
|
||||
waterlogged: false,
|
||||
};
|
||||
let actor =
|
||||
bedrock_chest_block_actor(properties.to_state_id(&Block::CHEST), position).unwrap();
|
||||
|
||||
assert_eq!(actor.get_int("pairx"), Some(4));
|
||||
assert_eq!(actor.get_int("pairz"), Some(7));
|
||||
assert_eq!(actor.get_bool("pairlead"), Some(true));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user