diff --git a/pumpkin-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 1356508f7..b97fb3e02 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -2,7 +2,10 @@ use crate::{bytebuf::ByteBufMut, codec::bit_set::BitSet, ClientPacket, VarInt}; use bytes::{BufMut, BytesMut}; use pumpkin_macros::client_packet; -use pumpkin_world::{chunk::ChunkData, DIRECT_PALETTE_BITS}; +use pumpkin_world::{ + chunk::{ChunkData, SUBCHUNKS_COUNT}, + DIRECT_PALETTE_BITS, +}; #[client_packet("play:level_chunk_with_light")] pub struct CChunkData<'a>(pub &'a ChunkData); @@ -14,19 +17,18 @@ impl ClientPacket for CChunkData<'_> { // Chunk Z buf.put_i32(self.0.position.z); - let heightmap_nbt = - pumpkin_nbt::serializer::to_bytes_unnamed(&self.0.blocks.heightmap).unwrap(); + let heightmap_nbt = pumpkin_nbt::serializer::to_bytes_unnamed(&self.0.heightmap).unwrap(); // Heightmaps buf.put_slice(&heightmap_nbt); let mut data_buf = BytesMut::new(); - self.0.blocks.iter_subchunks().for_each(|chunk| { - let block_count = chunk.len() as i16; + self.0.subchunks.array_iter().for_each(|subchunk| { + let block_count = subchunk.len() as i16; // Block count data_buf.put_i16(block_count); //// Block states - let palette = chunk; + let palette = &subchunk; // TODO: make dynamic block_size work // TODO: make direct block_size work enum PaletteType { @@ -58,11 +60,11 @@ impl ClientPacket for CChunkData<'_> { data_buf.put_var_int(&VarInt(*id as i32)); }); // Data array length - let data_array_len = chunk.len().div_ceil(64 / block_size as usize); + let data_array_len = subchunk.len().div_ceil(64 / block_size as usize); data_buf.put_var_int(&VarInt(data_array_len as i32)); data_buf.reserve(data_array_len * 8); - for block_clump in chunk.chunks(64 / block_size as usize) { + for block_clump in subchunk.chunks(64 / block_size as usize) { let mut out_long: i64 = 0; for block in block_clump.iter().rev() { let index = palette @@ -78,11 +80,11 @@ impl ClientPacket for CChunkData<'_> { // Bits per entry data_buf.put_u8(DIRECT_PALETTE_BITS as u8); // Data array length - let data_array_len = chunk.len().div_ceil(64 / DIRECT_PALETTE_BITS as usize); + let data_array_len = subchunk.len().div_ceil(64 / DIRECT_PALETTE_BITS as usize); data_buf.put_var_int(&VarInt(data_array_len as i32)); data_buf.reserve(data_array_len * 8); - for block_clump in chunk.chunks(64 / DIRECT_PALETTE_BITS as usize) { + for block_clump in subchunk.chunks(64 / DIRECT_PALETTE_BITS as usize) { let mut out_long: i64 = 0; let mut shift = 0; for block in block_clump { @@ -121,8 +123,8 @@ impl ClientPacket for CChunkData<'_> { // Empty Block Light Mask buf.put_bit_set(&BitSet(VarInt(1), vec![0])); - buf.put_var_int(&VarInt(self.0.blocks.subchunks_len() as i32)); - self.0.blocks.iter_subchunks().for_each(|chunk| { + buf.put_var_int(&VarInt(SUBCHUNKS_COUNT as i32)); + self.0.subchunks.array_iter().for_each(|chunk| { let mut chunk_light = [0u8; 2048]; for (i, _) in chunk.iter().enumerate() { // if !block .is_air() { diff --git a/pumpkin-world/src/chunk/anvil.rs b/pumpkin-world/src/chunk/anvil.rs index 54e2a20e3..926c65beb 100644 --- a/pumpkin-world/src/chunk/anvil.rs +++ b/pumpkin-world/src/chunk/anvil.rs @@ -357,7 +357,7 @@ impl AnvilChunkFormat { pub fn to_bytes(&self, chunk_data: &ChunkData) -> Result, ChunkSerializingError> { let mut sections = Vec::new(); - for (i, blocks) in chunk_data.blocks.iter_subchunks().enumerate() { + for (i, blocks) in chunk_data.subchunks.array_iter().enumerate() { // get unique blocks let unique_blocks: HashSet<_> = blocks.iter().collect(); @@ -384,7 +384,7 @@ impl AnvilChunkFormat { // Empty data if the palette only contains one index https://minecraft.fandom.com/wiki/Chunk_format // if palette.len() > 1 {} // TODO: Update to write empty data. Rn or read does not handle this elegantly - for block in blocks { + for block in blocks.iter() { // Push if next bit does not fit if bits_used_in_pack + block_bit_size as u32 > 64 { section_longs.push(current_pack_long); @@ -430,7 +430,7 @@ impl AnvilChunkFormat { x_pos: chunk_data.position.x, z_pos: chunk_data.position.z, status: super::ChunkStatus::Full, - heightmaps: chunk_data.blocks.heightmap.clone(), + heightmaps: chunk_data.heightmap.clone(), sections, }; @@ -545,10 +545,7 @@ mod tests { .iter() .find(|chunk| chunk.position == *at) .expect("Missing chunk"); - assert_eq!( - chunk.blocks.blocks, read_chunk.blocks.blocks, - "Chunks don't match" - ); + assert_eq!(chunk.subchunks, read_chunk.subchunks, "Chunks don't match"); } } diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 9c555b394..2281f57ba 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -1,8 +1,7 @@ use fastnbt::LongArray; use pumpkin_core::math::{ceil_log2, vector2::Vector2}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::ops::Index; +use std::{collections::HashMap, iter::repeat_with}; use thiserror::Error; use crate::{ @@ -14,9 +13,10 @@ use crate::{ pub mod anvil; -const CHUNK_AREA: usize = 16 * 16; -const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; -const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; +pub const CHUNK_AREA: usize = 16 * 16; +pub const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; +pub const SUBCHUNKS_COUNT: usize = WORLD_HEIGHT / 16; +pub const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; pub trait ChunkReader: Sync + Send { fn read_chunk( @@ -74,18 +74,44 @@ pub enum CompressionError { } pub struct ChunkData { - pub blocks: ChunkBlocks, + /// See description in `Subchunks` + pub subchunks: Subchunks, + /// See `https://minecraft.wiki/w/Heightmap` for more info + pub heightmap: ChunkHeightmaps, pub position: Vector2, } -pub struct ChunkBlocks { - // TODO make this a Vec that doesn't store the upper layers that only contain air +/// # Subchunks +/// Subchunks - its an areas in chunk, what are 16 blocks in height. +/// Current amouth is 24. +/// +/// Subchunks can be single and multi. +/// +/// Single means a single block in all chunk, like +/// chunk, what filled only air or only water. +/// +/// Multi means a normal chunk, what contains 24 subchunks. +#[derive(PartialEq, Debug)] +pub enum Subchunks { + Single(u16), + Multi(Box<[Subchunk; SUBCHUNKS_COUNT]>), +} + +/// # Subchunk +/// Subchunk - its an area in chunk, what are 16 blocks in height +/// +/// Subchunk can be single and multi. +/// +/// Single means a single block in all subchunk, like +/// subchunk, what filled only air or only water. +/// +/// Multi means a normal subchunk, what contains 4096 blocks. +#[derive(Clone, PartialEq, Debug)] +pub enum Subchunk { + Single(u16), // The packet relies on this ordering -> leave it like this for performance /// Ordering: yzx (y being the most significant) - blocks: Box<[u16; CHUNK_VOLUME]>, - - /// See `https://minecraft.wiki/w/Heightmap` for more info - pub heightmap: ChunkHeightmaps, + Multi(Box<[u16; SUBCHUNK_VOLUME]>), } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -174,42 +200,71 @@ impl Default for ChunkHeightmaps { } } -impl Default for ChunkBlocks { - fn default() -> Self { - Self { - blocks: Box::new([0; CHUNK_VOLUME]), - heightmap: ChunkHeightmaps::default(), +impl Subchunk { + /// Gets the given block in the chunk + pub fn get_block(&self, position: ChunkRelativeBlockCoordinates) -> Option { + match &self { + Self::Single(block) => Some(*block), + Self::Multi(blocks) => blocks.get(convert_index(position)).copied(), } } -} -impl ChunkBlocks { - pub const fn len(&self) -> usize { - self.blocks.len() + /// Sets the given block in the chunk, returning the old block + pub fn set_block(&mut self, position: ChunkRelativeBlockCoordinates, block_id: u16) { + // TODO @LUK_ESC? update the heightmap + self.set_block_no_heightmap_update(position, block_id) } - pub const fn is_empty(&self) -> bool { - self.blocks.is_empty() - } + /// 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( + &mut self, + position: ChunkRelativeBlockCoordinates, + new_block: u16, + ) { + match self { + Self::Single(block) => { + if *block != new_block { + let mut blocks = Box::new([*block; SUBCHUNK_VOLUME]); + blocks[convert_index(position)] = new_block; + + *self = Self::Multi(blocks) + } + } + Self::Multi(blocks) => { + blocks[convert_index(position)] = new_block; - pub const fn subchunks_len(&self) -> usize { - self.blocks.len().div_ceil(SUBCHUNK_VOLUME) + if blocks.iter().all(|b| *b == new_block) { + *self = Self::Single(new_block) + } + } + } } - pub fn empty_with_heightmap(heightmap: ChunkHeightmaps) -> Self { - Self { - blocks: Box::new([0; CHUNK_VOLUME]), - heightmap, + pub fn clone_as_array(&self) -> Box<[u16; SUBCHUNK_VOLUME]> { + match &self { + Self::Single(block) => Box::new([*block; SUBCHUNK_VOLUME]), + Self::Multi(blocks) => blocks.clone(), } } +} +impl Subchunks { /// Gets the given block in the chunk pub fn get_block(&self, position: ChunkRelativeBlockCoordinates) -> Option { - self.blocks.get(Self::convert_index(position)).copied() + match &self { + Self::Single(block) => Some(*block), + Self::Multi(subchunks) => subchunks + .get((position.y.get_absolute() / 16) as usize) + .and_then(|subchunk| subchunk.get_block(position)), + } } /// Sets the given block in the chunk, returning the old block - pub fn set_block(&mut self, position: ChunkRelativeBlockCoordinates, block_id: u16) -> u16 { + pub fn set_block(&mut self, position: ChunkRelativeBlockCoordinates, block_id: u16) { // TODO @LUK_ESC? update the heightmap self.set_block_no_heightmap_update(position, block_id) } @@ -222,20 +277,69 @@ impl ChunkBlocks { pub fn set_block_no_heightmap_update( &mut self, position: ChunkRelativeBlockCoordinates, - block: u16, - ) -> u16 { - std::mem::replace(&mut self.blocks[Self::convert_index(position)], block) + new_block: u16, + ) { + match self { + Self::Single(block) => { + if *block != new_block { + let mut subchunks = vec![Subchunk::Single(0); SUBCHUNKS_COUNT]; + + subchunks[(position.y.get_absolute() / 16) as usize] + .set_block(position, new_block); + + *self = Self::Multi(subchunks.try_into().unwrap()); + } + } + Self::Multi(subchunks) => { + subchunks[(position.y.get_absolute() / 16) as usize].set_block(position, new_block); + + if subchunks + .iter() + .all(|subchunk| *subchunk == Subchunk::Single(new_block)) + { + *self = Self::Single(new_block) + } + } + } } - pub fn iter_subchunks(&self) -> impl Iterator { - self.blocks - .chunks(SUBCHUNK_VOLUME) - .map(|subchunk| subchunk.try_into().unwrap()) + //TODO: Needs optimizations + pub fn array_iter(&self) -> Box> + '_> { + match self { + Self::Single(block) => { + Box::new(repeat_with(|| Box::new([*block; SUBCHUNK_VOLUME])).take(SUBCHUNKS_COUNT)) + } + Self::Multi(blocks) => { + Box::new(blocks.iter().map(|subchunk| subchunk.clone_as_array())) + } + } + } +} + +impl ChunkData { + /// Gets the given block in the chunk + pub fn get_block(&self, position: ChunkRelativeBlockCoordinates) -> Option { + self.subchunks.get_block(position) } - fn convert_index(index: ChunkRelativeBlockCoordinates) -> usize { - // % works for negative numbers as intended. - index.y.get_absolute() as usize * CHUNK_AREA + *index.z as usize * 16 + *index.x as usize + /// Sets the given block in the chunk, returning the old block + pub fn set_block(&mut self, position: ChunkRelativeBlockCoordinates, block_id: u16) { + // TODO @LUK_ESC? update the heightmap + self.subchunks.set_block(position, block_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( + &mut self, + position: ChunkRelativeBlockCoordinates, + block: u16, + ) { + self.subchunks + .set_block_no_heightmap_update(position, block); } #[expect(dead_code)] @@ -246,14 +350,6 @@ impl ChunkBlocks { } } -impl Index for ChunkBlocks { - type Output = u16; - - fn index(&self, index: ChunkRelativeBlockCoordinates) -> &Self::Output { - &self.blocks[Self::convert_index(index)] - } -} - // I can't use an tag because it will break ChunkNBT, but status need to have a big S, so "Status" #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] @@ -289,7 +385,7 @@ impl ChunkData { } // this needs to be boxed, otherwise it will cause a stack-overflow - let mut blocks = ChunkBlocks::empty_with_heightmap(chunk_data.heightmaps); + let mut subchunks = Subchunks::Single(0); let mut block_index = 0; // which block we're currently at for section in chunk_data.sections.into_iter() { @@ -336,7 +432,7 @@ impl ChunkData { // TODO allow indexing blocks directly so we can just use block_index and save some time? // this is fine because we initialized the heightmap of `blocks` // from the cached value in the world file - blocks.set_block_no_heightmap_update( + subchunks.set_block_no_heightmap_update( ChunkRelativeBlockCoordinates { z: ((block_index % CHUNK_AREA) / 16).into(), y: Height::from_absolute((block_index / CHUNK_AREA) as u16), @@ -356,7 +452,11 @@ impl ChunkData { } } - Ok(ChunkData { blocks, position }) + Ok(ChunkData { + subchunks, + heightmap: chunk_data.heightmaps, + position, + }) } } @@ -370,6 +470,10 @@ pub enum ChunkParsingError { ErrorDeserializingChunk(String), } +fn convert_index(index: ChunkRelativeBlockCoordinates) -> usize { + // % works for negative numbers as intended. + (index.y.get_absolute() % 16) as usize * CHUNK_AREA + *index.z as usize * 16 + *index.x as usize +} #[derive(Error, Debug)] pub enum ChunkSerializingError { #[error("Error serializing chunk: {0}")] diff --git a/pumpkin-world/src/generation/generator.rs b/pumpkin-world/src/generation/generator.rs index a570c8c33..754051a04 100644 --- a/pumpkin-world/src/generation/generator.rs +++ b/pumpkin-world/src/generation/generator.rs @@ -4,7 +4,7 @@ use pumpkin_core::math::vector3::Vector3; use crate::biome::Biome; use crate::block::block_state::BlockState; -use crate::chunk::{ChunkBlocks, ChunkData}; +use crate::chunk::{ChunkData, Subchunks}; use crate::coordinates::{BlockCoordinates, ChunkRelativeBlockCoordinates, XZBlockCoordinates}; use crate::generation::Seed; @@ -46,7 +46,7 @@ pub(crate) trait PerlinTerrainGenerator: Sync + Send { &self, coordinates: ChunkRelativeBlockCoordinates, at: BlockCoordinates, - blocks: &mut ChunkBlocks, + subchunks: &mut Subchunks, chunk_height: i16, biome: Biome, ); diff --git a/pumpkin-world/src/generation/generic_generator.rs b/pumpkin-world/src/generation/generic_generator.rs index de3f02cac..a6710e2b8 100644 --- a/pumpkin-world/src/generation/generic_generator.rs +++ b/pumpkin-world/src/generation/generic_generator.rs @@ -2,7 +2,7 @@ use noise::{NoiseFn, Perlin}; use pumpkin_core::math::vector2::Vector2; use crate::{ - chunk::{ChunkBlocks, ChunkData}, + chunk::{ChunkData, Subchunks}, coordinates::{ChunkRelativeBlockCoordinates, ChunkRelativeXZBlockCoordinates}, WORLD_LOWEST_Y, }; @@ -34,7 +34,7 @@ impl WorldGenerator for GenericGenerator { fn generate_chunk(&self, at: Vector2) -> ChunkData { - let mut blocks = ChunkBlocks::default(); + let mut subchunks = Subchunks::Single(0); self.terrain_generator.prepare_chunk(&at, &self.perlin); let noise_value = self.perlin.get([at.x as f64 / 16.0, at.z as f64 / 16.0]); @@ -64,7 +64,7 @@ impl WorldGenerator for GenericGen self.terrain_generator.generate_block( coordinates, coordinates.with_chunk_coordinates(at), - &mut blocks, + &mut subchunks, chunk_height, biome, ); @@ -73,7 +73,8 @@ impl WorldGenerator for GenericGen } ChunkData { - blocks, + subchunks, + heightmap: Default::default(), position: at, } } diff --git a/pumpkin-world/src/generation/implementation/overworld/biome/plains.rs b/pumpkin-world/src/generation/implementation/overworld/biome/plains.rs index 775d3e5f1..38618143d 100644 --- a/pumpkin-world/src/generation/implementation/overworld/biome/plains.rs +++ b/pumpkin-world/src/generation/implementation/overworld/biome/plains.rs @@ -5,7 +5,7 @@ use rand::Rng; use crate::{ biome::Biome, - chunk::ChunkBlocks, + chunk::Subchunks, coordinates::{BlockCoordinates, ChunkRelativeBlockCoordinates, XZBlockCoordinates}, generation::{ generator::{BiomeGenerator, GeneratorInit, PerlinTerrainGenerator}, @@ -46,7 +46,7 @@ impl PerlinTerrainGenerator for PlainsTerrainGenerator { &self, coordinates: ChunkRelativeBlockCoordinates, at: BlockCoordinates, - blocks: &mut ChunkBlocks, + subchunks: &mut Subchunks, chunk_height: i16, _: Biome, ) { @@ -55,13 +55,13 @@ impl PerlinTerrainGenerator for PlainsTerrainGenerator { let y = *at.y; if y == -64 { - blocks.set_block(coordinates, block_state!("bedrock").state_id); + subchunks.set_block(coordinates, block_state!("bedrock").state_id); } else if y >= -63 && y <= begin_stone_height { - blocks.set_block(coordinates, block_state!("stone").state_id); + subchunks.set_block(coordinates, block_state!("stone").state_id); } else if y >= begin_stone_height && y < begin_dirt_height { - blocks.set_block(coordinates, block_state!("dirt").state_id); + subchunks.set_block(coordinates, block_state!("dirt").state_id); } else if y == chunk_height - 2 { - blocks.set_block(coordinates, block_state!("grass_block").state_id); + subchunks.set_block(coordinates, block_state!("grass_block").state_id); } else if y == chunk_height - 1 { // TODO: generate flowers and grass let grass: u8 = rand::thread_rng().gen_range(0..7); @@ -70,24 +70,24 @@ impl PerlinTerrainGenerator for PlainsTerrainGenerator { if flower == 6 { match rand::thread_rng().gen_range(0..4) { 0 => { - blocks.set_block(coordinates, block_state!("dandelion").state_id); + subchunks.set_block(coordinates, block_state!("dandelion").state_id); } 1 => { - blocks.set_block(coordinates, block_state!("oxeye_daisy").state_id); + subchunks.set_block(coordinates, block_state!("oxeye_daisy").state_id); } 2 => { - blocks.set_block(coordinates, block_state!("cornflower").state_id); + subchunks.set_block(coordinates, block_state!("cornflower").state_id); } 3 => { - blocks.set_block(coordinates, block_state!("poppy").state_id); + subchunks.set_block(coordinates, block_state!("poppy").state_id); } _ => { - blocks.set_block(coordinates, block_state!("azure_bluet").state_id); + subchunks.set_block(coordinates, block_state!("azure_bluet").state_id); } } } else { // TODO: Tall grass, Tall grass data called `half`, There is `upper` and `lower` - blocks.set_block(coordinates, block_state!("short_grass").state_id); + subchunks.set_block(coordinates, block_state!("short_grass").state_id); } } } diff --git a/pumpkin-world/src/generation/implementation/test.rs b/pumpkin-world/src/generation/implementation/test.rs index 3a1f08f10..e94865459 100644 --- a/pumpkin-world/src/generation/implementation/test.rs +++ b/pumpkin-world/src/generation/implementation/test.rs @@ -10,7 +10,7 @@ use pumpkin_core::math::{vector2::Vector2, vector3::Vector3}; use crate::{ biome::Biome, block::block_state::BlockState, - chunk::{ChunkBlocks, ChunkData}, + chunk::{ChunkData, Subchunks}, coordinates::{ ChunkRelativeBlockCoordinates, ChunkRelativeXZBlockCoordinates, XZBlockCoordinates, }, @@ -40,7 +40,7 @@ impl Gen impl WorldGenerator for TestGenerator { fn generate_chunk(&self, at: Vector2) -> ChunkData { - let mut blocks = ChunkBlocks::default(); + let mut subchunks = Subchunks::Single(0); self.terrain_generator.prepare_chunk(&at); for x in 0..16u8 { @@ -68,14 +68,15 @@ impl WorldGenerator for TestGenerator = chunk.read().await; - let Some(id) = chunk.blocks.get_block(relative) else { + let Some(id) = chunk.subchunks.get_block(relative) else { return Err(GetBlockError::BlockOutOfWorldBounds); };