diff --git a/examples/combat.rs b/examples/combat.rs index d1f418c..8fc59a6 100644 --- a/examples/combat.rs +++ b/examples/combat.rs @@ -3,6 +3,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use log::LevelFilter; use valence::block::{BlockPos, BlockState}; +use valence::chunk::{Chunk, UnloadedChunk}; use valence::client::{ default_client_event, ClientEvent, ClientId, GameMode, InteractWithEntityKind, }; @@ -86,13 +87,16 @@ impl Config for Game { let (_, world) = server.worlds.insert(DimensionId::default(), ()); server.state = Some(server.player_lists.insert(()).0); - let min_y = server.shared.dimension(DimensionId::default()).min_y; + let dim = server.shared.dimension(DimensionId::default()); + let min_y = dim.min_y; + let height = dim.height as usize; // Create circular arena. let size = 2; for chunk_z in -size - 2..size + 2 { for chunk_x in -size - 2..size + 2 { - let chunk = world.chunks.insert([chunk_x, chunk_z], ()); + let mut chunk = UnloadedChunk::new(height); + let r = -size..size; if r.contains(&chunk_x) && r.contains(&chunk_z) { for z in 0..16 { @@ -107,6 +111,8 @@ impl Config for Game { } } } + + world.chunks.insert([chunk_x, chunk_z], chunk, ()); } } diff --git a/examples/conway.rs b/examples/conway.rs index b200255..6585e26 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -7,6 +7,7 @@ use num::Integer; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use valence::biome::Biome; use valence::block::BlockState; +use valence::chunk::{Chunk, UnloadedChunk}; use valence::client::{default_client_event, ClientEvent, Hand}; use valence::config::{Config, ServerListPing}; use valence::dimension::{Dimension, DimensionId}; @@ -109,7 +110,11 @@ impl Config for Game { for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 { for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { - world.chunks.insert((chunk_x as i32, chunk_z as i32), ()); + world.chunks.insert( + [chunk_x as i32, chunk_z as i32], + UnloadedChunk::default(), + (), + ); } } } diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index 6e0f6be..a3c9527 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -5,6 +5,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use log::LevelFilter; use valence::async_trait; use valence::block::{BlockPos, BlockState}; +use valence::chunk::UnloadedChunk; use valence::client::{default_client_event, GameMode}; use valence::config::{Config, ServerListPing}; use valence::dimension::DimensionId; @@ -80,7 +81,7 @@ impl Config for Game { let size = 5; for z in -size..size { for x in -size..size { - world.chunks.insert([x, z], ()); + world.chunks.insert([x, z], UnloadedChunk::default(), ()); } } diff --git a/examples/raycast.rs b/examples/raycast.rs index 03fe9e3..8b42986 100644 --- a/examples/raycast.rs +++ b/examples/raycast.rs @@ -4,6 +4,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use log::LevelFilter; use valence::async_trait; use valence::block::{BlockPos, BlockState}; +use valence::chunk::UnloadedChunk; use valence::client::{default_client_event, GameMode}; use valence::config::{Config, ServerListPing}; use valence::dimension::DimensionId; @@ -78,7 +79,7 @@ impl Config for Game { let size = 5; for z in -size..size { for x in -size..size { - world.chunks.insert([x, z], ()); + world.chunks.insert([x, z], UnloadedChunk::default(), ()); } } diff --git a/examples/terrain.rs b/examples/terrain.rs index 1ce7458..8d109a1 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -6,7 +6,7 @@ use noise::{NoiseFn, Seedable, SuperSimplex}; use rayon::iter::ParallelIterator; use valence::async_trait; use valence::block::{BlockState, PropName, PropValue}; -use valence::chunk::ChunkPos; +use valence::chunk::{Chunk, ChunkPos, UnloadedChunk}; use valence::client::{default_client_event, GameMode}; use valence::config::{Config, ServerListPing}; use valence::dimension::DimensionId; @@ -151,7 +151,7 @@ impl Config for Game { if let Some(chunk) = world.chunks.get_mut(pos) { chunk.state = true; } else { - world.chunks.insert(pos, true); + world.chunks.insert(pos, UnloadedChunk::default(), true); } } diff --git a/src/chunk.rs b/src/chunk.rs index 731b827..16f1df2 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,4 +1,10 @@ //! Chunks and related types. +//! +//! A chunk is a 16x16-block segment of a world with a height determined by the +//! [`Dimension`](crate::dimension::Dimension) of the world. +//! +//! In addition to blocks, chunks also contain [biomes](crate::biome::Biome). +//! Every 4x4x4 segment of blocks in a chunk corresponds to a biome. // TODO: https://github.com/rust-lang/rust/issues/88581 for div_ceil @@ -23,9 +29,9 @@ use crate::protocol::packets::s2c::play::{ use crate::protocol::{Encode, NbtBridge, VarInt, VarLong}; use crate::server::SharedServer; -/// A container for all [`Chunk`]s in a [`World`](crate::world::World). +/// A container for all [`LoadedChunk`]s in a [`World`](crate::world::World). pub struct Chunks { - chunks: HashMap>, + chunks: HashMap>, shared: SharedServer, dimension: DimensionId, } @@ -39,36 +45,49 @@ impl Chunks { } } - /// Creates an empty chunk at the provided position and returns a mutable - /// reference to it. + /// Consumes an [`UnloadedChunk`] and creates a [`LoadedChunk`] at a given + /// position. An exclusive reference to the new chunk is returned. /// /// If a chunk at the position already exists, then the old chunk - /// is overwritten. + /// is overwritten and its contents are dropped. + /// + /// The given chunk is resized to match the height of the world as if by + /// calling [`UnloadedChunk::resize`]. /// /// **Note**: For the vanilla Minecraft client to see a chunk, all chunks - /// adjacent to it must also be loaded. It is also important that clients - /// are not spawned within unloaded chunks via - /// [`spawn`](crate::client::Client::spawn). - pub fn insert(&mut self, pos: impl Into, state: C::ChunkState) -> &mut Chunk { - let section_count = (self.shared.dimension(self.dimension).height / 16) as u32; + /// adjacent to it must also be loaded. Clients should not be spawned within + /// unloaded chunks via [`spawn`](crate::client::Client::spawn). + pub fn insert( + &mut self, + pos: impl Into, + chunk: UnloadedChunk, + state: C::ChunkState, + ) -> &mut LoadedChunk { + let dimension_section_count = (self.shared.dimension(self.dimension).height / 16) as usize; let biome_registry_len = self.shared.biomes().len(); - let chunk = Chunk::new(section_count, biome_registry_len, state); + let loaded = LoadedChunk::new(chunk, dimension_section_count, biome_registry_len, state); match self.chunks.entry(pos.into()) { Entry::Occupied(mut oe) => { - oe.insert(chunk); + oe.insert(loaded); oe.into_mut() } - Entry::Vacant(ve) => ve.insert(chunk), + Entry::Vacant(ve) => ve.insert(loaded), } } /// Removes a chunk at the provided position. /// - /// If a chunk exists at the position, then it is deleted and its - /// `ChunkState` is returned. Otherwise, `None` is returned. - pub fn remove(&mut self, pos: impl Into) -> Option { - self.chunks.remove(&pos.into()).map(|c| c.state) + /// If a chunk exists at the position, then it is removed from the world and + /// its content is returned. Otherwise, `None` is returned. + pub fn remove(&mut self, pos: impl Into) -> Option<(UnloadedChunk, C::ChunkState)> { + let loaded = self.chunks.remove(&pos.into())?; + + let unloaded = UnloadedChunk { + sections: loaded.sections.into(), + }; + + Some((unloaded, loaded.state)) } /// Returns the number of loaded chunks. @@ -84,21 +103,21 @@ impl Chunks { /// Gets a shared reference to the chunk at the provided position. /// /// If there is no chunk at the position, then `None` is returned. - pub fn get(&self, pos: impl Into) -> Option<&Chunk> { + pub fn get(&self, pos: impl Into) -> Option<&LoadedChunk> { self.chunks.get(&pos.into()) } /// Gets an exclusive reference to the chunk at the provided position. /// /// If there is no chunk at the position, then `None` is returned. - pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut Chunk> { + pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut LoadedChunk> { self.chunks.get_mut(&pos.into()) } /// Removes all chunks for which `f` returns `true`. /// /// All chunks are visited in an unspecified order. - pub fn retain(&mut self, mut f: impl FnMut(ChunkPos, &mut Chunk) -> bool) { + pub fn retain(&mut self, mut f: impl FnMut(ChunkPos, &mut LoadedChunk) -> bool) { self.chunks.retain(|&pos, chunk| f(pos, chunk)) } @@ -111,7 +130,8 @@ impl Chunks { /// order. pub fn iter( &self, - ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ { + ) -> impl ExactSizeIterator)> + FusedIterator + Clone + '_ + { self.chunks.iter().map(|(&pos, chunk)| (pos, chunk)) } @@ -119,23 +139,27 @@ impl Chunks { /// unspecified order. pub fn iter_mut( &mut self, - ) -> impl ExactSizeIterator)> + FusedIterator + '_ { + ) -> impl ExactSizeIterator)> + FusedIterator + '_ { self.chunks.iter_mut().map(|(&pos, chunk)| (pos, chunk)) } /// Returns a parallel iterator over all chunks in the world in an /// unspecified order. - pub fn par_iter(&self) -> impl ParallelIterator)> + Clone + '_ { + pub fn par_iter( + &self, + ) -> impl ParallelIterator)> + Clone + '_ { self.chunks.par_iter().map(|(&pos, chunk)| (pos, chunk)) } /// Returns a parallel mutable iterator over all chunks in the world in an /// unspecified order. - pub fn par_iter_mut(&mut self) -> impl ParallelIterator)> + '_ { + pub fn par_iter_mut( + &mut self, + ) -> impl ParallelIterator)> + '_ { self.chunks.par_iter_mut().map(|(&pos, chunk)| (pos, chunk)) } - /// Gets the block state at a position. + /// Gets the block state at a global position in the world. /// /// If the position is not inside of a chunk, then `None` is returned. /// @@ -162,10 +186,11 @@ impl Chunks { } } - /// Sets the block state at a position. + /// Sets the block state at a global position in the world. /// /// If the position is inside of a chunk, then `true` is returned. - /// Otherwise, `false` is returned. + /// Otherwise, `false` is returned. The function has no effect if `false` is + /// returned. /// /// Note: if you need to set a large number of blocks, it may be more /// efficient write to the chunks directly with @@ -215,57 +240,119 @@ impl Chunks { } } -/// A chunk is a 16x16-block segment of a world with a height determined by the -/// [`Dimension`](crate::dimension::Dimension) of the world. -/// -/// In addition to blocks, chunks also contain [biomes](crate::biome::Biome). -/// Every 4x4x4 segment of blocks in a chunk corresponds to a biome. -pub struct Chunk { - /// Custom state. - pub state: C::ChunkState, - sections: Box<[ChunkSection]>, - // TODO block_entities: HashMap, - /// The MOTION_BLOCKING heightmap - heightmap: Vec, - created_this_tick: bool, -} - -impl Chunk { - fn new(section_count: u32, biome_registry_len: usize, data: C::ChunkState) -> Self { - let sect = ChunkSection { - blocks: [BlockState::AIR.to_raw(); 4096], - modified_count: 1, // Must be >0 so the chunk is initialized. - biomes: [BiomeId::default(); 64], - compact_data: Vec::new(), - }; - - let mut chunk = Self { - state: data, - sections: vec![sect; section_count as usize].into(), - heightmap: Vec::new(), - created_this_tick: true, - }; - - chunk.apply_modifications(biome_registry_len); - chunk - } - - /// Returns `true` if this chunk was created during the current tick. - pub fn created_this_tick(&self) -> bool { - self.created_this_tick - } - - /// Returns the height of this chunk in blocks. - pub fn height(&self) -> usize { - self.sections.len() * 16 - } +/// Operations that can be performed on a chunk. [`LoadedChunk`] and +/// [`UnloadedChunk`] implement this trait. +pub trait Chunk { + /// Returns the height of this chunk in blocks. The result is always a + /// multiple of 16. + fn height(&self) -> usize; /// Gets the block state at the provided offsets in the chunk. /// /// # Panics /// /// Panics if the offsets are outside the bounds of the chunk. - pub fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState { + fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState; + + /// Sets the block state at the provided offsets in the chunk. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. + fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState); + + /// Gets the biome at the provided biome offsets in the chunk. + /// + /// Note: the arguments are **not** block positions. Biomes are 4x4x4 + /// segments of a chunk, so `x` and `z` are in `0..=4`. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. + fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId; + + /// Sets the biome at the provided biome offsets in the chunk. + /// + /// Note: the arguments are **not** block positions. Biomes are 4x4x4 + /// segments of a chunk, so `x` and `z` are in `0..=4`. + /// + /// # Panics + /// + /// Panics if the offsets are outside the bounds of the chunk. + fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId); +} + +/// A chunk that is not loaded in any world. +pub struct UnloadedChunk { + sections: Vec, + // TODO: block_entities: HashMap, +} + +impl UnloadedChunk { + /// Constructs a new unloaded chunk containing only [`BlockState::AIR`] with + /// the given height in blocks. + /// + /// # Panics + /// + /// Panics if the value of `height` does not meet the following criteria: + /// `height % 16 == 0 && height <= 4064`. + pub fn new(height: usize) -> Self { + let mut chunk = Self { + sections: Vec::new(), + }; + + chunk.resize(height); + chunk + } + + /// Changes the height of the chunk to `new_height`. This is a potentially + /// expensive operation that may involve copying. + /// + /// The chunk is extended and truncated from the top. New blocks are always + /// [`BlockState::AIR`]. + /// + /// # Panics + /// + /// The constraints on `new_height` are the same as [`Self::new`]. + pub fn resize(&mut self, new_height: usize) { + assert!( + new_height % 16 == 0 && new_height <= 4064, + "invalid chunk height of {new_height}" + ); + + let old_height = self.sections.len() * 16; + + if new_height > old_height { + let sect = ChunkSection { + blocks: [BlockState::AIR.to_raw(); 4096], + modified_count: 0, + biomes: [BiomeId::default(); 64], + compact_data: Vec::new(), + }; + + let additional = (new_height - old_height) / 16; + self.sections.reserve_exact(additional); + self.sections.resize_with(new_height / 16, || sect.clone()); + debug_assert_eq!(self.sections.capacity(), self.sections.len()); + } else if new_height < old_height { + self.sections.truncate(new_height / 16); + } + } +} + +/// Constructs a new chunk with height `0`. +impl Default for UnloadedChunk { + fn default() -> Self { + Self::new(0) + } +} + +impl Chunk for UnloadedChunk { + fn height(&self) -> usize { + self.sections.len() * 16 + } + + fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState { assert!( x < 16 && y < self.height() && z < 16, "chunk block offsets must be within bounds" @@ -276,41 +363,17 @@ impl Chunk { ) } - /// Sets the block state at the provided offsets in the chunk. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. - pub fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { + fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { assert!( x < 16 && y < self.height() && z < 16, "chunk block offsets must be within bounds" ); - let sect = &mut self.sections[y / 16]; - let idx = x + z * 16 + y % 16 * 16 * 16; - - if block.to_raw() != sect.blocks[idx] & BLOCK_STATE_MASK { - if sect.blocks[idx] & !BLOCK_STATE_MASK == 0 { - sect.modified_count += 1; - } - sect.blocks[idx] = block.to_raw() | !BLOCK_STATE_MASK; - - // TODO: if the block type was modified and the old block type - // could be a block entity, then the block entity at this - // position must be cleared. - } + self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] = block.to_raw(); + // TODO: handle block entity here? } - /// Gets the biome at the provided biome offsets in the chunk. - /// - /// Note: the arguments are **not** block positions. Biomes are 4x4x4 - /// segments of a chunk, so `x` and `z` are in `0..=4`. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. - pub fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId { + fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId { assert!( x < 4 && y < self.height() / 4 && z < 4, "chunk biome offsets must be within bounds" @@ -319,21 +382,79 @@ impl Chunk { self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] } - /// Sets the biome at the provided biome offsets in the chunk. - /// - /// Note: the arguments are **not** block positions. Biomes are 4x4x4 - /// segments of a chunk, so `x` and `z` are in `0..=4`. - /// - /// # Panics - /// - /// Panics if the offsets are outside the bounds of the chunk. - pub fn set_biome(&mut self, x: usize, y: usize, z: usize, b: BiomeId) { + fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) { assert!( x < 4 && y < self.height() / 4 && z < 4, "chunk biome offsets must be within bounds" ); - self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = b; + self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = biome; + } +} + +/// A chunk which is currently loaded in a world. +pub struct LoadedChunk { + /// Custom state. + pub state: C::ChunkState, + sections: Box<[ChunkSection]>, + // TODO block_entities: HashMap, + /// The MOTION_BLOCKING heightmap + heightmap: Vec, + created_this_tick: bool, +} + +/// A 16x16x16 section of blocks, biomes, and light in a chunk. +#[derive(Clone)] +struct ChunkSection { + /// The block states in this section stored in x, z, y order. + /// The most significant bit is used to indicate if this block has been + /// modified. + blocks: [u16; 4096], + /// The number of modified blocks + modified_count: u16, + biomes: [BiomeId; 64], + compact_data: Vec, +} + +const BLOCK_STATE_MASK: u16 = 0x7fff; + +const _: () = assert!( + BlockState::max_raw() <= BLOCK_STATE_MASK, + "There is not enough space in the block state type to store the modified bit. A bit array \ + separate from the block state array should be created to keep track of modified blocks in \ + the chunk section." +); + +impl LoadedChunk { + fn new( + mut chunk: UnloadedChunk, + dimension_section_count: usize, + biome_registry_len: usize, + state: C::ChunkState, + ) -> Self { + chunk.resize(dimension_section_count * 16); + + let mut sections = chunk.sections.into_boxed_slice(); + + // Mark all sections as modified so the chunk is properly initialized. + for sect in sections.iter_mut() { + sect.modified_count = 1; + } + + let mut loaded = Self { + state, + sections, + heightmap: Vec::new(), + created_this_tick: true, + }; + + loaded.apply_modifications(biome_registry_len); + loaded + } + + /// Returns `true` if this chunk was created during the current tick. + pub fn created_this_tick(&self) -> bool { + self.created_this_tick } /// Gets the chunk data packet for this chunk with the given position. This @@ -468,6 +589,60 @@ impl Chunk { } } +impl Chunk for LoadedChunk { + fn height(&self) -> usize { + self.sections.len() * 16 + } + + fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState { + assert!( + x < 16 && y < self.height() && z < 16, + "chunk block offsets must be within bounds" + ); + + BlockState::from_raw_unchecked( + self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK, + ) + } + + fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { + assert!( + x < 16 && y < self.height() && z < 16, + "chunk block offsets must be within bounds" + ); + + let sect = &mut self.sections[y / 16]; + let idx = x + z * 16 + y % 16 * 16 * 16; + + if block.to_raw() != sect.blocks[idx] & BLOCK_STATE_MASK { + if sect.blocks[idx] & !BLOCK_STATE_MASK == 0 { + sect.modified_count += 1; + } + sect.blocks[idx] = block.to_raw() | !BLOCK_STATE_MASK; + + // TODO: handle block entity here? + } + } + + fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId { + assert!( + x < 4 && y < self.height() / 4 && z < 4, + "chunk biome offsets must be within bounds" + ); + + self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] + } + + fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) { + assert!( + x < 4 && y < self.height() / 4 && z < 4, + "chunk biome offsets must be within bounds" + ); + + self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = biome; + } +} + #[derive(Clone, Debug)] pub(crate) enum BlockChangePacket { Single(BlockUpdate), @@ -483,28 +658,6 @@ impl From for S2cPlayPacket { } } -/// A 16x16x16 section of blocks, biomes, and light in a chunk. -#[derive(Clone)] -struct ChunkSection { - /// The block states in this section, stored in x, z, y order. - /// The most significant bit is used to indicate if this block has been - /// modified. - blocks: [u16; 4096], - /// The number of modified blocks - modified_count: u16, - biomes: [BiomeId; 64], - compact_data: Vec, -} - -const BLOCK_STATE_MASK: u16 = 0x7fff; - -const _: () = assert!( - BlockState::max_raw() <= BLOCK_STATE_MASK, - "There is not enough space in the block state type to store the modified bit. A bit array \ - separate from the block state array should be created to keep track of modified blocks in \ - the chunk section." -); - /// Builds the MOTION_BLOCKING heightmap. fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec) { let height = sections.len() * 16; diff --git a/src/config.rs b/src/config.rs index 82f79ff..6705321 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,7 +30,8 @@ pub trait Config: Sized + Send + Sync + UnwindSafe + RefUnwindSafe + 'static { type EntityState: Send + Sync; /// Custom state to store with every [`World`](crate::world::World). type WorldState: Send + Sync; - /// Custom state to store with every [`Chunk`](crate::chunk::Chunk). + /// Custom state to store with every + /// [`LoadedChunk`](crate::chunk::LoadedChunk). type ChunkState: Send + Sync; /// Custom state to store with every /// [`PlayerList`](crate::player_list::PlayerList). diff --git a/src/lib.rs b/src/lib.rs index a3e5419..0c1aae3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ //! //! In Valence, many types are owned by the library but given out as mutable //! references for the user to modify. Examples of such types include [`World`], -//! [`Chunk`], [`Entity`], and [`Client`]. +//! [`LoadedChunk`], [`Entity`], and [`Client`]. //! //! **You must not call [`mem::swap`] on these references (or any other //! function that would move their location in memory).** Doing so breaks @@ -57,7 +57,7 @@ //! [`Worlds`]: crate::world::Worlds //! [`World`]: crate::world::World //! [`Chunks`]: crate::chunk::Chunks -//! [`Chunk`]: crate::chunk::Chunk +//! [`LoadedChunk`]: crate::chunk::LoadedChunk //! [`Entity`]: crate::entity::Entity //! [`Client`]: crate::client::Client