fix: bedrock crafting

This commit is contained in:
Alexander Medvedev 2026-06-29 21:20:40 +02:00
parent efa97ad174
commit 7ff990c468
16 changed files with 511 additions and 183 deletions

View File

@ -22,6 +22,7 @@ pub enum Action {
AbortBreak = 1, AbortBreak = 1,
StopBreak = 2, StopBreak = 2,
GetUpdatedBlock = 3, GetUpdatedBlock = 3,
/// Seems to be not used, or atleast not send by client
DropItem = 4, DropItem = 4,
StartSleeping = 5, StartSleeping = 5,
StopSleeping = 6, StopSleeping = 6,

View File

@ -12,8 +12,6 @@ use pumpkin_data::{Block, chunk::ChunkStatus, fluid::Fluid};
use pumpkin_nbt::{compound::NbtCompound, nbt_long_array}; use pumpkin_nbt::{compound::NbtCompound, nbt_long_array};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{debug, trace};
use uuid::Uuid;
use crate::{ use crate::{
chunk::{ chunk::{
@ -289,43 +287,11 @@ impl ChunkEntityData {
chunk_entity_data.position[1], chunk_entity_data.position[1],
))); )));
} }
let mut map = FxHashMap::default();
for entity_nbt in chunk_entity_data.entities {
if entity_nbt.is_empty() {
continue;
}
let uuid = if let Some(uuid) = entity_nbt.get_int_array("UUID") {
if uuid.len() != 4 {
debug!(
"Entity in chunk {},{} has invalid UUID array length {}: {:?}",
position.x,
position.y,
uuid.len(),
entity_nbt
);
continue;
}
Uuid::from_u128(
(uuid[0] as u128) << 96
| (uuid[1] as u128) << 64
| (uuid[2] as u128) << 32
| (uuid[3] as u128),
)
} else {
trace!(
"Entity in chunk {},{} is missing UUID: {:?}",
position.x, position.y, entity_nbt
);
continue;
};
map.insert(uuid, entity_nbt);
}
Ok(Self { Ok(Self {
x: position.x, x: position.x,
z: position.y, z: position.y,
data: Mutex::new(map), data: Mutex::new(chunk_entity_data.entities),
dirty: AtomicBool::new(false), dirty: AtomicBool::new(false),
}) })
} }
@ -334,7 +300,7 @@ impl ChunkEntityData {
let nbt = EntityNbt { let nbt = EntityNbt {
data_version: WORLD_DATA_VERSION, data_version: WORLD_DATA_VERSION,
position: [self.x, self.z], position: [self.x, self.z],
entities: self.data.lock().await.values().cloned().collect(), entities: self.data.lock().await.clone(),
}; };
let mut result = Vec::new(); let mut result = Vec::new();

View File

@ -89,7 +89,7 @@ pub struct ChunkEntityData {
pub x: i32, pub x: i32,
/// Chunk Z /// Chunk Z
pub z: i32, pub z: i32,
pub data: Mutex<FxHashMap<uuid::Uuid, NbtCompound>>, pub data: Mutex<Vec<NbtCompound>>,
pub dirty: AtomicBool, pub dirty: AtomicBool,
} }

View File

@ -8,6 +8,7 @@ pub struct Node {
pub stage: StagedChunkEnum, pub stage: StagedChunkEnum,
pub in_degree: u32, pub in_degree: u32,
pub in_queue: bool, pub in_queue: bool,
pub in_flight: bool,
pub edge: EdgeKey, pub edge: EdgeKey,
} }
@ -19,6 +20,7 @@ impl Node {
stage, stage,
in_degree: 0, in_degree: 0,
in_queue: false, in_queue: false,
in_flight: false,
edge: EdgeKey::null(), edge: EdgeKey::null(),
} }
} }

View File

@ -77,6 +77,7 @@ pub struct GenerationSchedule {
gen_pool: Option<Arc<rayon::ThreadPool>>, gen_pool: Option<Arc<rayon::ThreadPool>>,
listener: Arc<ChunkListener>, listener: Arc<ChunkListener>,
lighting_config: LightingEngineConfig, lighting_config: LightingEngineConfig,
last_unload: std::time::Instant,
} }
impl GenerationSchedule { impl GenerationSchedule {
@ -169,6 +170,7 @@ impl GenerationSchedule {
listener, listener,
chunk_map: Default::default(), chunk_map: Default::default(),
lighting_config, lighting_config,
last_unload: std::time::Instant::now(),
}; };
scheduler.work(level_sched); scheduler.work(level_sched);
}) })
@ -443,9 +445,12 @@ impl GenerationSchedule {
{ {
let task = &mut holder.tasks[i]; let task = &mut holder.tasks[i];
if !task.is_null() { if !task.is_null() {
self.waiting_for_chunks.remove(task); let is_in_flight = self.graph.nodes.get(*task).is_some_and(|n| n.in_flight);
self.drop_node(*task); if !is_in_flight {
*task = NodeKey::null(); self.waiting_for_chunks.remove(task);
self.drop_node(*task);
*task = NodeKey::null();
}
} }
} }
if new_stage == StagedChunkEnum::None if new_stage == StagedChunkEnum::None
@ -601,20 +606,13 @@ impl GenerationSchedule {
self.public_chunk_map.remove(&pos); self.public_chunk_map.remove(&pos);
holder.public = false; holder.public = false;
} }
let sc = Arc::strong_count(&chunk);
if sc == 1 { // Forcefully drop chunks when unloaded to prevent memory leaks
if chunk.is_dirty() { // from dangling strong references
chunks.push((pos, Chunk::Level(chunk))); if chunk.is_dirty() {
} chunks.push((pos, Chunk::Level(chunk)));
self.chunk_map.remove(&pos);
} else {
warn!(
"unload_chunk: chunk {pos:?} still has {} strong refs; cannot unload. holder.public={}",
sc, holder.public
);
self.unload_chunks.insert(pos);
holder.chunk = Some(Chunk::Level(chunk));
} }
self.chunk_map.remove(&pos);
} }
Chunk::Proto(chunk) => { Chunk::Proto(chunk) => {
debug_assert!(!holder.public); debug_assert!(!holder.public);
@ -646,8 +644,8 @@ impl GenerationSchedule {
let mut chunks = Vec::with_capacity(self.chunk_map.len()); let mut chunks = Vec::with_capacity(self.chunk_map.len());
for (pos, holder) in &mut self.chunk_map { for (pos, holder) in &mut self.chunk_map {
if let Some(chunk) = holder.chunk.take() { if let Some(chunk) = &holder.chunk {
let should_save = match &chunk { let should_save = match chunk {
Chunk::Level(sync_chunk) => sync_chunk.is_dirty(), Chunk::Level(sync_chunk) => sync_chunk.is_dirty(),
Chunk::Proto(proto) => { Chunk::Proto(proto) => {
save_proto_chunk save_proto_chunk
@ -660,9 +658,11 @@ impl GenerationSchedule {
}; };
if should_save { if should_save {
chunks.push((*pos, chunk)); let chunk_to_save = match chunk {
} else { Chunk::Level(sync_chunk) => Chunk::Level(sync_chunk.clone()),
holder.chunk = Some(chunk); Chunk::Proto(_) => holder.chunk.take().unwrap(),
};
chunks.push((*pos, chunk_to_save));
} }
} }
} }
@ -803,13 +803,14 @@ impl GenerationSchedule {
if was_public { if was_public {
self.apply_lighting_override(&chunk); self.apply_lighting_override(&chunk);
holder.chunk = Some(Chunk::Level(chunk.clone())); let public_chunk = chunk.clone();
self.public_chunk_map.insert(new_pos, chunk.clone()); self.public_chunk_map.insert(new_pos, public_chunk);
info!( info!(
"Notifying players: regenerated chunk at {:?} (was already public)", "Notifying players: regenerated chunk at {:?} (was already public)",
new_pos new_pos
); );
self.listener.process_new_chunk(new_pos, &chunk); self.listener.process_new_chunk(new_pos, &chunk);
holder.chunk = Some(Chunk::Level(chunk));
} else { } else {
self.apply_lighting_override(&chunk); self.apply_lighting_override(&chunk);
let public_chunk = chunk.clone(); let public_chunk = chunk.clone();
@ -1009,9 +1010,13 @@ impl GenerationSchedule {
// 1. Get latest world state (player moves, etc) // 1. Get latest world state (player moves, etc)
self.resort_work(self.send_level.get()); self.resort_work(self.send_level.get());
// Process unload queue continuously if there are chunks to unload // Process unload queue periodically (every 1 second) to batch writes together
if !self.unload_chunks.is_empty() { // and act as a brief memory cache if a player walks back into the chunk.
if !self.unload_chunks.is_empty()
&& self.last_unload.elapsed() >= std::time::Duration::from_secs(1)
{
self.process_unload_queue(); self.process_unload_queue();
self.last_unload = std::time::Instant::now();
} }
// 2. Process all pending chunk results from workers // 2. Process all pending chunk results from workers
@ -1056,6 +1061,7 @@ impl GenerationSchedule {
node.in_queue = false; node.in_queue = false;
continue; continue;
} }
node.in_flight = true;
let node = node.clone(); let node = node.clone();
if node.stage == StagedChunkEnum::Empty { if node.stage == StagedChunkEnum::Empty {
self.running_task_count += 1; self.running_task_count += 1;
@ -1114,6 +1120,7 @@ impl GenerationSchedule {
if !all_ready { if !all_ready {
if let Some(n) = self.graph.nodes.get_mut(task.1) { if let Some(n) = self.graph.nodes.get_mut(task.1) {
n.in_queue = false; n.in_queue = false;
n.in_flight = false;
} }
self.waiting_for_chunks.insert(task.1); self.waiting_for_chunks.insert(task.1);
// Close the TOCTOU window: the chunk we're waiting for may // Close the TOCTOU window: the chunk we're waiting for may

View File

@ -76,17 +76,17 @@ pub async fn io_read_work(
let (t_send, mut t_recv) = tokio::sync::mpsc::channel(1000); let (t_send, mut t_recv) = tokio::sync::mpsc::channel(1000);
let batch_len = batch.len();
let level_clone = level.clone(); let level_clone = level.clone();
let batch_clone = batch.clone();
let fetch_task = tokio::spawn(async move { let fetch_task = tokio::spawn(async move {
level_clone level_clone
.chunk_saver .chunk_saver
.fetch_chunks(&level_clone.level_folder, &batch_clone, t_send) .fetch_chunks(&level_clone.level_folder, &batch, t_send)
.await; .await;
}); });
for _ in 0..batch.len() { for _ in 0..batch_len {
let data = match t_recv.recv().await { let data = match t_recv.recv().await {
Some(res) => res, Some(res) => res,
None => break, None => break,

View File

@ -24,7 +24,7 @@ use pumpkin_data::dimension::Dimension;
use pumpkin_data::{Block, block_properties::has_random_ticks, fluid::Fluid}; use pumpkin_data::{Block, block_properties::has_random_ticks, fluid::Fluid};
use pumpkin_util::math::{position::BlockPos, vector2::Vector2}; use pumpkin_util::math::{position::BlockPos, vector2::Vector2};
use pumpkin_util::world_seed::Seed; use pumpkin_util::world_seed::Seed;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::FxHashSet;
use std::sync::{Arc, Mutex, Weak}; use std::sync::{Arc, Mutex, Weak};
use std::time::Duration; use std::time::Duration;
use std::{ use std::{
@ -119,8 +119,10 @@ pub struct RandomTickSample {
pub struct LevelFolder { pub struct LevelFolder {
pub root_folder: PathBuf, pub root_folder: PathBuf,
pub dim_folder: PathBuf,
pub region_folder: PathBuf, pub region_folder: PathBuf,
pub entities_folder: PathBuf, pub entities_folder: PathBuf,
pub poi_folder: PathBuf,
} }
impl Level { impl Level {
@ -132,16 +134,26 @@ impl Level {
dimension: Dimension, dimension: Dimension,
gen_pool: Option<Arc<rayon::ThreadPool>>, gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Self> { ) -> Arc<Self> {
let region_folder = root_folder.join("region"); let (namespace, name) = match dimension.minecraft_name.split_once(':') {
let entities_folder = root_folder.join("entities"); Some((ns, n)) => (ns, n),
None => ("minecraft", dimension.minecraft_name),
};
let dim_folder = root_folder.join("dimensions").join(namespace).join(name);
let region_folder = dim_folder.join("region");
let entities_folder = dim_folder.join("entities");
let poi_folder = dim_folder.join("poi");
std::fs::create_dir_all(&region_folder).expect("Failed to create Region folder"); std::fs::create_dir_all(&region_folder).expect("Failed to create Region folder");
std::fs::create_dir_all(&entities_folder).expect("Failed to create Entities folder"); std::fs::create_dir_all(&entities_folder).expect("Failed to create Entities folder");
std::fs::create_dir_all(&poi_folder).expect("Failed to create POI folder");
let level_folder = Arc::new(LevelFolder { let level_folder = Arc::new(LevelFolder {
root_folder, root_folder,
dim_folder,
region_folder, region_folder,
entities_folder, entities_folder,
poi_folder,
}); });
let seed = Seed(seed as u64); let seed = Seed(seed as u64);
@ -225,7 +237,7 @@ impl Level {
let arc_chunk = Arc::new(ChunkEntityData { let arc_chunk = Arc::new(ChunkEntityData {
x: pos.x, x: pos.x,
z: pos.y, z: pos.y,
data: tokio::sync::Mutex::new(FxHashMap::default()), data: tokio::sync::Mutex::new(Vec::new()),
dirty: AtomicBool::new(false), dirty: AtomicBool::new(false),
}); });
@ -246,7 +258,7 @@ impl Level {
let arc_chunk = Arc::new(ChunkEntityData { let arc_chunk = Arc::new(ChunkEntityData {
x: pos.x, x: pos.x,
z: pos.y, z: pos.y,
data: tokio::sync::Mutex::new(FxHashMap::default()), data: tokio::sync::Mutex::new(Vec::new()),
dirty: AtomicBool::new(false), dirty: AtomicBool::new(false),
}); });
@ -322,8 +334,9 @@ impl Level {
self.tasks.wait().await; self.tasks.wait().await;
self.chunk_system_tasks.wait().await; self.chunk_system_tasks.wait().await;
info!("Flushing data to disk for {}...", world_id); info!("Flushing chunk data to disk for {}...", world_id);
self.chunk_saver.block_and_await_ongoing_tasks().await; self.chunk_saver.block_and_await_ongoing_tasks().await;
info!("Flushing entity data to disk for {}...", world_id);
self.entity_saver.block_and_await_ongoing_tasks().await; self.entity_saver.block_and_await_ongoing_tasks().await;
// save all chunks currently in memory // save all chunks currently in memory
@ -521,9 +534,7 @@ impl Level {
.map(|entry| *entry.key()) .map(|entry| *entry.key())
.collect(); .collect();
if !entity_chunks_to_remove.is_empty() { // We do not clean them here because we want the caller to save any active entities in them first.
self.clean_entity_chunks(&entity_chunks_to_remove);
}
// if the difference is too big, we can shrink the loaded chunks // if the difference is too big, we can shrink the loaded chunks
// (1024 chunks is the equivalent to a 32x32 chunks area) // (1024 chunks is the equivalent to a 32x32 chunks area)

View File

@ -376,9 +376,9 @@ pub struct PoiStorage {
impl PoiStorage { impl PoiStorage {
#[must_use] #[must_use]
pub fn new(world_folder: &Path) -> Self { pub fn new(poi_folder: PathBuf) -> Self {
Self { Self {
folder: world_folder.join("poi"), folder: poi_folder,
regions: HashMap::new(), regions: HashMap::new(),
} }
} }
@ -532,7 +532,7 @@ mod tests {
let dir = std::env::temp_dir().join("pumpkin_poi_mca_test"); let dir = std::env::temp_dir().join("pumpkin_poi_mca_test");
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
let mut storage = PoiStorage::new(&dir); let mut storage = PoiStorage::new(dir.join("poi"));
storage.add_portal(BlockPos(Vector3::new(100, 64, 100))); storage.add_portal(BlockPos(Vector3::new(100, 64, 100)));
storage.add_portal(BlockPos(Vector3::new(110, 64, 100))); storage.add_portal(BlockPos(Vector3::new(110, 64, 100)));
@ -552,7 +552,7 @@ mod tests {
assert!(mca_path.exists()); assert!(mca_path.exists());
// Reload and verify // Reload and verify
let mut storage2 = PoiStorage::new(&dir); let mut storage2 = PoiStorage::new(dir.join("poi"));
let results2 = storage2.get_in_square( let results2 = storage2.get_in_square(
BlockPos(Vector3::new(105, 64, 100)), BlockPos(Vector3::new(105, 64, 100)),
16, 16,

View File

@ -3300,6 +3300,13 @@ impl NBTStorage for Entity {
if self.has_visual_fire.load(Relaxed) { if self.has_visual_fire.load(Relaxed) {
nbt.put_bool("HasVisualFire", true); nbt.put_bool("HasVisualFire", true);
} }
nbt.put_int("TicksFrozen", self.frozen_ticks.load(Relaxed));
if let Some(custom_name) = &**self.custom_name.load()
&& let Ok(name_json) = pumpkin_util::serde_json::to_string(custom_name)
{
nbt.put_string("CustomName", name_json);
}
nbt.put_bool("CustomNameVisible", self.custom_name_visible.load(Relaxed));
// todo more... // todo more...
}) })
@ -3340,6 +3347,15 @@ impl NBTStorage for Entity {
.store(nbt.get_int("PortalCooldown").unwrap_or(0) as u32, Relaxed); .store(nbt.get_int("PortalCooldown").unwrap_or(0) as u32, Relaxed);
self.has_visual_fire self.has_visual_fire
.store(nbt.get_bool("HasVisualFire").unwrap_or(false), Relaxed); .store(nbt.get_bool("HasVisualFire").unwrap_or(false), Relaxed);
self.frozen_ticks
.store(nbt.get_int("TicksFrozen").unwrap_or(0), Relaxed);
if let Some(name_json) = nbt.get_string("CustomName")
&& let Ok(component) = pumpkin_util::serde_json::from_str(name_json)
{
self.custom_name.store(Arc::new(Some(component)));
}
self.custom_name_visible
.store(nbt.get_bool("CustomNameVisible").unwrap_or(false), Relaxed);
// todo more... // todo more...
}) })
} }

View File

@ -886,11 +886,15 @@ impl Player {
let chunks_to_clean = level.mark_chunks_as_not_watched(&radial_chunks).await; let chunks_to_clean = level.mark_chunks_as_not_watched(&radial_chunks).await;
// Remove chunks with no watchers from the cache // Remove chunks with no watchers from the cache
if !chunks_to_clean.is_empty() { if !chunks_to_clean.is_empty() {
world.remove_entities_in_chunks(&chunks_to_clean).await;
level.clean_entity_chunks(&chunks_to_clean); level.clean_entity_chunks(&chunks_to_clean);
world.remove_entities_in_chunks(&chunks_to_clean);
} }
// Remove left over entries from all possiblily loaded chunks // Remove left over entries from all possiblily loaded chunks
level.clean_memory(); let cleaned_chunks = level.clean_memory();
if !cleaned_chunks.is_empty() {
world.remove_entities_in_chunks(&cleaned_chunks).await;
level.clean_entity_chunks(&cleaned_chunks);
}
debug!( debug!(
"Removed player id {} from world {} ({} chunks remain cached)", "Removed player id {} from world {} ({} chunks remain cached)",
@ -3093,6 +3097,7 @@ impl Player {
.await .await
{ {
screen_handler.set_received_stack(slot_index, updated_stack); screen_handler.set_received_stack(slot_index, updated_stack);
screen_handler.send_content_updates().await;
} }
} }
@ -4869,19 +4874,170 @@ impl InventoryPlayer for Player {
packet: &'a CSetContainerContent, packet: &'a CSetContainerContent,
) -> PlayerFuture<'a, ()> { ) -> PlayerFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.client.enqueue_packet(packet).await; match self.client.as_ref() {
ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor,
},
};
use pumpkin_protocol::codec::var_uint::VarUInt;
let window_id = packet.window_id.0 as u32;
let slots: Vec<NetworkItemStackDescriptor> = packet
.slot_data
.iter()
.map(|s| NetworkItemStackDescriptor::from(&*s.0))
.collect();
if window_id == 0 {
let bedrock_packet = CInventoryContent {
container_id: VarUInt(0),
slots,
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
};
bedrock.enqueue_packet(&bedrock_packet).await;
}
}
}
}) })
} }
fn enqueue_slot_packet<'a>(&'a self, packet: &'a CSetContainerSlot) -> PlayerFuture<'a, ()> { fn enqueue_slot_packet<'a>(&'a self, packet: &'a CSetContainerSlot) -> PlayerFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.client.enqueue_packet(packet).await; match self.client.as_ref() {
ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
client::inventory_slot::CInventorySlot,
network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor,
},
};
use pumpkin_protocol::codec::var_uint::VarUInt;
let window_id = packet.window_id;
tracing::info!(
"enqueue_slot_packet: window_id={}, slot={}",
window_id,
packet.slot
);
if window_id == 0 {
tracing::info!(
"enqueue_slot_packet: window_id is 0, sending CInventoryContent to Bedrock client"
);
let mut slots = Vec::with_capacity(36);
let main_inventory = &self.inventory().main_inventory;
for s in main_inventory {
let stack = s.lock().await;
slots.push(NetworkItemStackDescriptor::from(&*stack));
}
let bedrock_packet = CInventoryContent {
container_id: VarUInt(0),
slots,
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
};
bedrock.enqueue_packet(&bedrock_packet).await;
} else {
let slot_idx = packet.slot as usize;
let item_desc = NetworkItemStackDescriptor::from(&*packet.slot_data.0);
// Container screen
let current_handler = self.current_screen_handler.lock().await.clone();
let handler = current_handler.lock().await;
let window_type = handler.window_type();
let total_slots = handler.get_behaviour().slots.len();
let bedrock_info = if total_slots >= 36 {
let container_slots = total_slots - 36;
if slot_idx < container_slots {
if window_type == Some(WindowType::Crafting) {
if slot_idx == 0 {
Some((ContainerName::CreatedOutput, 0))
} else {
Some((
ContainerName::CraftingInput,
(32 + slot_idx - 1) as u8,
))
}
} else {
Some((ContainerName::LevelEntity, slot_idx as u8))
}
} else {
let inv_slot = slot_idx - container_slots;
if inv_slot < 27 {
Some((ContainerName::Inventory, (inv_slot + 9) as u8))
} else {
Some((ContainerName::Inventory, (inv_slot - 27) as u8))
}
}
} else {
None
};
if let Some((container_name, slot_id)) = bedrock_info {
let bedrock_packet = CInventorySlot {
window_id: VarUInt(window_id as u32),
inventory_slot: VarUInt(slot_id as u32),
container_name: Some(FullContainerName {
container_name,
dynamic_id: None,
}),
storage: None,
item: item_desc,
};
bedrock.enqueue_packet(&bedrock_packet).await;
}
}
}
}
}) })
} }
fn enqueue_cursor_packet<'a>(&'a self, packet: &'a CSetCursorItem) -> PlayerFuture<'a, ()> { fn enqueue_cursor_packet<'a>(&'a self, packet: &'a CSetCursorItem) -> PlayerFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.client.enqueue_packet(packet).await; match self.client.as_ref() {
ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor,
},
};
use pumpkin_protocol::codec::var_uint::VarUInt;
let item_desc = NetworkItemStackDescriptor::from(&*packet.stack.0);
let bedrock_packet = CInventoryContent {
container_id: VarUInt(59),
slots: vec![item_desc],
full_container_name: FullContainerName {
container_name: ContainerName::Cursor,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
};
bedrock.enqueue_packet(&bedrock_packet).await;
}
}
}) })
} }
@ -4899,7 +5055,42 @@ impl InventoryPlayer for Player {
packet: &'a CSetPlayerInventory, packet: &'a CSetPlayerInventory,
) -> PlayerFuture<'a, ()> { ) -> PlayerFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.client.enqueue_packet(packet).await; match self.client.as_ref() {
ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor,
},
};
use pumpkin_protocol::codec::var_uint::VarUInt;
tracing::info!(
"enqueue_slot_set_packet: slot={}, sending CInventoryContent to Bedrock client",
packet.slot.0
);
let mut slots = Vec::with_capacity(36);
let main_inventory = &self.inventory().main_inventory;
for s in main_inventory {
let stack = s.lock().await;
slots.push(NetworkItemStackDescriptor::from(&*stack));
}
let bedrock_packet = CInventoryContent {
container_id: VarUInt(0),
slots,
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
};
bedrock.enqueue_packet(&bedrock_packet).await;
}
}
}) })
} }

View File

@ -451,10 +451,69 @@ impl BedrockClient {
player: &Arc<Player>, player: &Arc<Player>,
packet: SInventoryTransaction, packet: SInventoryTransaction,
) { ) {
tracing::info!("handle_inventory_action: packet={:?}", packet);
let mut inventory_updated = false; let mut inventory_updated = false;
let mut updates = Vec::new();
let result = 0u8;
if packet.actions.is_empty() && packet.legacy_request_id.0 != 0 {
let mut player_screen_handler = player.player_screen_handler.lock().await;
for legacy_slot in &packet.legacy_set_item_slots {
let mapped_window_id = match legacy_slot.container_id {
28 | 29 => 0, // HotBar or Inventory
6 | 120 => 120, // Armor
34 | 119 => 119, // Offhand
other => other as i32,
};
for &slot_id in &legacy_slot.slots {
if let Some(screen_slot) =
map_bedrock_slot_to_screen_handler(mapped_window_id, slot_id as u32)
{
let current_stack = player_screen_handler
.get_slot(screen_slot)
.get_cloned_stack()
.await;
if !current_stack.is_empty() {
player.drop_item(current_stack.clone()).await;
player_screen_handler
.get_slot(screen_slot)
.set_stack(ItemStack::EMPTY.clone())
.await;
player_screen_handler
.set_received_stack(screen_slot, ItemStack::EMPTY.clone());
record_update(
&mut updates,
FullContainerName {
container_name: match legacy_slot.container_id {
28 => ContainerName::HotBar,
_ => ContainerName::Inventory,
},
dynamic_id: None,
},
slot_id,
0,
VarInt(0),
);
inventory_updated = true;
}
}
}
}
player_screen_handler.send_content_updates().await;
}
for action in &packet.actions { for action in &packet.actions {
if let Some(window_id) = action.window_id { use pumpkin_protocol::bedrock::server::inventory_transaction::InventoryActionSource;
let source_type = InventoryActionSource::from(action.source_type);
if source_type == InventoryActionSource::World {
let old_stack = descriptor_to_stack(&action.old_item);
let new_stack = descriptor_to_stack(&action.new_item);
if old_stack.is_empty() && !new_stack.is_empty() {
player.drop_item(new_stack).await;
}
} else if let Some(window_id) = action.window_id {
if let Some(screen_slot) = if let Some(screen_slot) =
map_bedrock_slot_to_screen_handler(window_id, action.inventory_slot) map_bedrock_slot_to_screen_handler(window_id, action.inventory_slot)
{ {
@ -635,6 +694,52 @@ impl BedrockClient {
player.living_entity.clear_active_hand().await; player.living_entity.clear_active_hand().await;
} }
} }
if packet.legacy_request_id.0 != 0 {
use pumpkin_protocol::bedrock::client::item_stack_response::{
CItemStackResponse, ItemStackResponse, ItemStackResponseContainerInfo,
ItemStackResponseSlotInfo,
};
let mut container_infos = Vec::new();
if result == 0 {
for update in updates {
let container_info = container_infos.iter_mut().find(
|info: &&mut ItemStackResponseContainerInfo| {
info.container_name == update.container_name
},
);
let slot_info = ItemStackResponseSlotInfo {
slot: update.slot_id,
hotbar_slot: update.slot_id,
count: update.count,
item_stack_id: update.stack_id,
custom_name: String::new(),
filtered_custom_name: String::new(),
durability_correction: VarInt(0),
};
if let Some(info) = container_info {
info.slots.push(slot_info);
} else {
container_infos.push(ItemStackResponseContainerInfo {
container_name: update.container_name.clone(),
slots: vec![slot_info],
});
}
}
}
self.enqueue_packet(&CItemStackResponse {
responses: vec![ItemStackResponse {
result,
request_id: packet.legacy_request_id,
container_infos,
}],
})
.await;
}
} }
pub async fn handle_interaction(&self, player: &Arc<Player>, packet: SInteraction) { pub async fn handle_interaction(&self, player: &Arc<Player>, packet: SInteraction) {
@ -868,6 +973,9 @@ impl BedrockClient {
player.mining.store(false, Ordering::Relaxed); player.mining.store(false, Ordering::Relaxed);
world.set_block_breaking(entity, location, -1).await; world.set_block_breaking(entity, location, -1).await;
} }
PlayerAction::DropItem => {
player.drop_held_item(false).await;
}
// TODO // TODO
_ => {} _ => {}
} }
@ -953,6 +1061,7 @@ impl BedrockClient {
let mut result = 0u8; // 0 = Success, 1 = Error let mut result = 0u8; // 0 = Success, 1 = Error
for action in request.actions { for action in request.actions {
tracing::info!("Processing ItemStackRequestAction: {:?}", action);
match action { match action {
ItemStackRequestAction::CraftCreative { ItemStackRequestAction::CraftCreative {
creative_item_id, creative_item_id,
@ -1041,6 +1150,29 @@ impl BedrockClient {
} }
source_stack.decrement(count); source_stack.decrement(count);
if source.container_name.container_name == ContainerName::CreatedOutput
{
if let Some(ref mut stack) = created_item {
stack.decrement(count);
if stack.is_empty() {
created_item = None;
}
}
} else if source.container_name.container_name == ContainerName::Cursor
{
let cursor_is_empty = screen_handler
.get_behaviour()
.cursor_stack
.lock()
.await
.is_empty();
if cursor_is_empty && let Some(ref mut stack) = created_item {
stack.decrement(count);
if stack.is_empty() {
created_item = None;
}
}
}
let source_stack = if source_stack.is_empty() { let source_stack = if source_stack.is_empty() {
ItemStack::EMPTY.clone() ItemStack::EMPTY.clone()
} else { } else {
@ -1554,11 +1686,23 @@ fn map_bedrock_container_slot(
ContainerName::Cursor => None, ContainerName::Cursor => None,
ContainerName::CraftingInput => { ContainerName::CraftingInput => {
if is_player_screen { if is_player_screen {
(slot_id < 4).then(|| 1 + slot_id as usize) if slot_id < 4 {
Some(1 + slot_id as usize)
} else if (28..32).contains(&slot_id) {
Some(1 + (slot_id - 28) as usize)
} else {
None
}
} else if screen_handler.window_type() } else if screen_handler.window_type()
== Some(pumpkin_data::screen::WindowType::Crafting) == Some(pumpkin_data::screen::WindowType::Crafting)
{ {
(slot_id < 9).then(|| 1 + slot_id as usize) if slot_id < 9 {
Some(1 + slot_id as usize)
} else if (32..41).contains(&slot_id) {
Some(1 + (slot_id - 32) as usize)
} else {
None
}
} else { } else {
None None
} }
@ -1800,6 +1944,11 @@ async fn get_slot_stack(
} }
if slot_info.container_name.container_name == ContainerName::Cursor { if slot_info.container_name.container_name == ContainerName::Cursor {
let cursor_lock = screen_handler.get_behaviour().cursor_stack.lock().await; let cursor_lock = screen_handler.get_behaviour().cursor_stack.lock().await;
if cursor_lock.is_empty()
&& let Some(stack) = created_item
{
return stack.clone();
}
return cursor_lock.clone(); return cursor_lock.clone();
} }
if let Some(screen_slot) = map_bedrock_container_slot( if let Some(screen_slot) = map_bedrock_container_slot(

View File

@ -31,7 +31,6 @@ use crate::{
server::{RecipeManager, Server}, server::{RecipeManager, Server},
world::World, world::World,
}; };
use pumpkin_world::level::SyncChunk;
pub struct WasmResource<T> { pub struct WasmResource<T> {
pub provider: T, pub provider: T,
@ -44,7 +43,7 @@ pub type JavaPlayerResource = WasmResource<Arc<Player>>;
pub type BedrockPlayerResource = WasmResource<Arc<Player>>; pub type BedrockPlayerResource = WasmResource<Arc<Player>>;
pub type EntityResource = WasmResource<Arc<dyn EntityBase>>; pub type EntityResource = WasmResource<Arc<dyn EntityBase>>;
pub type WorldResource = WasmResource<Arc<World>>; pub type WorldResource = WasmResource<Arc<World>>;
pub type ChunkResource = WasmResource<(Arc<World>, SyncChunk)>; pub type ChunkResource = WasmResource<(Arc<World>, Weak<pumpkin_world::chunk::ChunkData>)>;
pub type WorldBorderResource = WasmResource<Arc<World>>; pub type WorldBorderResource = WasmResource<Arc<World>>;
pub type ScoreboardResource = WasmResource<Arc<World>>; pub type ScoreboardResource = WasmResource<Arc<World>>;
pub type GuiResource = WasmResource<Arc<Mutex<PluginGui>>>; pub type GuiResource = WasmResource<Arc<Mutex<PluginGui>>>;
@ -157,7 +156,7 @@ impl PluginHostState {
pub fn add_chunk<T>( pub fn add_chunk<T>(
&mut self, &mut self,
world: Arc<World>, world: Arc<World>,
chunk: SyncChunk, chunk: Weak<pumpkin_world::chunk::ChunkData>,
) -> wasmtime::Result<wasmtime::component::Resource<T>> { ) -> wasmtime::Result<wasmtime::component::Resource<T>> {
let resource = self.resource_table.push(ChunkResource { let resource = self.resource_table.push(ChunkResource {
provider: (world, chunk), provider: (world, chunk),

View File

@ -229,7 +229,7 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
.get(&pos) .get(&pos)
.map(|c| c.value().clone()); .map(|c| c.value().clone());
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
let res = self.add_chunk(world_provider, chunk)?; let res = self.add_chunk(world_provider, std::sync::Arc::downgrade(&chunk))?;
Ok(Some(res)) Ok(Some(res))
} else { } else {
Ok(None) Ok(None)
@ -696,12 +696,18 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
async fn get_x(&mut self, chunk: Resource<WitChunk>) -> wasmtime::Result<i32> { async fn get_x(&mut self, chunk: Resource<WitChunk>) -> wasmtime::Result<i32> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
Ok(chunk_data.x) Ok(chunk_data.x)
} }
async fn get_z(&mut self, chunk: Resource<WitChunk>) -> wasmtime::Result<i32> { async fn get_z(&mut self, chunk: Resource<WitChunk>) -> wasmtime::Result<i32> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
Ok(chunk_data.z) Ok(chunk_data.z)
} }
@ -712,6 +718,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<u16> { ) -> wasmtime::Result<u16> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
Ok(chunk_data Ok(chunk_data
.section .section
.get_block_absolute_y(pos.x as usize, pos.y, pos.z as usize) .get_block_absolute_y(pos.x as usize, pos.y, pos.z as usize)
@ -725,6 +734,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<WitBlockState> { ) -> wasmtime::Result<WitBlockState> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
let id = chunk_data let id = chunk_data
.section .section
.get_block_absolute_y(pos.x as usize, pos.y, pos.z as usize) .get_block_absolute_y(pos.x as usize, pos.y, pos.z as usize)
@ -783,6 +795,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<()> { ) -> wasmtime::Result<()> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (world, chunk_data) = &chunk_res.provider; let (world, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
let replaced = let replaced =
chunk_data.set_block_absolute_y(pos.x as usize, pos.y, pos.z as usize, state); chunk_data.set_block_absolute_y(pos.x as usize, pos.y, pos.z as usize, state);
@ -804,6 +819,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<pumpkin::plugin::biomes::Biome> { ) -> wasmtime::Result<pumpkin::plugin::biomes::Biome> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
let id = chunk_data let id = chunk_data
.section .section
.get_rough_biome_absolute_y(pos.x as usize, pos.y, pos.z as usize) .get_rough_biome_absolute_y(pos.x as usize, pos.y, pos.z as usize)
@ -821,6 +839,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<Option<BlockEntityType>> { ) -> wasmtime::Result<Option<BlockEntityType>> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (world, chunk_data) = &chunk_res.provider; let (world, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
let absolute_pos = let absolute_pos =
BlockPos::new(chunk_data.x * 16 + pos.x, pos.y, chunk_data.z * 16 + pos.z); BlockPos::new(chunk_data.x * 16 + pos.x, pos.y, chunk_data.z * 16 + pos.z);
let block_entity = world.get_block_entity(&absolute_pos); let block_entity = world.get_block_entity(&absolute_pos);
@ -836,6 +857,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<i32> { ) -> wasmtime::Result<i32> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
Ok(chunk_data.heightmap.lock().unwrap().get( Ok(chunk_data.heightmap.lock().unwrap().get(
ChunkHeightmapType::WorldSurface, ChunkHeightmapType::WorldSurface,
x, x,
@ -851,6 +875,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<u8> { ) -> wasmtime::Result<u8> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
let section_index = (pos.y - chunk_data.section.min_y) as usize / 16; let section_index = (pos.y - chunk_data.section.min_y) as usize / 16;
Ok(chunk_data Ok(chunk_data
.light_engine .light_engine
@ -870,6 +897,9 @@ impl pumpkin::plugin::world::HostChunk for PluginHostState {
) -> wasmtime::Result<u8> { ) -> wasmtime::Result<u8> {
let chunk_res = self.get_chunk_res(&chunk)?; let chunk_res = self.get_chunk_res(&chunk)?;
let (_, chunk_data) = &chunk_res.provider; let (_, chunk_data) = &chunk_res.provider;
let Some(chunk_data) = chunk_data.upgrade() else {
return Err(wasmtime::Error::msg("Chunk unloaded"));
};
let section_index = (pos.y - chunk_data.section.min_y) as usize / 16; let section_index = (pos.y - chunk_data.section.min_y) as usize / 16;
Ok(chunk_data Ok(chunk_data
.light_engine .light_engine

View File

@ -196,12 +196,13 @@ impl Server {
let defaultgamemode = Mutex::new(DefaultGamemode { let defaultgamemode = Mutex::new(DefaultGamemode {
gamemode: basic_config.default_gamemode, gamemode: basic_config.default_gamemode,
}); });
let players_dir = world_path.join("players");
let player_data_storage = ServerPlayerData::new( let player_data_storage = ServerPlayerData::new(
world_path.join("playerdata"), players_dir.join("data"),
Duration::from_secs(advanced_config.player_data.save_player_cron_interval), Duration::from_secs(advanced_config.player_data.save_player_cron_interval),
advanced_config.player_data.save_player_data, advanced_config.player_data.save_player_data,
); );
let advancement_manager = Arc::new(AdvancementManager::new(world_path.clone(), true)); let advancement_manager = Arc::new(AdvancementManager::new(players_dir.clone(), true));
let white_list = AtomicBool::new(basic_config.white_list); let white_list = AtomicBool::new(basic_config.white_list);
let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps)); let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps));

View File

@ -114,8 +114,8 @@ pub async fn update_position(player: &Arc<Player>) {
.await; .await;
if !chunks_to_clean.is_empty() { if !chunks_to_clean.is_empty() {
world.remove_entities_in_chunks(&chunks_to_clean).await;
world.level.clean_entity_chunks(&chunks_to_clean); world.level.clean_entity_chunks(&chunks_to_clean);
world.remove_entities_in_chunks(&chunks_to_clean);
} }
if !loading_chunks.is_empty() { if !loading_chunks.is_empty() {

View File

@ -285,7 +285,7 @@ impl World {
let generation_settings = GenerationSettings::from_dimension(&dimension); let generation_settings = GenerationSettings::from_dimension(&dimension);
// Load portal POI from disk (PoiStorage::new automatically loads from disk if files exist) // Load portal POI from disk (PoiStorage::new automatically loads from disk if files exist)
let portal_poi = portal::PortalPoiStorage::new(&level.level_folder.root_folder); let portal_poi = portal::PortalPoiStorage::new(level.level_folder.poi_folder.clone());
let dragon_fight = (dimension.minecraft_name == Dimension::THE_END.minecraft_name) let dragon_fight = (dimension.minecraft_name == Dimension::THE_END.minecraft_name)
.then(|| Mutex::new(dragon_fight::DragonFight::new())); .then(|| Mutex::new(dragon_fight::DragonFight::new()));
Self { Self {
@ -377,7 +377,6 @@ impl World {
// First lets see if the entity was saved on an other chunk, and if the current chunk does not match we remove it // First lets see if the entity was saved on an other chunk, and if the current chunk does not match we remove it
// Otherwise we just update the nbt data // Otherwise we just update the nbt data
let base_entity = entity.get_entity(); let base_entity = entity.get_entity();
let uuid = base_entity.entity_uuid;
let current_chunk_coordinate = base_entity.block_pos.load().chunk_position(); let current_chunk_coordinate = base_entity.block_pos.load().chunk_position();
let mut nbt = NbtCompound::new(); let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await; entity.write_nbt(&mut nbt).await;
@ -388,37 +387,21 @@ impl World {
chunk.mark_dirty(true); chunk.mark_dirty(true);
let mut data = chunk.data.lock().await; let mut data = chunk.data.lock().await;
if old_chunk == current_chunk_coordinate { if old_chunk == current_chunk_coordinate {
data.insert(uuid, nbt); data.push(nbt);
return; return;
} }
// The chunk has changed, lets remove the entity from the old chunk // The chunk has changed, lets remove the entity from the old chunk
data.remove(&uuid); // TODO?
data.clear();
} }
// We did not continue, so lets save data in a new chunk // We did not continue, so lets save data in a new chunk
let chunk = self.level.get_entity_chunk(current_chunk_coordinate).await; let chunk = self.level.get_entity_chunk(current_chunk_coordinate).await;
let mut data = chunk.data.lock().await; let mut data = chunk.data.lock().await;
data.insert(uuid, nbt); data.push(nbt);
chunk.mark_dirty(true); chunk.mark_dirty(true);
} }
async fn remove_entity_data(&self, entity: &Entity) {
let current_chunk_coordinate = entity.block_pos.load().chunk_position();
if let Some(old_chunk) = entity.first_loaded_chunk_position.load() {
let old_chunk = old_chunk.to_vec2_i32();
let chunk = self.level.get_entity_chunk(old_chunk).await;
chunk.mark_dirty(true);
if old_chunk == current_chunk_coordinate {
chunk.data.lock().await.remove(&entity.entity_uuid);
} else {
let chunk = self.level.get_entity_chunk(current_chunk_coordinate).await;
// The chunk has changed, lets remove the entity from the old chunk
chunk.data.lock().await.remove(&entity.entity_uuid);
chunk.mark_dirty(true);
}
}
}
/// Sends an entity status update to all players tracking the specified entity. /// Sends an entity status update to all players tracking the specified entity.
pub fn send_entity_status(&self, entity: &Entity, status: EntityStatus) { pub fn send_entity_status(&self, entity: &Entity, status: EntityStatus) {
let chunk_pos = entity.chunk_pos.load(); let chunk_pos = entity.chunk_pos.load();
@ -1134,7 +1117,8 @@ impl World {
self.level.should_unload.store(true, Relaxed); self.level.should_unload.store(true, Relaxed);
let cleaned_chunks = self.level.clean_memory(); let cleaned_chunks = self.level.clean_memory();
if !cleaned_chunks.is_empty() { if !cleaned_chunks.is_empty() {
self.remove_entities_in_chunks(&cleaned_chunks); self.remove_entities_in_chunks(&cleaned_chunks).await;
self.level.clean_entity_chunks(&cleaned_chunks);
} }
// If autosave is configured and this tick will trigger an autosave, don't double notify // If autosave is configured and this tick will trigger an autosave, don't double notify
if self.level.autosave_ticks == 0 { if self.level.autosave_ticks == 0 {
@ -3526,6 +3510,7 @@ impl World {
let mut entity_receiver = self.level.receive_entity_chunks(chunks); let mut entity_receiver = self.level.receive_entity_chunks(chunks);
let level = self.level.clone(); let level = self.level.clone();
let world = self.clone(); let world = self.clone();
player.clone().spawn_task(async move { player.clone().spawn_task(async move {
'main: loop { 'main: loop {
let recv_result = tokio::select! { let recv_result = tokio::select! {
@ -3553,62 +3538,46 @@ impl World {
"Received chunk {:?}, but it is no longer watched... cleaning", "Received chunk {:?}, but it is no longer watched... cleaning",
&position &position
); );
let mut ids_to_remove = Vec::new();
for (uuid, entity_nbt) in chunk.data.lock().await.iter() { if first_load {
let Some(id) = entity_nbt.get_string("id") else { for entity_nbt in chunk.data.lock().await.iter() {
warn!("Entity has no ID"); let Some(id) = entity_nbt.get_string("id") else {
continue; continue;
}; };
let Some(entity_type) = let Some(entity_type) =
EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(id)) EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(id))
else { else {
warn!("Entity has no valid Entity Type {id}"); continue;
continue; };
};
// Pos is zero since it will read from nbt
let entity =
from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, *uuid);
entity.read_nbt_non_mut(entity_nbt).await;
let base_entity = entity.get_entity();
ids_to_remove.push(VarInt(base_entity.entity_id)); let entity = from_type(
entity_type,
Vector3::new(0.0, 0.0, 0.0),
&world,
Uuid::new_v4(),
);
entity.read_nbt_non_mut(entity_nbt).await;
let base_entity = entity.get_entity();
if first_load {
let mut nbt = NbtCompound::new(); let mut nbt = NbtCompound::new();
entity.write_nbt(&mut nbt).await; entity.write_nbt(&mut nbt).await;
if let Some(old_chunk) = base_entity.first_loaded_chunk_position.load() if let Some(old_chunk) = base_entity.first_loaded_chunk_position.load()
{ {
let old_chunk = old_chunk.to_vec2_i32(); let old_chunk = old_chunk.to_vec2_i32();
let chunk = world.level.get_entity_chunk(old_chunk).await; let chunk = world.level.get_entity_chunk(old_chunk).await;
chunk.mark_dirty(true); chunk.mark_dirty(true);
let base_entity = entity.get_entity();
let current_chunk_coordinate = let current_chunk_coordinate =
base_entity.block_pos.load().chunk_position(); base_entity.block_pos.load().chunk_position();
let mut data = chunk.data.lock().await; let mut data = chunk.data.lock().await;
if old_chunk == current_chunk_coordinate { if old_chunk == current_chunk_coordinate {
data.insert(*uuid, nbt); data.push(nbt);
continue; continue;
} }
// The chunk has changed, lets remove the entity from the old chunk // The chunk has changed, lets remove the entity from the old chunk
data.remove(uuid); data.clear();
}
}
}
if !ids_to_remove.is_empty() {
match player.client.as_ref() {
crate::net::ClientPlatform::Java(java) => {
java.enqueue_packet(&CRemoveEntities::new(&ids_to_remove))
.await;
}
crate::net::ClientPlatform::Bedrock(bedrock) => {
for id in &ids_to_remove {
bedrock
.enqueue_packet(&CRemoveActor::new(VarLong(id.0 as i64)))
.await;
}
} }
} }
} }
@ -3617,7 +3586,7 @@ impl World {
// Add all new Entities to the world // Add all new Entities to the world
let mut entities_to_add: Vec<Arc<dyn EntityBase>> = Vec::new(); let mut entities_to_add: Vec<Arc<dyn EntityBase>> = Vec::new();
for (uuid, entity_nbt) in chunk.data.lock().await.iter() { for entity_nbt in chunk.data.lock().await.iter() {
let Some(id) = entity_nbt.get_string("id") else { let Some(id) = entity_nbt.get_string("id") else {
debug!("Entity has no ID"); debug!("Entity has no ID");
continue; continue;
@ -3629,29 +3598,17 @@ impl World {
continue; continue;
}; };
// Check if this entity already exists in the world (e.g. another player
// is still online and tracking it). If so, just send the spawn packet
// for the existing entity to the reconnecting player instead of
// creating a duplicate with a different entity_id.
let existing = world
.entities
.load()
.iter()
.find(|e| e.get_entity().entity_uuid == *uuid)
.cloned();
if let Some(existing_entity) = existing {
let base_entity = existing_entity.get_entity();
player
.client
.enqueue_packet(&base_entity.create_spawn_packet())
.await;
existing_entity.init_data_tracker().await;
continue;
}
// Pos is zero since it will read from nbt // Pos is zero since it will read from nbt
let entity = from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, *uuid); let entity = from_type(
entity_type,
Vector3::new(0.0, 0.0, 0.0),
&world,
Uuid::new_v4(),
);
entity.read_nbt_non_mut(entity_nbt).await; entity.read_nbt_non_mut(entity_nbt).await;
entity.init_data_tracker().await;
let base_entity = entity.get_entity(); let base_entity = entity.get_entity();
// Clear velocity so the client does not replay the drop animation. // Clear velocity so the client does not replay the drop animation.
@ -3663,12 +3620,12 @@ impl World {
.client .client
.enqueue_packet(&base_entity.create_spawn_packet()) .enqueue_packet(&base_entity.create_spawn_packet())
.await; .await;
entity.init_data_tracker().await;
if first_load { if first_load {
entities_to_add.push(entity); entities_to_add.push(entity);
} }
} }
if first_load && !entities_to_add.is_empty() { if first_load && !entities_to_add.is_empty() {
world.entities.rcu(|current_entities| { world.entities.rcu(|current_entities| {
let mut new_entities = (**current_entities).clone(); let mut new_entities = (**current_entities).clone();
@ -4152,7 +4109,7 @@ impl World {
self.spawn_state.load().add_entity(self, entity.as_ref()); self.spawn_state.load().add_entity(self, entity.as_ref());
let chunk = self.level.get_entity_chunk(chunk_coordinate).await; let chunk = self.level.get_entity_chunk(chunk_coordinate).await;
chunk.data.lock().await.insert(base_entity.entity_uuid, nbt); chunk.data.lock().await.push(nbt);
chunk.mark_dirty(true); chunk.mark_dirty(true);
self.entities.rcu(|current_entities| { self.entities.rcu(|current_entities| {
@ -4162,6 +4119,7 @@ impl World {
}); });
} }
#[allow(clippy::unused_async)]
pub async fn remove_entity(&self, entity: &dyn EntityBase) { pub async fn remove_entity(&self, entity: &dyn EntityBase) {
let base_entity = entity.get_entity(); let base_entity = entity.get_entity();
self.spawn_state.load().remove_entity(self, entity); self.spawn_state.load().remove_entity(self, entity);
@ -4177,11 +4135,9 @@ impl World {
&CRemoveEntities::new(&[base_entity.entity_id.into()]), &CRemoveEntities::new(&[base_entity.entity_id.into()]),
&CRemoveActor::new(VarLong(base_entity.entity_id as i64)), &CRemoveActor::new(VarLong(base_entity.entity_id as i64)),
); );
self.remove_entity_data(base_entity).await;
} }
pub fn remove_entities_in_chunks(&self, chunks: &[Vector2<i32>]) { pub async fn remove_entities_in_chunks(&self, chunks: &[Vector2<i32>]) {
let chunks_set: FxHashSet<_> = chunks.iter().copied().collect(); let chunks_set: FxHashSet<_> = chunks.iter().copied().collect();
let mut entities_to_remove = Vec::new(); let mut entities_to_remove = Vec::new();
@ -4201,9 +4157,8 @@ impl World {
}); });
for entity in entities_to_remove { for entity in entities_to_remove {
self.save_entity(&entity).await;
self.spawn_state.load().remove_entity(self, entity.as_ref()); self.spawn_state.load().remove_entity(self, entity.as_ref());
// Important: We do NOT call remove_entity_data here because we want the entities
// to persist in the chunk data on disk. We only remove them from the active world (RAM).
} }
self.block_entities self.block_entities