From a36240dd40f02c390564f742bf735010526cf882 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Mon, 27 Apr 2026 23:16:19 +0200 Subject: [PATCH] fix: height maps --- Cargo.lock | 4 +- Cargo.toml | 2 +- pumpkin-util/src/math/bit_storage.rs | 62 ++++ pumpkin-util/src/math/mod.rs | 1 + pumpkin-world/src/chunk/format/mod.rs | 59 +-- pumpkin-world/src/chunk/io/file_manager.rs | 51 +-- pumpkin-world/src/chunk/mod.rs | 349 +++++++++++------- .../src/chunk_system/generation_cache.rs | 2 +- pumpkin-world/src/chunk_system/mod.rs | 2 +- pumpkin-world/src/chunk_system/schedule.rs | 7 +- .../src/chunk_system/worker_logic.rs | 30 +- pumpkin-world/src/level.rs | 141 ++++--- pumpkin-world/src/tick/scheduler.rs | 8 + pumpkin/src/data/player_server.rs | 37 +- pumpkin/src/entity/mod.rs | 15 + pumpkin/src/net/java/mod.rs | 34 +- pumpkin/src/server/mod.rs | 20 +- pumpkin/src/world/mod.rs | 238 ++++++++---- pumpkin/src/world/natural_spawner.rs | 226 ++++++++++-- 19 files changed, 890 insertions(+), 398 deletions(-) create mode 100644 pumpkin-util/src/math/bit_storage.rs diff --git a/Cargo.lock b/Cargo.lock index a052162d3..c7ecdca71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1848,9 +1848,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", diff --git a/Cargo.toml b/Cargo.toml index 9ff1eda79..294876aee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,7 @@ hmac = "=0.13.0" indexmap = "2.14" itertools = "0.14.0" libloading = "0.9" -lru = "0.17.0" +lru = "0.18.0" lz4-java-wrc = "0.2.0" md5 = "0.8" num-bigint = "0.4" diff --git a/pumpkin-util/src/math/bit_storage.rs b/pumpkin-util/src/math/bit_storage.rs new file mode 100644 index 000000000..11225186c --- /dev/null +++ b/pumpkin-util/src/math/bit_storage.rs @@ -0,0 +1,62 @@ +pub struct BitStorage> { + data: T, + bits: u8, + size: usize, + mask: u64, +} + +impl> BitStorage { + pub const fn new(bits: u8, size: usize, data: T) -> Self { + let mask = (1u64 << bits) - 1; + Self { + data, + bits, + size, + mask, + } + } + + pub fn get(&self, index: usize) -> u32 { + debug_assert!(index < self.size); + let bit_offset = index * self.bits as usize; + let array_idx = bit_offset / 64; + let bit_pos = (bit_offset % 64) as u32; + + let data = self.data.as_ref(); + let val = (data[array_idx] as u64) >> bit_pos; + let next_idx = array_idx + 1; + + let res = if bit_pos + self.bits as u32 > 64 && next_idx < data.len() { + val | (data[next_idx] as u64) << (64 - bit_pos) + } else { + val + }; + + (res & self.mask) as u32 + } + + pub const fn data(&self) -> &T { + &self.data + } +} + +impl + AsMut<[i64]>> BitStorage { + pub fn set(&mut self, index: usize, value: u32) { + debug_assert!(index < self.size); + debug_assert!(value as u64 <= self.mask); + let bit_offset = index * self.bits as usize; + let array_idx = bit_offset / 64; + let bit_pos = (bit_offset % 64) as u32; + + let data = self.data.as_mut(); + data[array_idx] = (data[array_idx] as u64 & !(self.mask << bit_pos) + | (value as u64 & self.mask) << bit_pos) as i64; + let next_idx = array_idx + 1; + + if bit_pos + self.bits as u32 > 64 && next_idx < data.len() { + let next_bits = bit_pos + self.bits as u32 - 64; + data[next_idx] = (data[next_idx] as u64 & !((1u64 << next_bits) - 1) + | (value as u64 & self.mask) >> (64 - bit_pos)) as i64; + } + } +} diff --git a/pumpkin-util/src/math/mod.rs b/pumpkin-util/src/math/mod.rs index 20fd64549..260734187 100644 --- a/pumpkin-util/src/math/mod.rs +++ b/pumpkin-util/src/math/mod.rs @@ -1,6 +1,7 @@ use num_traits::{Float, One, PrimInt, Zero}; pub mod atomic_f32; +pub mod bit_storage; pub mod block_box; pub mod boundingbox; pub mod bounds; diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index 4c40d11d3..2c6ac3b89 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -113,40 +113,39 @@ impl ChunkData { position.x, position.y, chunk_data.x_pos, chunk_data.z_pos, ))); } - let (block_lights, sky_lights, block_palettes, biome_palettes) = chunk_data - .sections - .into_iter() - .map(|section| { - // When loading light data, missing data should default to 0 (no light) - let block_light = section - .block_light - .map_or(LightContainer::Empty(0), LightContainer::Full); - let sky_light = section - .sky_light - .map_or(LightContainer::Empty(0), LightContainer::Full); + let section_count = chunk_data.sections.len(); + let mut block_lights = Vec::with_capacity(section_count); + let mut sky_lights = Vec::with_capacity(section_count); + let mut block_palettes = Vec::with_capacity(section_count); + let mut biome_palettes = Vec::with_capacity(section_count); - // Convert NBT to Palettes - let block_palette = section + for section in chunk_data.sections { + // When loading light data, missing data should default to 0 (no light) + block_lights.push( + section + .block_light + .map_or(LightContainer::Empty(0), LightContainer::Full), + ); + sky_lights.push( + section + .sky_light + .map_or(LightContainer::Empty(0), LightContainer::Full), + ); + + // Convert NBT to Palettes + block_palettes.push( + section .block_states .map(BlockPalette::from_disk_nbt) - .unwrap_or_default(); - let biome_palette = section + .unwrap_or_default(), + ); + biome_palettes.push( + section .biomes .map(BiomePalette::from_disk_nbt) - .unwrap_or_default(); - - (block_light, sky_light, block_palette, biome_palette) - }) - .fold( - (Vec::new(), Vec::new(), Vec::new(), Vec::new()), - |(mut bl, mut sl, mut bp, mut bip), (b_l, s_l, b_p, bi_p)| { - bl.push(b_l); - sl.push(s_l); - bp.push(b_p); - bip.push(bi_p); - (bl, sl, bp, bip) - }, + .unwrap_or_default(), ); + } // Assemble the LightEngine let light_engine = ChunkLight { @@ -156,11 +155,13 @@ impl ChunkData { // Assemble the ChunkSections let min_y = section_coords::section_to_block(chunk_data.min_y_section); - let random_tick_sections = ChunkSections::build_random_tick_sections_cache(&block_palettes); + let (random_tick_sections, randomly_ticking_mask) = + ChunkSections::build_random_tick_sections_cache(&block_palettes); let section = ChunkSections { count: block_palettes.len(), block_sections: RwLock::new(block_palettes.into_boxed_slice()), random_tick_sections: RwLock::new(random_tick_sections), + randomly_ticking_mask: std::sync::atomic::AtomicU32::new(randomly_ticking_mask), biome_sections: RwLock::new(biome_palettes.into_boxed_slice()), min_y, }; diff --git a/pumpkin-world/src/chunk/io/file_manager.rs b/pumpkin-world/src/chunk/io/file_manager.rs index 8a3dc1b36..ff0018bcd 100644 --- a/pumpkin-world/src/chunk/io/file_manager.rs +++ b/pumpkin-world/src/chunk/io/file_manager.rs @@ -37,7 +37,7 @@ pub struct ChunkFileManager> { // - Guarantee that there is only one serializer per file at a time // - Lazily load files as to support point 1 // - Allow for ease of usage (able to return the serializer from a function) - file_locks: RwLock>>, + file_locks: RwLock>>>, watchers: RwLock>, chunk_config: S::ChunkConfig, @@ -62,8 +62,11 @@ impl> ChunkSerializerLazyLoader { /// We can only remove this entry from the map if we are the only ones with a reference to it /// IMPORTANT: This must be called within the write lock of the parent map - fn can_remove(&self) -> bool { - match self.internal.get() { + fn can_remove(loader: &Arc) -> bool { + if Arc::strong_count(loader) > 1 { + return false; + } + match loader.internal.get() { Some(arc) => { // A strong count of 1 means it's only in the map Arc::strong_count(arc) == 1 @@ -128,16 +131,17 @@ impl> ChunkFileManager { // We use a lazy loader here to quickly make an insertion into the map without holding the // lock for too long starving other threads - if let Some(serializer_loader) = self.file_locks.read().await.get(path) { + let loader = self.file_locks.read().await.get(path).cloned(); + if let Some(serializer_loader) = loader { serializer_loader.get().await } else { - self.file_locks - .write() - .await + let mut locks = self.file_locks.write().await; + let loader = locks .entry(path.into()) - .or_insert_with(|| ChunkSerializerLazyLoader::new(path.into())) - .get() - .await + .or_insert_with(|| Arc::new(ChunkSerializerLazyLoader::new(path.into()))) + .clone(); + drop(locks); + loader.get().await } } } @@ -320,7 +324,8 @@ where // We only need to update the chunk if it is dirty if chunk_is_dirty { - chunk_serializer.write().await.update_chunk(&*chunk, &self.chunk_config).await?; + let mut writer = chunk_serializer.write().await; + writer.update_chunk(&*chunk, &self.chunk_config).await?; } Ok::<(), ChunkWritingError>(()) } @@ -329,12 +334,10 @@ where futures::future::try_join_all(update_tasks).await?; trace!("Updated data for file {}", path.display()); - let is_watched = self - .watchers - .read() - .await - .get(&path) - .is_some_and(|count| count != &0); + let is_watched = { + let watchers = self.watchers.read().await; + watchers.get(&path).is_some_and(|count| count != &0) + }; if !is_watched { // With the modification done, we can drop the write lock but keep the read lock @@ -352,18 +355,16 @@ where // Decrement strong count drop(chunk_serializer); + let should_remove = { + let watchers = self.watchers.read().await; + watchers.get(&path).is_none_or(|count| count == &0) + }; - if self - .watchers - .read() - .await - .get(&path) - .is_none_or(|count| count == &0) - { + if should_remove { let mut locks = self.file_locks.write().await; let can_remove = if let Some(loader) = locks.get(&path) { - loader.can_remove() + ChunkSerializerLazyLoader::can_remove(loader) } else { true }; diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index f3e0c002e..06b34412f 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -12,13 +12,10 @@ use pumpkin_nbt::nbt_long_array; use pumpkin_util::math::position::BlockPos; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::ops::{BitAnd, BitOr}; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, RwLock}; +use std::sync::RwLock; +use std::sync::{Arc, atomic::AtomicBool}; use thiserror::Error; use tokio::sync::Mutex; -use tracing::info; pub mod format; pub mod io; @@ -108,6 +105,7 @@ pub struct ChunkSections { pub count: usize, pub block_sections: RwLock>, pub random_tick_sections: RwLock>, + pub randomly_ticking_mask: std::sync::atomic::AtomicU32, pub biome_sections: RwLock>, pub min_y: i32, } @@ -174,6 +172,20 @@ impl TryFrom for ChunkHeightmapType { } } +impl ChunkHeightmapType { + pub fn is_opaque(&self, block_state: &BlockState) -> bool { + let block = Block::get_raw_id_from_state_id(block_state.id); + match self { + Self::WorldSurface => !block_state.is_air(), + Self::MotionBlocking => blocks_movement(block_state, block) || block_state.is_liquid(), + Self::MotionBlockingNoLeaves => { + (blocks_movement(block_state, block) || block_state.is_liquid()) + && !MINECRAFT_LEAVES.1.contains(&block) + } + } + } +} + #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "UPPERCASE")] pub struct ChunkHeightmaps { @@ -186,103 +198,90 @@ pub struct ChunkHeightmaps { } impl ChunkHeightmaps { - pub fn set(&mut self, heightmap: ChunkHeightmapType, pos: BlockPos, min_y: i32) { + pub fn set(&mut self, heightmap: ChunkHeightmapType, x: i32, z: i32, height: i32, min_y: i32) { let data = match heightmap { ChunkHeightmapType::WorldSurface => &mut self.world_surface, ChunkHeightmapType::MotionBlocking => &mut self.motion_blocking, ChunkHeightmapType::MotionBlockingNoLeaves => &mut self.motion_blocking_no_leaves, }; - let local_x = (pos.0.x & 15) as usize; - let local_z = (pos.0.z & 15) as usize; - - let adjust_height = (pos.0.y + min_y.abs()) as usize; - - assert!(adjust_height <= 2 << 9); - - //chunk column index in 16*16 chunk. + let local_x = (x & 15) as usize; + let local_z = (z & 15) as usize; let column_idx = local_z * 16 + local_x; - // Each height value uses 9 bits, calculate starting bit position - let bit_start_idx = column_idx * 9; + // In Minecraft 1.16+, height is stored as (y - min_y + 1). 0 means below min_y. + // It uses 9 bits per value, packed such that they do not cross u64 boundaries. + // 64 / 9 = 7 values per u64. + let val = (height - min_y + 1).max(0) as u64; - // Find where these 9 bits start within a 64-bit packed array element - // We use bit_start_index % 63, which means the last bit of i64 won't be used, - // but this avoids the hassle of bit concatenation. - let packed_array_bit_start_idx = bit_start_idx as u32 % 63; + let array_idx = column_idx / 7; + let shift = (column_idx % 7) * 9; - let mask = { - if packed_array_bit_start_idx == 0 { - //0b0000_0000_0111_1111_... - !(0x1FF << (64 - 9)) - } else { - !(0x1FF << (64 - packed_array_bit_start_idx - 9)) - } - }; + let mask = 0x1FFu64 << shift; - let height_bit_bytes = adjust_height - .wrapping_shl(64 - 9 - packed_array_bit_start_idx) - .to_ne_bytes(); - let height = i64::from_ne_bytes(height_bit_bytes); - - let packed_array_idx = column_idx / 7; - - data[packed_array_idx] = data[packed_array_idx].bitand(mask).bitor(height); + let mut current = data[array_idx] as u64; + current = (current & !mask) | ((val & 0x1FF) << shift); + data[array_idx] = current as i64; } #[must_use] pub fn get(&self, heightmap: ChunkHeightmapType, x: i32, z: i32, min_y: i32) -> i32 { - let local_x = (x & 15) as usize; - let local_z = (z & 15) as usize; - - let column_idx = local_z * 16 + local_x; - let bit_start_idx = column_idx * 9; - - let packed_array_bit_start_idx = bit_start_idx as u32 % 63; - - let mask = { - if packed_array_bit_start_idx == 0 { - //0b1111_1111_1000_0000_... - 0x1ff << (64 - 9) - } else { - 0x1ff << (64 - packed_array_bit_start_idx - 9) - } - }; - - let packed_array_idx = column_idx / 7; - let data = match heightmap { ChunkHeightmapType::WorldSurface => &self.world_surface, ChunkHeightmapType::MotionBlocking => &self.motion_blocking, ChunkHeightmapType::MotionBlockingNoLeaves => &self.motion_blocking_no_leaves, }; - let height_bit_bytes_i64 = data[packed_array_idx].bitand(mask).to_ne_bytes(); - (u64::from_ne_bytes(height_bit_bytes_i64) - .wrapping_shr(64 - (packed_array_bit_start_idx + 9)) as i32) - - min_y.abs() + let local_x = (x & 15) as usize; + let local_z = (z & 15) as usize; + let column_idx = local_z * 16 + local_x; + + let array_idx = column_idx / 7; + let shift = (column_idx % 7) * 9; + + let current = data[array_idx] as u64; + let val = (current >> shift) & 0x1FF; + + (val as i32) + min_y - 1 } - pub fn log_heightmap(&self, _type: ChunkHeightmapType, min_y: i32) { - let mut header = "Z/X".to_string(); - for x in 0..16 { - let _ = write!(header, "{x:4}"); + #[expect(clippy::too_many_arguments)] + pub fn update( + &mut self, + heightmap_type: ChunkHeightmapType, + local_x: i32, + local_y: i32, + local_z: i32, + block_state: &BlockState, + min_y: i32, + get_block: F, + ) -> bool + where + F: Fn(i32) -> &'static BlockState, + { + let first_available = self.get(heightmap_type, local_x, local_z, min_y) + 1; + if local_y <= first_available - 2 { + return false; } - let grid: String = (0..16) - .map(|z| { - let mut row = format!("{z:3}"); - row.push_str( - &(0..16) - .map(|x| format!("{:4}", self.get(_type, x, z, min_y))) - .collect::(), - ); - row - }) - .collect::>() - .join("\n"); + if heightmap_type.is_opaque(block_state) { + if local_y >= first_available { + self.set(heightmap_type, local_x, local_z, local_y, min_y); + return true; + } + } else if first_available - 1 == local_y { + for y in (min_y..local_y).rev() { + let state = get_block(y); + if heightmap_type.is_opaque(state) { + self.set(heightmap_type, local_x, local_z, y, min_y); + return true; + } + } + self.set(heightmap_type, local_x, local_z, min_y - 1, min_y); + return true; + } - info!("\nHeightMap:\n{header}\n{grid}"); + false } } @@ -303,31 +302,39 @@ impl ChunkSections { #[must_use] pub fn build_random_tick_sections_cache( block_sections: &[BlockPalette], - ) -> Box<[RandomTickSectionCache]> { - block_sections + ) -> (Box<[RandomTickSectionCache]>, u32) { + let mut mask = 0; + let cache = block_sections .iter() - .map(|section| { + .enumerate() + .map(|(i, section)| { let (random_ticking_block_count, random_ticking_fluid_count) = section.random_ticking_counts(); + if random_ticking_block_count > 0 || random_ticking_fluid_count > 0 { + mask |= 1 << i; + } RandomTickSectionCache { random_ticking_block_count, random_ticking_fluid_count, } }) .collect::>() - .into_boxed_slice() + .into_boxed_slice(); + (cache, mask) } #[must_use] pub fn new(num_sections: usize, min_y: i32) -> Self { let block_sections = vec![BlockPalette::default(); num_sections].into_boxed_slice(); - let random_tick_sections = Self::build_random_tick_sections_cache(&block_sections); + let (random_tick_sections, randomly_ticking_mask) = + Self::build_random_tick_sections_cache(&block_sections); let biome_sections = vec![BiomePalette::default(); num_sections].into_boxed_slice(); Self { count: num_sections, block_sections: RwLock::new(block_sections), random_tick_sections: RwLock::new(random_tick_sections), + randomly_ticking_mask: std::sync::atomic::AtomicU32::new(randomly_ticking_mask), biome_sections: RwLock::new(biome_sections), min_y, } @@ -349,6 +356,21 @@ impl ChunkSections { } } + pub fn set_block_absolute_y( + &self, + relative_x: usize, + y: i32, + relative_z: usize, + block_state_id: BlockStateId, + ) -> BlockStateId { + let y = y - self.min_y; + if y < 0 { + return Block::AIR.default_state.id; + } + let relative_y = y as usize; + self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id) + } + #[must_use] pub fn get_rough_biome_absolute_y( &self, @@ -370,23 +392,6 @@ impl ChunkSections { } } - /// Returns the replaced block state ID - pub fn set_block_absolute_y( - &self, - relative_x: usize, - y: i32, - relative_z: usize, - block_state_id: BlockStateId, - ) -> BlockStateId { - let y = y - self.min_y; - if y < 0 { - return Block::AIR.default_state.id; - } - let relative_y = y as usize; - - self.set_relative_block(relative_x, relative_y, relative_z, block_state_id) - } - /// Gets the given block in the chunk fn get_relative_block( &self, @@ -415,7 +420,6 @@ impl ChunkSections { relative_z: usize, block_state_id: BlockStateId, ) -> BlockStateId { - // TODO @LUK_ESC? update the heightmap self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id) } @@ -473,6 +477,18 @@ impl ChunkSections { .saturating_add(1); } + // Update the bitmask + let mut mask = self + .randomly_ticking_mask + .load(std::sync::atomic::Ordering::Relaxed); + if random_tick_cache.is_randomly_ticking() { + mask |= 1 << section_index; + } else { + mask &= !(1 << section_index); + } + self.randomly_ticking_mask + .store(mask, std::sync::atomic::Ordering::Relaxed); + return replaced_block_state_id; } 0 @@ -531,6 +547,62 @@ impl ChunkSections { } impl ChunkData { + /// Returns the replaced block state ID + pub fn set_block_absolute_y( + &self, + relative_x: usize, + y: i32, + relative_z: usize, + block_state_id: BlockStateId, + ) -> BlockStateId { + let min_y = self.section.min_y; + let y_rel = y - min_y; + if y_rel < 0 { + return Block::AIR.default_state.id; + } + let relative_y = y_rel as usize; + + let old = self.section.set_block_no_heightmap_update( + relative_x, + relative_y, + relative_z, + block_state_id, + ); + if old != block_state_id { + let state = BlockState::from_id(block_state_id); + self.update_heightmap(relative_x, relative_y, relative_z, state); + } + old + } + + fn update_heightmap( + &self, + relative_x: usize, + relative_y: usize, + relative_z: usize, + block_state: &BlockState, + ) { + let mut heightmap = self.heightmap.lock().unwrap(); + let min_y = self.section.min_y; + let x = relative_x as i32; + let y = relative_y as i32 + min_y; + let z = relative_z as i32; + + for &hm_type in &[ + ChunkHeightmapType::WorldSurface, + ChunkHeightmapType::MotionBlocking, + ChunkHeightmapType::MotionBlockingNoLeaves, + ] { + heightmap.update(hm_type, x, z, y, block_state, min_y, |y_at| { + let id = self + .section + .get_block_absolute_y(relative_x, y_at, relative_z) + .unwrap_or(0); + BlockState::from_id(id) + }); + } + } + /// Gets the given block in the chunk #[inline] #[must_use] @@ -553,7 +625,8 @@ impl ChunkData { relative_z: usize, block_state_id: BlockStateId, ) { - // TODO @LUK_ESC? update the heightmap + let state = BlockState::from_id(block_state_id); + self.update_heightmap(relative_x, relative_y, relative_z, state); self.section .set_relative_block(relative_x, relative_y, relative_z, block_state_id); } @@ -606,45 +679,35 @@ impl ChunkData { let mut has_found = [false, false, false]; for y in (self.section.min_y..=start_height).rev() { - let pos = BlockPos::new(x as i32, y, z as i32); let state_id = self.section.get_block_absolute_y(x, y, z).unwrap(); let block_state = BlockState::from_id(state_id); - let block = Block::get_raw_id_from_state_id(state_id); - if !block_state.is_air() && !has_found[ChunkHeightmapType::WorldSurface as usize] { - heightmaps.set(ChunkHeightmapType::WorldSurface, pos, self.section.min_y); - has_found[ChunkHeightmapType::WorldSurface as usize] = true; + for hm_type in [ + ChunkHeightmapType::WorldSurface, + ChunkHeightmapType::MotionBlocking, + ChunkHeightmapType::MotionBlockingNoLeaves, + ] { + let idx = hm_type as usize; + if !has_found[idx] && hm_type.is_opaque(block_state) { + heightmaps.set(hm_type, x as i32, z as i32, y, self.section.min_y); + has_found[idx] = true; + } } - let is_motion_blocking = blocks_movement(block_state, block) - || Fluid::from_id(block).is_some_and(|fluid| !fluid.states.is_empty()); - - if !has_found[ChunkHeightmapType::MotionBlocking as usize] && is_motion_blocking { - heightmaps.set(ChunkHeightmapType::MotionBlocking, pos, self.section.min_y); - has_found[ChunkHeightmapType::MotionBlocking as usize] = true; - } - - if !has_found[ChunkHeightmapType::MotionBlockingNoLeaves as usize] - && is_motion_blocking - && !MINECRAFT_LEAVES.1.contains(&block) - { - heightmaps.set( - ChunkHeightmapType::MotionBlockingNoLeaves, - pos, - self.section.min_y, - ); - has_found[ChunkHeightmapType::MotionBlockingNoLeaves as usize] = true; - } - - if !has_found.contains(&false) { + if has_found.iter().all(|&found| found) { return; } } - let pos = BlockPos::new(x as i32, self.section.min_y, z as i32); for (idx, is_set) in has_found.iter().enumerate() { if !(*is_set) { - heightmaps.set(idx.try_into().unwrap(), pos, self.section.min_y); + heightmaps.set( + idx.try_into().unwrap(), + x as i32, + z as i32, + self.section.min_y - 1, + self.section.min_y, + ); } } } @@ -690,7 +753,7 @@ mod tests { let mut sections = vec![BlockPalette::default(), BlockPalette::default()]; sections[1].set(0, 0, 0, Block::LAVA.default_state.id); - let cache = ChunkSections::build_random_tick_sections_cache(§ions); + let (cache, _mask) = ChunkSections::build_random_tick_sections_cache(§ions); assert!(!cache[0].is_randomly_ticking()); assert!(cache[1].random_ticking_fluid_count > 0); assert!(cache[1].is_randomly_ticking()); @@ -735,4 +798,32 @@ mod tests { assert!(cache[0].is_randomly_ticking()); } } + + #[test] + fn test_heightmap_is_opaque() { + use crate::chunk::ChunkHeightmapType; + + let air = Block::AIR.default_state; + let stone = Block::STONE.default_state; + let leaves = Block::OAK_LEAVES.default_state; + let water = Block::WATER.default_state; + + // WORLD_SURFACE: Everything except air + assert!(!ChunkHeightmapType::WorldSurface.is_opaque(air)); + assert!(ChunkHeightmapType::WorldSurface.is_opaque(stone)); + assert!(ChunkHeightmapType::WorldSurface.is_opaque(leaves)); + assert!(ChunkHeightmapType::WorldSurface.is_opaque(water)); + + // MOTION_BLOCKING: Blocks movement OR is liquid + assert!(!ChunkHeightmapType::MotionBlocking.is_opaque(air)); + assert!(ChunkHeightmapType::MotionBlocking.is_opaque(stone)); + assert!(ChunkHeightmapType::MotionBlocking.is_opaque(leaves)); // Leaves block movement + assert!(ChunkHeightmapType::MotionBlocking.is_opaque(water)); // Water is liquid + + // MOTION_BLOCKING_NO_LEAVES: Blocks movement OR is liquid, but NOT leaves + assert!(!ChunkHeightmapType::MotionBlockingNoLeaves.is_opaque(air)); + assert!(ChunkHeightmapType::MotionBlockingNoLeaves.is_opaque(stone)); + assert!(!ChunkHeightmapType::MotionBlockingNoLeaves.is_opaque(leaves)); // Excludes leaves + assert!(ChunkHeightmapType::MotionBlockingNoLeaves.is_opaque(water)); // Water is liquid + } } diff --git a/pumpkin-world/src/chunk_system/generation_cache.rs b/pumpkin-world/src/chunk_system/generation_cache.rs index 4570585c4..db2102598 100644 --- a/pumpkin-world/src/chunk_system/generation_cache.rs +++ b/pumpkin-world/src/chunk_system/generation_cache.rs @@ -194,7 +194,7 @@ impl GenerationCache for Cache { } match &mut self.chunks[(dx * self.size + dz) as usize] { Chunk::Level(data) => { - data.section.set_block_absolute_y( + data.set_block_absolute_y( (pos.x & 15) as usize, pos.y, (pos.z & 15) as usize, diff --git a/pumpkin-world/src/chunk_system/mod.rs b/pumpkin-world/src/chunk_system/mod.rs index d9104615b..275ecd66c 100644 --- a/pumpkin-world/src/chunk_system/mod.rs +++ b/pumpkin-world/src/chunk_system/mod.rs @@ -12,7 +12,7 @@ pub type ChunkPos = pumpkin_util::math::vector2::Vector2; pub type ChunkLevel = HashMapType; pub type IOLock = std::sync::Arc<( std::sync::Mutex>, - std::sync::Condvar, + tokio::sync::Notify, )>; pub mod channel; diff --git a/pumpkin-world/src/chunk_system/schedule.rs b/pumpkin-world/src/chunk_system/schedule.rs index 0a016a2e4..327c1f3f6 100644 --- a/pumpkin-world/src/chunk_system/schedule.rs +++ b/pumpkin-world/src/chunk_system/schedule.rs @@ -19,7 +19,7 @@ use std::cmp::{Ordering, max}; use std::collections::{BinaryHeap, HashMap}; use std::mem::swap; use std::sync::atomic::Ordering::Relaxed; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use tracing::{debug, error, info, trace, warn}; @@ -99,7 +99,10 @@ impl GenerationSchedule { let (send_gen, recv_gen) = crossfire::compat::mpmc::bounded_blocking(gen_thread_count + 5); - let io_lock = Arc::new((Mutex::new(HashMapType::default()), Condvar::new())); + let io_lock = Arc::new(( + Mutex::new(HashMapType::default()), + tokio::sync::Notify::new(), + )); for _ in 0..io_read_thread_count { level.chunk_system_tasks.spawn(io_read_work( diff --git a/pumpkin-world/src/chunk_system/worker_logic.rs b/pumpkin-world/src/chunk_system/worker_logic.rs index fe11e8c1d..217867e27 100644 --- a/pumpkin-world/src/chunk_system/worker_logic.rs +++ b/pumpkin-world/src/chunk_system/worker_logic.rs @@ -63,24 +63,31 @@ pub async fn io_read_work( debug!("io read thread start"); let biome_mixer_seed = hash_seed(level.world_gen.random_config.seed); let dimension = &level.world_gen.dimension; - let (t_send, mut t_recv) = tokio::sync::mpsc::channel(16); // Cleaner loop and async recv while let Ok(batch) = recv.recv().await { for pos in &batch { // Lock handling - tokio::task::block_in_place(|| { - let mut data = lock.0.lock().unwrap(); - while data.contains_key(pos) { - data = lock.1.wait(data).unwrap(); + loop { + let notified = lock.1.notified(); + if !lock.0.lock().unwrap().contains_key(pos) { + break; } - }); + notified.await; + } } - level - .chunk_saver - .fetch_chunks(&level.level_folder, &batch, t_send.clone()) - .await; + let (t_send, mut t_recv) = tokio::sync::mpsc::channel(1000); + + let level_clone = level.clone(); + let batch_clone = batch.clone(); + + let fetch_task = tokio::spawn(async move { + level_clone + .chunk_saver + .fetch_chunks(&level_clone.level_folder, &batch_clone, t_send) + .await; + }); for _ in 0..batch.len() { let data = match t_recv.recv().await { @@ -168,6 +175,7 @@ pub async fn io_read_work( } } } + let _ = fetch_task.await; } debug!("io read thread stop"); } @@ -211,7 +219,7 @@ pub async fn io_write_work(recv: AsyncRx>, level: Arc, SyncChunk>>, loaded_entity_chunks: Arc, SyncEntityChunk>>, + pub chunks_with_scheduled_ticks: Arc>>, pub chunk_loading: Mutex, chunk_watchers: Arc, usize>>, @@ -194,6 +195,7 @@ impl Level { schedule_tick_counts: AtomicU64::new(0), loaded_chunks: Arc::new(DashMap::new()), loaded_entity_chunks: Arc::new(DashMap::new()), + chunks_with_scheduled_ticks: Arc::new(dashmap::DashSet::new()), chunk_loading: Mutex::new(ChunkLoading::new(level_channel.clone())), chunk_watchers: Arc::new(DashMap::new()), tasks: TaskTracker::new(), @@ -436,69 +438,83 @@ impl Level { }); } - pub fn get_tick_data(&self) -> TickData { + pub fn get_tick_data(&self, active_chunks: &FxHashSet>) -> TickData { let mut ticks = TickData { block_ticks: Vec::new(), fluid_ticks: Vec::new(), - random_ticks: Vec::with_capacity(self.loaded_chunks.len() * 3), + random_ticks: Vec::with_capacity(active_chunks.len() * 3), block_entities: Vec::new(), }; - // Clone the Arcs quickly to release map shard locks! - let chunks_to_tick: Vec = self - .loaded_chunks - .iter() - .map(|entry| entry.value().clone()) - .collect(); + // 1. Process active chunks (random ticks, block entities) + for pos in active_chunks { + if let Some(chunk) = self.loaded_chunks.get(pos) { + let chunk = chunk.value(); + let chunk_x_base = chunk.x * 16; + let chunk_z_base = chunk.z * 16; + let section_count = chunk.section.count; - for chunk in chunks_to_tick { - let chunk_x_base = chunk.x * 16; - let chunk_z_base = chunk.z * 16; - let section_count = chunk.section.count; + ticks + .block_entities + .extend(chunk.block_entities.lock().unwrap().values().cloned()); - ticks - .block_entities - .extend(chunk.block_entities.lock().unwrap().values().cloned()); + // Use the bitmask to skip sections + let mask = chunk.section.randomly_ticking_mask.load(Ordering::Relaxed); + if mask != 0 { + let sections = chunk.section.block_sections.read().unwrap(); + let min_y = chunk.section.min_y; - // Acquire the read lock once per chunk to avoid per-section lock overhead - let sections = chunk.section.block_sections.read().unwrap(); - let random_tick_sections = chunk.section.random_tick_sections.read().unwrap(); - let min_y = chunk.section.min_y; + for i in 0..section_count { + if (mask & (1 << i)) == 0 { + continue; + } + let y_base = min_y + (i as i32 * 16); + for _ in 0..3 { + let r = rand::random::(); + let x_offset = (r & 0xF) as usize; + let z_offset = (r >> 8 & 0xF) as usize; + let y_in_section = ((r >> 4) & 0xF) as usize; - for i in 0..section_count { - if !random_tick_sections[i].is_randomly_ticking() { - continue; - } - let y_base = min_y + (i as i32 * 16); - for _ in 0..3 { - // Generate a fresh random per tick attempt - let r = rand::random::(); - let x_offset = (r & 0xF) as usize; - let z_offset = (r >> 8 & 0xF) as usize; - let y_in_section = ((r >> 4) & 0xF) as usize; - - // Read directly from the already-locked section palette - let block_state_id = sections[i].get(x_offset, y_in_section, z_offset); - let tick_block = has_random_ticks(block_state_id); - let tick_fluid = has_random_ticking_fluid(block_state_id); - if tick_block || tick_fluid { - let absolute_y = y_base + y_in_section as i32; - ticks.random_ticks.push(RandomTickSample { - position: BlockPos::new( - chunk_x_base + x_offset as i32, - absolute_y, - chunk_z_base + z_offset as i32, - ), - tick_block, - tick_fluid, - }); + let block_state_id = sections[i].get(x_offset, y_in_section, z_offset); + let tick_block = has_random_ticks(block_state_id); + let tick_fluid = has_random_ticking_fluid(block_state_id); + if tick_block || tick_fluid { + ticks.random_ticks.push(RandomTickSample { + position: BlockPos::new( + chunk_x_base + x_offset as i32, + y_base + y_in_section as i32, + chunk_z_base + z_offset as i32, + ), + tick_block, + tick_fluid, + }); + } + } } } } - drop(random_tick_sections); - drop(sections); - ticks.block_ticks.append(&mut chunk.block_ticks.step_tick()); - ticks.fluid_ticks.append(&mut chunk.fluid_ticks.step_tick()); + } + + // 2. Process chunks with scheduled ticks + // We collect keys first to avoid holding DashSet shard lock while accessing loaded_chunks (deadlock risk) + let scheduled_chunk_pos: Vec<_> = self + .chunks_with_scheduled_ticks + .iter() + .map(|p| *p) + .collect(); + for pos in scheduled_chunk_pos { + if let Some(chunk) = self.loaded_chunks.get(&pos) { + let chunk = chunk.value(); + ticks.block_ticks.append(&mut chunk.block_ticks.step_tick()); + ticks.fluid_ticks.append(&mut chunk.fluid_ticks.step_tick()); + + // Remove from set if it no longer has ticks + if !chunk.block_ticks.has_ticks() && !chunk.fluid_ticks.has_ticks() { + self.chunks_with_scheduled_ticks.remove(&pos); + } + } else { + self.chunks_with_scheduled_ticks.remove(&pos); // Chunk unloaded + } } ticks.block_ticks.sort_unstable(); @@ -517,8 +533,17 @@ impl Level { pub fn clean_memory(&self) { self.chunk_watchers.retain(|_, watcher| *watcher != 0); - self.loaded_entity_chunks - .retain(|at, _| self.chunk_watchers.get(at).is_some()); + + let entity_chunks_to_remove: Vec<_> = self + .loaded_entity_chunks + .iter() + .filter(|entry| !self.chunk_watchers.contains_key(entry.key())) + .map(|entry| *entry.key()) + .collect(); + + for pos in entity_chunks_to_remove { + self.loaded_entity_chunks.remove(&pos); + } // if the difference is too big, we can shrink the loaded chunks // (1024 chunks is the equivalent to a 32x32 chunks area) @@ -713,7 +738,7 @@ impl Level { ) -> BlockStateId { let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); self.get_or_fetch_chunk(chunk_coordinate, |chunk| { - let replaced_block_state_id = chunk.section.set_block_absolute_y( + let replaced_block_state_id = chunk.set_block_absolute_y( relative.x as usize, relative.y, relative.z as usize, @@ -823,10 +848,12 @@ impl Level { value: unsafe { &*std::ptr::from_ref::(block) }, }; - self.get_or_fetch_chunk(block_pos.chunk_position(), |chunk| { + let chunk_pos = block_pos.chunk_position(); + self.get_or_fetch_chunk(chunk_pos, |chunk| { chunk.block_ticks.schedule_tick(&scheduled_tick, tick_order); }) .await; + self.chunks_with_scheduled_ticks.insert(chunk_pos); } pub async fn schedule_fluid_tick( @@ -844,10 +871,12 @@ impl Level { value: unsafe { &*std::ptr::from_ref::(fluid) }, }; - self.get_or_fetch_chunk(block_pos.chunk_position(), |chunk| { + let chunk_pos = block_pos.chunk_position(); + self.get_or_fetch_chunk(chunk_pos, |chunk| { chunk.fluid_ticks.schedule_tick(&scheduled_tick, tick_order); }) .await; + self.chunks_with_scheduled_ticks.insert(chunk_pos); } pub async fn is_block_tick_scheduled( diff --git a/pumpkin-world/src/tick/scheduler.rs b/pumpkin-world/src/tick/scheduler.rs index 1126311c1..3e09a3dcd 100644 --- a/pumpkin-world/src/tick/scheduler.rs +++ b/pumpkin-world/src/tick/scheduler.rs @@ -59,6 +59,14 @@ impl<'a, T: std::hash::Hash + Eq> ChunkTickScheduler<&'a T> { .contains(&(pos, value)) } + pub fn has_ticks(&self) -> bool { + !self + .queued_ticks + .lock() + .unwrap_or_else(|e| e.into_inner()) + .is_empty() + } + #[must_use] pub fn to_vec(&self) -> Vec> { let offset = self.offset.load(Ordering::SeqCst); diff --git a/pumpkin/src/data/player_server.rs b/pumpkin/src/data/player_server.rs index 17ab3aac1..6641d01bb 100644 --- a/pumpkin/src/data/player_server.rs +++ b/pumpkin/src/data/player_server.rs @@ -56,8 +56,12 @@ impl ServerPlayerData { let mut nbt = PNbtCompound::new(); player.write_nbt(&mut nbt).await; + let storage = self.storage.clone(); + let uuid = player.gameprofile.id; // Save to disk - self.storage.save_player_data(&player.gameprofile.id, nbt)?; + tokio::task::spawn_blocking(move || storage.save_player_data(&uuid, nbt)) + .await + .expect("Player data save panicked")?; Ok(()) } @@ -81,8 +85,14 @@ impl ServerPlayerData { let mut nbt = PNbtCompound::new(); player.write_nbt(&mut nbt).await; + let storage = self.storage.clone(); + let uuid = player.gameprofile.id; // Save to disk periodically to prevent data loss on server crash - if let Err(e) = self.storage.save_player_data(&player.gameprofile.id, nbt) { + if let Err(e) = + tokio::task::spawn_blocking(move || storage.save_player_data(&uuid, nbt)) + .await + .expect("Player data periodic save panicked") + { error!( "Failed to save player data for {}: {e}", player.gameprofile.id, @@ -128,8 +138,17 @@ impl ServerPlayerData { /// # Returns /// /// A Result indicating success or the error that occurred. - pub fn load_data(&self, uuid: &uuid::Uuid) -> Result, PlayerDataError> { - match self.storage.load_player_data(uuid) { + pub async fn load_data( + &self, + uuid: &uuid::Uuid, + ) -> Result, PlayerDataError> { + let storage = self.storage.clone(); + let uuid = *uuid; + let result = tokio::task::spawn_blocking(move || storage.load_player_data(&uuid)) + .await + .expect("Player data load panicked"); + + match result { Ok((should_load, data)) => { if !should_load { // No data to load, continue with default data @@ -170,10 +189,16 @@ impl ServerPlayerData { return Ok(()); } - let uuid = &player.gameprofile.id; + let uuid = player.gameprofile.id; let mut nbt = PNbtCompound::new(); player.write_nbt(&mut nbt).await; - self.storage.save_player_data(uuid, nbt) + + let storage = self.storage.clone(); + tokio::task::spawn_blocking(move || storage.save_player_data(&uuid, nbt)) + .await + .expect("Player data extract and save panicked")?; + + Ok(()) } } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 5f1fd039e..f21b248e5 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -14,6 +14,7 @@ use crossbeam::atomic::AtomicCell; use living::LivingEntity; use player::Player; use pumpkin_data::BlockState; +use pumpkin_data::biome::Biome; use pumpkin_data::block_properties::{EnumVariants, Integer0To15, blocks_movement}; use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::dimension::Dimension; @@ -485,6 +486,9 @@ pub struct Entity { pub first_loaded_chunk_position: AtomicCell>>, + pub current_biome: ArcSwap<&'static Biome>, + pub last_biome_update_pos: AtomicCell, + pub portal_cooldown: AtomicU32, pub portal_manager: Mutex>>, @@ -614,6 +618,8 @@ impl Entity { vehicle: Mutex::new(None), riding_cooldown: AtomicI32::new(0), age: AtomicI32::new(0), + current_biome: ArcSwap::new(Arc::new(&Biome::PLAINS)), + last_biome_update_pos: AtomicCell::new(BlockPos::new(floor_x, floor_y, floor_z)), portal_cooldown: AtomicU32::new(0), portal_manager: Mutex::new(None), custom_name: ArcSwap::new(Arc::new(None)), @@ -2781,6 +2787,15 @@ impl EntityBase for Entity { self.was_in_powder_snow .store(was_in_powder_snow, Ordering::Relaxed); self.is_in_powder_snow.store(false, Ordering::Relaxed); + + let block_pos = self.block_pos.load(); + if self.last_biome_update_pos.load() != block_pos { + let world = self.world.load(); + let biome = world.level.get_rough_biome(&block_pos).await; + self.current_biome.store(Arc::new(biome)); + self.last_biome_update_pos.store(block_pos); + } + self.update_last_pos(); self.tick_portal(caller).await; self.update_fluid_state(caller).await; diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index 69cd964eb..cbc8b1ae6 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -533,26 +533,28 @@ impl JavaClient { } } - let mut writer = writer.lock().await; - let mut send_failed = false; - for packet in &packet_batch { - if let Err(err) = writer.write_packet(packet.data.clone()).await { - send_failed = true; - // It is expected that the packet will fail if we are closed - if !close_token.is_cancelled() { - warn!("Failed to send packet to client {id}: {err}"); + let send_failed = { + let mut writer = writer.lock().await; + let mut failed = false; + for packet in &packet_batch { + if let Err(err) = writer.write_packet(packet.data.clone()).await { + failed = true; + // It is expected that the packet will fail if we are closed + if !close_token.is_cancelled() { + warn!("Failed to send packet to client {id}: {err}"); + } + break; } - break; } - } - if !send_failed && let Err(err) = writer.flush().await { - send_failed = true; - if !close_token.is_cancelled() { - warn!("Failed to flush packet batch for client {id}: {err}"); + if !failed && let Err(err) = writer.flush().await { + failed = true; + if !close_token.is_cancelled() { + warn!("Failed to flush packet batch for client {id}: {err}"); + } } - } - drop(writer); + failed + }; if send_failed { // We now need to close the connection to the client since the stream is in an unknown state. diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 85d4cbc6e..e9fa278cf 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -179,7 +179,11 @@ impl Server { let level_info = level_info.unwrap_or_else(|err| { warn!("Failed to get level_info, using default instead: {err}"); - LevelData::default(basic_config.seed) + let default_data = LevelData::default(basic_config.seed); + if let Err(err) = AnvilLevelInfo.write_world_info(&default_data, &world_path) { + error!("Failed to save level.dat: {err}"); + } + default_data }); let seed = level_info.world_gen_settings.seed; @@ -423,7 +427,7 @@ impl Server { let gamemode = self.defaultgamemode.lock().await.gamemode; let (world, nbt) = - if let Ok(Some(mut data)) = self.player_data_storage.load_data(&profile.id) { + if let Ok(Some(mut data)) = self.player_data_storage.load_data(&profile.id).await { let _version = data.get_int().unwrap_or(0); if let Ok(dimension_key) = data.get_string() { if let Some(dimension) = Dimension::from_name(&dimension_key) { @@ -799,7 +803,7 @@ impl Server { /// Ticks essential server functions that must run even when the game is frozen. /// This includes player ticking (network, keep-alives) and flushing world updates to clients. - pub async fn tick_players_and_network(&self) { + pub async fn tick_players_and_network(self: &Arc) { let worlds = self.worlds.load(); for world in worlds.iter() { @@ -807,12 +811,18 @@ impl Server { world.flush_synced_block_events().await; } + let mut set = JoinSet::new(); for world in worlds.iter() { let players = world.players.load(); for player in players.iter() { - player.tick(self).await; + let player_clone = player.clone(); + let server_clone = self.clone(); + set.spawn(async move { + player_clone.tick(&server_clone).await; + }); } } + set.join_all().await; } /// Ticks the game logic for all worlds. This is the part that is affected by `/tick freeze`. pub async fn tick_worlds(self: &Arc) { @@ -825,7 +835,7 @@ impl Server { let server = self.clone(); set.spawn(async move { - world.tick(&server).await; + world.tick(server).await; }); } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 2f6666c6e..e880aa561 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -155,6 +155,8 @@ use weather::Weather; type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; +use rustc_hash::FxHashSet; + impl PumpkinError for GetBlockError { fn is_kick(&self) -> bool { false @@ -211,6 +213,8 @@ pub struct World { pub portal_poi: Mutex, /// End Dragon fight manager (only present in `THE_END` dimension). pub dragon_fight: Option>, + pub spawn_state: ArcSwap, + pub active_chunks: ArcSwap>>, } impl PartialEq for World { @@ -255,10 +259,40 @@ impl World { portal_poi: Mutex::new(portal_poi), dragon_fight: (dimension == Dimension::THE_END) .then(|| Mutex::new(dragon_fight::DragonFight::new())), + spawn_state: ArcSwap::new(Arc::new(SpawnState::empty())), + active_chunks: ArcSwap::new(Arc::new(FxHashSet::default())), server, } } + pub fn update_active_chunks(self: &Arc) { + let mut active_chunks = FxHashSet::default(); + for player in self.players.load().iter() { + let center = player.living_entity.entity.chunk_pos.load(); + // TODO: gamerule for view distance/ticking distance + for dx in -8..=8 { + for dy in -8..=8 { + active_chunks.insert(center.add_raw(dx, dy)); + } + } + } + + let mut spawnable_chunks = 0; + for pos in &active_chunks { + if self.level.is_chunk_loaded(pos) { + spawnable_chunks += 1; + } + } + + self.active_chunks.store(Arc::new(active_chunks)); + + self.spawn_state.store(Arc::new(SpawnState::new( + spawnable_chunks, + &self.entities, + self, + ))); + } + pub fn get_lighting_config(&self) -> LightingEngineConfig { self.server .upgrade() @@ -745,12 +779,13 @@ impl World { .await; } - pub async fn tick(self: &Arc, server: &Server) { + pub async fn tick(self: &Arc, server: Arc) { let start = tokio::time::Instant::now(); // IMPORTANT: send flush_block_updates first to prevent issues with CAcknowledgeBlockChange self.flush_block_updates().await; self.flush_synced_block_events().await; + self.update_active_chunks(); self.tick_environment().await; let chunk_start = tokio::time::Instant::now(); @@ -758,33 +793,64 @@ impl World { let chunk_elapsed = chunk_start.elapsed(); let player_start = tokio::time::Instant::now(); - let players = self.players.load(); + let players = self.players.load().clone(); let player_count = players.len(); + + let mut player_tasks = tokio::task::JoinSet::new(); for player in players.iter() { - player.tick(server).await; + let player_clone = player.clone(); + let server_clone = server.clone(); + player_tasks.spawn(async move { + player_clone.tick(&server_clone).await; + }); + } + while let Some(res) = player_tasks.join_next().await { + if let Err(e) = res { + error!("Player tick panicked: {:?}", e); + } } let player_elapsed = player_start.elapsed(); let entity_start = tokio::time::Instant::now(); - let entities_to_tick = self.entities.load(); + let entities_to_tick = self.entities.load().clone(); let entity_count = entities_to_tick.len(); + let mut entity_tasks = tokio::task::JoinSet::new(); for entity in entities_to_tick.iter() { - entity.get_entity().age.fetch_add(1, Relaxed); - entity.tick(entity, server).await; + let entity_clone = entity.clone(); + let server_clone = server.clone(); + let players_clone = players.clone(); + entity_tasks.spawn(async move { + entity_clone.get_entity().age.fetch_add(1, Relaxed); + entity_clone.tick(&entity_clone, &server_clone).await; - for player in players.iter() { - if player - .living_entity - .entity - .bounding_box - .load() - .expand(1.0, 0.5, 1.0) - .intersects(&entity.get_entity().bounding_box.load()) - { - entity.on_player_collision(player).await; - break; + let entity_inner = entity_clone.get_entity(); + let entity_bb = entity_inner.bounding_box.load(); + + for player in players_clone.iter() { + let player_pos = player.living_entity.entity.pos.load(); + let entity_pos = entity_inner.pos.load(); + + if (player_pos.x - entity_pos.x).abs() < 5.0 + && (player_pos.y - entity_pos.y).abs() < 5.0 + && (player_pos.z - entity_pos.z).abs() < 5.0 + && player + .living_entity + .entity + .bounding_box + .load() + .expand(1.0, 0.5, 1.0) + .intersects(&entity_bb) + { + entity_clone.on_player_collision(player).await; + break; + } } + }); + } + while let Some(res) = entity_tasks.join_next().await { + if let Err(e) = res { + error!("Entity tick panicked: {:?}", e); } } let entity_elapsed = entity_start.elapsed(); @@ -912,7 +978,8 @@ impl World { #[expect(clippy::too_many_lines)] pub async fn tick_chunks(self: &Arc) { - let tick_data = self.level.get_tick_data(); + let active_chunks = self.active_chunks.load(); + let tick_data = self.level.get_tick_data(&active_chunks); for scheduled_tick in tick_data.block_ticks { let block = self.get_block(&scheduled_tick.position).await; if let Some(pumpkin_block) = self.block_registry.get_pumpkin_block(block.id) { @@ -966,28 +1033,17 @@ impl World { } } - let mut spawning_chunks_map = HashMap::new(); - // TODO use FixedPlayerDistanceChunkTracker - - for i in self.players.load().iter() { - let center = i.living_entity.entity.chunk_pos.load(); - for dx in -8..=8 { - for dy in -8..=8 { - // if dx.abs() <= 2 || dy.abs() <= 2 || dx.abs() >= 6 || dy.abs() >= 6 { // this is only for debug, spawning runs too slow - // continue; - // } - let chunk_pos = center.add_raw(dx, dy); - if !spawning_chunks_map.contains_key(&chunk_pos) - && let Some(chunk) = self.level.try_get_chunk(&chunk_pos) - { - spawning_chunks_map.entry(chunk_pos).or_insert(chunk); - } - } - } + let world_simple: Arc = self.clone(); + let mut block_entity_tasks = JoinSet::new(); + for block_entity in tick_data.block_entities { + let world_simple = world_simple.clone(); + block_entity_tasks.spawn(async move { + block_entity.tick(&world_simple).await; + }); } + while block_entity_tasks.join_next().await.is_some() {} - let spawn_state = - SpawnState::new(spawning_chunks_map.len() as i32, &self.entities, self).await; // TODO store it + let spawn_state = self.spawn_state.load(); // TODO gamerule this.spawnEnemies || this.spawnFriendlies let (spawn_mobs, spawn_monsters, peaceful) = { @@ -1009,13 +1065,21 @@ impl World { spawn_passives, ); + if spawn_list.is_empty() { + return; + } + + let mut spawning_chunks = Vec::new(); + for pos in active_chunks.iter() { + if let Some(chunk) = self.level.try_get_chunk(pos) { + spawning_chunks.push((*pos, chunk)); + } + } + // log::debug!("spawning list size {}", spawn_list.len()); - let mut spawning_chunks: Vec<(Vector2, Arc)> = - spawning_chunks_map.into_iter().collect(); spawning_chunks.shuffle(&mut rng()); let mut spawn_tasks = JoinSet::new(); - let spawn_state = Arc::new(spawn_state); let spawn_list = Arc::new(spawn_list); for (pos, chunk) in spawning_chunks { @@ -1030,17 +1094,6 @@ impl World { }); } while spawn_tasks.join_next().await.is_some() {} - - let world: Arc = self.clone(); - - let mut block_entity_tasks = JoinSet::new(); - for block_entity in tick_data.block_entities { - let world = world.clone(); - block_entity_tasks.spawn(async move { - block_entity.tick(&world).await; - }); - } - while block_entity_tasks.join_next().await.is_some() {} } pub async fn get_fluid_collisions(self: &Arc, bounding_box: BoundingBox) -> Vec<&Fluid> { @@ -2533,6 +2586,7 @@ impl World { let mut ids_to_remove = Vec::new(); let mut entities_to_load = Vec::new(); + let mut entity_data = Vec::new(); { let mut data = chunk.data.lock().await; for (uuid, entity_nbt) in data.iter_mut() { @@ -2544,16 +2598,19 @@ impl World { warn!("Entity has no valid Entity Type {id}"); 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) - .await; - entity_nbt.read_pos = 0; - entity.read_nbt_non_mut(entity_nbt).await; - entities_to_load.push(entity); + entity_data.push((*uuid, entity_type, entity_nbt.clone())); } } + for (uuid, entity_type, mut entity_nbt) in entity_data { + // 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).await; + entity_nbt.read_pos = 0; + entity.read_nbt_non_mut(&mut entity_nbt).await; + entities_to_load.push(entity); + } + for entity in entities_to_load { let base_entity = entity.get_entity(); ids_to_remove.push(VarInt(base_entity.entity_id)); @@ -2605,23 +2662,30 @@ impl World { // Add all new Entities to the world let mut entities_to_add: Vec> = Vec::new(); + let mut entity_data = Vec::new(); + { + let mut data = chunk.data.lock().await; + for (uuid, entity_nbt) in data.iter_mut() { + let Ok(id) = entity_nbt.get_string() else { + debug!("Entity has no ID"); + continue; + }; + let Some(entity_type) = + EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(&id)) + else { + warn!("Entity has no valid Entity Type {id}"); + continue; + }; + entity_data.push((*uuid, entity_type, entity_nbt.clone())); + } + } - for (uuid, entity_nbt) in chunk.data.lock().await.iter_mut() { - let Ok(id) = entity_nbt.get_string() else { - debug!("Entity has no ID"); - continue; - }; - let Some(entity_type) = - EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(&id)) - else { - warn!("Entity has no valid Entity Type {id}"); - continue; - }; + for (uuid, entity_type, mut entity_nbt) in entity_data { // 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).await; + from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, uuid).await; entity_nbt.read_pos = 0; - entity.read_nbt_non_mut(entity_nbt).await; + entity.read_nbt_non_mut(&mut entity_nbt).await; let base_entity = entity.get_entity(); player .client @@ -3053,10 +3117,19 @@ impl World { pub async fn add_entity_silent(&self, entity: Arc) { let base_entity = entity.get_entity(); - let chunk_coordinate = base_entity.block_pos.load().chunk_position(); + + // Update biome + let block_pos = base_entity.block_pos.load(); + let biome = self.level.get_rough_biome(&block_pos).await; + base_entity.current_biome.store(Arc::new(biome)); + base_entity.last_biome_update_pos.store(block_pos); + + let chunk_coordinate = block_pos.chunk_position(); let mut nbt = PNbtCompound::new(); entity.write_nbt(&mut nbt).await; + self.spawn_state.load().add_entity(self, entity.as_ref()); + let chunk = self.level.get_entity_chunk(chunk_coordinate).await; chunk.data.lock().await.insert(base_entity.entity_uuid, nbt); chunk.mark_dirty(true); @@ -3068,18 +3141,23 @@ impl World { }); } - pub async fn remove_entity(&self, entity: &Entity) { + pub async fn remove_entity(&self, entity: &dyn EntityBase) { + let base_entity = entity.get_entity(); + self.spawn_state.load().remove_entity(self, entity); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone(); - new_entities.retain(|e| e.get_entity().entity_uuid != entity.entity_uuid); + new_entities.retain(|e| e.get_entity().entity_uuid != base_entity.entity_uuid); new_entities }); - let chunk_pos = entity.chunk_pos.load(); - self.broadcast_to_chunk(chunk_pos, &CRemoveEntities::new(&[entity.entity_id.into()])) - .await; + let chunk_pos = base_entity.chunk_pos.load(); + self.broadcast_to_chunk( + chunk_pos, + &CRemoveEntities::new(&[base_entity.entity_id.into()]), + ) + .await; - self.remove_entity_data(entity).await; + self.remove_entity_data(base_entity).await; } pub async fn set_block_breaking(&self, from: &Entity, location: BlockPos, progress: i32) { @@ -3104,7 +3182,7 @@ impl World { let replaced_block_state_id = self .level .get_or_fetch_chunk(chunk_coordinate, |chunk| { - let replaced_block_state_id = chunk.section.set_block_absolute_y( + let replaced_block_state_id = chunk.set_block_absolute_y( relative.x as usize, relative.y, relative.z as usize, diff --git a/pumpkin/src/world/natural_spawner.rs b/pumpkin/src/world/natural_spawner.rs index 40b256cd6..120352c01 100644 --- a/pumpkin/src/world/natural_spawner.rs +++ b/pumpkin/src/world/natural_spawner.rs @@ -47,11 +47,24 @@ impl fmt::Debug for MobCounts { } } +impl Clone for MobCounts { + fn clone(&self) -> Self { + Self(std::array::from_fn(|i| { + AtomicI32::new(self.0[i].load(Relaxed)) + })) + } +} + impl MobCounts { #[inline] pub fn add(&self, category: &'static MobCategory) { self.0[category.id].fetch_add(1, Relaxed); } + + #[inline] + pub fn remove(&self, category: &'static MobCategory) { + self.0[category.id].fetch_sub(1, Relaxed); + } #[inline] pub fn can_spawn(&self, category: &'static MobCategory) -> bool { self.0[category.id].load(Relaxed) < category.max @@ -63,6 +76,23 @@ pub struct LocalMobCapCalculator { players_near_chunk: DashMap, Vec>, } +impl Clone for LocalMobCapCalculator { + fn clone(&self) -> Self { + let player_mob_counts = DashMap::new(); + for r in &self.player_mob_counts { + player_mob_counts.insert(*r.key(), r.value().clone()); + } + let players_near_chunk = DashMap::new(); + for r in &self.players_near_chunk { + players_near_chunk.insert(*r.key(), r.value().clone()); + } + Self { + player_mob_counts, + players_near_chunk, + } + } +} + impl Default for LocalMobCapCalculator { fn default() -> Self { Self { @@ -87,7 +117,7 @@ impl LocalMobCapCalculator { dx * dx + dy * dy } - fn get_players_near(&self, world: &Arc, chunk_pos: Vector2) -> Vec { + fn get_players_near(&self, world: &World, chunk_pos: Vector2) -> Vec { if let Some(players) = self.players_near_chunk.get(&chunk_pos) { return players.value().clone(); } @@ -105,12 +135,7 @@ impl LocalMobCapCalculator { players } - pub fn add_mob( - &self, - chunk_pos: Vector2, - world: &Arc, - category: &'static MobCategory, - ) { + pub fn add_mob(&self, chunk_pos: Vector2, world: &World, category: &'static MobCategory) { let players = self.get_players_near(world, chunk_pos); for player in players { self.player_mob_counts @@ -120,10 +145,24 @@ impl LocalMobCapCalculator { } } + pub fn remove_mob( + &self, + chunk_pos: Vector2, + world: &World, + category: &'static MobCategory, + ) { + let players = self.get_players_near(world, chunk_pos); + for player in players { + if let Some(count) = self.player_mob_counts.get(&player) { + count.remove(category); + } + } + } + pub fn can_spawn( &self, category: &'static MobCategory, - world: &Arc, + world: &World, chunk_pos: Vector2, ) -> bool { let players = self.get_players_near(world, chunk_pos); @@ -140,7 +179,7 @@ impl LocalMobCapCalculator { } } -#[derive(Debug)] +#[derive(Debug, Clone)] struct PointCharge(Vector3, f64); impl PointCharge { @@ -153,6 +192,12 @@ impl PointCharge { #[derive(Default, Debug)] struct PotentialCalculator(std::sync::Mutex>); +impl Clone for PotentialCalculator { + fn clone(&self) -> Self { + Self(std::sync::Mutex::new(self.0.lock().unwrap().clone())) + } +} + impl PotentialCalculator { pub fn add_charge(&self, pos: &BlockPos, charge: f64) { if charge != 0. { @@ -162,6 +207,16 @@ impl PotentialCalculator { .push(PointCharge(pos.to_f64(), charge)); } } + + pub fn remove_charge(&self, pos: &BlockPos, charge: f64) { + if charge != 0. { + let mut charges = self.0.lock().unwrap(); + let pos_f64 = pos.to_f64(); + if let Some(idx) = charges.iter().position(|c| c.0 == pos_f64 && c.1 == charge) { + charges.swap_remove(idx); + } + } + } pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 { if charge == 0. { return 0.; @@ -186,6 +241,18 @@ pub struct SpawnState { last_checked: AtomicCell>, } +impl Clone for SpawnState { + fn clone(&self) -> Self { + Self { + spawnable_chunk_count: self.spawnable_chunk_count, + mob_category_counts: self.mob_category_counts.clone(), + spawn_potential: self.spawn_potential.clone(), + local_mob_cap_calculator: self.local_mob_cap_calculator.clone(), + last_checked: AtomicCell::new(self.last_checked.load()), + } + } +} + impl fmt::Debug for SpawnState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("SpawnState") @@ -199,7 +266,64 @@ impl fmt::Debug for SpawnState { } impl SpawnState { - pub async fn new( + #[must_use] + pub fn empty() -> Self { + Self { + spawnable_chunk_count: 0, + mob_category_counts: MobCounts::default(), + spawn_potential: PotentialCalculator::default(), + local_mob_cap_calculator: LocalMobCapCalculator::default(), + last_checked: AtomicCell::new(None), + } + } + + pub const fn set_spawnable_chunk_count(&mut self, count: i32) { + self.spawnable_chunk_count = count; + } + + pub fn add_entity(&self, world: &World, entity: &dyn EntityBase) { + let base_entity = entity.get_entity(); + let entity_type = base_entity.entity_type; + if !entity_type.mob || entity_type.category == &MobCategory::MISC { + return; + } + let entity_pos = base_entity.block_pos.load(); + let biome = base_entity.current_biome.load(); + if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) { + self.spawn_potential.add_charge(&entity_pos, cost.charge); + } + if entity_type.mob { + self.local_mob_cap_calculator.add_mob( + base_entity.chunk_pos.load(), + world, + entity_type.category, + ); + self.mob_category_counts.add(entity_type.category); + } + } + + pub fn remove_entity(&self, world: &World, entity: &dyn EntityBase) { + let base_entity = entity.get_entity(); + let entity_type = base_entity.entity_type; + if !entity_type.mob || entity_type.category == &MobCategory::MISC { + return; + } + let entity_pos = base_entity.block_pos.load(); + let biome = base_entity.current_biome.load(); + if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) { + self.spawn_potential.remove_charge(&entity_pos, cost.charge); + } + if entity_type.mob { + self.local_mob_cap_calculator.remove_mob( + base_entity.chunk_pos.load(), + world, + entity_type.category, + ); + self.mob_category_counts.remove(entity_type.category); + } + } + + pub fn new( chunk_count: i32, entities: &ArcSwap>>, world: &Arc, @@ -207,6 +331,7 @@ impl SpawnState { let potential = PotentialCalculator::default(); let local_mob_cap = LocalMobCapCalculator::default(); let counter = MobCounts::default(); + let active_chunks = world.active_chunks.load(); for entity in entities.load().iter() { let entity = entity.get_entity(); let entity_type = entity.entity_type; @@ -214,13 +339,17 @@ impl SpawnState { // TODO (mob.isPersistenceRequired() || mob.requiresCustomPersistence()) continue; } + let chunk_pos = entity.chunk_pos.load(); + if !active_chunks.contains(&chunk_pos) { + continue; + } let entity_pos = entity.block_pos.load(); - let biome = world.level.get_rough_biome(&entity_pos).await; + let biome = entity.current_biome.load(); if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) { potential.add_charge(&entity_pos, cost.charge); } if entity_type.mob { - local_mob_cap.add_mob(entity.chunk_pos.load(), world, entity_type.category); + local_mob_cap.add_mob(chunk_pos, world, entity_type.category); } counter.add(entity_type.category); } @@ -364,12 +493,12 @@ pub fn get_random_pos_within( let x = (chunk_pos.x << 4) + rng.next_bounded_i32(16); let z = (chunk_pos.y << 4) + rng.next_bounded_i32(16); - let temp_y = chunk - .heightmap - .lock() - .unwrap() - .get(ChunkHeightmapType::WorldSurface, x, z, min_y) - + 1; + let temp_y = chunk.heightmap.lock().unwrap().get( + ChunkHeightmapType::WorldSurface, + x, + z, + chunk.section.min_y, + ) + 1; let y = rng.next_inbetween_i32(min_y, temp_y); BlockPos::new(x, y, z) } @@ -383,20 +512,34 @@ pub async fn spawn_category_for_position( ) { let mut batch_buffer = vec![]; let mut spawn_cluster_size = 0; - let mut new_pos = pos; let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect(); - for _ in 0..3 { - let mut new_x = new_pos.0.x; - let mut new_z = new_pos.0.z; + 'group_loop: for _ in 0..3 { + let mut new_x = pos.0.x; + let mut new_z = pos.0.z; let mut random_group_size = (rng().random::() * 4.).ceil() as i32; let mut inc = 0; + let mut current_spawner = None; - 'outer: while inc < random_group_size { + 'spawn_loop: while inc < random_group_size { new_x += rng().random_range(0..6) - rng().random_range(0..6); new_z += rng().random_range(0..6) - rng().random_range(0..6); - new_pos = BlockPos::new(new_x, new_pos.0.y, new_z); + let mut new_pos = BlockPos::new(new_x, pos.0.y, new_z); + + if current_spawner.is_none() { + let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else { + break 'spawn_loop; + }; + current_spawner = Some(spawner); + random_group_size = rng().random_range(spawner.min_count..=spawner.max_count); + } + + let spawner = current_spawner.unwrap(); + let entity_type = + &EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap(); + + new_pos = adjust_spawn_position(world, new_pos, entity_type).await; let spawn_pos_f64 = Vector3::new( f64::from(new_pos.0.x) + 0.5, @@ -410,14 +553,6 @@ pub async fn spawn_category_for_position( continue; } - let Some(spawner) = get_random_spawn_mob_at(world, category, &new_pos).await else { - break 'outer; - }; - - random_group_size = rng().random_range(spawner.min_count..=spawner.max_count); - let entity_type = - &EntityType::from_name(spawner.r#type.strip_prefix("minecraft:").unwrap()).unwrap(); - if !is_valid_spawn_position_for_type( world, &new_pos, @@ -444,7 +579,7 @@ pub async fn spawn_category_for_position( batch_buffer.push(entity); spawn_state.after_spawn(entity_type, &new_pos, world).await; if spawn_cluster_size >= entity_type.limit_per_chunk { - return; + break 'group_loop; } inc += 1; @@ -584,7 +719,7 @@ pub async fn is_valid_spawn_position_for_type( return false; } // TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes - world + if !world .is_space_empty(BoundingBox::new_from_pos( f64::from(block_pos.0.x) + 0.5, f64::from(block_pos.0.y), @@ -596,6 +731,10 @@ pub async fn is_valid_spawn_position_for_type( }, )) .await + { + return false; + } + true } pub async fn is_spawn_position_ok( @@ -629,6 +768,25 @@ pub async fn is_spawn_position_ok( } } +pub async fn adjust_spawn_position( + world: &World, + pos: BlockPos, + entity_type: &'static EntityType, +) -> BlockPos { + if matches!( + entity_type.spawn_restriction.location, + SpawnLocation::OnGround + ) { + let below = pos.down(); + let state = world.get_block_state(&below).await; + // Approximation of isPathfindable(LAND) + if !state.is_full_cube() && !state.is_liquid() { + return below; + } + } + pos +} + #[must_use] pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool { if state.is_full_cube() {