Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider using Subchunks instead of ChunkBlocks #458

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
26 changes: 14 additions & 12 deletions pumpkin-protocol/src/client/play/c_chunk_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
205 changes: 153 additions & 52 deletions pumpkin-world/src/chunk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use pumpkin_core::math::vector2::Vector2;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::HashMap;
use std::ops::Index;
use thiserror::Error;

use crate::{
Expand All @@ -15,9 +14,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(
Expand Down Expand Up @@ -54,18 +54,43 @@ 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<i32>,
}
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.
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(Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -151,42 +176,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<u16> {
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<u16> {
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)
}
Expand All @@ -199,20 +253,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)
}
}
}
}

//TODO: Needs optimizations
suprohub marked this conversation as resolved.
Show resolved Hide resolved
pub fn array_iter(&self) -> Box<dyn Iterator<Item = Box<[u16; SUBCHUNK_VOLUME]>> + '_> {
match self {
Self::Single(block) => {
Box::new(vec![Box::new([*block; SUBCHUNK_VOLUME]); SUBCHUNKS_COUNT].into_iter())
}
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<u16> {
self.subchunks.get_block(position)
}

pub fn iter_subchunks(&self) -> impl Iterator<Item = &[u16; SUBCHUNK_VOLUME]> {
self.blocks
.chunks(SUBCHUNK_VOLUME)
.map(|subchunk| subchunk.try_into().unwrap())
/// 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);
}

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
/// 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)]
Expand All @@ -223,14 +326,6 @@ impl ChunkBlocks {
}
}

impl Index<ChunkRelativeBlockCoordinates> for ChunkBlocks {
type Output = u16;

fn index(&self, index: ChunkRelativeBlockCoordinates) -> &Self::Output {
&self.blocks[Self::convert_index(index)]
}
}

impl ChunkData {
pub fn from_bytes(chunk_data: &[u8], at: Vector2<i32>) -> Result<Self, ChunkParsingError> {
if fastnbt::from_bytes::<ChunkStatus>(chunk_data)
Expand All @@ -244,7 +339,7 @@ impl ChunkData {
.map_err(|e| ChunkParsingError::ErrorDeserializingChunk(e.to_string()))?;

// 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() {
Expand Down Expand Up @@ -290,7 +385,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),
Expand All @@ -311,7 +406,8 @@ impl ChunkData {
}

Ok(ChunkData {
blocks,
subchunks,
heightmap: chunk_data.heightmaps,
position: at,
})
}
Expand All @@ -326,3 +422,8 @@ pub enum ChunkParsingError {
#[error("Error deserializing chunk: {0}")]
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
}
4 changes: 2 additions & 2 deletions pumpkin-world/src/generation/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
);
Expand Down
Loading
Loading