// SPDX-License-Identifier: AGPL-3.0-only //! Core data structures representing the voxel world. use bytemuck::{Pod, Zeroable}; use serde::{Deserialize, Serialize}; /// The size of a chunk along one axis in blocks. pub const CHUNK_SIZE: usize = 32; /// The total number of blocks within a single chunk. pub const CHUNK_VOLUME: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; /// A unique identifier representing a type of block in the world. #[repr(transparent)] #[derive( Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Pod, Zeroable, )] pub struct BlockId(pub u16); impl BlockId { /// The block identifier representing empty space. pub const AIR: BlockId = BlockId(0); } /// A spatial volume containing voxel data. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Chunk { /// The array of blocks contained within the chunk. pub blocks: Box<[BlockId]>, } impl Chunk { /// Calculates the one-dimensional index for a three-dimensional block coordinate. #[inline] #[must_use] pub const fn index(x: usize, y: usize, z: usize) -> usize { (x * CHUNK_SIZE * CHUNK_SIZE) + (z * CHUNK_SIZE) + y } /// Retrieves the block identifier at the specified coordinates. #[must_use] pub fn get(&self, x: usize, y: usize, z: usize) -> BlockId { self.blocks[Self::index(x, y, z)] } /// Sets the block identifier at the specified coordinates. pub fn set(&mut self, x: usize, y: usize, z: usize, block: BlockId) { self.blocks[Self::index(x, y, z)] = block; } } impl Default for Chunk { fn default() -> Self { Self { blocks: vec![BlockId::AIR; CHUNK_VOLUME].into_boxed_slice(), } } } /// The three-dimensional spatial coordinates of a chunk in the world. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ChunkPos { /// The X coordinate of the chunk. pub x: i32, /// The Y coordinate of the chunk. pub y: i32, /// The Z coordinate of the chunk. pub z: i32, } impl ChunkPos { /// Initializes a new chunk position. #[must_use] pub fn new(x: i32, y: i32, z: i32) -> Self { Self { x, y, z } } /// Initializes a new chunk position from a world-space position measured in blocks. #[must_use] #[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] pub fn from_world(x: f64, y: f64, z: f64) -> Self { ChunkPos { x: (x.floor() as i32).div_euclid(CHUNK_SIZE as i32), y: (y.floor() as i32).div_euclid(CHUNK_SIZE as i32), z: (z.floor() as i32).div_euclid(CHUNK_SIZE as i32), } } } #[cfg(test)] mod tests { use super::ChunkPos; #[test] fn from_world_maps_positive_positions() { // A block at 40 falls in chunk 1 (chunk 1 spans blocks 32..=63). assert_eq!(ChunkPos::from_world(40.0, 0.0, 0.0).x, 1); // The last block of chunk 0 (block 31) stays in chunk 0. assert_eq!(ChunkPos::from_world(31.0, 0.0, 0.0).x, 0); } #[test] fn from_world_floors_negative_positions() { // Block -1 belongs to chunk -1, not chunk 0: this is the div_euclid contract. assert_eq!(ChunkPos::from_world(-1.0, 0.0, 0.0).x, -1); // Block -33 belongs to chunk -2 (chunk -2 spans blocks -64..=-33). assert_eq!(ChunkPos::from_world(-33.0, 0.0, 0.0).x, -2); } #[test] fn from_world_floors_fractional_positions() { // A position of -0.5 lies inside block -1, which is in chunk -1. assert_eq!(ChunkPos::from_world(-0.5, 0.0, 0.0).x, -1); } }