use crate::chunk::format::LightContainer; use crate::tick::scheduler::ChunkTickScheduler; use palette::{BiomePalette, BlockPalette, has_random_ticking_fluid}; use pumpkin_data::block_properties::{blocks_movement, has_random_ticks, is_air}; use pumpkin_data::chunk::ChunkStatus; use pumpkin_data::fluid::Fluid; use pumpkin_data::tag::Block::MINECRAFT_LEAVES; use pumpkin_data::{Block, BlockState, BlockStateId}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; use rustc_hash::FxHashMap; use std::sync::RwLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; use thiserror::Error; use tokio::sync::Mutex; pub mod format; pub mod io; pub mod palette; // TODO pub const CHUNK_WIDTH: usize = BlockPalette::SIZE; pub const CHUNK_AREA: usize = CHUNK_WIDTH * CHUNK_WIDTH; pub const BIOME_VOLUME: usize = BiomePalette::VOLUME; pub const SUBCHUNK_VOLUME: usize = CHUNK_AREA * CHUNK_WIDTH; #[derive(Error, Debug)] pub enum ChunkReadingError { #[error("Io error: {0}")] IoError(std::io::Error), #[error("Invalid header")] InvalidHeader, #[error("Region is invalid")] RegionIsInvalid, #[error("Compression error {0}")] Compression(CompressionError), #[error("Tried to read chunk which does not exist")] ChunkNotExist, #[error("Failed to parse chunk from bytes: {0}")] ParsingError(ChunkParsingError), } #[derive(Error, Debug)] pub enum ChunkWritingError { #[error("Io error: {0}")] IoError(std::io::Error), #[error("Compression error {0}")] Compression(CompressionError), #[error("Chunk serializing error: {0}")] ChunkSerializingError(String), } #[derive(Error, Debug)] pub enum CompressionError { #[error("Compression scheme not recognised")] UnknownCompression, #[error("Error while working with zlib compression: {0}")] ZlibError(std::io::Error), #[error("Error while working with Gzip compression: {0}")] GZipError(std::io::Error), #[error("Error while working with LZ4 compression: {0}")] LZ4Error(std::io::Error), #[error("Error while working with zstd compression: {0}")] ZstdError(std::io::Error), } // Clone here cause we want to clone a snapshot of the chunk so we don't block writing for too long pub struct ChunkData { pub section: ChunkSections, /// See `https://minecraft.wiki/w/Heightmap` for more info pub heightmap: std::sync::Mutex, pub x: i32, pub z: i32, pub block_ticks: ChunkTickScheduler<&'static Block>, pub fluid_ticks: ChunkTickScheduler<&'static Fluid>, pub pending_block_entities: std::sync::Mutex>, pub light_engine: std::sync::Mutex, pub light_populated: AtomicBool, pub status: ChunkStatus, pub blending_data: Option, pub dirty: AtomicBool, pub inhabited_time: AtomicU64, pub custom_data: std::sync::Mutex, } pub struct ChunkEntityData { /// Chunk X pub x: i32, /// Chunk Z pub z: i32, pub data: Mutex>, pub dirty: AtomicBool, } /// Represents pure block data for a chunk. /// Subchunks are vertical portions of a chunk. They are 16 blocks tall. /// There are currently 24 subchunks per chunk. /// /// A chunk can be: /// - Subchunks: 24 separate subchunks are stored. pub struct ChunkSections { pub count: usize, pub block_sections: RwLock>, pub random_tick_sections: RwLock>>, pub randomly_ticking_mask: std::sync::atomic::AtomicU32, pub biome_sections: RwLock>, pub min_y: i32, } #[derive(Default, Clone, Copy)] pub struct RandomTickSectionCache { pub random_ticking_block_count: u16, pub random_ticking_fluid_count: u16, } impl RandomTickSectionCache { #[must_use] pub const fn is_randomly_ticking(&self) -> bool { self.random_ticking_block_count > 0 || self.random_ticking_fluid_count > 0 } } impl ChunkSections { #[cfg(test)] #[must_use] pub fn dump_blocks(&self) -> Vec { self.block_sections .read() .unwrap() .iter() .flat_map(|section| section.iter()) .collect() } #[cfg(test)] #[must_use] pub fn dump_biomes(&self) -> Vec { self.biome_sections .read() .unwrap() .iter() .flat_map(|section| section.iter()) .collect() } } #[derive(Default, Clone)] pub struct ChunkLight { pub sky_light: Box<[LightContainer]>, pub block_light: Box<[LightContainer]>, } #[derive(Debug, Clone, Copy)] pub enum ChunkHeightmapType { WorldSurface = 0, MotionBlocking = 1, MotionBlockingNoLeaves = 2, } impl TryFrom for ChunkHeightmapType { type Error = &'static str; fn try_from(value: usize) -> Result { match value { 0 => Ok(Self::WorldSurface), 1 => Ok(Self::MotionBlocking), 2 => Ok(Self::MotionBlockingNoLeaves), _ => Err("Invalid usize value for ChunkHeightmapType. The value should be 0~2."), } } } impl ChunkHeightmapType { #[must_use] pub fn is_opaque(&self, block_state: &BlockState) -> bool { let block = block_state.id.to_block_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()) && !block.has_tag(MINECRAFT_LEAVES) } } } } #[derive(Debug, Clone)] pub struct ChunkHeightmaps { pub world_surface: Option>, pub motion_blocking: Option>, pub motion_blocking_no_leaves: Option>, } impl ChunkHeightmaps { pub fn set(&mut self, heightmap: ChunkHeightmapType, x: i32, z: i32, height: i32, min_y: i32) { let data = match heightmap { ChunkHeightmapType::WorldSurface => &mut self.world_surface, ChunkHeightmapType::MotionBlocking => &mut self.motion_blocking, ChunkHeightmapType::MotionBlockingNoLeaves => &mut self.motion_blocking_no_leaves, }; let data = data.get_or_insert_with(|| vec![0; 37].into_boxed_slice()); let local_x = (x & 15) as usize; let local_z = (z & 15) as usize; let column_idx = local_z * 16 + local_x; // In Minecraft 1.16+, height is stored as (y - min_y + 1). 0 means below min_y. // It uses 9 bits per value, packed such that they do not cross u64 boundaries. // 64 / 9 = 7 values per u64. let val = (height - min_y + 1).max(0) as u64; let array_idx = column_idx / 7; let shift = (column_idx % 7) * 9; let mask = 0x1FFu64 << shift; let mut current = data[array_idx] as u64; current = (current & !mask) | ((val & 0x1FF) << shift); data[array_idx] = current as i64; } #[must_use] pub fn get(&self, heightmap: ChunkHeightmapType, x: i32, z: i32, min_y: i32) -> i32 { let data = match heightmap { ChunkHeightmapType::WorldSurface => &self.world_surface, ChunkHeightmapType::MotionBlocking => &self.motion_blocking, ChunkHeightmapType::MotionBlockingNoLeaves => &self.motion_blocking_no_leaves, }; let Some(data) = data else { return min_y - 1; }; let local_x = (x & 15) as usize; let local_z = (z & 15) as usize; let column_idx = local_z * 16 + local_x; let array_idx = column_idx / 7; let shift = (column_idx % 7) * 9; let current = data[array_idx] as u64; let val = (current >> shift) & 0x1FF; (val as i32) + min_y - 1 } #[expect(clippy::too_many_arguments)] pub fn update( &mut self, heightmap_type: ChunkHeightmapType, local_x: i32, local_y: i32, local_z: i32, block_state: &BlockState, min_y: i32, get_block: F, ) -> bool where F: Fn(i32) -> &'static BlockState, { let first_available = self.get(heightmap_type, local_x, local_z, min_y) + 1; if local_y <= first_available - 2 { return false; } if heightmap_type.is_opaque(block_state) { if local_y >= first_available { self.set(heightmap_type, local_x, local_z, local_y, min_y); return true; } } else if first_available - 1 == local_y { for y in (min_y..local_y).rev() { let state = get_block(y); if heightmap_type.is_opaque(state) { self.set(heightmap_type, local_x, local_z, y, min_y); return true; } } self.set(heightmap_type, local_x, local_z, min_y - 1, min_y); return true; } false } } /// The Heightmap for a completely empty chunk impl Default for ChunkHeightmaps { fn default() -> Self { Self { motion_blocking: None, motion_blocking_no_leaves: None, world_surface: None, } } } impl ChunkSections { #[must_use] pub fn build_random_tick_sections_cache( block_sections: &[BlockPalette], ) -> (Option>, u32) { let mut mask = 0; let mut has_ticks = false; let cache = block_sections .iter() .enumerate() .map(|(i, section)| { let (random_ticking_block_count, random_ticking_fluid_count) = section.random_ticking_counts(); if random_ticking_block_count > 0 || random_ticking_fluid_count > 0 { mask |= 1 << i; has_ticks = true; } RandomTickSectionCache { random_ticking_block_count, random_ticking_fluid_count, } }) .collect::>() .into_boxed_slice(); if has_ticks { (Some(cache), mask) } else { (None, 0) } } #[must_use] pub fn new(num_sections: usize, min_y: i32) -> Self { let block_sections = vec![BlockPalette::default(); num_sections].into_boxed_slice(); let (random_tick_sections, randomly_ticking_mask) = Self::build_random_tick_sections_cache(&block_sections); let biome_sections = vec![BiomePalette::default(); num_sections].into_boxed_slice(); Self { count: num_sections, block_sections: RwLock::new(block_sections), random_tick_sections: RwLock::new(random_tick_sections), randomly_ticking_mask: std::sync::atomic::AtomicU32::new(randomly_ticking_mask), biome_sections: RwLock::new(biome_sections), min_y, } } #[must_use] pub(crate) fn from_palettes( block_sections: Box<[BlockPalette]>, biome_sections: Box<[BiomePalette]>, min_y: i32, ) -> Self { assert_eq!( block_sections.len(), biome_sections.len(), "block and biome section counts must match" ); let count = block_sections.len(); let (random_tick_sections, randomly_ticking_mask) = Self::build_random_tick_sections_cache(&block_sections); Self { count, block_sections: RwLock::new(block_sections), random_tick_sections: RwLock::new(random_tick_sections), randomly_ticking_mask: std::sync::atomic::AtomicU32::new(randomly_ticking_mask), biome_sections: RwLock::new(biome_sections), min_y, } } #[must_use] pub fn get_block_absolute_y( &self, relative_x: usize, y: i32, relative_z: usize, ) -> Option { let y = y - self.min_y; if y < 0 { None } else { let relative_y = y as usize; self.get_relative_block(relative_x, relative_y, relative_z) } } pub fn set_block_absolute_y( &self, relative_x: usize, y: i32, relative_z: usize, block_state_id: BlockStateId, ) -> BlockStateId { let y = y - self.min_y; if y < 0 { return Block::AIR.default_state.id; } let relative_y = y as usize; self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id) } #[must_use] pub fn get_rough_biome_absolute_y( &self, relative_x: usize, y: i32, relative_z: usize, ) -> Option { let y = y - self.min_y; if y < 0 { None } else { let relative_y = y as usize; self.get_noise_biome( relative_y / BlockPalette::SIZE, relative_x >> 2 & 3, relative_y >> 2 & 3, relative_z >> 2 & 3, ) } } /// Gets the given block in the chunk fn get_relative_block( &self, relative_x: usize, relative_y: usize, relative_z: usize, ) -> Option { debug_assert!(relative_x < BlockPalette::SIZE); debug_assert!(relative_z < BlockPalette::SIZE); let section_index = relative_y / BlockPalette::SIZE; let relative_y = relative_y % BlockPalette::SIZE; self.block_sections .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .get(section_index) .map(|section| section.get(relative_x, relative_y, relative_z)) } /// Sets the given block in the chunk, returning the old block state ID #[inline] pub fn set_relative_block( &self, relative_x: usize, relative_y: usize, relative_z: usize, block_state_id: BlockStateId, ) -> BlockStateId { self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id) } /// Sets the given block in the chunk, returning the old block /// Contrary to `set_block` this does not update the heightmap. /// /// Only use this if you know you don't need to update the heightmap /// or if you manually set the heightmap in `empty_with_heightmap` pub fn set_block_no_heightmap_update( &self, relative_x: usize, relative_y: usize, relative_z: usize, block_state_id: BlockStateId, ) -> BlockStateId { debug_assert!(relative_x < BlockPalette::SIZE); debug_assert!(relative_z < BlockPalette::SIZE); let section_index = relative_y / BlockPalette::SIZE; let relative_y = relative_y % BlockPalette::SIZE; // Keep lock order consistent to avoid deadlocks: block sections first, then random-tick cache. let mut sections = self .block_sections .write() .unwrap_or_else(std::sync::PoisonError::into_inner); let mut random_tick_sections_guard = self .random_tick_sections .write() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(section) = sections.get_mut(section_index) { let replaced_block_state_id = section.set(relative_x, relative_y, relative_z, block_state_id); if replaced_block_state_id == block_state_id { return replaced_block_state_id; } if (has_random_ticks(block_state_id) || has_random_ticking_fluid(block_state_id)) && random_tick_sections_guard.is_none() { let new_cache = vec![RandomTickSectionCache::default(); self.count].into_boxed_slice(); *random_tick_sections_guard = Some(new_cache); } if let Some(random_tick_sections) = random_tick_sections_guard.as_mut() { let random_tick_cache = &mut random_tick_sections[section_index]; if has_random_ticks(replaced_block_state_id) { random_tick_cache.random_ticking_block_count = random_tick_cache .random_ticking_block_count .saturating_sub(1); } if has_random_ticking_fluid(replaced_block_state_id) { random_tick_cache.random_ticking_fluid_count = random_tick_cache .random_ticking_fluid_count .saturating_sub(1); } if has_random_ticks(block_state_id) { random_tick_cache.random_ticking_block_count = random_tick_cache .random_ticking_block_count .saturating_add(1); } if has_random_ticking_fluid(block_state_id) { random_tick_cache.random_ticking_fluid_count = random_tick_cache .random_ticking_fluid_count .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; } BlockStateId::AIR } pub fn set_relative_biome( &self, relative_x: usize, relative_y: usize, relative_z: usize, biome_id: u8, ) { debug_assert!(relative_x < BiomePalette::SIZE); debug_assert!(relative_z < BiomePalette::SIZE); let section_index = relative_y / BiomePalette::SIZE; let relative_y = relative_y % BiomePalette::SIZE; if let Some(section) = self .biome_sections .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .get_mut(section_index) { section.set(relative_x, relative_y, relative_z, biome_id); } } #[must_use] pub fn get_noise_biome( &self, index: usize, scale_x: usize, scale_y: usize, scale_z: usize, ) -> Option { debug_assert!(scale_x < BiomePalette::SIZE); debug_assert!(scale_z < BiomePalette::SIZE); self.biome_sections .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .get(index) .map(|section| section.get(scale_x, scale_y, scale_z)) } #[must_use] pub fn get_top_y(&self, relative_x: usize, relative_z: usize, first_y: i32) -> Option { debug_assert!(relative_x < BlockPalette::SIZE); debug_assert!(relative_z < BlockPalette::SIZE); let mut y = first_y; while y >= self.min_y { if let Some(block_state_id) = self.get_block_absolute_y(relative_x, y, relative_z) && !is_air(block_state_id) { return Some(y); } y -= 1; } None } } impl ChunkData { #[must_use] pub fn empty(x: i32, z: i32) -> Self { Self { section: ChunkSections::new(24, -64), heightmap: std::sync::Mutex::new(ChunkHeightmaps::default()), x, z, block_ticks: ChunkTickScheduler::default(), fluid_ticks: ChunkTickScheduler::default(), pending_block_entities: std::sync::Mutex::new(FxHashMap::default()), light_engine: std::sync::Mutex::new(ChunkLight::default()), light_populated: std::sync::atomic::AtomicBool::new(false), status: ChunkStatus::Full, blending_data: None, dirty: std::sync::atomic::AtomicBool::new(false), inhabited_time: std::sync::atomic::AtomicU64::new(0), custom_data: std::sync::Mutex::new(NbtCompound::new()), } } #[must_use] pub fn empty_sync(x: i32, z: i32) -> std::sync::Arc { std::sync::Arc::new(Self::empty(x, z)) } /// 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_or_else(std::sync::PoisonError::into_inner); 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(BlockStateId::AIR); BlockState::from_id(id) }); } } /// Gets the given block in the chunk #[inline] #[must_use] pub fn get_relative_block( &self, relative_x: usize, relative_y: usize, relative_z: usize, ) -> Option { self.section .get_relative_block(relative_x, relative_y, relative_z) } /// Sets the given block in the chunk #[inline] pub fn set_relative_block( &mut self, relative_x: usize, relative_y: usize, relative_z: usize, block_state_id: BlockStateId, ) { let state = BlockState::from_id(block_state_id); self.update_heightmap(relative_x, relative_y, relative_z, state); self.section .set_relative_block(relative_x, relative_y, relative_z, block_state_id); } /// Sets the given block in the chunk, returning the old block /// Contrary to `set_block` this does not update the heightmap. /// /// Only use this if you know you don't need to update the heightmap /// or if you manually set the heightmap in `empty_with_heightmap` #[inline] pub fn set_block_no_heightmap_update( &mut self, relative_x: usize, relative_y: usize, relative_z: usize, block_state_id: BlockStateId, ) { self.section .set_relative_block(relative_x, relative_y, relative_z, block_state_id); } //TODO: Tracking heightmaps update. pub fn calculate_heightmap(&self) -> ChunkHeightmaps { let highest_non_empty_subchunk = self.get_highest_non_empty_subchunk(); let mut heightmaps = ChunkHeightmaps::default(); for x in 0..16 { for z in 0..16 { self.populate_heightmaps(&mut heightmaps, highest_non_empty_subchunk, x, z); } } // log::info!("WorldSurface:"); // heightmaps.log_heightmap(ChunkHeightmapType::WorldSurface, self.section.min_y); // log::info!("MotionBlocking:"); // heightmaps.log_heightmap(ChunkHeightmapType::MotionBlocking, self.section.min_y); // log::info!("min_y: {}", self.section.min_y); heightmaps } #[inline] fn populate_heightmaps( &self, heightmaps: &mut ChunkHeightmaps, start_sub_chunk: usize, x: usize, z: usize, ) { let start_height = (start_sub_chunk as i32) * 16 - self.section.min_y.abs() + 15; let mut has_found = [false, false, false]; for y in (self.section.min_y..=start_height).rev() { let Some(state_id) = self.section.get_block_absolute_y(x, y, z) else { continue; }; let block_state = BlockState::from_id(state_id); for hm_type in [ ChunkHeightmapType::WorldSurface, ChunkHeightmapType::MotionBlocking, ChunkHeightmapType::MotionBlockingNoLeaves, ] { let idx = hm_type as usize; if !has_found[idx] && hm_type.is_opaque(block_state) { heightmaps.set(hm_type, x as i32, z as i32, y, self.section.min_y); has_found[idx] = true; } } if has_found.iter().all(|&found| found) { return; } } for (idx, is_set) in has_found.iter().enumerate() { if !(*is_set) && let Ok(hm_type) = idx.try_into() { heightmaps.set( hm_type, x as i32, z as i32, self.section.min_y - 1, self.section.min_y, ); } } } #[must_use] pub fn get_highest_non_empty_subchunk(&self) -> usize { self.section .block_sections .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .iter() .enumerate() .rev() .find(|(_, sub)| !sub.has_only_air()) .map_or(0, |(idx, _)| idx) } } #[derive(Error, Debug)] pub enum ChunkParsingError { #[error("Failed reading chunk status {0}")] FailedReadStatus(pumpkin_nbt::Error), #[error("The chunk isn't generated yet")] ChunkNotGenerated, #[error("Error deserializing chunk: {0}")] ErrorDeserializingChunk(String), } #[derive(Error, Debug)] pub enum ChunkSerializingError { #[error("Error serializing chunk: {0}")] ErrorSerializingChunk(pumpkin_nbt::Error), } #[cfg(test)] mod tests { use super::ChunkSections; use crate::chunk::palette::BlockPalette; use pumpkin_data::{Block, block_properties::has_random_ticks}; #[test] fn random_tick_cache_initializes_from_palette_contents() { let mut sections = vec![BlockPalette::default(), BlockPalette::default()]; sections[1].set(0, 0, 0, Block::LAVA.default_state.id); let (cache, _mask) = ChunkSections::build_random_tick_sections_cache(§ions); let cache = cache.unwrap(); assert!(!cache[0].is_randomly_ticking()); assert!(cache[1].random_ticking_fluid_count > 0); assert!(cache[1].is_randomly_ticking()); } #[test] fn random_tick_cache_updates_on_block_mutation() { let min_y = -64; let sections = ChunkSections::new(1, min_y); assert!( sections .random_tick_sections .read() .unwrap() .as_ref() .is_none_or(|c| !c[0].is_randomly_ticking()), "fresh sections should not be randomly ticking" ); let random_block_state = Block::WHEAT.default_state.id; assert!( has_random_ticks(random_block_state), "test requires a known randomly ticking block state" ); sections.set_block_absolute_y(0, min_y, 0, random_block_state); { let cache = sections.random_tick_sections.read().unwrap(); let cache = cache.as_ref().unwrap(); assert_eq!(cache[0].random_ticking_block_count, 1); assert_eq!(cache[0].random_ticking_fluid_count, 0); assert!(cache[0].is_randomly_ticking()); }; sections.set_block_absolute_y(0, min_y, 0, Block::STONE.default_state.id); { let cache = sections.random_tick_sections.read().unwrap(); let cache = cache.as_ref().unwrap(); assert_eq!(cache[0].random_ticking_block_count, 0); assert_eq!(cache[0].random_ticking_fluid_count, 0); assert!(!cache[0].is_randomly_ticking()); }; sections.set_block_absolute_y(0, min_y, 0, Block::LAVA.default_state.id); { let cache = sections.random_tick_sections.read().unwrap(); let cache = cache.as_ref().unwrap(); assert!(cache[0].random_ticking_fluid_count > 0); assert!(cache[0].is_randomly_ticking()); } } #[test] fn 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 } #[test] fn chunk_custom_data() { use pumpkin_nbt::tag::NbtTag; let chunk = super::ChunkData::empty(0, 0); assert!(!chunk.has_custom_data("my_plugin", "test_key")); assert_eq!(chunk.get_custom_data("my_plugin", "test_key"), None); chunk.set_custom_data( "my_plugin", "test_key", NbtTag::String("hello_pumpkin".into()), ); assert!(chunk.has_custom_data("my_plugin", "test_key")); assert_eq!( chunk.get_custom_data("my_plugin", "test_key"), Some(NbtTag::String("hello_pumpkin".into())) ); chunk.set_custom_data("my_plugin", "number_key", NbtTag::Int(42)); assert_eq!( chunk.get_custom_data("my_plugin", "number_key"), Some(NbtTag::Int(42)) ); chunk.remove_custom_data("my_plugin", "test_key"); assert!(!chunk.has_custom_data("my_plugin", "test_key")); assert!(chunk.has_custom_data("my_plugin", "number_key")); } }