aboutsummaryrefslogtreecommitdiff
path: root/azalea-world/src/chunk
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2026-03-21 08:05:27 +0330
committermat <git@matdoes.dev>2026-03-21 11:35:31 +0700
commit88606d9ce9e13fcdd4ab5ce26e52630dee614c1e (patch)
treeee9d1db9871eba381a876e7472946f267280a8ff /azalea-world/src/chunk
parent7965bb7953bfcabd475e213db335d90e0db28497 (diff)
downloadazalea-drasl-88606d9ce9e13fcdd4ab5ce26e52630dee614c1e.tar.xz
Extensible ChunkStorage
Co-authored-by: sdwhw <191973436+sdwhw@users.noreply.github.com>
Diffstat (limited to 'azalea-world/src/chunk')
-rw-r--r--azalea-world/src/chunk/mod.rs305
-rw-r--r--azalea-world/src/chunk/partial.rs246
-rw-r--r--azalea-world/src/chunk/storage.rs242
3 files changed, 793 insertions, 0 deletions
diff --git a/azalea-world/src/chunk/mod.rs b/azalea-world/src/chunk/mod.rs
new file mode 100644
index 00000000..cd52026a
--- /dev/null
+++ b/azalea-world/src/chunk/mod.rs
@@ -0,0 +1,305 @@
+pub mod partial;
+pub mod storage;
+
+use std::{
+ collections::HashMap,
+ fmt::Debug,
+ io,
+ io::{Cursor, Write},
+};
+
+use azalea_block::block_state::{BlockState, BlockStateIntegerRepr};
+use azalea_buf::{AzBuf, BufReadError};
+use azalea_core::{
+ heightmap_kind::HeightmapKind,
+ position::{ChunkBiomePos, ChunkBlockPos, ChunkSectionBiomePos, ChunkSectionBlockPos},
+};
+use azalea_registry::data::Biome;
+use tracing::warn;
+
+use crate::{heightmap::Heightmap, palette::PalettedContainer};
+
+const SECTION_HEIGHT: u32 = 16;
+
+/// A single chunk in a world (16*?*16 blocks).
+///
+/// This only contains blocks and biomes. You can derive the height of the chunk
+/// from the number of sections, but you need a [`ChunkStorage`] to get the
+/// minimum Y coordinate.
+#[derive(Debug)]
+pub struct Chunk {
+ pub sections: Box<[Section]>,
+ /// Heightmaps are used for identifying the surface blocks in a chunk.
+ /// Usually for clients only `WorldSurface` and `MotionBlocking` are
+ /// present.
+ pub heightmaps: HashMap<HeightmapKind, Heightmap>,
+}
+
+/// A section of a chunk, i.e. a 16*16*16 block area.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Section {
+ /// The number of non-empty blocks in the section, which is initialized
+ /// based on a value sent to us by the server.
+ ///
+ /// This may be updated every time [`Self::get_and_set_block_state`] is
+ /// called.
+ pub block_count: u16,
+ pub states: PalettedContainer<BlockState>,
+ pub biomes: PalettedContainer<Biome>,
+}
+
+/// Get the actual stored view distance for the selected view distance.
+///
+/// For some reason, Minecraft stores an extra 3 chunks.
+pub fn calculate_chunk_storage_range(view_distance: u32) -> u32 {
+ u32::max(view_distance, 2) + 3
+}
+
+impl Default for Chunk {
+ fn default() -> Self {
+ Chunk {
+ sections: vec![Section::default(); (384 / 16) as usize].into(),
+ heightmaps: HashMap::new(),
+ }
+ }
+}
+
+impl Chunk {
+ pub fn read_with_dimension_height(
+ buf: &mut Cursor<&[u8]>,
+ dimension_height: u32,
+ min_y: i32,
+ heightmaps_data: &[(HeightmapKind, Box<[u64]>)],
+ ) -> Result<Self, BufReadError> {
+ let section_count = dimension_height / SECTION_HEIGHT;
+ let mut sections = Vec::with_capacity(section_count as usize);
+ for _ in 0..section_count {
+ let section = Section::azalea_read(buf)?;
+ sections.push(section);
+ }
+ let sections = sections.into_boxed_slice();
+
+ let mut heightmaps = HashMap::new();
+ for (kind, data) in heightmaps_data {
+ let data = data.clone();
+ let heightmap = Heightmap::new(*kind, dimension_height, min_y, data);
+ heightmaps.insert(*kind, heightmap);
+ }
+
+ Ok(Chunk {
+ sections,
+ heightmaps,
+ })
+ }
+
+ pub fn get_block_state(&self, pos: &ChunkBlockPos, min_y: i32) -> Option<BlockState> {
+ get_block_state_from_sections(&self.sections, pos, min_y)
+ }
+
+ #[must_use = "Use Chunk::set_block_state instead if you don't need the previous state"]
+ pub fn get_and_set_block_state(
+ &mut self,
+ pos: &ChunkBlockPos,
+ state: BlockState,
+ min_y: i32,
+ ) -> BlockState {
+ let section_index = section_index(pos.y, min_y);
+ let Some(section) = self.sections.get_mut(section_index as usize) else {
+ warn!(
+ "Tried to get and set block state {state:?} at out-of-bounds relative chunk position {pos:?}",
+ );
+ return BlockState::AIR;
+ };
+ let chunk_section_pos = ChunkSectionBlockPos::from(pos);
+ let previous_state = section.get_and_set_block_state(chunk_section_pos, state);
+
+ for heightmap in self.heightmaps.values_mut() {
+ heightmap.update(pos, state, &self.sections);
+ }
+
+ previous_state
+ }
+
+ pub fn set_block_state(&mut self, pos: &ChunkBlockPos, state: BlockState, min_y: i32) {
+ let section_index = section_index(pos.y, min_y);
+ let Some(section) = self.sections.get_mut(section_index as usize) else {
+ warn!(
+ "Tried to set block state {state:?} at out-of-bounds relative chunk position {pos:?}",
+ );
+ return;
+ };
+ let chunk_section_pos = ChunkSectionBlockPos::from(pos);
+ section.get_and_set_block_state(chunk_section_pos, state);
+
+ for heightmap in self.heightmaps.values_mut() {
+ heightmap.update(pos, state, &self.sections);
+ }
+ }
+
+ /// Get the biome at the given position, or `None` if it's out of bounds.
+ pub fn get_biome(&self, pos: ChunkBiomePos, min_y: i32) -> Option<Biome> {
+ if pos.y < min_y {
+ // y position is out of bounds
+ return None;
+ }
+ let section_index = section_index(pos.y, min_y);
+ let Some(section) = self.sections.get(section_index as usize) else {
+ warn!("Tried to get biome at out-of-bounds relative chunk position {pos:?}",);
+ return None;
+ };
+ let chunk_section_pos = ChunkSectionBiomePos::from(pos);
+ Some(section.get_biome(chunk_section_pos))
+ }
+}
+
+/// Get the block state at the given position from a list of sections. Returns
+/// `None` if the position is out of bounds.
+#[inline]
+pub fn get_block_state_from_sections(
+ sections: &[Section],
+ pos: &ChunkBlockPos,
+ min_y: i32,
+) -> Option<BlockState> {
+ if pos.y < min_y {
+ // y position is out of bounds
+ return None;
+ }
+ let section_index = section_index(pos.y, min_y) as usize;
+ if section_index >= sections.len() {
+ // y position is out of bounds
+ return None;
+ };
+ let section = &sections[section_index];
+
+ let chunk_section_pos = ChunkSectionBlockPos::from(pos);
+ Some(section.get_block_state(chunk_section_pos))
+}
+
+impl AzBuf for Section {
+ fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
+ let block_count = u16::azalea_read(buf)?;
+
+ // this is commented out because the vanilla server is wrong
+ // TODO: ^ this comment was written ages ago. needs more investigation.
+ // assert!(
+ // block_count <= 16 * 16 * 16,
+ // "A section has more blocks than what should be possible. This is a bug!"
+ // );
+
+ let states = PalettedContainer::<BlockState>::read(buf)?;
+
+ for i in 0..states.storage.size() {
+ if !BlockState::is_valid_state(states.storage.get(i) as BlockStateIntegerRepr) {
+ return Err(BufReadError::Custom(format!(
+ "Invalid block state {} (index {i}) found in section.",
+ states.storage.get(i)
+ )));
+ }
+ }
+
+ let biomes = PalettedContainer::<Biome>::read(buf)?;
+ Ok(Section {
+ block_count,
+ states,
+ biomes,
+ })
+ }
+ fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
+ self.block_count.azalea_write(buf)?;
+ self.states.write(buf)?;
+ self.biomes.write(buf)?;
+ Ok(())
+ }
+}
+
+impl Section {
+ pub fn get_block_state(&self, pos: ChunkSectionBlockPos) -> BlockState {
+ self.states.get(pos)
+ }
+ pub fn get_and_set_block_state(
+ &mut self,
+ pos: ChunkSectionBlockPos,
+ state: BlockState,
+ ) -> BlockState {
+ let previous_state = self.states.get_and_set(pos, state);
+
+ if previous_state.is_air() && !state.is_air() {
+ self.block_count += 1;
+ } else if !previous_state.is_air() && state.is_air() {
+ self.block_count -= 1;
+ }
+
+ previous_state
+ }
+
+ pub fn get_biome(&self, pos: ChunkSectionBiomePos) -> Biome {
+ self.biomes.get(pos)
+ }
+ pub fn set_biome(&mut self, pos: ChunkSectionBiomePos, biome: Biome) {
+ self.biomes.set(pos, biome);
+ }
+ pub fn get_and_set_biome(&mut self, pos: ChunkSectionBiomePos, biome: Biome) -> Biome {
+ self.biomes.get_and_set(pos, biome)
+ }
+}
+
+/// Get the index of where a section is in a chunk based on its y coordinate
+/// and the minimum y coordinate of the world.
+#[inline]
+pub fn section_index(y: i32, min_y: i32) -> u32 {
+ if y < min_y {
+ #[cfg(debug_assertions)]
+ tracing::warn!("y ({y}) must be at least {min_y}");
+ #[cfg(not(debug_assertions))]
+ tracing::trace!("y ({y}) must be at least {min_y}")
+ };
+ let min_section_index = min_y >> 4;
+ ((y >> 4) - min_section_index) as u32
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::palette::SectionPos;
+
+ #[test]
+ fn test_section_index() {
+ assert_eq!(section_index(0, 0), 0);
+ assert_eq!(section_index(128, 0), 8);
+ assert_eq!(section_index(127, 0), 7);
+ assert_eq!(section_index(0, -64), 4);
+ assert_eq!(section_index(-64, -64), 0);
+ assert_eq!(section_index(-49, -64), 0);
+ assert_eq!(section_index(-48, -64), 1);
+ assert_eq!(section_index(128, -64), 12);
+ }
+
+ #[test]
+ fn serialize_and_deserialize_section() {
+ let mut states = PalettedContainer::new();
+
+ states.set(
+ SectionPos::new(1, 2, 3),
+ BlockState::try_from(BlockState::MAX_STATE).unwrap(),
+ );
+ states.set(
+ SectionPos::new(4, 5, 6),
+ BlockState::try_from(BlockState::MAX_STATE).unwrap(),
+ );
+ let biomes = PalettedContainer::new();
+ let section = Section {
+ block_count: 2,
+ states,
+ biomes,
+ };
+
+ let mut buf = Vec::new();
+ section.azalea_write(&mut buf).unwrap();
+
+ let mut cur = Cursor::new(buf.as_slice());
+ let deserialized_section = Section::azalea_read(&mut cur).unwrap();
+ assert_eq!(cur.position(), buf.len() as u64);
+
+ assert_eq!(section, deserialized_section);
+ }
+}
diff --git a/azalea-world/src/chunk/partial.rs b/azalea-world/src/chunk/partial.rs
new file mode 100644
index 00000000..e0f4912b
--- /dev/null
+++ b/azalea-world/src/chunk/partial.rs
@@ -0,0 +1,246 @@
+use std::{
+ fmt::{self, Debug},
+ io::Cursor,
+ sync::Arc,
+};
+
+use azalea_block::BlockState;
+use azalea_buf::BufReadError;
+use azalea_core::{
+ heightmap_kind::HeightmapKind,
+ position::{BlockPos, ChunkBlockPos, ChunkPos},
+};
+use parking_lot::RwLock;
+use tracing::{debug, trace, warn};
+
+use crate::{Chunk, chunk::storage::ChunkStorage};
+
+/// An efficient storage of chunks for a client that has a limited render
+/// distance.
+///
+/// This has support for using a shared [`ChunkStorage`].
+pub struct PartialChunkStorage {
+ /// The center of the view, i.e. the chunk the player is currently in.
+ view_center: ChunkPos,
+ pub(crate) chunk_radius: u32,
+ view_range: u32,
+ // chunks is a list of size chunk_radius * chunk_radius
+ chunks: Box<[Option<Arc<RwLock<Chunk>>>]>,
+}
+
+impl PartialChunkStorage {
+ pub fn new(chunk_radius: u32) -> Self {
+ let view_range = chunk_radius * 2 + 1;
+ PartialChunkStorage {
+ view_center: ChunkPos::new(0, 0),
+ chunk_radius,
+ view_range,
+ chunks: vec![None; (view_range * view_range) as usize].into(),
+ }
+ }
+
+ /// Update the chunk to center the view on.
+ ///
+ /// This should be called when the client receives a `SetChunkCacheCenter`
+ /// packet.
+ pub fn update_view_center(&mut self, view_center: ChunkPos) {
+ // this code block makes it force unload the chunks that are out of range after
+ // updating the view center. it's usually fine without it but the commented code
+ // is there in case you want to temporarily uncomment to test something
+
+ // ```
+ // for index in 0..self.chunks.len() {
+ // let chunk_pos = self.chunk_pos_from_index(index);
+ // if !in_range_for_view_center_and_radius(&chunk_pos, view_center, self.chunk_radius) {
+ // self.chunks[index] = None;
+ // }
+ // }
+ // ```
+
+ self.view_center = view_center;
+ }
+
+ /// Get the center of the view. This is usually the chunk that the player is
+ /// in.
+ pub fn view_center(&self) -> ChunkPos {
+ self.view_center
+ }
+
+ pub fn view_range(&self) -> u32 {
+ self.view_range
+ }
+
+ pub fn index_from_chunk_pos(&self, chunk_pos: &ChunkPos) -> usize {
+ let view_range = self.view_range as i32;
+
+ let x = i32::rem_euclid(chunk_pos.x, view_range) * view_range;
+ let z = i32::rem_euclid(chunk_pos.z, view_range);
+ (x + z) as usize
+ }
+
+ pub fn chunk_pos_from_index(&self, index: usize) -> ChunkPos {
+ let view_range = self.view_range as i32;
+
+ // find the base from the view center
+ let base_x = self.view_center.x.div_euclid(view_range) * view_range;
+ let base_z = self.view_center.z.div_euclid(view_range) * view_range;
+
+ // add the offset from the base
+ let offset_x = index as i32 / view_range;
+ let offset_z = index as i32 % view_range;
+
+ ChunkPos::new(base_x + offset_x, base_z + offset_z)
+ }
+
+ pub fn in_range(&self, chunk_pos: &ChunkPos) -> bool {
+ in_range_for_view_center_and_radius(chunk_pos, self.view_center, self.chunk_radius)
+ }
+
+ pub fn set_block_state(
+ &self,
+ pos: BlockPos,
+ state: BlockState,
+ chunk_storage: &ChunkStorage,
+ ) -> Option<BlockState> {
+ if pos.y < chunk_storage.min_y()
+ || pos.y >= (chunk_storage.min_y() + chunk_storage.height() as i32)
+ {
+ return None;
+ }
+ let chunk_pos = ChunkPos::from(pos);
+ let chunk_lock = chunk_storage.get(&chunk_pos)?;
+ let mut chunk = chunk_lock.write();
+ Some(chunk.get_and_set_block_state(&ChunkBlockPos::from(pos), state, chunk_storage.min_y()))
+ }
+
+ pub fn replace_with_packet_data(
+ &mut self,
+ pos: &ChunkPos,
+ data: &mut Cursor<&[u8]>,
+ heightmaps: &[(HeightmapKind, Box<[u64]>)],
+ chunk_storage: &mut ChunkStorage,
+ ) -> Result<(), BufReadError> {
+ debug!("Replacing chunk at {:?}", pos);
+ if !self.in_range(pos) {
+ warn!("Ignoring chunk since it's not in the view range: {pos:?}");
+ return Ok(());
+ }
+
+ let chunk = Chunk::read_with_dimension_height(
+ data,
+ chunk_storage.height(),
+ chunk_storage.min_y(),
+ heightmaps,
+ )?;
+
+ self.set(pos, Some(chunk), chunk_storage);
+ trace!("Loaded chunk {pos:?}");
+
+ Ok(())
+ }
+
+ /// Get a [`Chunk`] within render distance, or `None` if it's not loaded.
+ /// Use [`ChunkStorage::get`] to get a chunk from the shared storage.
+ pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> {
+ if !self.in_range(pos) {
+ warn!(
+ "Chunk at {:?} is not in the render distance (center: {:?}, {} chunks)",
+ pos, self.view_center, self.chunk_radius,
+ );
+ return None;
+ }
+
+ let index = self.index_from_chunk_pos(pos);
+ self.chunks[index].as_ref()
+ }
+ /// Get a mutable reference to a [`Chunk`] within render distance, or
+ /// `None` if it's not loaded.
+ ///
+ /// Use [`ChunkStorage::get`] to get a chunk from the shared storage.
+ pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> {
+ if !self.in_range(pos) {
+ return None;
+ }
+
+ let index = self.index_from_chunk_pos(pos);
+
+ Some(&mut self.chunks[index])
+ }
+
+ /// Set a chunk in the shared storage and reference it from the limited
+ /// storage.
+ ///
+ /// Use [`Self::limited_set`] if you already have an `Arc<RwLock<Chunk>>`.
+ ///
+ /// # Panics
+ /// If the chunk is not in the render distance.
+ pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Chunk>, chunk_storage: &mut ChunkStorage) {
+ let new_chunk = chunk.map(|c| chunk_storage.upsert(*pos, c));
+ self.limited_set(pos, new_chunk);
+ }
+
+ /// Set a chunk in our limited storage, useful if your chunk is already
+ /// referenced somewhere else and you want to make it also be referenced by
+ /// this storage.
+ ///
+ /// Use [`Self::set`] if you don't already have an `Arc<RwLock<Chunk>>`.
+ ///
+ /// # Panics
+ /// If the chunk is not in the render distance.
+ pub fn limited_set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) {
+ if let Some(chunk_mut) = self.limited_get_mut(pos) {
+ *chunk_mut = chunk;
+ }
+ }
+
+ /// Get an iterator over all the chunks in the storage.
+ pub fn chunks(&self) -> impl Iterator<Item = &Option<Arc<RwLock<Chunk>>>> {
+ self.chunks.iter()
+ }
+}
+
+impl Debug for PartialChunkStorage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("PartialChunkStorage")
+ .field("view_center", &self.view_center)
+ .field("chunk_radius", &self.chunk_radius)
+ .field("view_range", &self.view_range)
+ // .field("chunks", &self.chunks)
+ .field("chunks", &format_args!("{} items", self.chunks.len()))
+ .finish()
+ }
+}
+
+impl Default for PartialChunkStorage {
+ fn default() -> Self {
+ Self::new(8)
+ }
+}
+
+pub fn in_range_for_view_center_and_radius(
+ chunk_pos: &ChunkPos,
+ view_center: ChunkPos,
+ chunk_radius: u32,
+) -> bool {
+ (chunk_pos.x - view_center.x).unsigned_abs() <= chunk_radius
+ && (chunk_pos.z - view_center.z).unsigned_abs() <= chunk_radius
+}
+
+#[cfg(test)]
+mod tests {
+ use azalea_core::position::ChunkPos;
+
+ use crate::chunk::partial::PartialChunkStorage;
+
+ #[test]
+ fn test_chunk_pos_from_index() {
+ let mut partial_chunk_storage = PartialChunkStorage::new(5);
+ partial_chunk_storage.update_view_center(ChunkPos::new(0, -1));
+ assert_eq!(
+ partial_chunk_storage.chunk_pos_from_index(
+ partial_chunk_storage.index_from_chunk_pos(&ChunkPos::new(2, -1))
+ ),
+ ChunkPos::new(2, -1),
+ );
+ }
+}
diff --git a/azalea-world/src/chunk/storage.rs b/azalea-world/src/chunk/storage.rs
new file mode 100644
index 00000000..926c0b44
--- /dev/null
+++ b/azalea-world/src/chunk/storage.rs
@@ -0,0 +1,242 @@
+use std::{
+ any::Any,
+ collections::hash_map::Entry,
+ fmt::{self, Debug},
+ ops::{Deref, DerefMut},
+ sync::{Arc, Weak},
+};
+
+use azalea_block::{BlockState, fluid_state::FluidState};
+use azalea_core::position::{BlockPos, ChunkBiomePos, ChunkBlockPos, ChunkPos};
+use azalea_registry::data::Biome;
+use nohash_hasher::IntMap;
+use parking_lot::RwLock;
+
+use crate::Chunk;
+
+/// An abstract chunk storage backed by a [`ChunkStorageTrait`] implementation.
+///
+/// By default, this wraps a [`WeakChunkStorage`].
+pub struct ChunkStorage(pub Box<dyn ChunkStorageTrait>);
+
+pub trait ChunkStorageTrait: Send + Sync + Any {
+ /// Return the lowest y coordinate in the world, usually `-64`.
+ fn min_y(&self) -> i32;
+ /// Return the height of the world in blocks, usually `384`.
+ fn height(&self) -> u32;
+ /// Return a reference to the chunk from the storage.
+ #[must_use]
+ fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>>;
+ /// Insert the chunk into the storage and return a reference to it.
+ ///
+ /// Since the storage may be a [`WeakChunkStorage`], you must immediately
+ /// put the returned `Arc<RwLock<Chunk>>` somewhere (probably a
+ /// [`PartialChunkStorage`](crate::PartialChunkStorage)).
+ #[must_use]
+ fn upsert(&mut self, pos: ChunkPos, chunk: Chunk) -> Arc<RwLock<Chunk>>;
+ fn chunks(&self) -> Box<[&ChunkPos]>;
+ fn clone_box(&self) -> Box<dyn ChunkStorageTrait>;
+
+ // these impls are here instead of in the `impl ChunkStorage` so rust is able to
+ // inline more and thus optimize them a lot better.
+
+ /// Returns the [`BlockState`] at the given position.
+ ///
+ /// If the block is outside of the world, then `None` is returned.
+ fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
+ let chunk = self.get(&ChunkPos::from(pos))?;
+ let chunk = chunk.read();
+ chunk.get_block_state(&ChunkBlockPos::from(pos), self.min_y())
+ }
+ /// Set a [`BlockState`] at the given position.
+ ///
+ /// Returns the block that was previously there, or `None` if the position
+ /// is outside of the world.
+ fn set_block_state(&self, pos: BlockPos, state: BlockState) -> Option<BlockState> {
+ if pos.y < self.min_y() || pos.y >= (self.min_y() + self.height() as i32) {
+ return None;
+ }
+ let chunk = self.get(&ChunkPos::from(pos))?;
+ let mut chunk = chunk.write();
+ Some(chunk.get_and_set_block_state(&ChunkBlockPos::from(pos), state, self.min_y()))
+ }
+ fn get_fluid_state(&self, pos: BlockPos) -> Option<FluidState> {
+ let block_state = self.get_block_state(pos)?;
+ Some(FluidState::from(block_state))
+ }
+ fn get_biome(&self, pos: BlockPos) -> Option<Biome> {
+ let chunk = self.get(&ChunkPos::from(pos))?;
+ let chunk = chunk.read();
+ chunk.get_biome(ChunkBiomePos::from(pos), self.min_y())
+ }
+}
+impl ChunkStorage {
+ /// Create a storage backed by a [`WeakChunkStorage`] with the given world
+ /// dimensions.
+ pub fn new(height: u32, min_y: i32) -> Self {
+ Self(Box::new(WeakChunkStorage::new(height, min_y)))
+ }
+
+ /// Create a storage backed by a custom [`ChunkStorageTrait`]
+ /// implementation.
+ pub fn new_with(inner: Box<dyn ChunkStorageTrait>) -> Self {
+ Self(inner)
+ }
+}
+
+impl Deref for ChunkStorage {
+ type Target = dyn ChunkStorageTrait;
+
+ fn deref(&self) -> &Self::Target {
+ &*self.0
+ }
+}
+impl DerefMut for ChunkStorage {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut *self.0
+ }
+}
+impl Clone for ChunkStorage {
+ fn clone(&self) -> Self {
+ Self(self.0.clone_box())
+ }
+}
+
+/// A storage for chunks where they're only stored weakly, so if they're not
+/// actively being used somewhere else they'll be forgotten.
+///
+/// This is used for shared worlds.
+///
+/// This is relatively cheap to clone since it's just an `IntMap` with `Weak`
+/// pointers.
+#[derive(Clone, Debug)]
+pub struct WeakChunkStorage {
+ /// The height of the world.
+ ///
+ /// To get the maximum y position (exclusive), you have to combine this with
+ /// [`Self::min_y`].
+ pub height: u32,
+ /// The lowest y position in the world that can still have blocks placed on
+ /// it.
+ ///
+ /// This exists because in modern Minecraft versions, worlds can extend
+ /// below y=0.
+ pub min_y: i32,
+ pub map: IntMap<ChunkPos, Weak<RwLock<Chunk>>>,
+}
+
+impl WeakChunkStorage {
+ pub fn new(height: u32, min_y: i32) -> Self {
+ WeakChunkStorage {
+ height,
+ min_y,
+ map: IntMap::default(),
+ }
+ }
+}
+impl ChunkStorageTrait for WeakChunkStorage {
+ fn min_y(&self) -> i32 {
+ self.min_y
+ }
+ fn height(&self) -> u32 {
+ self.height
+ }
+ fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
+ self.map.get(pos).and_then(|chunk| chunk.upgrade())
+ }
+
+ fn upsert(&mut self, pos: ChunkPos, chunk: Chunk) -> Arc<RwLock<Chunk>> {
+ match self.map.entry(pos) {
+ Entry::Occupied(mut e) => {
+ if let Some(existing) = e.get_mut().upgrade() {
+ *existing.write() = chunk;
+ existing
+ } else {
+ let arc = Arc::new(RwLock::new(chunk));
+ e.insert(Arc::downgrade(&arc));
+ arc
+ }
+ }
+ Entry::Vacant(e) => {
+ let arc = Arc::new(RwLock::new(chunk));
+ e.insert(Arc::downgrade(&arc));
+ arc
+ }
+ }
+ }
+
+ fn chunks(&self) -> Box<[&ChunkPos]> {
+ self.map.keys().collect::<Vec<_>>().into_boxed_slice()
+ }
+
+ fn clone_box(&self) -> Box<dyn ChunkStorageTrait> {
+ Box::new(self.clone())
+ }
+}
+
+impl Default for WeakChunkStorage {
+ fn default() -> Self {
+ Self::new(384, -64)
+ }
+}
+impl Default for ChunkStorage {
+ fn default() -> Self {
+ Self::new(384, -64)
+ }
+}
+
+impl Debug for ChunkStorage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("ChunkStorage")
+ .field("min_y", &self.0.min_y())
+ .field("height", &self.0.height())
+ .field("chunk_count", &self.0.chunks().len())
+ .finish()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use azalea_core::position::{BlockPos, ChunkPos};
+
+ use crate::{
+ Chunk,
+ chunk::{partial::PartialChunkStorage, storage::ChunkStorage},
+ };
+
+ #[test]
+ fn test_out_of_bounds_y() {
+ let mut chunk_storage = ChunkStorage::default();
+ let mut partial_chunk_storage = PartialChunkStorage::default();
+ partial_chunk_storage.set(
+ &ChunkPos { x: 0, z: 0 },
+ Some(Chunk::default()),
+ &mut chunk_storage,
+ );
+ assert!(
+ chunk_storage
+ .get_block_state(BlockPos { x: 0, y: 319, z: 0 })
+ .is_some()
+ );
+ assert!(
+ chunk_storage
+ .get_block_state(BlockPos { x: 0, y: 320, z: 0 })
+ .is_none()
+ );
+ assert!(
+ chunk_storage
+ .get_block_state(BlockPos { x: 0, y: 338, z: 0 })
+ .is_none()
+ );
+ assert!(
+ chunk_storage
+ .get_block_state(BlockPos { x: 0, y: -64, z: 0 })
+ .is_some()
+ );
+ assert!(
+ chunk_storage
+ .get_block_state(BlockPos { x: 0, y: -65, z: 0 })
+ .is_none()
+ );
+ }
+}