Pumpkin/pumpkin-world/src/level.rs
Alexander Medvedev ac021d5d47 chore: more work
- fixed nether again, not the dead lock or whatever it is but the server crash
- further replace Notify with CancellationToken
- fixed chunk loading on inital spawn
- more stronghold work
2026-01-21 23:08:56 +01:00

845 lines
30 KiB
Rust

use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel};
use crate::generation::generator::VanillaGenerator;
use crate::{
BlockStateId,
block::{RawBlockState, entities::BlockEntity},
chunk::{
ChunkData, ChunkEntityData, ChunkReadingError,
format::{anvil::AnvilChunkFile, linear::LinearFile},
io::{Dirtiable, FileIO, LoadedData, file_manager::ChunkFileManager},
},
generation::get_world_gen,
tick::{OrderedTick, ScheduledTick, TickPriority},
world::BlockRegistryExt,
};
use crossbeam::channel::Sender;
use dashmap::DashMap;
use log::trace;
use num_traits::Zero;
use pumpkin_config::{chunk::ChunkConfig, world::LevelConfig};
use pumpkin_data::biome::Biome;
use pumpkin_data::dimension::Dimension;
use pumpkin_data::{Block, block_properties::has_random_ticks, fluid::Fluid};
use pumpkin_util::math::{position::BlockPos, vector2::Vector2};
use pumpkin_util::world_seed::Seed;
use rand::{Rng, SeedableRng, rngs::SmallRng};
use std::sync::Mutex;
use tokio_util::sync::CancellationToken;
// use std::time::Duration;
use std::{
collections::HashMap,
path::PathBuf,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
};
// use tokio::runtime::Handle;
use tokio::{
select,
sync::{
RwLock,
mpsc::{self, UnboundedReceiver},
oneshot,
},
task::JoinHandle,
};
use tokio_util::task::TaskTracker;
pub type SyncChunk = Arc<RwLock<ChunkData>>;
pub type SyncEntityChunk = Arc<RwLock<ChunkEntityData>>;
/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
///
/// Key features include:
///
/// - **Chunk Loading:** Efficiently loads chunks from disk.
/// - **Chunk Caching:** Stores accessed chunks in memory for faster access.
/// - **Chunk Generation:** Generates new chunks on-demand using a specified `WorldGenerator`.
///
/// For more details on world generation, refer to the `WorldGenerator` module.
pub struct Level {
pub seed: Seed,
pub block_registry: Arc<dyn BlockRegistryExt>,
pub level_folder: LevelFolder,
/// Counts the number of ticks that have been scheduled for this world
schedule_tick_counts: AtomicU64,
// Chunks that are paired with chunk watchers. When a chunk is no longer watched, it is removed
// from the loaded chunks map and sent to the underlying ChunkIO
pub loaded_chunks: Arc<DashMap<Vector2<i32>, SyncChunk>>,
loaded_entity_chunks: Arc<DashMap<Vector2<i32>, SyncEntityChunk>>,
pub chunk_loading: Mutex<ChunkLoading>,
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
pub chunk_saver: Arc<dyn FileIO<Data = SyncChunk>>,
entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>>,
pub world_gen: Arc<VanillaGenerator>,
/// Tracks tasks associated with this world instance
tasks: TaskTracker,
pub chunk_system_tasks: TaskTracker,
/// Notification that interrupts tasks for shutdown
pub cancel_token: CancellationToken,
pub shut_down_chunk_system: AtomicBool,
pub should_save: AtomicBool,
pub should_unload: AtomicBool,
gen_entity_request_tx: Sender<Vector2<i32>>,
pending_entity_generations: Arc<DashMap<Vector2<i32>, Vec<oneshot::Sender<SyncEntityChunk>>>>,
pub level_channel: Arc<LevelChannel>,
pub thread_tracker: Mutex<Vec<thread::JoinHandle<()>>>,
pub chunk_listener: Arc<ChunkListener>,
}
pub struct TickData {
pub block_ticks: Vec<OrderedTick<&'static Block>>,
pub fluid_ticks: Vec<OrderedTick<&'static Fluid>>,
pub random_ticks: Vec<ScheduledTick<()>>,
pub block_entities: Vec<Arc<dyn BlockEntity>>,
}
#[derive(Clone)]
pub struct LevelFolder {
pub root_folder: PathBuf,
pub region_folder: PathBuf,
pub entities_folder: PathBuf,
}
#[ignore]
#[cfg(feature = "tokio_taskdump")]
pub async fn dump() {
// let handle = Handle::current();
// if let Ok(dump) = timeout(Duration::from_secs(100), handle.dump()).await {
// for (i, task) in dump.tasks().iter().enumerate() {
// let trace = task.trace();
// log::error!("TASK {i}:");
// log::error!("{trace}\n");
// }
// }
}
impl Level {
pub fn from_root_folder(
level_config: &LevelConfig,
root_folder: PathBuf,
block_registry: Arc<dyn BlockRegistryExt>,
seed: i64,
dimension: Dimension,
) -> Arc<Self> {
let region_folder = root_folder.join("region");
let entities_folder = root_folder.join("entities");
std::fs::create_dir_all(&region_folder).expect("Failed to create Region folder");
std::fs::create_dir_all(&entities_folder).expect("Failed to create Entities folder");
let level_folder = LevelFolder {
root_folder,
region_folder,
entities_folder,
};
let seed = Seed(seed as u64);
let world_gen = get_world_gen(seed, dimension).into();
let chunk_saver: Arc<dyn FileIO<Data = SyncChunk>> = match &level_config.chunk {
ChunkConfig::Linear(config) => Arc::new(
ChunkFileManager::<LinearFile<ChunkData>>::new(config.clone()),
),
ChunkConfig::Anvil(config) => Arc::new(
ChunkFileManager::<AnvilChunkFile<ChunkData>>::new(config.clone()),
),
};
let entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>> = match &level_config.chunk {
ChunkConfig::Linear(config) => Arc::new(
ChunkFileManager::<LinearFile<ChunkEntityData>>::new(config.clone()),
),
ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::<
AnvilChunkFile<ChunkEntityData>,
>::new(config.clone())),
};
let (gen_entity_request_tx, gen_entity_request_rx) = crossbeam::channel::unbounded();
let pending_entity_generations = Arc::new(DashMap::new());
let level_channel = Arc::new(LevelChannel::new());
let thread_tracker = Mutex::new(Vec::new());
let listener = Arc::new(ChunkListener::new());
let level_ref = Arc::new(Self {
seed,
block_registry,
world_gen,
level_folder,
chunk_saver,
entity_saver,
schedule_tick_counts: AtomicU64::new(0),
loaded_chunks: Arc::new(DashMap::new()),
loaded_entity_chunks: Arc::new(DashMap::new()),
chunk_loading: Mutex::new(ChunkLoading::new(level_channel.clone())),
chunk_watchers: Arc::new(DashMap::new()),
tasks: TaskTracker::new(),
chunk_system_tasks: TaskTracker::new(),
cancel_token: CancellationToken::new(),
shut_down_chunk_system: AtomicBool::new(false),
should_save: AtomicBool::new(false),
should_unload: AtomicBool::new(false),
gen_entity_request_tx,
pending_entity_generations: pending_entity_generations.clone(),
level_channel: level_channel.clone(),
thread_tracker,
chunk_listener: listener.clone(),
});
let num_threads = num_cpus::get().saturating_sub(2).max(1);
GenerationSchedule::create(
4,
num_threads,
level_ref.clone(),
level_channel,
listener,
level_ref.thread_tracker.lock().unwrap().as_mut(),
);
for thread_id in 0..(num_threads / 2).max(1) {
let level_clone = level_ref.clone();
let pending_clone = pending_entity_generations.clone();
let rx = gen_entity_request_rx.clone();
let builder =
thread::Builder::new().name(format!("Entity Chunk Generation Thread {thread_id}"));
builder
.spawn(move || {
while let Ok(pos) = rx.recv() {
if level_clone.cancel_token.is_cancelled() {
break;
}
let chunk = ChunkEntityData {
x: pos.x,
z: pos.y,
data: HashMap::new(),
dirty: true,
};
let arc_chunk = Arc::new(RwLock::new(chunk));
level_clone
.loaded_entity_chunks
.insert(pos, arc_chunk.clone());
if let Some((_, waiters)) = pending_clone.remove(&pos) {
for tx in waiters {
let _ = tx.send(arc_chunk.clone());
}
}
}
})
.unwrap();
}
level_ref
}
/// Spawns a task associated with this world. All tasks spawned with this method are awaited
/// when the client. This means tasks should complete in a reasonable (no looping) amount of time.
pub fn spawn_task<F>(&self, task: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
self.tasks.spawn(task)
}
pub async fn shutdown(&self) {
log::info!("Saving level...");
self.cancel_token.cancel();
self.shut_down_chunk_system.store(true, Ordering::Relaxed);
self.level_channel.notify();
self.tasks.close();
self.chunk_system_tasks.close();
let handles = {
let mut lock = self.thread_tracker.lock().unwrap();
lock.drain(..).collect::<Vec<_>>()
};
log::info!("Joining {} synchronous threads...", handles.len());
tokio::task::spawn_blocking(move || {
for handle in handles {
let _ = handle.join();
}
})
.await
.unwrap();
log::debug!("Awaiting remaining async tasks...");
self.tasks.wait().await;
self.chunk_system_tasks.wait().await;
log::info!("Flushing savers to disk...");
self.chunk_saver.block_and_await_ongoing_tasks().await;
self.entity_saver.block_and_await_ongoing_tasks().await;
// save all chunks currently in memory
// let chunks_to_write = self
// .loaded_chunks
// .iter()
// .map(|chunk| (*chunk.key(), chunk.value().clone()))
// .collect::<Vec<_>>();
// self.loaded_chunks.clear();
// TODO: I think the chunk_saver should be at the server level
// self.chunk_saver.clear_watched_chunks().await;
// self.write_chunks(chunks_to_write).await;
// save all chunks currently in memory
let chunks_to_write = self
.loaded_entity_chunks
.iter()
.map(|chunk| (*chunk.key(), chunk.value().clone()))
.collect::<Vec<_>>();
self.loaded_entity_chunks.clear();
// TODO: I think the chunk_saver should be at the server level
self.entity_saver.clear_watched_chunks().await;
self.write_entity_chunks(chunks_to_write).await;
}
pub fn loaded_chunk_count(&self) -> usize {
self.loaded_chunks.len()
}
pub async fn clean_up_log(&self) {
self.chunk_saver.clean_up_log().await;
self.entity_saver.clean_up_log().await;
}
pub fn list_cached(&self) {
for entry in self.loaded_chunks.iter() {
log::debug!("In map: {:?}", entry.key());
}
}
/// Marks chunks as "watched" by a unique player. When no players are watching a chunk,
/// it is removed from memory. Should only be called on chunks the player was not watching
/// before
pub async fn mark_chunks_as_newly_watched(&self, chunks: &[Vector2<i32>]) {
for chunk in chunks {
self.chunk_watchers
.entry(*chunk)
.and_modify(|value| {
if let Some(new_value) = value.checked_add(1) {
*value = new_value;
} else {
log::error!("Watching overflow on chunk {:?}", chunk);
}
})
.or_insert(1);
}
// self.chunk_saver
// .watch_chunks(&self.level_folder, chunks)
// .await;
self.entity_saver
.watch_chunks(&self.level_folder, chunks)
.await;
}
/// Marks chunks no longer "watched" by a unique player. When no players are watching a chunk,
/// it is removed from memory. Should only be called on chunks the player was watching before
pub async fn mark_chunks_as_not_watched(&self, chunks: &[Vector2<i32>]) -> Vec<Vector2<i32>> {
let mut chunks_to_clean = Vec::new();
for chunk in chunks {
if let Some(mut count) = self.chunk_watchers.get_mut(chunk) {
let value = count.value_mut();
*value = value.saturating_sub(1);
if *value == 0 {
drop(count);
self.chunk_watchers.remove(chunk);
chunks_to_clean.push(*chunk);
}
}
}
self.entity_saver
.unwatch_chunks(&self.level_folder, chunks)
.await;
chunks_to_clean
}
/// Returns whether the chunk should be removed from memory
#[inline]
pub async fn mark_chunk_as_not_watched(&self, chunk: Vector2<i32>) -> bool {
!self.mark_chunks_as_not_watched(&[chunk]).await.is_empty()
}
pub async fn clean_entity_chunks(self: &Arc<Self>, chunks: &[Vector2<i32>]) {
// Care needs to be take here because of interweaving case:
// 1) Remove chunk from cache
// 2) Another player wants same chunk
// 3) Load (old) chunk from serializer
// 4) Write (new) chunk from serializer
// Now outdated chunk data is cached and will be written later
let chunks_with_no_watchers = chunks
.iter()
.filter_map(|pos| {
// Only chunks that have no entry in the watcher map or have 0 watchers
if self
.chunk_watchers
.get(pos)
.is_none_or(|count| count.is_zero())
{
self.loaded_entity_chunks
.get(pos)
.map(|chunk| (*pos, chunk.value().clone()))
} else {
None
}
})
.collect::<Vec<_>>();
let level = self.clone();
self.spawn_task(async move {
let chunks_to_remove = chunks_with_no_watchers.clone();
level.write_entity_chunks(chunks_with_no_watchers).await;
// Only after we have written the chunks to the serializer do we remove them from the
// cache
for (pos, _) in chunks_to_remove {
let _ = level.loaded_entity_chunks.remove_if(&pos, |_, _| {
// Recheck that there is no one watching
level
.chunk_watchers
.get(&pos)
.is_none_or(|count| count.is_zero())
});
}
});
}
// Gets random ticks, block ticks and fluid ticks
pub async fn get_tick_data(&self) -> TickData {
let mut ticks = TickData {
block_ticks: Vec::new(),
fluid_ticks: Vec::new(),
random_ticks: Vec::with_capacity(self.loaded_chunks.len() * 3 * 16 * 16),
block_entities: Vec::new(),
};
let mut rng = SmallRng::from_rng(&mut rand::rng());
let chunks = self
.loaded_chunks
.iter()
.map(|x| x.value().clone())
.collect::<Vec<_>>();
for chunk_sync in chunks {
// Try to get a READ lock first.
// Most chunks won't have pending ticks, so this is very fast.
let chunk = chunk_sync.read().await;
let chunk_x_base = chunk.x * 16;
let chunk_z_base = chunk.z * 16;
let mut section_blocks = Vec::new();
for i in 0..chunk.section.sections.len() {
let mut section_block_data = Vec::new();
//TODO use game rules to determine how many random ticks to perform
for _ in 0..3 {
let r = rng.random::<u32>();
let x_offset = (r & 0xF) as i32;
let y_offset = ((r >> 4) & 0xF) as i32 - 32;
let z_offset = (r >> 8 & 0xF) as i32;
let random_pos = BlockPos::new(
chunk_x_base + x_offset,
i as i32 * 16 + y_offset,
chunk_z_base + z_offset,
);
let block_state_id = chunk
.section
.get_block_absolute_y(x_offset as usize, random_pos.0.y, z_offset as usize)
.unwrap_or(Block::AIR.default_state.id);
section_block_data.push((random_pos, block_state_id));
}
section_blocks.push(section_block_data);
}
for section_data in section_blocks {
ticks
.random_ticks
.extend(
section_data
.into_iter()
.filter_map(|(random_pos, block_state_id)| {
if has_random_ticks(block_state_id) {
Some(ScheduledTick {
position: random_pos,
delay: 0,
priority: TickPriority::Normal,
value: (),
})
} else {
None
}
}),
);
}
ticks
.block_entities
.extend(chunk.block_entities.values().cloned());
drop(chunk);
let mut chunk_write = chunk_sync.write().await;
ticks
.block_ticks
.append(&mut chunk_write.block_ticks.step_tick());
ticks
.fluid_ticks
.append(&mut chunk_write.fluid_ticks.step_tick());
}
ticks.block_ticks.sort_unstable();
ticks.fluid_ticks.sort_unstable();
ticks
}
pub async fn clean_entity_chunk(self: &Arc<Self>, chunk: &Vector2<i32>) {
self.clean_entity_chunks(&[*chunk]).await;
}
pub fn is_chunk_watched(&self, chunk: &Vector2<i32>) -> bool {
self.chunk_watchers.get(chunk).is_some()
}
pub fn clean_memory(&self) {
self.chunk_watchers.retain(|_, watcher| !watcher.is_zero());
self.loaded_entity_chunks
.retain(|at, _| self.chunk_watchers.get(at).is_some());
// if the difference is too big, we can shrink the loaded chunks
// (1024 chunks is the equivalent to a 32x32 chunks area)
if self.chunk_watchers.capacity() - self.chunk_watchers.len() >= 4096 {
self.chunk_watchers.shrink_to_fit();
}
// if the difference is too big, we can shrink the loaded chunks
// (1024 chunks is the equivalent to a 32x32 chunks area)
// if self.loaded_chunks.capacity() - self.loaded_chunks.len() >= 4096 {
// self.loaded_chunks.shrink_to_fit();
// }
if self.loaded_entity_chunks.capacity() - self.loaded_entity_chunks.len() >= 4096 {
self.loaded_entity_chunks.shrink_to_fit();
}
}
pub async fn get_chunk(self: &Arc<Self>, pos: Vector2<i32>) -> SyncChunk {
// Check if already in memory
if let Some(chunk) = self.loaded_chunks.get(&pos) {
return chunk.clone();
}
let recv = self.chunk_listener.add_single_chunk_listener(pos);
{
let mut lock = self.chunk_loading.lock().unwrap();
lock.add_ticket(pos, 31);
lock.send_change();
}
if let Some(chunk) = self.loaded_chunks.get(&pos) {
chunk.clone()
} else {
recv.await
.expect("Chunk listener dropped without sending chunk")
}
}
async fn load_single_entity_chunk(
&self,
pos: Vector2<i32>,
) -> Result<(SyncEntityChunk, bool), ChunkReadingError> {
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
self.entity_saver
.fetch_chunks(&self.level_folder, &[pos], tx)
.await;
match rx.recv().await {
Some(LoadedData::Loaded(chunk)) => Ok((chunk, false)),
Some(LoadedData::Missing(_)) => Err(ChunkReadingError::ChunkNotExist),
Some(LoadedData::Error((_, err))) => Err(err),
None => Err(ChunkReadingError::ChunkNotExist),
}
}
pub fn receive_entity_chunks(
self: &Arc<Self>,
chunks: Vec<Vector2<i32>>,
) -> UnboundedReceiver<(SyncEntityChunk, bool)> {
let (sender, receiver) = mpsc::unbounded_channel();
let level = self.clone();
self.spawn_task(async move {
let cancel_notifier = level.cancel_token.cancelled();
let fetch_task = async {
let mut to_fetch = Vec::new();
for pos in &chunks {
if let Some(chunk) = level.loaded_entity_chunks.get(pos) {
let _ = sender.send((chunk.clone(), false));
} else {
to_fetch.push(*pos);
}
}
if !to_fetch.is_empty() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<
LoadedData<SyncEntityChunk, ChunkReadingError>,
>(to_fetch.len());
level
.entity_saver
.fetch_chunks(&level.level_folder, &to_fetch, tx)
.await;
while let Some(data) = rx.recv().await {
match data {
LoadedData::Loaded(chunk) => {
let tmp_chunk = chunk.read().await;
let pos = Vector2::new(tmp_chunk.x, tmp_chunk.z);
drop(tmp_chunk);
level.loaded_entity_chunks.insert(pos, chunk.clone());
let _ = sender.send((chunk, false));
}
LoadedData::Missing(pos) | LoadedData::Error((pos, _)) => {
let sender_clone = sender.clone();
let level_clone = level.clone();
tokio::spawn(async move {
let (tx, rx) = oneshot::channel();
match level_clone.pending_entity_generations.entry(pos) {
dashmap::mapref::entry::Entry::Occupied(mut entry) => {
entry.get_mut().push(tx);
}
dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert(vec![tx]);
let _ = level_clone.gen_entity_request_tx.send(pos);
}
}
if let Ok(chunk) = rx.await {
let _ = sender_clone.send((chunk, true));
}
});
}
}
}
}
};
select! {
() = cancel_notifier => {},
() = fetch_task => {}
}
});
receiver
}
pub async fn get_entity_chunk(self: &Arc<Self>, pos: Vector2<i32>) -> SyncEntityChunk {
if let Some(chunk) = self.loaded_entity_chunks.get(&pos) {
return chunk.clone();
}
match self.load_single_entity_chunk(pos).await {
Ok((chunk, _)) => {
self.loaded_entity_chunks.insert(pos, chunk.clone());
chunk
}
Err(_) => {
let (tx, rx) = oneshot::channel();
match self.pending_entity_generations.entry(pos) {
dashmap::mapref::entry::Entry::Occupied(mut entry) => {
entry.get_mut().push(tx);
}
dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert(vec![tx]);
let _ = self.gen_entity_request_tx.send(pos);
}
}
rx.await.expect("Entity generation worker dropped")
}
}
}
pub async fn get_block_state(self: &Arc<Self>, position: &BlockPos) -> RawBlockState {
let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position();
let chunk = self.get_chunk(chunk_coordinate).await;
let Some(id) = chunk.read().await.section.get_block_absolute_y(
relative.x as usize,
relative.y,
relative.z as usize,
) else {
return RawBlockState(Block::VOID_AIR.default_state.id);
};
RawBlockState(id)
}
pub async fn get_rough_biome(self: &Arc<Self>, position: &BlockPos) -> &'static Biome {
let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position();
let chunk = self.get_chunk(chunk_coordinate).await;
let Some(id) = chunk.read().await.section.get_rough_biome_absolute_y(
relative.x as usize,
relative.y,
relative.z as usize,
) else {
return &Biome::THE_VOID;
};
Biome::from_id(id).unwrap()
}
pub async fn set_block_state(
self: &Arc<Self>,
position: &BlockPos,
block_state_id: BlockStateId,
) -> BlockStateId {
let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position();
let chunk = self.get_chunk(chunk_coordinate).await;
let mut chunk = chunk.write().await;
let replaced_block_state_id = chunk.section.set_block_absolute_y(
relative.x as usize,
relative.y,
relative.z as usize,
block_state_id,
);
if replaced_block_state_id != block_state_id {
chunk.mark_dirty(true);
}
replaced_block_state_id
}
pub async fn write_chunks(&self, chunks_to_write: Vec<(Vector2<i32>, SyncChunk)>) {
if chunks_to_write.is_empty() {
return;
}
let chunk_saver = self.chunk_saver.clone();
let level_folder = self.level_folder.clone();
trace!("Sending chunks to ChunkIO {:}", chunks_to_write.len());
if let Err(error) = chunk_saver
.save_chunks(&level_folder, chunks_to_write)
.await
{
log::error!("Failed writing Chunk to disk {error}");
}
}
pub async fn write_entity_chunks(&self, chunks_to_write: Vec<(Vector2<i32>, SyncEntityChunk)>) {
if chunks_to_write.is_empty() {
return;
}
let chunk_saver = self.entity_saver.clone();
let level_folder = self.level_folder.clone();
trace!("Sending chunks to ChunkIO {:}", chunks_to_write.len());
if let Err(error) = chunk_saver
.save_chunks(&level_folder, chunks_to_write)
.await
{
log::error!("Failed writing Chunk to disk {error}");
}
}
pub fn try_get_chunk(&self, coordinates: &Vector2<i32>) -> Option<Arc<RwLock<ChunkData>>> {
self.loaded_chunks
.get(coordinates)
.map(|x| x.value().clone())
}
pub fn try_get_entity_chunk(
&self,
coordinates: Vector2<i32>,
) -> Option<dashmap::mapref::one::Ref<'_, Vector2<i32>, Arc<RwLock<ChunkEntityData>>>> {
self.loaded_entity_chunks.try_get(&coordinates).try_unwrap()
}
pub async fn schedule_block_tick(
self: &Arc<Self>,
block: &Block,
block_pos: BlockPos,
delay: u8,
priority: TickPriority,
) {
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let tick_order = self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed);
let mut chunk = chunk.write().await;
chunk.block_ticks.schedule_tick(
&ScheduledTick {
delay,
position: block_pos,
priority,
value: unsafe { &*(block as *const Block) },
},
tick_order,
);
}
pub async fn schedule_fluid_tick(
self: &Arc<Self>,
fluid: &Fluid,
block_pos: BlockPos,
delay: u8,
priority: TickPriority,
) {
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let tick_order = self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed);
let mut chunk = chunk.write().await;
chunk.fluid_ticks.schedule_tick(
&ScheduledTick {
delay,
position: block_pos,
priority,
value: unsafe { &*(fluid as *const Fluid) },
},
tick_order,
);
}
pub async fn is_block_tick_scheduled(
self: &Arc<Self>,
block_pos: &BlockPos,
block: &Block,
) -> bool {
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let chunk = chunk.read().await;
chunk.block_ticks.is_scheduled(*block_pos, block)
}
pub async fn is_fluid_tick_scheduled(
self: &Arc<Self>,
block_pos: &BlockPos,
fluid: &Fluid,
) -> bool {
let chunk = self.get_chunk(block_pos.chunk_position()).await;
let chunk = chunk.read().await;
chunk.fluid_ticks.is_scheduled(*block_pos, fluid)
}
}