fix: height maps
This commit is contained in:
parent
3c6f15d5eb
commit
a36240dd40
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
62
pumpkin-util/src/math/bit_storage.rs
Normal file
62
pumpkin-util/src/math/bit_storage.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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(§ions);
|
let (cache, _mask) = ChunkSections::build_random_tick_sections_cache(§ions);
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user