Split Chunk into loaded and unloaded types (#37)

These changes will pave the way for nonblocking terrain generation in `terrain.rs`.
This commit is contained in:
Ryan Johnson 2022-09-08 19:33:24 -07:00 committed by GitHub
parent a4c8b282a5
commit 20546e2fb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 309 additions and 142 deletions

View file

@ -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, ());
}
}

View file

@ -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(),
(),
);
}
}
}

View file

@ -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(), ());
}
}

View file

@ -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(), ());
}
}

View file

@ -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);
}
}

View file

@ -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<C: Config> {
chunks: HashMap<ChunkPos, Chunk<C>>,
chunks: HashMap<ChunkPos, LoadedChunk<C>>,
shared: SharedServer<C>,
dimension: DimensionId,
}
@ -39,36 +45,49 @@ impl<C: Config> Chunks<C> {
}
}
/// 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<ChunkPos>, state: C::ChunkState) -> &mut Chunk<C> {
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<ChunkPos>,
chunk: UnloadedChunk,
state: C::ChunkState,
) -> &mut LoadedChunk<C> {
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<ChunkPos>) -> Option<C::ChunkState> {
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<ChunkPos>) -> 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<C: Config> Chunks<C> {
/// 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<ChunkPos>) -> Option<&Chunk<C>> {
pub fn get(&self, pos: impl Into<ChunkPos>) -> Option<&LoadedChunk<C>> {
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<ChunkPos>) -> Option<&mut Chunk<C>> {
pub fn get_mut(&mut self, pos: impl Into<ChunkPos>) -> Option<&mut LoadedChunk<C>> {
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<C>) -> bool) {
pub fn retain(&mut self, mut f: impl FnMut(ChunkPos, &mut LoadedChunk<C>) -> bool) {
self.chunks.retain(|&pos, chunk| f(pos, chunk))
}
@ -111,7 +130,8 @@ impl<C: Config> Chunks<C> {
/// order.
pub fn iter(
&self,
) -> impl ExactSizeIterator<Item = (ChunkPos, &Chunk<C>)> + FusedIterator + Clone + '_ {
) -> impl ExactSizeIterator<Item = (ChunkPos, &LoadedChunk<C>)> + FusedIterator + Clone + '_
{
self.chunks.iter().map(|(&pos, chunk)| (pos, chunk))
}
@ -119,23 +139,27 @@ impl<C: Config> Chunks<C> {
/// unspecified order.
pub fn iter_mut(
&mut self,
) -> impl ExactSizeIterator<Item = (ChunkPos, &mut Chunk<C>)> + FusedIterator + '_ {
) -> impl ExactSizeIterator<Item = (ChunkPos, &mut LoadedChunk<C>)> + 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<Item = (ChunkPos, &Chunk<C>)> + Clone + '_ {
pub fn par_iter(
&self,
) -> impl ParallelIterator<Item = (ChunkPos, &LoadedChunk<C>)> + 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<Item = (ChunkPos, &mut Chunk<C>)> + '_ {
pub fn par_iter_mut(
&mut self,
) -> impl ParallelIterator<Item = (ChunkPos, &mut LoadedChunk<C>)> + '_ {
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<C: Config> Chunks<C> {
}
}
/// 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<C: Config> Chunks<C> {
}
}
/// 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<C: Config> {
/// Custom state.
pub state: C::ChunkState,
sections: Box<[ChunkSection]>,
// TODO block_entities: HashMap<u32, BlockEntity>,
/// The MOTION_BLOCKING heightmap
heightmap: Vec<i64>,
created_this_tick: bool,
}
impl<C: Config> Chunk<C> {
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<ChunkSection>,
// TODO: block_entities: HashMap<u32, BlockEntity>,
}
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<C: Config> Chunk<C> {
)
}
/// 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<C: Config> Chunk<C> {
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<C: Config> {
/// Custom state.
pub state: C::ChunkState,
sections: Box<[ChunkSection]>,
// TODO block_entities: HashMap<u32, BlockEntity>,
/// The MOTION_BLOCKING heightmap
heightmap: Vec<i64>,
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<u8>,
}
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<C: Config> LoadedChunk<C> {
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<C: Config> Chunk<C> {
}
}
impl<C: Config> Chunk for LoadedChunk<C> {
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<BlockChangePacket> 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<u8>,
}
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<i64>) {
let height = sections.len() * 16;

View file

@ -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).

View file

@ -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