fix: height maps

This commit is contained in:
Alexander Medvedev 2026-04-27 23:16:19 +02:00
parent 3c6f15d5eb
commit a36240dd40
19 changed files with 890 additions and 398 deletions

4
Cargo.lock generated
View File

@ -1848,9 +1848,9 @@ dependencies = [
[[package]] [[package]]
name = "idna_adapter" name = "idna_adapter"
version = "1.2.1" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [ dependencies = [
"icu_normalizer", "icu_normalizer",
"icu_properties", "icu_properties",

View File

@ -130,7 +130,7 @@ hmac = "=0.13.0"
indexmap = "2.14" indexmap = "2.14"
itertools = "0.14.0" itertools = "0.14.0"
libloading = "0.9" libloading = "0.9"
lru = "0.17.0" lru = "0.18.0"
lz4-java-wrc = "0.2.0" lz4-java-wrc = "0.2.0"
md5 = "0.8" md5 = "0.8"
num-bigint = "0.4" num-bigint = "0.4"

View File

@ -0,0 +1,62 @@
pub struct BitStorage<T: AsRef<[i64]>> {
data: T,
bits: u8,
size: usize,
mask: u64,
}
impl<T: AsRef<[i64]>> BitStorage<T> {
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<T: AsRef<[i64]> + AsMut<[i64]>> BitStorage<T> {
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;
}
}
}

View File

@ -1,6 +1,7 @@
use num_traits::{Float, One, PrimInt, Zero}; use num_traits::{Float, One, PrimInt, Zero};
pub mod atomic_f32; pub mod atomic_f32;
pub mod bit_storage;
pub mod block_box; pub mod block_box;
pub mod boundingbox; pub mod boundingbox;
pub mod bounds; pub mod bounds;

View File

@ -113,40 +113,39 @@ impl ChunkData {
position.x, position.y, chunk_data.x_pos, chunk_data.z_pos, position.x, position.y, chunk_data.x_pos, chunk_data.z_pos,
))); )));
} }
let (block_lights, sky_lights, block_palettes, biome_palettes) = chunk_data let section_count = chunk_data.sections.len();
.sections let mut block_lights = Vec::with_capacity(section_count);
.into_iter() let mut sky_lights = Vec::with_capacity(section_count);
.map(|section| { let mut block_palettes = Vec::with_capacity(section_count);
let mut biome_palettes = Vec::with_capacity(section_count);
for section in chunk_data.sections {
// When loading light data, missing data should default to 0 (no light) // When loading light data, missing data should default to 0 (no light)
let block_light = section block_lights.push(
section
.block_light .block_light
.map_or(LightContainer::Empty(0), LightContainer::Full); .map_or(LightContainer::Empty(0), LightContainer::Full),
let sky_light = section );
sky_lights.push(
section
.sky_light .sky_light
.map_or(LightContainer::Empty(0), LightContainer::Full); .map_or(LightContainer::Empty(0), LightContainer::Full),
);
// Convert NBT to Palettes // Convert NBT to Palettes
let block_palette = section block_palettes.push(
section
.block_states .block_states
.map(BlockPalette::from_disk_nbt) .map(BlockPalette::from_disk_nbt)
.unwrap_or_default(); .unwrap_or_default(),
let biome_palette = section );
biome_palettes.push(
section
.biomes .biomes
.map(BiomePalette::from_disk_nbt) .map(BiomePalette::from_disk_nbt)
.unwrap_or_default(); .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)
},
); );
}
// Assemble the LightEngine // Assemble the LightEngine
let light_engine = ChunkLight { let light_engine = ChunkLight {
@ -156,11 +155,13 @@ impl ChunkData {
// Assemble the ChunkSections // Assemble the ChunkSections
let min_y = section_coords::section_to_block(chunk_data.min_y_section); 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 { let section = ChunkSections {
count: block_palettes.len(), count: block_palettes.len(),
block_sections: RwLock::new(block_palettes.into_boxed_slice()), block_sections: RwLock::new(block_palettes.into_boxed_slice()),
random_tick_sections: RwLock::new(random_tick_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_palettes.into_boxed_slice()), biome_sections: RwLock::new(biome_palettes.into_boxed_slice()),
min_y, min_y,
}; };

View File

@ -37,7 +37,7 @@ pub struct ChunkFileManager<S: ChunkSerializer<WriteBackend = PathBuf>> {
// - Guarantee that there is only one serializer per file at a time // - Guarantee that there is only one serializer per file at a time
// - Lazily load files as to support point 1 // - Lazily load files as to support point 1
// - Allow for ease of usage (able to return the serializer from a function) // - Allow for ease of usage (able to return the serializer from a function)
file_locks: RwLock<BTreeMap<PathBuf, ChunkSerializerLazyLoader<S>>>, file_locks: RwLock<BTreeMap<PathBuf, Arc<ChunkSerializerLazyLoader<S>>>>,
watchers: RwLock<BTreeMap<PathBuf, usize>>, watchers: RwLock<BTreeMap<PathBuf, usize>>,
chunk_config: S::ChunkConfig, chunk_config: S::ChunkConfig,
@ -62,8 +62,11 @@ impl<S: ChunkSerializer<WriteBackend = PathBuf>> ChunkSerializerLazyLoader<S> {
/// We can only remove this entry from the map if we are the only ones with a reference to it /// 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 /// IMPORTANT: This must be called within the write lock of the parent map
fn can_remove(&self) -> bool { fn can_remove(loader: &Arc<Self>) -> bool {
match self.internal.get() { if Arc::strong_count(loader) > 1 {
return false;
}
match loader.internal.get() {
Some(arc) => { Some(arc) => {
// A strong count of 1 means it's only in the map // A strong count of 1 means it's only in the map
Arc::strong_count(arc) == 1 Arc::strong_count(arc) == 1
@ -128,16 +131,17 @@ impl<S: ChunkSerializer<WriteBackend = PathBuf>> ChunkFileManager<S> {
// We use a lazy loader here to quickly make an insertion into the map without holding the // We use a lazy loader here to quickly make an insertion into the map without holding the
// lock for too long starving other threads // 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 serializer_loader.get().await
} else { } else {
self.file_locks let mut locks = self.file_locks.write().await;
.write() let loader = locks
.await
.entry(path.into()) .entry(path.into())
.or_insert_with(|| ChunkSerializerLazyLoader::new(path.into())) .or_insert_with(|| Arc::new(ChunkSerializerLazyLoader::new(path.into())))
.get() .clone();
.await drop(locks);
loader.get().await
} }
} }
} }
@ -320,7 +324,8 @@ where
// We only need to update the chunk if it is dirty // We only need to update the chunk if it is dirty
if chunk_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>(()) Ok::<(), ChunkWritingError>(())
} }
@ -329,12 +334,10 @@ where
futures::future::try_join_all(update_tasks).await?; futures::future::try_join_all(update_tasks).await?;
trace!("Updated data for file {}", path.display()); trace!("Updated data for file {}", path.display());
let is_watched = self let is_watched = {
.watchers let watchers = self.watchers.read().await;
.read() watchers.get(&path).is_some_and(|count| count != &0)
.await };
.get(&path)
.is_some_and(|count| count != &0);
if !is_watched { if !is_watched {
// With the modification done, we can drop the write lock but keep the read lock // With the modification done, we can drop the write lock but keep the read lock
@ -352,18 +355,16 @@ where
// Decrement strong count // Decrement strong count
drop(chunk_serializer); drop(chunk_serializer);
let should_remove = {
let watchers = self.watchers.read().await;
watchers.get(&path).is_none_or(|count| count == &0)
};
if self if should_remove {
.watchers
.read()
.await
.get(&path)
.is_none_or(|count| count == &0)
{
let mut locks = self.file_locks.write().await; let mut locks = self.file_locks.write().await;
let can_remove = if let Some(loader) = locks.get(&path) { let can_remove = if let Some(loader) = locks.get(&path) {
loader.can_remove() ChunkSerializerLazyLoader::can_remove(loader)
} else { } else {
true true
}; };

View File

@ -12,13 +12,10 @@ use pumpkin_nbt::nbt_long_array;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write; use std::sync::RwLock;
use std::ops::{BitAnd, BitOr}; use std::sync::{Arc, atomic::AtomicBool};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, RwLock};
use thiserror::Error; use thiserror::Error;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::info;
pub mod format; pub mod format;
pub mod io; pub mod io;
@ -108,6 +105,7 @@ pub struct ChunkSections {
pub count: usize, pub count: usize,
pub block_sections: RwLock<Box<[BlockPalette]>>, pub block_sections: RwLock<Box<[BlockPalette]>>,
pub random_tick_sections: RwLock<Box<[RandomTickSectionCache]>>, pub random_tick_sections: RwLock<Box<[RandomTickSectionCache]>>,
pub randomly_ticking_mask: std::sync::atomic::AtomicU32,
pub biome_sections: RwLock<Box<[BiomePalette]>>, pub biome_sections: RwLock<Box<[BiomePalette]>>,
pub min_y: i32, pub min_y: i32,
} }
@ -174,6 +172,20 @@ impl TryFrom<usize> 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)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "UPPERCASE")] #[serde(rename_all = "UPPERCASE")]
pub struct ChunkHeightmaps { pub struct ChunkHeightmaps {
@ -186,103 +198,90 @@ pub struct ChunkHeightmaps {
} }
impl 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 { let data = match heightmap {
ChunkHeightmapType::WorldSurface => &mut self.world_surface, ChunkHeightmapType::WorldSurface => &mut self.world_surface,
ChunkHeightmapType::MotionBlocking => &mut self.motion_blocking, ChunkHeightmapType::MotionBlocking => &mut self.motion_blocking,
ChunkHeightmapType::MotionBlockingNoLeaves => &mut self.motion_blocking_no_leaves, ChunkHeightmapType::MotionBlockingNoLeaves => &mut self.motion_blocking_no_leaves,
}; };
let local_x = (pos.0.x & 15) as usize; let local_x = (x & 15) as usize;
let local_z = (pos.0.z & 15) as usize; let local_z = (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 column_idx = local_z * 16 + local_x; let column_idx = local_z * 16 + local_x;
// Each height value uses 9 bits, calculate starting bit position // In Minecraft 1.16+, height is stored as (y - min_y + 1). 0 means below min_y.
let bit_start_idx = column_idx * 9; // 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 let array_idx = column_idx / 7;
// We use bit_start_index % 63, which means the last bit of i64 won't be used, let shift = (column_idx % 7) * 9;
// but this avoids the hassle of bit concatenation.
let packed_array_bit_start_idx = bit_start_idx as u32 % 63;
let mask = { let mask = 0x1FFu64 << shift;
if packed_array_bit_start_idx == 0 {
//0b0000_0000_0111_1111_...
!(0x1FF << (64 - 9))
} else {
!(0x1FF << (64 - packed_array_bit_start_idx - 9))
}
};
let height_bit_bytes = adjust_height let mut current = data[array_idx] as u64;
.wrapping_shl(64 - 9 - packed_array_bit_start_idx) current = (current & !mask) | ((val & 0x1FF) << shift);
.to_ne_bytes(); data[array_idx] = current as i64;
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);
} }
#[must_use] #[must_use]
pub fn get(&self, heightmap: ChunkHeightmapType, x: i32, z: i32, min_y: i32) -> i32 { 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 { let data = match heightmap {
ChunkHeightmapType::WorldSurface => &self.world_surface, ChunkHeightmapType::WorldSurface => &self.world_surface,
ChunkHeightmapType::MotionBlocking => &self.motion_blocking, ChunkHeightmapType::MotionBlocking => &self.motion_blocking,
ChunkHeightmapType::MotionBlockingNoLeaves => &self.motion_blocking_no_leaves, 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) let local_x = (x & 15) as usize;
.wrapping_shr(64 - (packed_array_bit_start_idx + 9)) as i32) let local_z = (z & 15) as usize;
- min_y.abs() 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) { #[expect(clippy::too_many_arguments)]
let mut header = "Z/X".to_string(); pub fn update<F>(
for x in 0..16 { &mut self,
let _ = write!(header, "{x:4}"); 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) if heightmap_type.is_opaque(block_state) {
.map(|z| { if local_y >= first_available {
let mut row = format!("{z:3}"); self.set(heightmap_type, local_x, local_z, local_y, min_y);
row.push_str( return true;
&(0..16) }
.map(|x| format!("{:4}", self.get(_type, x, z, min_y))) } else if first_available - 1 == local_y {
.collect::<String>(), for y in (min_y..local_y).rev() {
); let state = get_block(y);
row if heightmap_type.is_opaque(state) {
}) self.set(heightmap_type, local_x, local_z, y, min_y);
.collect::<Vec<_>>() return true;
.join("\n"); }
}
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] #[must_use]
pub fn build_random_tick_sections_cache( pub fn build_random_tick_sections_cache(
block_sections: &[BlockPalette], block_sections: &[BlockPalette],
) -> Box<[RandomTickSectionCache]> { ) -> (Box<[RandomTickSectionCache]>, u32) {
block_sections let mut mask = 0;
let cache = block_sections
.iter() .iter()
.map(|section| { .enumerate()
.map(|(i, section)| {
let (random_ticking_block_count, random_ticking_fluid_count) = let (random_ticking_block_count, random_ticking_fluid_count) =
section.random_ticking_counts(); section.random_ticking_counts();
if random_ticking_block_count > 0 || random_ticking_fluid_count > 0 {
mask |= 1 << i;
}
RandomTickSectionCache { RandomTickSectionCache {
random_ticking_block_count, random_ticking_block_count,
random_ticking_fluid_count, random_ticking_fluid_count,
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_boxed_slice() .into_boxed_slice();
(cache, mask)
} }
#[must_use] #[must_use]
pub fn new(num_sections: usize, min_y: i32) -> Self { pub fn new(num_sections: usize, min_y: i32) -> Self {
let block_sections = vec![BlockPalette::default(); num_sections].into_boxed_slice(); 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(); let biome_sections = vec![BiomePalette::default(); num_sections].into_boxed_slice();
Self { Self {
count: num_sections, count: num_sections,
block_sections: RwLock::new(block_sections), block_sections: RwLock::new(block_sections),
random_tick_sections: RwLock::new(random_tick_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), biome_sections: RwLock::new(biome_sections),
min_y, 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] #[must_use]
pub fn get_rough_biome_absolute_y( pub fn get_rough_biome_absolute_y(
&self, &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 /// Gets the given block in the chunk
fn get_relative_block( fn get_relative_block(
&self, &self,
@ -415,7 +420,6 @@ impl ChunkSections {
relative_z: usize, relative_z: usize,
block_state_id: BlockStateId, block_state_id: BlockStateId,
) -> BlockStateId { ) -> BlockStateId {
// TODO @LUK_ESC? update the heightmap
self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id) self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id)
} }
@ -473,6 +477,18 @@ impl ChunkSections {
.saturating_add(1); .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; return replaced_block_state_id;
} }
0 0
@ -531,6 +547,62 @@ impl ChunkSections {
} }
impl ChunkData { 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 /// Gets the given block in the chunk
#[inline] #[inline]
#[must_use] #[must_use]
@ -553,7 +625,8 @@ impl ChunkData {
relative_z: usize, relative_z: usize,
block_state_id: BlockStateId, 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 self.section
.set_relative_block(relative_x, relative_y, relative_z, block_state_id); .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]; let mut has_found = [false, false, false];
for y in (self.section.min_y..=start_height).rev() { 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 state_id = self.section.get_block_absolute_y(x, y, z).unwrap();
let block_state = BlockState::from_id(state_id); 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] { for hm_type in [
heightmaps.set(ChunkHeightmapType::WorldSurface, pos, self.section.min_y); ChunkHeightmapType::WorldSurface,
has_found[ChunkHeightmapType::WorldSurface as usize] = true; ChunkHeightmapType::MotionBlocking,
}
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, ChunkHeightmapType::MotionBlockingNoLeaves,
pos, ] {
self.section.min_y, let idx = hm_type as usize;
); if !has_found[idx] && hm_type.is_opaque(block_state) {
has_found[ChunkHeightmapType::MotionBlockingNoLeaves as usize] = true; heightmaps.set(hm_type, x as i32, z as i32, y, self.section.min_y);
has_found[idx] = true;
}
} }
if !has_found.contains(&false) { if has_found.iter().all(|&found| found) {
return; return;
} }
} }
let pos = BlockPos::new(x as i32, self.section.min_y, z as i32);
for (idx, is_set) in has_found.iter().enumerate() { for (idx, is_set) in has_found.iter().enumerate() {
if !(*is_set) { 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()]; let mut sections = vec![BlockPalette::default(), BlockPalette::default()];
sections[1].set(0, 0, 0, Block::LAVA.default_state.id); sections[1].set(0, 0, 0, Block::LAVA.default_state.id);
let cache = ChunkSections::build_random_tick_sections_cache(&sections); let (cache, _mask) = ChunkSections::build_random_tick_sections_cache(&sections);
assert!(!cache[0].is_randomly_ticking()); assert!(!cache[0].is_randomly_ticking());
assert!(cache[1].random_ticking_fluid_count > 0); assert!(cache[1].random_ticking_fluid_count > 0);
assert!(cache[1].is_randomly_ticking()); assert!(cache[1].is_randomly_ticking());
@ -735,4 +798,32 @@ mod tests {
assert!(cache[0].is_randomly_ticking()); 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
}
} }

View File

@ -194,7 +194,7 @@ impl GenerationCache for Cache {
} }
match &mut self.chunks[(dx * self.size + dz) as usize] { match &mut self.chunks[(dx * self.size + dz) as usize] {
Chunk::Level(data) => { Chunk::Level(data) => {
data.section.set_block_absolute_y( data.set_block_absolute_y(
(pos.x & 15) as usize, (pos.x & 15) as usize,
pos.y, pos.y,
(pos.z & 15) as usize, (pos.z & 15) as usize,

View File

@ -12,7 +12,7 @@ pub type ChunkPos = pumpkin_util::math::vector2::Vector2<i32>;
pub type ChunkLevel = HashMapType<ChunkPos, i8>; pub type ChunkLevel = HashMapType<ChunkPos, i8>;
pub type IOLock = std::sync::Arc<( pub type IOLock = std::sync::Arc<(
std::sync::Mutex<HashMapType<ChunkPos, u8>>, std::sync::Mutex<HashMapType<ChunkPos, u8>>,
std::sync::Condvar, tokio::sync::Notify,
)>; )>;
pub mod channel; pub mod channel;

View File

@ -19,7 +19,7 @@ use std::cmp::{Ordering, max};
use std::collections::{BinaryHeap, HashMap}; use std::collections::{BinaryHeap, HashMap};
use std::mem::swap; use std::mem::swap;
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use tracing::{debug, error, info, trace, warn}; 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 (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 { for _ in 0..io_read_thread_count {
level.chunk_system_tasks.spawn(io_read_work( level.chunk_system_tasks.spawn(io_read_work(

View File

@ -63,24 +63,31 @@ pub async fn io_read_work(
debug!("io read thread start"); debug!("io read thread start");
let biome_mixer_seed = hash_seed(level.world_gen.random_config.seed); let biome_mixer_seed = hash_seed(level.world_gen.random_config.seed);
let dimension = &level.world_gen.dimension; let dimension = &level.world_gen.dimension;
let (t_send, mut t_recv) = tokio::sync::mpsc::channel(16);
// Cleaner loop and async recv // Cleaner loop and async recv
while let Ok(batch) = recv.recv().await { while let Ok(batch) = recv.recv().await {
for pos in &batch { for pos in &batch {
// Lock handling // Lock handling
tokio::task::block_in_place(|| { loop {
let mut data = lock.0.lock().unwrap(); let notified = lock.1.notified();
while data.contains_key(pos) { if !lock.0.lock().unwrap().contains_key(pos) {
data = lock.1.wait(data).unwrap(); break;
}
notified.await;
} }
});
} }
level 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 .chunk_saver
.fetch_chunks(&level.level_folder, &batch, t_send.clone()) .fetch_chunks(&level_clone.level_folder, &batch_clone, 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 {
@ -168,6 +175,7 @@ pub async fn io_read_work(
} }
} }
} }
let _ = fetch_task.await;
} }
debug!("io read thread stop"); debug!("io read thread stop");
} }
@ -211,7 +219,7 @@ pub async fn io_write_work(recv: AsyncRx<Vec<(ChunkPos, Chunk)>>, level: Arc<Lev
if *rc == 1 { if *rc == 1 {
entry.remove(); entry.remove();
drop(data); drop(data);
lock.1.notify_all(); lock.1.notify_waiters();
} else { } else {
*rc -= 1; *rc -= 1;
} }

View File

@ -22,7 +22,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; use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use std::{ use std::{
@ -72,6 +72,7 @@ pub struct Level {
// from the loaded chunks map and sent to the underlying ChunkIO // from the loaded chunks map and sent to the underlying ChunkIO
pub loaded_chunks: Arc<DashMap<Vector2<i32>, SyncChunk>>, pub loaded_chunks: Arc<DashMap<Vector2<i32>, SyncChunk>>,
loaded_entity_chunks: Arc<DashMap<Vector2<i32>, SyncEntityChunk>>, loaded_entity_chunks: Arc<DashMap<Vector2<i32>, SyncEntityChunk>>,
pub chunks_with_scheduled_ticks: Arc<dashmap::DashSet<Vector2<i32>>>,
pub chunk_loading: Mutex<ChunkLoading>, pub chunk_loading: Mutex<ChunkLoading>,
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>, chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
@ -194,6 +195,7 @@ impl Level {
schedule_tick_counts: AtomicU64::new(0), schedule_tick_counts: AtomicU64::new(0),
loaded_chunks: Arc::new(DashMap::new()), loaded_chunks: Arc::new(DashMap::new()),
loaded_entity_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_loading: Mutex::new(ChunkLoading::new(level_channel.clone())),
chunk_watchers: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()),
tasks: TaskTracker::new(), tasks: TaskTracker::new(),
@ -436,22 +438,18 @@ impl Level {
}); });
} }
pub fn get_tick_data(&self) -> TickData { pub fn get_tick_data(&self, active_chunks: &FxHashSet<Vector2<i32>>) -> TickData {
let mut ticks = TickData { let mut ticks = TickData {
block_ticks: Vec::new(), block_ticks: Vec::new(),
fluid_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(), block_entities: Vec::new(),
}; };
// Clone the Arcs quickly to release map shard locks! // 1. Process active chunks (random ticks, block entities)
let chunks_to_tick: Vec<SyncChunk> = self for pos in active_chunks {
.loaded_chunks if let Some(chunk) = self.loaded_chunks.get(pos) {
.iter() let chunk = chunk.value();
.map(|entry| entry.value().clone())
.collect();
for chunk in chunks_to_tick {
let chunk_x_base = chunk.x * 16; let chunk_x_base = chunk.x * 16;
let chunk_z_base = chunk.z * 16; let chunk_z_base = chunk.z * 16;
let section_count = chunk.section.count; let section_count = chunk.section.count;
@ -460,33 +458,31 @@ impl Level {
.block_entities .block_entities
.extend(chunk.block_entities.lock().unwrap().values().cloned()); .extend(chunk.block_entities.lock().unwrap().values().cloned());
// Acquire the read lock once per chunk to avoid per-section lock overhead // 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 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; let min_y = chunk.section.min_y;
for i in 0..section_count { for i in 0..section_count {
if !random_tick_sections[i].is_randomly_ticking() { if (mask & (1 << i)) == 0 {
continue; continue;
} }
let y_base = min_y + (i as i32 * 16); let y_base = min_y + (i as i32 * 16);
for _ in 0..3 { for _ in 0..3 {
// Generate a fresh random per tick attempt
let r = rand::random::<u32>(); let r = rand::random::<u32>();
let x_offset = (r & 0xF) as usize; let x_offset = (r & 0xF) as usize;
let z_offset = (r >> 8 & 0xF) as usize; let z_offset = (r >> 8 & 0xF) as usize;
let y_in_section = ((r >> 4) & 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 block_state_id = sections[i].get(x_offset, y_in_section, z_offset);
let tick_block = has_random_ticks(block_state_id); let tick_block = has_random_ticks(block_state_id);
let tick_fluid = has_random_ticking_fluid(block_state_id); let tick_fluid = has_random_ticking_fluid(block_state_id);
if tick_block || tick_fluid { if tick_block || tick_fluid {
let absolute_y = y_base + y_in_section as i32;
ticks.random_ticks.push(RandomTickSample { ticks.random_ticks.push(RandomTickSample {
position: BlockPos::new( position: BlockPos::new(
chunk_x_base + x_offset as i32, chunk_x_base + x_offset as i32,
absolute_y, y_base + y_in_section as i32,
chunk_z_base + z_offset as i32, chunk_z_base + z_offset as i32,
), ),
tick_block, tick_block,
@ -495,10 +491,30 @@ impl Level {
} }
} }
} }
drop(random_tick_sections); }
drop(sections); }
}
// 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.block_ticks.append(&mut chunk.block_ticks.step_tick());
ticks.fluid_ticks.append(&mut chunk.fluid_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(); ticks.block_ticks.sort_unstable();
@ -517,8 +533,17 @@ impl Level {
pub fn clean_memory(&self) { pub fn clean_memory(&self) {
self.chunk_watchers.retain(|_, watcher| *watcher != 0); 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 // 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)
@ -713,7 +738,7 @@ impl Level {
) -> BlockStateId { ) -> BlockStateId {
let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position();
self.get_or_fetch_chunk(chunk_coordinate, |chunk| { 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.x as usize,
relative.y, relative.y,
relative.z as usize, relative.z as usize,
@ -823,10 +848,12 @@ impl Level {
value: unsafe { &*std::ptr::from_ref::<Block>(block) }, value: unsafe { &*std::ptr::from_ref::<Block>(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); chunk.block_ticks.schedule_tick(&scheduled_tick, tick_order);
}) })
.await; .await;
self.chunks_with_scheduled_ticks.insert(chunk_pos);
} }
pub async fn schedule_fluid_tick( pub async fn schedule_fluid_tick(
@ -844,10 +871,12 @@ impl Level {
value: unsafe { &*std::ptr::from_ref::<Fluid>(fluid) }, value: unsafe { &*std::ptr::from_ref::<Fluid>(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); chunk.fluid_ticks.schedule_tick(&scheduled_tick, tick_order);
}) })
.await; .await;
self.chunks_with_scheduled_ticks.insert(chunk_pos);
} }
pub async fn is_block_tick_scheduled( pub async fn is_block_tick_scheduled(

View File

@ -59,6 +59,14 @@ impl<'a, T: std::hash::Hash + Eq> ChunkTickScheduler<&'a T> {
.contains(&(pos, value)) .contains(&(pos, value))
} }
pub fn has_ticks(&self) -> bool {
!self
.queued_ticks
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_empty()
}
#[must_use] #[must_use]
pub fn to_vec(&self) -> Vec<ScheduledTick<&'a T>> { pub fn to_vec(&self) -> Vec<ScheduledTick<&'a T>> {
let offset = self.offset.load(Ordering::SeqCst); let offset = self.offset.load(Ordering::SeqCst);

View File

@ -56,8 +56,12 @@ impl ServerPlayerData {
let mut nbt = PNbtCompound::new(); let mut nbt = PNbtCompound::new();
player.write_nbt(&mut nbt).await; player.write_nbt(&mut nbt).await;
let storage = self.storage.clone();
let uuid = player.gameprofile.id;
// Save to disk // 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(()) Ok(())
} }
@ -81,8 +85,14 @@ impl ServerPlayerData {
let mut nbt = PNbtCompound::new(); let mut nbt = PNbtCompound::new();
player.write_nbt(&mut nbt).await; 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 // 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!( error!(
"Failed to save player data for {}: {e}", "Failed to save player data for {}: {e}",
player.gameprofile.id, player.gameprofile.id,
@ -128,8 +138,17 @@ impl ServerPlayerData {
/// # Returns /// # Returns
/// ///
/// A Result indicating success or the error that occurred. /// A Result indicating success or the error that occurred.
pub fn load_data(&self, uuid: &uuid::Uuid) -> Result<Option<PNbtCompound>, PlayerDataError> { pub async fn load_data(
match self.storage.load_player_data(uuid) { &self,
uuid: &uuid::Uuid,
) -> Result<Option<PNbtCompound>, 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)) => { Ok((should_load, data)) => {
if !should_load { if !should_load {
// No data to load, continue with default data // No data to load, continue with default data
@ -170,10 +189,16 @@ impl ServerPlayerData {
return Ok(()); return Ok(());
} }
let uuid = &player.gameprofile.id; let uuid = player.gameprofile.id;
let mut nbt = PNbtCompound::new(); let mut nbt = PNbtCompound::new();
player.write_nbt(&mut nbt).await; 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(())
} }
} }

View File

@ -14,6 +14,7 @@ use crossbeam::atomic::AtomicCell;
use living::LivingEntity; use living::LivingEntity;
use player::Player; use player::Player;
use pumpkin_data::BlockState; use pumpkin_data::BlockState;
use pumpkin_data::biome::Biome;
use pumpkin_data::block_properties::{EnumVariants, Integer0To15, blocks_movement}; use pumpkin_data::block_properties::{EnumVariants, Integer0To15, blocks_movement};
use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::dimension::Dimension; use pumpkin_data::dimension::Dimension;
@ -485,6 +486,9 @@ pub struct Entity {
pub first_loaded_chunk_position: AtomicCell<Option<Vector3<i32>>>, pub first_loaded_chunk_position: AtomicCell<Option<Vector3<i32>>>,
pub current_biome: ArcSwap<&'static Biome>,
pub last_biome_update_pos: AtomicCell<BlockPos>,
pub portal_cooldown: AtomicU32, pub portal_cooldown: AtomicU32,
pub portal_manager: Mutex<Option<Mutex<PortalManager>>>, pub portal_manager: Mutex<Option<Mutex<PortalManager>>>,
@ -614,6 +618,8 @@ impl Entity {
vehicle: Mutex::new(None), vehicle: Mutex::new(None),
riding_cooldown: AtomicI32::new(0), riding_cooldown: AtomicI32::new(0),
age: 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_cooldown: AtomicU32::new(0),
portal_manager: Mutex::new(None), portal_manager: Mutex::new(None),
custom_name: ArcSwap::new(Arc::new(None)), custom_name: ArcSwap::new(Arc::new(None)),
@ -2781,6 +2787,15 @@ impl EntityBase for Entity {
self.was_in_powder_snow self.was_in_powder_snow
.store(was_in_powder_snow, Ordering::Relaxed); .store(was_in_powder_snow, Ordering::Relaxed);
self.is_in_powder_snow.store(false, 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.update_last_pos();
self.tick_portal(caller).await; self.tick_portal(caller).await;
self.update_fluid_state(caller).await; self.update_fluid_state(caller).await;

View File

@ -533,11 +533,12 @@ impl JavaClient {
} }
} }
let send_failed = {
let mut writer = writer.lock().await; let mut writer = writer.lock().await;
let mut send_failed = false; let mut failed = false;
for packet in &packet_batch { for packet in &packet_batch {
if let Err(err) = writer.write_packet(packet.data.clone()).await { if let Err(err) = writer.write_packet(packet.data.clone()).await {
send_failed = true; failed = true;
// It is expected that the packet will fail if we are closed // It is expected that the packet will fail if we are closed
if !close_token.is_cancelled() { if !close_token.is_cancelled() {
warn!("Failed to send packet to client {id}: {err}"); warn!("Failed to send packet to client {id}: {err}");
@ -546,13 +547,14 @@ impl JavaClient {
} }
} }
if !send_failed && let Err(err) = writer.flush().await { if !failed && let Err(err) = writer.flush().await {
send_failed = true; failed = true;
if !close_token.is_cancelled() { if !close_token.is_cancelled() {
warn!("Failed to flush packet batch for client {id}: {err}"); warn!("Failed to flush packet batch for client {id}: {err}");
} }
} }
drop(writer); failed
};
if send_failed { if send_failed {
// We now need to close the connection to the client since the stream is in an unknown state. // We now need to close the connection to the client since the stream is in an unknown state.

View File

@ -179,7 +179,11 @@ impl Server {
let level_info = level_info.unwrap_or_else(|err| { let level_info = level_info.unwrap_or_else(|err| {
warn!("Failed to get level_info, using default instead: {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; let seed = level_info.world_gen_settings.seed;
@ -423,7 +427,7 @@ impl Server {
let gamemode = self.defaultgamemode.lock().await.gamemode; let gamemode = self.defaultgamemode.lock().await.gamemode;
let (world, nbt) = 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); let _version = data.get_int().unwrap_or(0);
if let Ok(dimension_key) = data.get_string() { if let Ok(dimension_key) = data.get_string() {
if let Some(dimension) = Dimension::from_name(&dimension_key) { 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. /// 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. /// 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<Self>) {
let worlds = self.worlds.load(); let worlds = self.worlds.load();
for world in worlds.iter() { for world in worlds.iter() {
@ -807,12 +811,18 @@ impl Server {
world.flush_synced_block_events().await; world.flush_synced_block_events().await;
} }
let mut set = JoinSet::new();
for world in worlds.iter() { for world in worlds.iter() {
let players = world.players.load(); let players = world.players.load();
for player in players.iter() { 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`. /// Ticks the game logic for all worlds. This is the part that is affected by `/tick freeze`.
pub async fn tick_worlds(self: &Arc<Self>) { pub async fn tick_worlds(self: &Arc<Self>) {
@ -825,7 +835,7 @@ impl Server {
let server = self.clone(); let server = self.clone();
set.spawn(async move { set.spawn(async move {
world.tick(&server).await; world.tick(server).await;
}); });
} }

View File

@ -155,6 +155,8 @@ use weather::Weather;
type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
use rustc_hash::FxHashSet;
impl PumpkinError for GetBlockError { impl PumpkinError for GetBlockError {
fn is_kick(&self) -> bool { fn is_kick(&self) -> bool {
false false
@ -211,6 +213,8 @@ pub struct World {
pub portal_poi: Mutex<portal::PortalPoiStorage>, pub portal_poi: Mutex<portal::PortalPoiStorage>,
/// End Dragon fight manager (only present in `THE_END` dimension). /// End Dragon fight manager (only present in `THE_END` dimension).
pub dragon_fight: Option<Mutex<dragon_fight::DragonFight>>, pub dragon_fight: Option<Mutex<dragon_fight::DragonFight>>,
pub spawn_state: ArcSwap<SpawnState>,
pub active_chunks: ArcSwap<FxHashSet<Vector2<i32>>>,
} }
impl PartialEq for World { impl PartialEq for World {
@ -255,10 +259,40 @@ impl World {
portal_poi: Mutex::new(portal_poi), portal_poi: Mutex::new(portal_poi),
dragon_fight: (dimension == Dimension::THE_END) dragon_fight: (dimension == Dimension::THE_END)
.then(|| Mutex::new(dragon_fight::DragonFight::new())), .then(|| Mutex::new(dragon_fight::DragonFight::new())),
spawn_state: ArcSwap::new(Arc::new(SpawnState::empty())),
active_chunks: ArcSwap::new(Arc::new(FxHashSet::default())),
server, server,
} }
} }
pub fn update_active_chunks(self: &Arc<Self>) {
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 { pub fn get_lighting_config(&self) -> LightingEngineConfig {
self.server self.server
.upgrade() .upgrade()
@ -745,12 +779,13 @@ impl World {
.await; .await;
} }
pub async fn tick(self: &Arc<Self>, server: &Server) { pub async fn tick(self: &Arc<Self>, server: Arc<Server>) {
let start = tokio::time::Instant::now(); let start = tokio::time::Instant::now();
// IMPORTANT: send flush_block_updates first to prevent issues with CAcknowledgeBlockChange // IMPORTANT: send flush_block_updates first to prevent issues with CAcknowledgeBlockChange
self.flush_block_updates().await; self.flush_block_updates().await;
self.flush_synced_block_events().await; self.flush_synced_block_events().await;
self.update_active_chunks();
self.tick_environment().await; self.tick_environment().await;
let chunk_start = tokio::time::Instant::now(); let chunk_start = tokio::time::Instant::now();
@ -758,34 +793,65 @@ impl World {
let chunk_elapsed = chunk_start.elapsed(); let chunk_elapsed = chunk_start.elapsed();
let player_start = tokio::time::Instant::now(); let player_start = tokio::time::Instant::now();
let players = self.players.load(); let players = self.players.load().clone();
let player_count = players.len(); let player_count = players.len();
let mut player_tasks = tokio::task::JoinSet::new();
for player in players.iter() { 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 player_elapsed = player_start.elapsed();
let entity_start = tokio::time::Instant::now(); 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 entity_count = entities_to_tick.len();
let mut entity_tasks = tokio::task::JoinSet::new();
for entity in entities_to_tick.iter() { for entity in entities_to_tick.iter() {
entity.get_entity().age.fetch_add(1, Relaxed); let entity_clone = entity.clone();
entity.tick(entity, server).await; 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() { let entity_inner = entity_clone.get_entity();
if player 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 .living_entity
.entity .entity
.bounding_box .bounding_box
.load() .load()
.expand(1.0, 0.5, 1.0) .expand(1.0, 0.5, 1.0)
.intersects(&entity.get_entity().bounding_box.load()) .intersects(&entity_bb)
{ {
entity.on_player_collision(player).await; entity_clone.on_player_collision(player).await;
break; 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(); let entity_elapsed = entity_start.elapsed();
@ -912,7 +978,8 @@ impl World {
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]
pub async fn tick_chunks(self: &Arc<Self>) { pub async fn tick_chunks(self: &Arc<Self>) {
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 { for scheduled_tick in tick_data.block_ticks {
let block = self.get_block(&scheduled_tick.position).await; let block = self.get_block(&scheduled_tick.position).await;
if let Some(pumpkin_block) = self.block_registry.get_pumpkin_block(block.id) { 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(); let world_simple: Arc<dyn SimpleWorld> = self.clone();
// TODO use FixedPlayerDistanceChunkTracker 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() {}
for i in self.players.load().iter() { let spawn_state = self.spawn_state.load();
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 spawn_state =
SpawnState::new(spawning_chunks_map.len() as i32, &self.entities, self).await; // TODO store it
// TODO gamerule this.spawnEnemies || this.spawnFriendlies // TODO gamerule this.spawnEnemies || this.spawnFriendlies
let (spawn_mobs, spawn_monsters, peaceful) = { let (spawn_mobs, spawn_monsters, peaceful) = {
@ -1009,13 +1065,21 @@ impl World {
spawn_passives, 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()); // log::debug!("spawning list size {}", spawn_list.len());
let mut spawning_chunks: Vec<(Vector2<i32>, Arc<ChunkData>)> =
spawning_chunks_map.into_iter().collect();
spawning_chunks.shuffle(&mut rng()); spawning_chunks.shuffle(&mut rng());
let mut spawn_tasks = JoinSet::new(); let mut spawn_tasks = JoinSet::new();
let spawn_state = Arc::new(spawn_state);
let spawn_list = Arc::new(spawn_list); let spawn_list = Arc::new(spawn_list);
for (pos, chunk) in spawning_chunks { for (pos, chunk) in spawning_chunks {
@ -1030,17 +1094,6 @@ impl World {
}); });
} }
while spawn_tasks.join_next().await.is_some() {} while spawn_tasks.join_next().await.is_some() {}
let world: Arc<dyn SimpleWorld> = 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<Self>, bounding_box: BoundingBox) -> Vec<&Fluid> { pub async fn get_fluid_collisions(self: &Arc<Self>, bounding_box: BoundingBox) -> Vec<&Fluid> {
@ -2533,6 +2586,7 @@ impl World {
let mut ids_to_remove = Vec::new(); let mut ids_to_remove = Vec::new();
let mut entities_to_load = Vec::new(); let mut entities_to_load = Vec::new();
let mut entity_data = Vec::new();
{ {
let mut data = chunk.data.lock().await; let mut data = chunk.data.lock().await;
for (uuid, entity_nbt) in data.iter_mut() { for (uuid, entity_nbt) in data.iter_mut() {
@ -2544,15 +2598,18 @@ impl World {
warn!("Entity has no valid Entity Type {id}"); warn!("Entity has no valid Entity Type {id}");
continue; continue;
}; };
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 // Pos is zero since it will read from nbt
let entity = let entity =
from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, *uuid) from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, uuid).await;
.await;
entity_nbt.read_pos = 0; entity_nbt.read_pos = 0;
entity.read_nbt_non_mut(entity_nbt).await; entity.read_nbt_non_mut(&mut entity_nbt).await;
entities_to_load.push(entity); entities_to_load.push(entity);
} }
}
for entity in entities_to_load { for entity in entities_to_load {
let base_entity = entity.get_entity(); let base_entity = entity.get_entity();
@ -2605,8 +2662,10 @@ 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();
let mut entity_data = Vec::new();
for (uuid, entity_nbt) in chunk.data.lock().await.iter_mut() { {
let mut data = chunk.data.lock().await;
for (uuid, entity_nbt) in data.iter_mut() {
let Ok(id) = entity_nbt.get_string() else { let Ok(id) = entity_nbt.get_string() else {
debug!("Entity has no ID"); debug!("Entity has no ID");
continue; continue;
@ -2617,11 +2676,16 @@ impl World {
warn!("Entity has no valid Entity Type {id}"); warn!("Entity has no valid Entity Type {id}");
continue; continue;
}; };
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 // Pos is zero since it will read from nbt
let entity = 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_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(); let base_entity = entity.get_entity();
player player
.client .client
@ -3053,10 +3117,19 @@ impl World {
pub async fn add_entity_silent(&self, entity: Arc<dyn EntityBase>) { pub async fn add_entity_silent(&self, entity: Arc<dyn EntityBase>) {
let base_entity = entity.get_entity(); 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(); let mut nbt = PNbtCompound::new();
entity.write_nbt(&mut nbt).await; 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; let chunk = self.level.get_entity_chunk(chunk_coordinate).await;
chunk.data.lock().await.insert(base_entity.entity_uuid, nbt); chunk.data.lock().await.insert(base_entity.entity_uuid, nbt);
chunk.mark_dirty(true); 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| { self.entities.rcu(|current_entities| {
let mut new_entities = (**current_entities).clone(); 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 new_entities
}); });
let chunk_pos = entity.chunk_pos.load(); let chunk_pos = base_entity.chunk_pos.load();
self.broadcast_to_chunk(chunk_pos, &CRemoveEntities::new(&[entity.entity_id.into()])) self.broadcast_to_chunk(
chunk_pos,
&CRemoveEntities::new(&[base_entity.entity_id.into()]),
)
.await; .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) { 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 let replaced_block_state_id = self
.level .level
.get_or_fetch_chunk(chunk_coordinate, |chunk| { .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.x as usize,
relative.y, relative.y,
relative.z as usize, relative.z as usize,

View File

@ -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 { impl MobCounts {
#[inline] #[inline]
pub fn add(&self, category: &'static MobCategory) { pub fn add(&self, category: &'static MobCategory) {
self.0[category.id].fetch_add(1, Relaxed); 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] #[inline]
pub fn can_spawn(&self, category: &'static MobCategory) -> bool { pub fn can_spawn(&self, category: &'static MobCategory) -> bool {
self.0[category.id].load(Relaxed) < category.max self.0[category.id].load(Relaxed) < category.max
@ -63,6 +76,23 @@ pub struct LocalMobCapCalculator {
players_near_chunk: DashMap<Vector2<i32>, Vec<i32>>, players_near_chunk: DashMap<Vector2<i32>, Vec<i32>>,
} }
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 { impl Default for LocalMobCapCalculator {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -87,7 +117,7 @@ impl LocalMobCapCalculator {
dx * dx + dy * dy dx * dx + dy * dy
} }
fn get_players_near(&self, world: &Arc<World>, chunk_pos: Vector2<i32>) -> Vec<i32> { fn get_players_near(&self, world: &World, chunk_pos: Vector2<i32>) -> Vec<i32> {
if let Some(players) = self.players_near_chunk.get(&chunk_pos) { if let Some(players) = self.players_near_chunk.get(&chunk_pos) {
return players.value().clone(); return players.value().clone();
} }
@ -105,12 +135,7 @@ impl LocalMobCapCalculator {
players players
} }
pub fn add_mob( pub fn add_mob(&self, chunk_pos: Vector2<i32>, world: &World, category: &'static MobCategory) {
&self,
chunk_pos: Vector2<i32>,
world: &Arc<World>,
category: &'static MobCategory,
) {
let players = self.get_players_near(world, chunk_pos); let players = self.get_players_near(world, chunk_pos);
for player in players { for player in players {
self.player_mob_counts self.player_mob_counts
@ -120,10 +145,24 @@ impl LocalMobCapCalculator {
} }
} }
pub fn remove_mob(
&self,
chunk_pos: Vector2<i32>,
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( pub fn can_spawn(
&self, &self,
category: &'static MobCategory, category: &'static MobCategory,
world: &Arc<World>, world: &World,
chunk_pos: Vector2<i32>, chunk_pos: Vector2<i32>,
) -> bool { ) -> bool {
let players = self.get_players_near(world, chunk_pos); let players = self.get_players_near(world, chunk_pos);
@ -140,7 +179,7 @@ impl LocalMobCapCalculator {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
struct PointCharge(Vector3<f64>, f64); struct PointCharge(Vector3<f64>, f64);
impl PointCharge { impl PointCharge {
@ -153,6 +192,12 @@ impl PointCharge {
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct PotentialCalculator(std::sync::Mutex<Vec<PointCharge>>); struct PotentialCalculator(std::sync::Mutex<Vec<PointCharge>>);
impl Clone for PotentialCalculator {
fn clone(&self) -> Self {
Self(std::sync::Mutex::new(self.0.lock().unwrap().clone()))
}
}
impl PotentialCalculator { impl PotentialCalculator {
pub fn add_charge(&self, pos: &BlockPos, charge: f64) { pub fn add_charge(&self, pos: &BlockPos, charge: f64) {
if charge != 0. { if charge != 0. {
@ -162,6 +207,16 @@ impl PotentialCalculator {
.push(PointCharge(pos.to_f64(), charge)); .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 { pub fn get_potential_energy_change(&self, pos: &BlockPos, charge: f64) -> f64 {
if charge == 0. { if charge == 0. {
return 0.; return 0.;
@ -186,6 +241,18 @@ pub struct SpawnState {
last_checked: AtomicCell<Option<(BlockPos, &'static EntityType, f64)>>, last_checked: AtomicCell<Option<(BlockPos, &'static EntityType, f64)>>,
} }
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 { impl fmt::Debug for SpawnState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("SpawnState") f.debug_struct("SpawnState")
@ -199,7 +266,64 @@ impl fmt::Debug for SpawnState {
} }
impl 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, chunk_count: i32,
entities: &ArcSwap<Vec<Arc<dyn EntityBase>>>, entities: &ArcSwap<Vec<Arc<dyn EntityBase>>>,
world: &Arc<World>, world: &Arc<World>,
@ -207,6 +331,7 @@ impl SpawnState {
let potential = PotentialCalculator::default(); let potential = PotentialCalculator::default();
let local_mob_cap = LocalMobCapCalculator::default(); let local_mob_cap = LocalMobCapCalculator::default();
let counter = MobCounts::default(); let counter = MobCounts::default();
let active_chunks = world.active_chunks.load();
for entity in entities.load().iter() { for entity in entities.load().iter() {
let entity = entity.get_entity(); let entity = entity.get_entity();
let entity_type = entity.entity_type; let entity_type = entity.entity_type;
@ -214,13 +339,17 @@ impl SpawnState {
// TODO (mob.isPersistenceRequired() || mob.requiresCustomPersistence()) // TODO (mob.isPersistenceRequired() || mob.requiresCustomPersistence())
continue; continue;
} }
let chunk_pos = entity.chunk_pos.load();
if !active_chunks.contains(&chunk_pos) {
continue;
}
let entity_pos = entity.block_pos.load(); 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) { if let Some(cost) = biome.spawn_costs.get(entity_type.resource_name) {
potential.add_charge(&entity_pos, cost.charge); potential.add_charge(&entity_pos, cost.charge);
} }
if entity_type.mob { 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); 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 x = (chunk_pos.x << 4) + rng.next_bounded_i32(16);
let z = (chunk_pos.y << 4) + rng.next_bounded_i32(16); let z = (chunk_pos.y << 4) + rng.next_bounded_i32(16);
let temp_y = chunk let temp_y = chunk.heightmap.lock().unwrap().get(
.heightmap ChunkHeightmapType::WorldSurface,
.lock() x,
.unwrap() z,
.get(ChunkHeightmapType::WorldSurface, x, z, min_y) chunk.section.min_y,
+ 1; ) + 1;
let y = rng.next_inbetween_i32(min_y, temp_y); let y = rng.next_inbetween_i32(min_y, temp_y);
BlockPos::new(x, y, z) BlockPos::new(x, y, z)
} }
@ -383,20 +512,34 @@ pub async fn spawn_category_for_position(
) { ) {
let mut batch_buffer = vec![]; let mut batch_buffer = vec![];
let mut spawn_cluster_size = 0; let mut spawn_cluster_size = 0;
let mut new_pos = pos;
let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect(); let player_positions: Vec<_> = world.players.load().iter().map(|p| p.position()).collect();
for _ in 0..3 { 'group_loop: for _ in 0..3 {
let mut new_x = new_pos.0.x; let mut new_x = pos.0.x;
let mut new_z = new_pos.0.z; let mut new_z = pos.0.z;
let mut random_group_size = (rng().random::<f32>() * 4.).ceil() as i32; let mut random_group_size = (rng().random::<f32>() * 4.).ceil() as i32;
let mut inc = 0; 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_x += rng().random_range(0..6) - rng().random_range(0..6);
new_z += 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( let spawn_pos_f64 = Vector3::new(
f64::from(new_pos.0.x) + 0.5, f64::from(new_pos.0.x) + 0.5,
@ -410,14 +553,6 @@ pub async fn spawn_category_for_position(
continue; 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( if !is_valid_spawn_position_for_type(
world, world,
&new_pos, &new_pos,
@ -444,7 +579,7 @@ pub async fn spawn_category_for_position(
batch_buffer.push(entity); batch_buffer.push(entity);
spawn_state.after_spawn(entity_type, &new_pos, world).await; spawn_state.after_spawn(entity_type, &new_pos, world).await;
if spawn_cluster_size >= entity_type.limit_per_chunk { if spawn_cluster_size >= entity_type.limit_per_chunk {
return; break 'group_loop;
} }
inc += 1; inc += 1;
@ -584,7 +719,7 @@ pub async fn is_valid_spawn_position_for_type(
return false; return false;
} }
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes // 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( .is_space_empty(BoundingBox::new_from_pos(
f64::from(block_pos.0.x) + 0.5, f64::from(block_pos.0.x) + 0.5,
f64::from(block_pos.0.y), f64::from(block_pos.0.y),
@ -596,6 +731,10 @@ pub async fn is_valid_spawn_position_for_type(
}, },
)) ))
.await .await
{
return false;
}
true
} }
pub async fn is_spawn_position_ok( 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] #[must_use]
pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool { pub fn is_valid_empty_spawn_block(state: &'static BlockState) -> bool {
if state.is_full_cube() { if state.is_full_cube() {