From be98a730b001a57c0fbc272953f169a63200709d Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sat, 24 Aug 2024 22:28:46 +0200 Subject: [PATCH 01/23] Added abstraction for coordinates. These ensure that there aren't any out of bound values and make the code significantly more readable --- .../src/client/play/c_chunk_data.rs | 4 +- pumpkin-world/src/chunk.rs | 29 ++-- pumpkin-world/src/coordinates.rs | 133 ++++++++++++++++++ pumpkin-world/src/level.rs | 27 ++-- pumpkin-world/src/lib.rs | 1 + pumpkin-world/src/radial_chunk_iterator.rs | 16 ++- pumpkin/src/server.rs | 2 +- 7 files changed, 172 insertions(+), 40 deletions(-) create mode 100644 pumpkin-world/src/coordinates.rs diff --git a/pumpkin-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 484be4fd6..076a3f1a5 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -11,9 +11,9 @@ pub struct CChunkData<'a>(pub &'a ChunkData); impl<'a> ClientPacket for CChunkData<'a> { fn write(&self, buf: &mut crate::bytebuf::ByteBuffer) { // Chunk X - buf.put_i32(self.0.position.0); + buf.put_i32(self.0.position.x); // Chunk Z - buf.put_i32(self.0.position.1); + buf.put_i32(self.0.position.z); let heightmap_nbt = fastnbt::to_bytes_with_opts(&self.0.heightmaps, fastnbt::SerOpts::network_nbt()) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index c3fe94ff7..6aae014d0 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -2,11 +2,15 @@ use std::collections::HashMap; use fastnbt::LongArray; -use crate::{level::WorldError, vector3::Vector3, WORLD_HEIGHT, WORLD_Y_START_AT}; +use crate::{ + coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates}, + level::WorldError, + WORLD_HEIGHT, +}; pub struct ChunkData { pub blocks: Box<[i32; 16 * 16 * WORLD_HEIGHT]>, - pub position: (i32, i32), + pub position: ChunkCoordinates, pub heightmaps: ChunkHeightmaps, } @@ -49,7 +53,7 @@ struct ChunkNbt { } impl ChunkData { - pub fn from_bytes(chunk_data: Vec, at: (i32, i32)) -> Result { + pub fn from_bytes(chunk_data: Vec, at: ChunkCoordinates) -> Result { let chunk_data = match fastnbt::from_bytes::(chunk_data.as_slice()) { Ok(v) => v, Err(err) => return Err(WorldError::ErrorDeserializingChunk(err.to_string())), @@ -111,19 +115,14 @@ impl ChunkData { }) } /// Sets the given block in the chunk, returning the old block - pub fn set_block(&mut self, at: Vector3, block_id: i32) -> Result { - let x = at.x - self.position.0 * 16; - let z = at.z - self.position.1 * 16; - let y = at.y - WORLD_Y_START_AT; - if !(0..16).contains(&x) - || !(0..16).contains(&z) - || !(0..(WORLD_HEIGHT as i32)).contains(&y) - { - return Err(WorldError::BlockOutsideChunk); - } - + pub fn set_block( + &mut self, + at: ChunkRelativeBlockCoordinates, + block_id: i32, + ) -> Result { Ok(std::mem::replace( - &mut self.blocks[(y * 16 * 16 + z * 16 + x) as usize], + &mut self.blocks + [(at.y.get_absolute() * 16 * 16 + *at.z as u16 * 16 + *at.x as u16) as usize], block_id, )) } diff --git a/pumpkin-world/src/coordinates.rs b/pumpkin-world/src/coordinates.rs new file mode 100644 index 000000000..eea7430a9 --- /dev/null +++ b/pumpkin-world/src/coordinates.rs @@ -0,0 +1,133 @@ +use std::ops::Deref; + +use crate::{WORLD_HEIGHT, WORLD_Y_START_AT}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Height { + height: i16, +} + +impl Height { + pub fn new(height: i16) -> Self { + assert!(height <= (WORLD_HEIGHT as i16 - WORLD_Y_START_AT.abs() as i16)); + assert!(height >= WORLD_Y_START_AT as i16); + + Self { height } + } + + pub fn from_absolute(height: u16) -> Self { + Self::new(height as i16 - WORLD_Y_START_AT.abs() as i16) + } + + /// Absolute height ranges from `0..WORLD_HEIGHT` + /// instead of `WORLD_Y_START_AT..(WORLD_HEIGHT-WORLD_Y_START_AT)` + pub fn get_absolute(&self) -> u16 { + (self.height + WORLD_Y_START_AT.abs() as i16) as u16 + } +} + +impl Deref for Height { + type Target = i16; + + fn deref(&self) -> &Self::Target { + &self.height + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkRelativeScalar { + scalar: u8, +} + +macro_rules! derive_chunk_relative_scalar_from_int_impl { + ($integer:ty) => { + impl From<$integer> for ChunkRelativeScalar { + fn from(scalar: $integer) -> Self { + assert!(scalar < 16); + Self { + scalar: scalar as u8, + } + } + } + }; +} + +derive_chunk_relative_scalar_from_int_impl! {u8} +derive_chunk_relative_scalar_from_int_impl! {u16} +derive_chunk_relative_scalar_from_int_impl! {u32} +derive_chunk_relative_scalar_from_int_impl! {u64} +derive_chunk_relative_scalar_from_int_impl! {u128} +derive_chunk_relative_scalar_from_int_impl! {usize} + +impl Deref for ChunkRelativeScalar { + type Target = u8; + + fn deref(&self) -> &Self::Target { + &self.scalar + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlockCoordinates { + pub x: i32, + pub y: Height, + pub z: i32, +} + +/// BlockCoordinates that do not specify a height. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct XZBlockCoordinates { + pub x: i32, + pub z: i32, +} + +/// Coordinates of a block relative to a chunk +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkRelativeBlockCoordinates { + pub x: ChunkRelativeScalar, + pub y: Height, + pub z: ChunkRelativeScalar, +} + +impl ChunkRelativeBlockCoordinates { + pub fn with_chunk_coordinates(&self, chunk_coordinates: ChunkCoordinates) -> BlockCoordinates { + BlockCoordinates { + x: *self.x as i32 + chunk_coordinates.x * 16, + y: self.y, + z: *self.z as i32 + chunk_coordinates.z * 16, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkRelativeXZBlockCoordinates { + pub x: ChunkRelativeScalar, + pub z: ChunkRelativeScalar, +} + +impl ChunkRelativeXZBlockCoordinates { + pub fn with_chunk_coordinates( + &self, + chunk_coordinates: ChunkCoordinates, + ) -> XZBlockCoordinates { + XZBlockCoordinates { + x: *self.x as i32 + chunk_coordinates.x * 16, + z: *self.z as i32 + chunk_coordinates.z * 16, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkCoordinates { + pub x: i32, + pub z: i32, +} + +macro_rules! impl_get_absolute_height { + ($struct_name:ident) => { + impl $struct_name {} + }; +} + +impl_get_absolute_height! {BlockCoordinates} +impl_get_absolute_height! {ChunkRelativeBlockCoordinates} diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 3ccff077d..210ed47e1 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -10,7 +10,7 @@ use rayon::prelude::*; use thiserror::Error; use tokio::sync::mpsc; -use crate::chunk::ChunkData; +use crate::{chunk::ChunkData, coordinates::ChunkCoordinates}; #[allow(dead_code)] /// The Level represents a @@ -102,15 +102,15 @@ impl Level { /// Note: The order of the output chunks will almost never be in the same order as the order of input chunks pub async fn read_chunks( &self, - chunks: Vec<(i32, i32)>, - channel: mpsc::Sender<((i32, i32), Result)>, + chunks: Vec, + channel: mpsc::Sender<(ChunkCoordinates, Result)>, ) { chunks .into_par_iter() .map(|chunk| { let region = ( - ((chunk.0 as f32) / 32.0).floor() as i32, - ((chunk.1 as f32) / 32.0).floor() as i32, + ((chunk.x as f32) / 32.0).floor() as i32, + ((chunk.z as f32) / 32.0).floor() as i32, ); let channel = channel.clone(); @@ -157,8 +157,8 @@ impl Level { } let modulus = |a: i32, b: i32| ((a % b) + b) % b; - let chunk_x = modulus(chunk.0, 32) as u32; - let chunk_z = modulus(chunk.1, 32) as u32; + let chunk_x = modulus(chunk.x, 32) as u32; + let chunk_z = modulus(chunk.z, 32) as u32; let channel = channel.clone(); let table_entry = (chunk_x + chunk_z * 32) * 4; @@ -170,23 +170,20 @@ impl Level { let size = location_table[table_entry as usize + 3] as usize * 4096; if offset == 0 && size == 0 { - let _ = - channel.blocking_send(((chunk.0, chunk.1), Err(WorldError::ChunkNotFound))); + let _ = channel.blocking_send((chunk, Err(WorldError::ChunkNotFound))); return; } // Read the file using the offset and size let mut file_buf = { let seek_result = region_file.seek(std::io::SeekFrom::Start(offset)); if seek_result.is_err() { - let _ = channel - .blocking_send(((chunk.0, chunk.1), Err(WorldError::RegionIsInvalid))); + let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); return; } let mut out = vec![0; size]; let read_result = region_file.read_exact(&mut out); if read_result.is_err() { - let _ = channel - .blocking_send(((chunk.0, chunk.1), Err(WorldError::RegionIsInvalid))); + let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); return; } out @@ -199,7 +196,7 @@ impl Level { Some(c) => c, None => { let _ = channel.blocking_send(( - (chunk.0, chunk.1), + chunk, Err(WorldError::Compression( CompressionError::UnknownCompression, )), @@ -216,7 +213,7 @@ impl Level { Ok(data) => data, Err(e) => { channel - .blocking_send(((chunk.0, chunk.1), Err(WorldError::Compression(e)))) + .blocking_send((chunk, Err(WorldError::Compression(e)))) .expect("Failed to send Compression error"); return; } diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 143942f5e..4bd5c2cc0 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -1,6 +1,7 @@ use level::Level; pub mod chunk; +pub mod coordinates; pub mod dimension; pub const WORLD_HEIGHT: usize = 384; pub const WORLD_Y_START_AT: i32 = -64; diff --git a/pumpkin-world/src/radial_chunk_iterator.rs b/pumpkin-world/src/radial_chunk_iterator.rs index e5ada80d2..5eccb4698 100644 --- a/pumpkin-world/src/radial_chunk_iterator.rs +++ b/pumpkin-world/src/radial_chunk_iterator.rs @@ -1,7 +1,9 @@ +use crate::coordinates::ChunkCoordinates; + pub struct RadialIterator { radius: u32, direction: usize, - current: (i32, i32), + current: ChunkCoordinates, step_size: i32, steps_taken: u32, steps_in_direction: i32, @@ -12,7 +14,7 @@ impl RadialIterator { RadialIterator { radius, direction: 0, - current: (0, 0), + current: ChunkCoordinates { x: 0, z: 0 }, step_size: 1, steps_taken: 0, steps_in_direction: 0, @@ -21,7 +23,7 @@ impl RadialIterator { } impl Iterator for RadialIterator { - type Item = (i32, i32); + type Item = ChunkCoordinates; fn next(&mut self) -> Option { if self.steps_taken >= self.radius * self.radius * 4 { @@ -34,10 +36,10 @@ impl Iterator for RadialIterator { // Move in the current direction match self.direction { - 0 => self.current.0 += 1, // Right - 1 => self.current.1 += 1, // Up - 2 => self.current.0 -= 1, // Left - 3 => self.current.1 -= 1, // Down + 0 => self.current.x += 1, // East + 1 => self.current.z += 1, // North + 2 => self.current.x -= 1, // West + 3 => self.current.z -= 1, // South _ => {} } diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index 76ab4029b..7bd254c26 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -374,7 +374,7 @@ impl Server { Err(_) => continue, }; #[cfg(debug_assertions)] - if _chunk_pos == (0, 0) { + if _chunk_pos == (pumpkin_world::coordinates::ChunkCoordinates { x: 0, z: 0 }) { use pumpkin_protocol::bytebuf::ByteBuffer; let mut test = ByteBuffer::empty(); CChunkData(&chunk_data).write(&mut test); From a0260acd8b3930cc6b0a6333f5b92b200a398c8d Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sat, 24 Aug 2024 22:57:42 +0200 Subject: [PATCH 02/23] `WORLD_Y_START_AT` refactor Renamed `WORLD_Y_START_AT` to `WORLD_LOWEST_Y`. Added `WORLD_Y_END_AT`. Changed the type to i16 for ergonomics. Moved the constants under the use statements for readability. --- pumpkin-world/src/coordinates.rs | 12 ++++++------ pumpkin-world/src/lib.rs | 10 ++++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pumpkin-world/src/coordinates.rs b/pumpkin-world/src/coordinates.rs index eea7430a9..0873d0e80 100644 --- a/pumpkin-world/src/coordinates.rs +++ b/pumpkin-world/src/coordinates.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use crate::{WORLD_HEIGHT, WORLD_Y_START_AT}; +use crate::{WORLD_LOWEST_Y, WORLD_MAX_Y}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Height { @@ -9,20 +9,20 @@ pub struct Height { impl Height { pub fn new(height: i16) -> Self { - assert!(height <= (WORLD_HEIGHT as i16 - WORLD_Y_START_AT.abs() as i16)); - assert!(height >= WORLD_Y_START_AT as i16); + assert!(height <= WORLD_MAX_Y); + assert!(height >= WORLD_LOWEST_Y); Self { height } } pub fn from_absolute(height: u16) -> Self { - Self::new(height as i16 - WORLD_Y_START_AT.abs() as i16) + Self::new(height as i16 - WORLD_LOWEST_Y.abs()) } /// Absolute height ranges from `0..WORLD_HEIGHT` - /// instead of `WORLD_Y_START_AT..(WORLD_HEIGHT-WORLD_Y_START_AT)` + /// instead of `WORLD_LOWEST_Y..WORLD_MAX_Y` pub fn get_absolute(&self) -> u16 { - (self.height + WORLD_Y_START_AT.abs() as i16) as u16 + (self.height + WORLD_LOWEST_Y.abs()) as u16 } } diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 4bd5c2cc0..b204eef2b 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -1,18 +1,20 @@ use level::Level; +pub mod block; pub mod chunk; pub mod coordinates; pub mod dimension; -pub const WORLD_HEIGHT: usize = 384; -pub const WORLD_Y_START_AT: i32 = -64; -pub const DIRECT_PALETTE_BITS: u32 = 15; -pub mod block; pub mod global_registry; pub mod item; mod level; pub mod radial_chunk_iterator; pub mod vector3; +pub const WORLD_HEIGHT: usize = 384; +pub const WORLD_LOWEST_Y: i16 = -64; +pub const WORLD_MAX_Y: i16 = WORLD_HEIGHT as i16 - WORLD_LOWEST_Y.abs(); +pub const DIRECT_PALETTE_BITS: u32 = 15; + pub struct World { pub level: Level, // entities, players... From 03fcdc2e32367f6fb89ddda3cb17f98f15bf1b7f Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sat, 24 Aug 2024 23:11:13 +0200 Subject: [PATCH 03/23] Imported serde::Deserialize to avoid spelling it out frequently. --- pumpkin-world/src/block/block_registry.rs | 7 ++++--- pumpkin-world/src/chunk.rs | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index fa2b790b6..1a36df8d5 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -1,26 +1,27 @@ use std::collections::HashMap; use lazy_static::lazy_static; +use serde::Deserialize; use crate::level::WorldError; const BLOCKS_JSON: &str = include_str!("../../assets/blocks.json"); -#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlockDefinition { #[serde(rename = "type")] kind: String, block_set_type: Option, } -#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlockState { default: Option, id: i64, properties: Option>, } -#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlocksElement { definition: BlockDefinition, properties: Option>>, diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index 6aae014d0..c8c0a2adc 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use fastnbt::LongArray; +use serde::{Deserialize, Serialize}; use crate::{ coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates}, @@ -14,27 +15,27 @@ pub struct ChunkData { pub heightmaps: ChunkHeightmaps, } -#[derive(serde::Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "PascalCase")] struct PaletteEntry { name: String, properties: Option>, } -#[derive(serde::Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] struct ChunkSectionBlockStates { data: Option, palette: Vec, } -#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "UPPERCASE")] pub struct ChunkHeightmaps { motion_blocking: LongArray, world_surface: LongArray, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Debug)] #[allow(dead_code)] struct ChunkSection { #[serde(rename = "Y")] @@ -42,7 +43,7 @@ struct ChunkSection { block_states: Option, } -#[derive(serde::Deserialize, Debug)] +#[derive(Deserialize, Debug)] #[allow(dead_code)] struct ChunkNbt { #[serde(rename = "DataVersion")] From 5a21a687d81014a61c0ff82c9ab08ced296e30b7 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sat, 24 Aug 2024 23:34:47 +0200 Subject: [PATCH 04/23] Block structs refractor + added comments renamed a few fields for clarity. added examples for fields that might not immediately be obvious. removed some `Option`s that are not necessary. renamed `BlockElement` to `BlockType` because you could mistake `BlockElement` for the main way to represent a Block (e.g. in Chunks). --- pumpkin-world/src/block/block_registry.rs | 37 +++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index 1a36df8d5..0d79d6d03 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -9,27 +9,46 @@ const BLOCKS_JSON: &str = include_str!("../../assets/blocks.json"); #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlockDefinition { + /// e.g. minecraft:door or minecraft:button #[serde(rename = "type")] - kind: String, - block_set_type: Option, + category: String, + + /// Specifies the variant of the blocks category. + /// e.g. minecraft:iron_door has the variant iron + #[serde(rename = "block_set_type")] + variant: Option, } +/// One possible state of a Block. +/// This could e.g. be an extended piston facing left. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlockState { - default: Option, id: i64, - properties: Option>, + + /// Whether this is the default state of the Block + #[serde(default, rename = "default")] + is_default: bool, + + /// The propertise active for this `BlockState`. + #[serde(default)] + properties: HashMap, } +/// A fully-fledged block definition. +/// Stores the category, variant, all of the possible states and all of the possible properties. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlocksElement { +pub struct BlockType { definition: BlockDefinition, - properties: Option>>, states: Vec, + + // TODO is this safe to remove? It's currently not used in the Project. @lukas0008 @Snowiiii + /// A list of valid property keys/values for a block. + #[serde(default, rename = "properties")] + valid_properties: HashMap>, } lazy_static! { - pub static ref BLOCKS: HashMap = + pub static ref BLOCKS: HashMap = serde_json::from_str(BLOCKS_JSON).expect("Could not parse block.json registry."); } @@ -45,13 +64,13 @@ pub fn block_id_and_properties_to_block_state_id( None => Ok(block .states .iter() - .find(|state| state.default.unwrap_or(false)) + .find(|state| state.is_default) .expect("Each block should have at least one default state") .id), Some(properties) => block .states .iter() - .find(|state| state.properties.as_ref() == Some(properties)) + .find(|state| &state.properties == properties) .map(|state| state.id) .ok_or(WorldError::BlockStateIdNotFound), }; From 43164ee4f12daad0a8ec77202105361e353d5c90 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 09:04:15 +0200 Subject: [PATCH 05/23] Added abstraction for `BlockId` `i32` is really ambiguous and it is not immediately obvious what it is supposed to represent. `BlockId` is much more readable. Changed internal type to u16 because we currently have ~30k/64k block states. (and i32 doesn't make a lot of sense for an Id, except when replicating mojang behavior) --- .../src/client/play/c_chunk_data.rs | 12 ++- pumpkin-world/src/block/block_registry.rs | 77 ++++++++++++------- pumpkin-world/src/chunk.rs | 18 ++--- pumpkin/src/client/player_packet.rs | 13 +--- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/pumpkin-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 076a3f1a5..87eef9972 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::{bytebuf::ByteBuffer, BitSet, ClientPacket, VarInt}; use itertools::Itertools; use pumpkin_macros::packet; -use pumpkin_world::{chunk::ChunkData, DIRECT_PALETTE_BITS}; +use pumpkin_world::{block::block_registry::BlockId, chunk::ChunkData, DIRECT_PALETTE_BITS}; #[packet(0x27)] pub struct CChunkData<'a>(pub &'a ChunkData); @@ -26,7 +26,11 @@ impl<'a> ClientPacket for CChunkData<'a> { let block_count = chunk .iter() .dedup() - .filter(|block| **block != 0 && **block != 12959 && **block != 12958) + .filter(|block| { + !block.is_air() + && **block != BlockId::from_id(12959) + && **block != BlockId::from_id(12958) + }) .count() as i16; // Block count data_buf.put_i16(block_count); @@ -63,7 +67,7 @@ impl<'a> ClientPacket for CChunkData<'a> { palette.iter().enumerate().for_each(|(i, id)| { palette_map.insert(*id, i); // Palette - data_buf.put_var_int(&VarInt(**id)); + data_buf.put_var_int(&VarInt(id.get_id_mojang_repr())); }); for block_clump in chunk.chunks(64 / block_size as usize) { let mut out_long: i64 = 0; @@ -83,7 +87,7 @@ impl<'a> ClientPacket for CChunkData<'a> { let mut out_long: i64 = 0; let mut shift = 0; for block in block_clump { - out_long |= (*block as i64) << shift; + out_long |= (block.get_id() as i64) << shift; shift += DIRECT_PALETTE_BITS; } block_data_array.push(out_long); diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index 0d79d6d03..0abdd37fb 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -7,6 +7,56 @@ use crate::level::WorldError; const BLOCKS_JSON: &str = include_str!("../../assets/blocks.json"); +// 0 is air -> reasonable default +#[derive(Default, Deserialize, Debug, Hash, Clone, Copy, PartialEq, Eq)] +#[serde(transparent)] +pub struct BlockId { + data: u16, +} + +impl BlockId { + pub fn new( + block_id: &str, + properties: Option<&HashMap>, + ) -> Result { + let mut block_states = BLOCKS + .get(block_id) + .ok_or(WorldError::BlockStateIdNotFound)? + .states + .iter(); + + let block_state = match properties { + Some(properties) => match block_states.find(|state| &state.properties == properties) { + Some(state) => state, + None => return Err(WorldError::BlockStateIdNotFound), + }, + None => block_states + .find(|state| state.is_default) + .expect("Every Block should have at least 1 default state"), + }; + + Ok(block_state.id) + } + + pub fn from_id(id: u16) -> Self { + // TODO: add check if the id is actually valid + Self { data: id } + } + + pub fn is_air(&self) -> bool { + self.data == 0 + } + + pub fn get_id(&self) -> u16 { + self.data + } + + /// An i32 is the way mojang internally represents their Blocks + pub fn get_id_mojang_repr(&self) -> i32 { + self.data as i32 + } +} + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlockDefinition { /// e.g. minecraft:door or minecraft:button @@ -23,7 +73,7 @@ pub struct BlockDefinition { /// This could e.g. be an extended piston facing left. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct BlockState { - id: i64, + id: BlockId, /// Whether this is the default state of the Block #[serde(default, rename = "default")] @@ -51,28 +101,3 @@ lazy_static! { pub static ref BLOCKS: HashMap = serde_json::from_str(BLOCKS_JSON).expect("Could not parse block.json registry."); } - -pub fn block_id_and_properties_to_block_state_id( - block_id: &str, - properties: Option<&HashMap>, -) -> Result { - let block = match BLOCKS.get(block_id) { - Some(block) => block, - None => return Err(WorldError::BlockStateIdNotFound), - }; - let block_state_id = match properties { - None => Ok(block - .states - .iter() - .find(|state| state.is_default) - .expect("Each block should have at least one default state") - .id), - Some(properties) => block - .states - .iter() - .find(|state| &state.properties == properties) - .map(|state| state.id) - .ok_or(WorldError::BlockStateIdNotFound), - }; - block_state_id -} diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index c8c0a2adc..40f033bcb 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -4,13 +4,14 @@ use fastnbt::LongArray; use serde::{Deserialize, Serialize}; use crate::{ + block::block_registry::BlockId, coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates}, level::WorldError, WORLD_HEIGHT, }; pub struct ChunkData { - pub blocks: Box<[i32; 16 * 16 * WORLD_HEIGHT]>, + pub blocks: Box<[BlockId; 16 * 16 * WORLD_HEIGHT]>, pub position: ChunkCoordinates, pub heightmaps: ChunkHeightmaps, } @@ -61,23 +62,18 @@ impl ChunkData { }; // this needs to be boxed, otherwise it will cause a stack-overflow - let mut blocks = Box::new([0; 16 * 16 * WORLD_HEIGHT]); + let mut blocks = Box::new([BlockId::default(); 16 * 16 * WORLD_HEIGHT]); for (k, section) in chunk_data.sections.into_iter().enumerate() { let block_states = match section.block_states { Some(states) => states, None => continue, // this should instead fill all blocks with the only element of the palette }; + let palette = block_states .palette .iter() - .map(|entry| { - crate::block::block_registry::block_id_and_properties_to_block_state_id( - &entry.name, - entry.properties.as_ref(), - ) - .map(|v| v as i32) - }) + .map(|entry| BlockId::new(&entry.name, entry.properties.as_ref())) .collect::, _>>()?; let block_data = match block_states.data { None => continue, @@ -119,8 +115,8 @@ impl ChunkData { pub fn set_block( &mut self, at: ChunkRelativeBlockCoordinates, - block_id: i32, - ) -> Result { + block_id: BlockId, + ) -> Result { Ok(std::mem::replace( &mut self.blocks [(at.y.get_absolute() * 16 * 16 + *at.z as u16 * 16 + *at.x as u16) as usize], diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 5cee5a1ae..3ce92017d 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -25,7 +25,7 @@ use pumpkin_protocol::{ SUseItemOn, Status, }, }; -use pumpkin_world::block::BlockFace; +use pumpkin_world::block::{block_registry::BlockId, BlockFace}; use pumpkin_world::global_registry; use super::PlayerConfig; @@ -400,21 +400,16 @@ impl Player { let minecraft_id = global_registry::find_minecraft_id(global_registry::ITEM_REGISTRY, item.item_id) .expect("All item ids are in the global registry"); - if let Ok(block_state_id) = - pumpkin_world::block::block_registry::block_id_and_properties_to_block_state_id( - minecraft_id, - None, - ) - { + if let Ok(block_state_id) = BlockId::new(minecraft_id, None) { server.broadcast_packet( self, - &CBlockUpdate::new(&location, (block_state_id as i32).into()), + &CBlockUpdate::new(&location, block_state_id.get_id_mojang_repr().into()), ); server.broadcast_packet( self, &CBlockUpdate::new( &WorldPosition(location.0 + face.to_offset()), - (block_state_id as i32).into(), + block_state_id.get_id_mojang_repr().into(), ), ); } From 33ec123148f51abcb53409f15f42f71f8541255a Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 10:27:53 +0200 Subject: [PATCH 06/23] Added `BlockIdentifierNotFound` Error to differentiate from `BlockStateIdNotFound` --- pumpkin-world/src/block/block_registry.rs | 2 +- pumpkin-world/src/level.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index 0abdd37fb..e5df4d15a 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -21,7 +21,7 @@ impl BlockId { ) -> Result { let mut block_states = BLOCKS .get(block_id) - .ok_or(WorldError::BlockStateIdNotFound)? + .ok_or(WorldError::BlockIdentifierNotFound)? .states .iter(); diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 210ed47e1..5bfb24466 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -34,6 +34,8 @@ pub enum WorldError { Compression(CompressionError), #[error("Error deserializing chunk: {0}")] ErrorDeserializingChunk(String), + #[error("The requested block identifier does not exist")] + BlockIdentifierNotFound, #[error("The requested block state id does not exist")] BlockStateIdNotFound, #[error("The block is not inside of the chunk")] From fe56d32463a82323af1c9c1ebf8ce545e1846af6 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 10:54:56 +0200 Subject: [PATCH 07/23] Renamed `block_size` to `block_bit_size` for readability --- pumpkin-world/src/chunk.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index 40f033bcb..d2010dae3 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -80,7 +80,7 @@ impl ChunkData { Some(d) => d, } .into_inner(); - let block_size = { + let block_bit_size = { let size = 64 - (palette.len() as i64 - 1).leading_zeros(); if size >= 4 { size @@ -89,16 +89,16 @@ impl ChunkData { } }; - let mask = (1 << block_size) - 1; + let mask = (1 << block_bit_size) - 1; let mut blocks_left = 16 * 16 * 16; 'block_loop: for (j, block) in block_data.iter().enumerate() { - for i in 0..64 / block_size { + for i in 0..64 / block_bit_size { if blocks_left <= 0 { break 'block_loop; } - let index = (block >> (i * block_size)) & mask; + let index = (block >> (i * block_bit_size)) & mask; let block = palette[index as usize]; - blocks[k * 16 * 16 * 16 + j * ((64 / block_size) as usize) + i as usize] = + blocks[k * 16 * 16 * 16 + j * ((64 / block_bit_size) as usize) + i as usize] = block; blocks_left -= 1; } From 4565133ef501b34eb99df357c654aeac128984e8 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 11:16:48 +0200 Subject: [PATCH 08/23] Refractored `ChunkData::from_bytes` Simply count blocks instead of doing some complex operation with k/j/i Marked comment with TODO Replaced if/else condition with std::cmp::max Removed some redundant `.enumerate()`s --- pumpkin-world/src/chunk.rs | 44 +++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index d2010dae3..e2c76e4fc 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use std::collections::HashMap; use fastnbt::LongArray; @@ -63,11 +64,12 @@ impl ChunkData { // this needs to be boxed, otherwise it will cause a stack-overflow let mut blocks = Box::new([BlockId::default(); 16 * 16 * WORLD_HEIGHT]); + let mut block_index = 0; // which block we're currently at - for (k, section) in chunk_data.sections.into_iter().enumerate() { + for section in chunk_data.sections.into_iter() { let block_states = match section.block_states { Some(states) => states, - None => continue, // this should instead fill all blocks with the only element of the palette + None => continue, // TODO @lukas0008 this should instead fill all blocks with the only element of the palette }; let palette = block_states @@ -75,32 +77,40 @@ impl ChunkData { .iter() .map(|entry| BlockId::new(&entry.name, entry.properties.as_ref())) .collect::, _>>()?; + let block_data = match block_states.data { - None => continue, + None => { + // We skipped placing an empty subchunk. + // We need to increase the y coordinate of the next subchunk being placed. + block_index += 16 * 16 * 16; + continue; + } Some(d) => d, } .into_inner(); + + // How many bits each block has in one of the pallete u64s let block_bit_size = { let size = 64 - (palette.len() as i64 - 1).leading_zeros(); - if size >= 4 { - size - } else { - 4 - } + max(4, size) }; + // How many blocks there are in one of the palletes u64s + let blocks_in_pallete = 64 / block_bit_size; let mask = (1 << block_bit_size) - 1; - let mut blocks_left = 16 * 16 * 16; - 'block_loop: for (j, block) in block_data.iter().enumerate() { - for i in 0..64 / block_bit_size { - if blocks_left <= 0 { - break 'block_loop; - } + 'block_loop: for block in block_data.iter() { + for i in 0..blocks_in_pallete { let index = (block >> (i * block_bit_size)) & mask; let block = palette[index as usize]; - blocks[k * 16 * 16 * 16 + j * ((64 / block_bit_size) as usize) + i as usize] = - block; - blocks_left -= 1; + + blocks[block_index] = block; + block_index += 1; + + // if `SUBCHUNK_VOLUME `is not divisible by `blocks_in_pallete` the block_data + // can sometimes spill into other subchunks. We avoid that by aborting early + if (block_index % (16 * 16 * 16)) == 0 { + break 'block_loop; + } } } } From 1d1dd2ed6cc94da19adae9e4f0e41b6a203a5910 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 11:31:50 +0200 Subject: [PATCH 09/23] Added consts `CHUNK_AREA`, `SUBCHUNK_VOLUME` and `CHUNK_VOLUME`, --- pumpkin-world/src/chunk.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index e2c76e4fc..951d975cd 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -11,8 +11,12 @@ use crate::{ WORLD_HEIGHT, }; +const CHUNK_AREA: usize = 16 * 16; +const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; +const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; + pub struct ChunkData { - pub blocks: Box<[BlockId; 16 * 16 * WORLD_HEIGHT]>, + pub blocks: Box<[BlockId; CHUNK_VOLUME]>, pub position: ChunkCoordinates, pub heightmaps: ChunkHeightmaps, } @@ -63,7 +67,7 @@ impl ChunkData { }; // this needs to be boxed, otherwise it will cause a stack-overflow - let mut blocks = Box::new([BlockId::default(); 16 * 16 * WORLD_HEIGHT]); + let mut blocks = Box::new([BlockId::default(); CHUNK_VOLUME]); let mut block_index = 0; // which block we're currently at for section in chunk_data.sections.into_iter() { @@ -82,7 +86,7 @@ impl ChunkData { None => { // We skipped placing an empty subchunk. // We need to increase the y coordinate of the next subchunk being placed. - block_index += 16 * 16 * 16; + block_index += SUBCHUNK_VOLUME; continue; } Some(d) => d, @@ -108,7 +112,7 @@ impl ChunkData { // if `SUBCHUNK_VOLUME `is not divisible by `blocks_in_pallete` the block_data // can sometimes spill into other subchunks. We avoid that by aborting early - if (block_index % (16 * 16 * 16)) == 0 { + if (block_index % SUBCHUNK_VOLUME) == 0 { break 'block_loop; } } @@ -129,7 +133,7 @@ impl ChunkData { ) -> Result { Ok(std::mem::replace( &mut self.blocks - [(at.y.get_absolute() * 16 * 16 + *at.z as u16 * 16 + *at.x as u16) as usize], + [at.y.get_absolute() as usize * CHUNK_AREA + (*at.z * 16 + *at.x) as usize], block_id, )) } From 52c34caaa64902dc77c948787d90fde4acd64e53 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 12:17:58 +0200 Subject: [PATCH 10/23] Added `ChunkBlocks` abstraction for `ChunkData` ChunkBlocks makes it easy & safer to access blocks of a chunk. It also makes it possible to consistently handle the heightmap updates in 1 place. --- .../src/client/play/c_chunk_data.rs | 4 +- pumpkin-world/src/chunk.rs | 113 +++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/pumpkin-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 87eef9972..0f4ea20ff 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -16,13 +16,13 @@ impl<'a> ClientPacket for CChunkData<'a> { buf.put_i32(self.0.position.z); let heightmap_nbt = - fastnbt::to_bytes_with_opts(&self.0.heightmaps, fastnbt::SerOpts::network_nbt()) + fastnbt::to_bytes_with_opts(&self.0.blocks.heightmap, fastnbt::SerOpts::network_nbt()) .unwrap(); // Heightmaps buf.put_slice(&heightmap_nbt); let mut data_buf = ByteBuffer::empty(); - self.0.blocks.chunks(16 * 16 * 16).for_each(|chunk| { + self.0.blocks.iter_subchunks().for_each(|chunk| { let block_count = chunk .iter() .dedup() diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index 951d975cd..f0a27f5f3 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -1,12 +1,13 @@ use std::cmp::max; use std::collections::HashMap; +use std::ops::Index; use fastnbt::LongArray; use serde::{Deserialize, Serialize}; use crate::{ block::block_registry::BlockId, - coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates}, + coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates, Height}, level::WorldError, WORLD_HEIGHT, }; @@ -16,9 +17,19 @@ const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; pub struct ChunkData { - pub blocks: Box<[BlockId; CHUNK_VOLUME]>, + pub blocks: ChunkBlocks, pub position: ChunkCoordinates, - pub heightmaps: ChunkHeightmaps, +} + +pub struct ChunkBlocks { + // TODO make this a Vec that doesn't store the upper layers that only contain air + + // The packet relies on this ordering -> leave it like this for performance + /// Ordering: yzx (y being the most significant) + blocks: Box<[BlockId; CHUNK_VOLUME]>, + + /// See `https://minecraft.fandom.com/wiki/Heightmap` for more info + pub heightmap: ChunkHeightmaps, } #[derive(Deserialize, Debug, Clone)] @@ -59,6 +70,74 @@ struct ChunkNbt { heightmaps: ChunkHeightmaps, } +// TODO @DaniD3v implement once Fre is done with heightmaps +// impl Default for ChunkBlocks { +// fn default() -> Self { +// Self { +// blocks: Box::new([BlockId::default(); CHUNK_VOLUME]), +// heightmap: todo!(), +// } +// } +// } + +impl ChunkBlocks { + pub fn empty_with_heightmap(heightmap: ChunkHeightmaps) -> Self { + Self { + blocks: Box::new([BlockId::default(); CHUNK_VOLUME]), + heightmap, + } + } + + /// Sets the given block in the chunk, returning the old block + pub fn set_block( + &mut self, + position: ChunkRelativeBlockCoordinates, + block: BlockId, + ) -> BlockId { + // TODO @LUK_ESC? update the heightmap + self.set_block_no_heightmap_update(position, block) + } + + /// 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: BlockId, + ) -> BlockId { + std::mem::replace(&mut self.blocks[Self::convert_index(position)], block) + } + + pub fn iter_subchunks(&self) -> impl Iterator { + self.blocks + .chunks(SUBCHUNK_VOLUME) + .map(|subchunk| subchunk.try_into().unwrap()) + } + + 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 + } + + #[allow(dead_code)] + fn calculate_heightmap(&self) -> ChunkHeightmaps { + // figure out how LongArray is formatted + // figure out how to find out if block is motion blocking + todo!() + } +} + +impl Index for ChunkBlocks { + type Output = BlockId; + + fn index(&self, index: ChunkRelativeBlockCoordinates) -> &Self::Output { + &self.blocks[Self::convert_index(index)] + } +} + impl ChunkData { pub fn from_bytes(chunk_data: Vec, at: ChunkCoordinates) -> Result { let chunk_data = match fastnbt::from_bytes::(chunk_data.as_slice()) { @@ -67,7 +146,7 @@ impl ChunkData { }; // this needs to be boxed, otherwise it will cause a stack-overflow - let mut blocks = Box::new([BlockId::default(); CHUNK_VOLUME]); + let mut blocks = ChunkBlocks::empty_with_heightmap(chunk_data.heightmaps); let mut block_index = 0; // which block we're currently at for section in chunk_data.sections.into_iter() { @@ -107,7 +186,18 @@ impl ChunkData { let index = (block >> (i * block_bit_size)) & mask; let block = palette[index as usize]; - blocks[block_index] = block; + // TODO allow indexing blocks directly so we can just use block_index and save some time? + // this is fine because we initalized the heightmap of `blocks` + // from the cached value in the world file + blocks.set_block_no_heightmap_update( + ChunkRelativeBlockCoordinates { + z: ((block_index % CHUNK_AREA) / 16).into(), + y: Height::from_absolute((block_index / CHUNK_AREA) as u16), + x: (block_index % 16).into(), + }, + block, + ); + block_index += 1; // if `SUBCHUNK_VOLUME `is not divisible by `blocks_in_pallete` the block_data @@ -122,19 +212,6 @@ impl ChunkData { Ok(ChunkData { blocks, position: at, - heightmaps: chunk_data.heightmaps, }) } - /// Sets the given block in the chunk, returning the old block - pub fn set_block( - &mut self, - at: ChunkRelativeBlockCoordinates, - block_id: BlockId, - ) -> Result { - Ok(std::mem::replace( - &mut self.blocks - [at.y.get_absolute() as usize * CHUNK_AREA + (*at.z * 16 + *at.x) as usize], - block_id, - )) - } } From a8d9c89da27fb109b4713c4ce26e6e5234c7129f Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 18:26:25 +0200 Subject: [PATCH 11/23] Implemented Default for `ChunkHeightmaps` and `ChunkBlocks` --- pumpkin-world/src/chunk.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index f0a27f5f3..3952c770e 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -70,15 +70,25 @@ struct ChunkNbt { heightmaps: ChunkHeightmaps, } -// TODO @DaniD3v implement once Fre is done with heightmaps -// impl Default for ChunkBlocks { -// fn default() -> Self { -// Self { -// blocks: Box::new([BlockId::default(); CHUNK_VOLUME]), -// heightmap: todo!(), -// } -// } -// } +/// The Heightmap for a completely empty chunk +impl Default for ChunkHeightmaps { + fn default() -> Self { + Self { + // 0 packed into an i64 7 times. + motion_blocking: LongArray::new(vec![0; 37]), + world_surface: LongArray::new(vec![0; 37]), + } + } +} + +impl Default for ChunkBlocks { + fn default() -> Self { + Self { + blocks: Box::new([BlockId::default(); CHUNK_VOLUME]), + heightmap: ChunkHeightmaps::default(), + } + } +} impl ChunkBlocks { pub fn empty_with_heightmap(heightmap: ChunkHeightmaps) -> Self { From bef3ddef50f619155370b68ef707b511e7c25714 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Sun, 25 Aug 2024 22:45:12 +0200 Subject: [PATCH 12/23] Replaced redundant `.map` with `.for_each` --- pumpkin-world/src/level.rs | 202 ++++++++++++++++++------------------- 1 file changed, 99 insertions(+), 103 deletions(-) diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 5bfb24466..84a0c9de8 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -107,125 +107,121 @@ impl Level { chunks: Vec, channel: mpsc::Sender<(ChunkCoordinates, Result)>, ) { - chunks - .into_par_iter() - .map(|chunk| { - let region = ( - ((chunk.x as f32) / 32.0).floor() as i32, - ((chunk.z as f32) / 32.0).floor() as i32, - ); - let channel = channel.clone(); + chunks.into_par_iter().for_each(|chunk| { + let region = ( + ((chunk.x as f32) / 32.0).floor() as i32, + ((chunk.z as f32) / 32.0).floor() as i32, + ); + let channel = channel.clone(); - // return different error when file is not found (because that means that the chunks have just not been generated yet) - let mut region_file = match OpenOptions::new().read(true).open( - self.region_folder - .join(format!("r.{}.{}.mca", region.0, region.1)), - ) { - Ok(f) => f, - Err(err) => match err.kind() { - std::io::ErrorKind::NotFound => { - let _ = channel.blocking_send((chunk, Err(WorldError::RegionNotFound))); - return; - } - _ => { - let _ = channel - .blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; - } - }, - }; + // return different error when file is not found (because that means that the chunks have just not been generated yet) + let mut region_file = match OpenOptions::new().read(true).open( + self.region_folder + .join(format!("r.{}.{}.mca", region.0, region.1)), + ) { + Ok(f) => f, + Err(err) => match err.kind() { + std::io::ErrorKind::NotFound => { + let _ = channel.blocking_send((chunk, Err(WorldError::RegionNotFound))); + return; + } + _ => { + let _ = + channel.blocking_send((chunk, Err(WorldError::IoError(err.kind())))); + return; + } + }, + }; - let mut location_table: [u8; 4096] = [0; 4096]; - let mut timestamp_table: [u8; 4096] = [0; 4096]; + let mut location_table: [u8; 4096] = [0; 4096]; + let mut timestamp_table: [u8; 4096] = [0; 4096]; - // fill the location and timestamp tables - { - match region_file.read_exact(&mut location_table) { - Ok(_) => {} - Err(err) => { - let _ = channel - .blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; - } + // fill the location and timestamp tables + { + match region_file.read_exact(&mut location_table) { + Ok(_) => {} + Err(err) => { + let _ = + channel.blocking_send((chunk, Err(WorldError::IoError(err.kind())))); + return; } - match region_file.read_exact(&mut timestamp_table) { - Ok(_) => {} - Err(err) => { - let _ = channel - .blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; - } + } + match region_file.read_exact(&mut timestamp_table) { + Ok(_) => {} + Err(err) => { + let _ = + channel.blocking_send((chunk, Err(WorldError::IoError(err.kind())))); + return; } } + } - let modulus = |a: i32, b: i32| ((a % b) + b) % b; - let chunk_x = modulus(chunk.x, 32) as u32; - let chunk_z = modulus(chunk.z, 32) as u32; - let channel = channel.clone(); - let table_entry = (chunk_x + chunk_z * 32) * 4; + let modulus = |a: i32, b: i32| ((a % b) + b) % b; + let chunk_x = modulus(chunk.x, 32) as u32; + let chunk_z = modulus(chunk.z, 32) as u32; + let channel = channel.clone(); + let table_entry = (chunk_x + chunk_z * 32) * 4; - let mut offset = vec![0u8]; - offset.extend_from_slice( - &location_table[table_entry as usize..table_entry as usize + 3], - ); - let offset = u32::from_be_bytes(offset.try_into().unwrap()) as u64 * 4096; - let size = location_table[table_entry as usize + 3] as usize * 4096; + let mut offset = vec![0u8]; + offset + .extend_from_slice(&location_table[table_entry as usize..table_entry as usize + 3]); + let offset = u32::from_be_bytes(offset.try_into().unwrap()) as u64 * 4096; + let size = location_table[table_entry as usize + 3] as usize * 4096; - if offset == 0 && size == 0 { - let _ = channel.blocking_send((chunk, Err(WorldError::ChunkNotFound))); + if offset == 0 && size == 0 { + let _ = channel.blocking_send((chunk, Err(WorldError::ChunkNotFound))); + return; + } + // Read the file using the offset and size + let mut file_buf = { + let seek_result = region_file.seek(std::io::SeekFrom::Start(offset)); + if seek_result.is_err() { + let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); return; } - // Read the file using the offset and size - let mut file_buf = { - let seek_result = region_file.seek(std::io::SeekFrom::Start(offset)); - if seek_result.is_err() { - let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); - return; - } - let mut out = vec![0; size]; - let read_result = region_file.read_exact(&mut out); - if read_result.is_err() { - let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); - return; - } - out - }; + let mut out = vec![0; size]; + let read_result = region_file.read_exact(&mut out); + if read_result.is_err() { + let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); + return; + } + out + }; - // TODO: check checksum to make sure chunk is not corrupted - let header = file_buf.drain(0..5).collect_vec(); + // TODO: check checksum to make sure chunk is not corrupted + let header = file_buf.drain(0..5).collect_vec(); - let compression = match Compression::from_byte(header[4]) { - Some(c) => c, - None => { - let _ = channel.blocking_send(( - chunk, - Err(WorldError::Compression( - CompressionError::UnknownCompression, - )), - )); - return; - } - }; + let compression = match Compression::from_byte(header[4]) { + Some(c) => c, + None => { + let _ = channel.blocking_send(( + chunk, + Err(WorldError::Compression( + CompressionError::UnknownCompression, + )), + )); + return; + } + }; - let size = u32::from_be_bytes(header[..4].try_into().unwrap()); + let size = u32::from_be_bytes(header[..4].try_into().unwrap()); - // size includes the compression scheme byte, so we need to subtract 1 - let chunk_data = file_buf.drain(0..size as usize - 1).collect_vec(); - let decompressed_chunk = match Self::decompress_data(compression, chunk_data) { - Ok(data) => data, - Err(e) => { - channel - .blocking_send((chunk, Err(WorldError::Compression(e)))) - .expect("Failed to send Compression error"); - return; - } - }; + // size includes the compression scheme byte, so we need to subtract 1 + let chunk_data = file_buf.drain(0..size as usize - 1).collect_vec(); + let decompressed_chunk = match Self::decompress_data(compression, chunk_data) { + Ok(data) => data, + Err(e) => { + channel + .blocking_send((chunk, Err(WorldError::Compression(e)))) + .expect("Failed to send Compression error"); + return; + } + }; - channel - .blocking_send((chunk, ChunkData::from_bytes(decompressed_chunk, chunk))) - .expect("Error sending decompressed chunk"); - }) - .collect::>(); + channel + .blocking_send((chunk, ChunkData::from_bytes(decompressed_chunk, chunk))) + .expect("Error sending decompressed chunk"); + }) } fn decompress_data( From cf283323fccf0261083072675d2e6a1424bae348 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Mon, 26 Aug 2024 13:47:56 +0200 Subject: [PATCH 13/23] `BlockId` refactor. Moved `BlockId` to it's own file. Prefixed all of the `block_registry` types with Registry to avoid confusion. Made stuff that should not be public private. --- .../src/client/play/c_chunk_data.rs | 12 +-- pumpkin-world/src/block/block_id.rs | 58 +++++++++++++ pumpkin-world/src/block/block_registry.rs | 81 ++++--------------- pumpkin-world/src/block/mod.rs | 8 +- pumpkin-world/src/chunk.rs | 2 +- pumpkin/src/client/player_packet.rs | 2 +- 6 files changed, 82 insertions(+), 81 deletions(-) create mode 100644 pumpkin-world/src/block/block_id.rs diff --git a/pumpkin-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 0f4ea20ff..0c640c85c 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::{bytebuf::ByteBuffer, BitSet, ClientPacket, VarInt}; use itertools::Itertools; use pumpkin_macros::packet; -use pumpkin_world::{block::block_registry::BlockId, chunk::ChunkData, DIRECT_PALETTE_BITS}; +use pumpkin_world::{chunk::ChunkData, DIRECT_PALETTE_BITS}; #[packet(0x27)] pub struct CChunkData<'a>(pub &'a ChunkData); @@ -23,15 +23,7 @@ impl<'a> ClientPacket for CChunkData<'a> { let mut data_buf = ByteBuffer::empty(); self.0.blocks.iter_subchunks().for_each(|chunk| { - let block_count = chunk - .iter() - .dedup() - .filter(|block| { - !block.is_air() - && **block != BlockId::from_id(12959) - && **block != BlockId::from_id(12958) - }) - .count() as i16; + let block_count = chunk.iter().dedup().filter(|block| !block.is_air()).count() as i16; // Block count data_buf.put_i16(block_count); //// Block states diff --git a/pumpkin-world/src/block/block_id.rs b/pumpkin-world/src/block/block_id.rs new file mode 100644 index 000000000..80380842c --- /dev/null +++ b/pumpkin-world/src/block/block_id.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use super::block_registry::BLOCKS; +use crate::level::WorldError; + +// 0 is air -> reasonable default +#[derive(Default, Deserialize, Debug, Hash, Clone, Copy, PartialEq, Eq)] +#[serde(transparent)] +pub struct BlockId { + data: u16, +} + +impl BlockId { + pub const AIR: Self = Self::from_id(0); + + pub fn new( + text_id: &str, + properties: Option<&HashMap>, + ) -> Result { + let mut block_states = BLOCKS + .get(text_id) + .ok_or(WorldError::BlockIdentifierNotFound)? + .states + .iter(); + + let block_state = match properties { + Some(properties) => match block_states.find(|state| &state.properties == properties) { + Some(state) => state, + None => return Err(WorldError::BlockStateIdNotFound), + }, + None => block_states + .find(|state| state.is_default) + .expect("Every Block should have at least 1 default state"), + }; + + Ok(block_state.id) + } + + pub const fn from_id(id: u16) -> Self { + // TODO: add check if the id is actually valid + Self { data: id } + } + + pub fn is_air(&self) -> bool { + self.data == 0 || self.data == 12959 || self.data == 12958 + } + + pub fn get_id(&self) -> u16 { + self.data + } + + /// An i32 is the way mojang internally represents their Blocks + pub fn get_id_mojang_repr(&self) -> i32 { + self.data as i32 + } +} diff --git a/pumpkin-world/src/block/block_registry.rs b/pumpkin-world/src/block/block_registry.rs index e5df4d15a..c6c9b96f4 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -3,101 +3,50 @@ use std::collections::HashMap; use lazy_static::lazy_static; use serde::Deserialize; -use crate::level::WorldError; +use super::block_id::BlockId; -const BLOCKS_JSON: &str = include_str!("../../assets/blocks.json"); - -// 0 is air -> reasonable default -#[derive(Default, Deserialize, Debug, Hash, Clone, Copy, PartialEq, Eq)] -#[serde(transparent)] -pub struct BlockId { - data: u16, -} - -impl BlockId { - pub fn new( - block_id: &str, - properties: Option<&HashMap>, - ) -> Result { - let mut block_states = BLOCKS - .get(block_id) - .ok_or(WorldError::BlockIdentifierNotFound)? - .states - .iter(); - - let block_state = match properties { - Some(properties) => match block_states.find(|state| &state.properties == properties) { - Some(state) => state, - None => return Err(WorldError::BlockStateIdNotFound), - }, - None => block_states - .find(|state| state.is_default) - .expect("Every Block should have at least 1 default state"), - }; - - Ok(block_state.id) - } - - pub fn from_id(id: u16) -> Self { - // TODO: add check if the id is actually valid - Self { data: id } - } - - pub fn is_air(&self) -> bool { - self.data == 0 - } - - pub fn get_id(&self) -> u16 { - self.data - } - - /// An i32 is the way mojang internally represents their Blocks - pub fn get_id_mojang_repr(&self) -> i32 { - self.data as i32 - } +lazy_static! { + pub static ref BLOCKS: HashMap = + serde_json::from_str(include_str!("../../assets/blocks.json")) + .expect("Could not parse block.json registry."); } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlockDefinition { +pub struct RegistryBlockDefinition { /// e.g. minecraft:door or minecraft:button #[serde(rename = "type")] - category: String, + pub category: String, /// Specifies the variant of the blocks category. /// e.g. minecraft:iron_door has the variant iron #[serde(rename = "block_set_type")] - variant: Option, + pub variant: Option, } /// One possible state of a Block. /// This could e.g. be an extended piston facing left. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlockState { - id: BlockId, +pub struct RegistryBlockState { + pub id: BlockId, /// Whether this is the default state of the Block #[serde(default, rename = "default")] - is_default: bool, + pub is_default: bool, /// The propertise active for this `BlockState`. #[serde(default)] - properties: HashMap, + pub properties: HashMap, } /// A fully-fledged block definition. /// Stores the category, variant, all of the possible states and all of the possible properties. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlockType { - definition: BlockDefinition, - states: Vec, +pub struct RegistryBlockType { + pub definition: RegistryBlockDefinition, + pub states: Vec, // TODO is this safe to remove? It's currently not used in the Project. @lukas0008 @Snowiiii /// A list of valid property keys/values for a block. #[serde(default, rename = "properties")] valid_properties: HashMap>, } - -lazy_static! { - pub static ref BLOCKS: HashMap = - serde_json::from_str(BLOCKS_JSON).expect("Could not parse block.json registry."); -} diff --git a/pumpkin-world/src/block/mod.rs b/pumpkin-world/src/block/mod.rs index 1009f9fe3..59dc8a083 100644 --- a/pumpkin-world/src/block/mod.rs +++ b/pumpkin-world/src/block/mod.rs @@ -1,9 +1,11 @@ +use crate::vector3::Vector3; use num_derive::FromPrimitive; -use crate::vector3::Vector3; +pub mod block_id; +mod block_registry; + +pub use block_id::BlockId; -pub mod block_registry; -pub use block_registry::BLOCKS; #[derive(FromPrimitive)] pub enum BlockFace { Bottom = 0, diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index 3952c770e..677990594 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -6,7 +6,7 @@ use fastnbt::LongArray; use serde::{Deserialize, Serialize}; use crate::{ - block::block_registry::BlockId, + block::BlockId, coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates, Height}, level::WorldError, WORLD_HEIGHT, diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 3ce92017d..13b077af0 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -25,7 +25,7 @@ use pumpkin_protocol::{ SUseItemOn, Status, }, }; -use pumpkin_world::block::{block_registry::BlockId, BlockFace}; +use pumpkin_world::block::{BlockFace, BlockId}; use pumpkin_world::global_registry; use super::PlayerConfig; From cdb0e6b761b2de4ea852427aaf827d8d2d3a3883 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Mon, 26 Aug 2024 17:28:11 +0200 Subject: [PATCH 14/23] Refractored coordinates.rs --- pumpkin-world/src/coordinates.rs | 81 +++++++++++++++++--------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/pumpkin-world/src/coordinates.rs b/pumpkin-world/src/coordinates.rs index 0873d0e80..93056f531 100644 --- a/pumpkin-world/src/coordinates.rs +++ b/pumpkin-world/src/coordinates.rs @@ -1,31 +1,38 @@ use std::ops::Deref; +use num_traits::{PrimInt, Signed, Unsigned}; +use serde::{Deserialize, Serialize}; + use crate::{WORLD_LOWEST_Y, WORLD_MAX_Y}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(transparent)] pub struct Height { height: i16, } impl Height { - pub fn new(height: i16) -> Self { - assert!(height <= WORLD_MAX_Y); - assert!(height >= WORLD_LOWEST_Y); - - Self { height } - } - pub fn from_absolute(height: u16) -> Self { - Self::new(height as i16 - WORLD_LOWEST_Y.abs()) + (height as i16 - WORLD_LOWEST_Y.abs()).into() } /// Absolute height ranges from `0..WORLD_HEIGHT` /// instead of `WORLD_LOWEST_Y..WORLD_MAX_Y` - pub fn get_absolute(&self) -> u16 { + pub fn get_absolute(self) -> u16 { (self.height + WORLD_LOWEST_Y.abs()) as u16 } } +impl From for Height { + fn from(height: T) -> Self { + let height = height.to_i16().unwrap(); + + assert!(height <= WORLD_MAX_Y); + assert!(height >= WORLD_LOWEST_Y); + Self { height } + } +} + impl Deref for Height { type Target = i16; @@ -39,25 +46,14 @@ pub struct ChunkRelativeScalar { scalar: u8, } -macro_rules! derive_chunk_relative_scalar_from_int_impl { - ($integer:ty) => { - impl From<$integer> for ChunkRelativeScalar { - fn from(scalar: $integer) -> Self { - assert!(scalar < 16); - Self { - scalar: scalar as u8, - } - } - } - }; -} +impl From for ChunkRelativeScalar { + fn from(scalar: T) -> Self { + let scalar = scalar.to_u8().unwrap(); -derive_chunk_relative_scalar_from_int_impl! {u8} -derive_chunk_relative_scalar_from_int_impl! {u16} -derive_chunk_relative_scalar_from_int_impl! {u32} -derive_chunk_relative_scalar_from_int_impl! {u64} -derive_chunk_relative_scalar_from_int_impl! {u128} -derive_chunk_relative_scalar_from_int_impl! {usize} + assert!(scalar < 16); + Self { scalar } + } +} impl Deref for ChunkRelativeScalar { type Target = u8; @@ -81,6 +77,16 @@ pub struct XZBlockCoordinates { pub z: i32, } +impl XZBlockCoordinates { + pub fn with_y(self, height: Height) -> BlockCoordinates { + BlockCoordinates { + x: self.x, + y: height, + z: self.z, + } + } +} + /// Coordinates of a block relative to a chunk #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ChunkRelativeBlockCoordinates { @@ -90,7 +96,7 @@ pub struct ChunkRelativeBlockCoordinates { } impl ChunkRelativeBlockCoordinates { - pub fn with_chunk_coordinates(&self, chunk_coordinates: ChunkCoordinates) -> BlockCoordinates { + pub fn with_chunk_coordinates(self, chunk_coordinates: ChunkCoordinates) -> BlockCoordinates { BlockCoordinates { x: *self.x as i32 + chunk_coordinates.x * 16, y: self.y, @@ -115,6 +121,14 @@ impl ChunkRelativeXZBlockCoordinates { z: *self.z as i32 + chunk_coordinates.z * 16, } } + + pub fn with_y(self, height: Height) -> ChunkRelativeBlockCoordinates { + ChunkRelativeBlockCoordinates { + x: self.x, + y: height, + z: self.z, + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -122,12 +136,3 @@ pub struct ChunkCoordinates { pub x: i32, pub z: i32, } - -macro_rules! impl_get_absolute_height { - ($struct_name:ident) => { - impl $struct_name {} - }; -} - -impl_get_absolute_height! {BlockCoordinates} -impl_get_absolute_height! {ChunkRelativeBlockCoordinates} From 17b285081a8b41152f9a700ef2c396ede8d0470d Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Tue, 27 Aug 2024 22:17:14 +0200 Subject: [PATCH 15/23] Refractored `read_chunk` Added new error types to indicate a chunk should be generated. Renamed to `fetch_chunks` to indicate that it no longer just reads chunks (might write to the worldfile in the future) Take a reference to a slice instead of an owned vector because the data is Copy anyways. Remove the ChunkCoordinates that are sent over the mpsc Sender. - The coordinates are stored in ChunkData anyways. - If there is an Error the coordinates should not matter because the error can't be recovered from. Moved the reading from file logic to `read_chunk`. - So that we don't always have to repeat `channel.blocking_send(...); return;` - To make it clearly distinct from the main program logic. Replaced some verbose match statements with `Result.map_err()`. --- pumpkin-world/src/level.rs | 214 +++++++++++++++++-------------------- pumpkin/src/server.rs | 14 ++- 2 files changed, 107 insertions(+), 121 deletions(-) diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 84a0c9de8..c95eec840 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,7 +1,7 @@ use std::{ fs::OpenOptions, io::{Read, Seek}, - path::PathBuf, + path::{Path, PathBuf}, }; use flate2::{bufread::ZlibDecoder, read::GzDecoder}; @@ -24,12 +24,10 @@ pub enum WorldError { // using ErrorKind instead of Error, beacuse the function read_chunks and read_region_chunks is designed to return an error on a per-chunk basis, while std::io::Error does not implement Copy or Clone #[error("Io error: {0}")] IoError(std::io::ErrorKind), - #[error("Region not found")] - RegionNotFound, #[error("Region is invalid")] RegionIsInvalid, - #[error("Chunk not found")] - ChunkNotFound, + #[error("The chunk isn't generated yet: {0}")] + ChunkNotGenerated(ChunkNotGeneratedError), #[error("Compression Error")] Compression(CompressionError), #[error("Error deserializing chunk: {0}")] @@ -42,6 +40,18 @@ pub enum WorldError { BlockOutsideChunk, } +#[derive(Error, Debug)] +pub enum ChunkNotGeneratedError { + #[error("The region file does not exist.")] + RegionFileMissing, + + #[error("The chunks generation is incomplete.")] + IncompleteGeneration, + + #[error("Chunk not found.")] + NotFound, +} + #[derive(Error, Debug)] pub enum CompressionError { #[error("Compression scheme not recognised")] @@ -98,130 +108,108 @@ impl Level { // .1 // } - /// Read many chunks in a world + /// Reads/Generates many chunks in a world /// MUST be called from a tokio runtime thread /// /// Note: The order of the output chunks will almost never be in the same order as the order of input chunks - pub async fn read_chunks( + pub async fn fetch_chunks( &self, - chunks: Vec, - channel: mpsc::Sender<(ChunkCoordinates, Result)>, + chunks: &[ChunkCoordinates], + channel: mpsc::Sender>, ) { - chunks.into_par_iter().for_each(|chunk| { - let region = ( - ((chunk.x as f32) / 32.0).floor() as i32, - ((chunk.z as f32) / 32.0).floor() as i32, - ); + chunks.into_par_iter().copied().for_each(|at| { let channel = channel.clone(); - // return different error when file is not found (because that means that the chunks have just not been generated yet) - let mut region_file = match OpenOptions::new().read(true).open( - self.region_folder - .join(format!("r.{}.{}.mca", region.0, region.1)), - ) { - Ok(f) => f, - Err(err) => match err.kind() { - std::io::ErrorKind::NotFound => { - let _ = channel.blocking_send((chunk, Err(WorldError::RegionNotFound))); - return; - } - _ => { - let _ = - channel.blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; + channel + .blocking_send(match Self::read_chunk(&self.region_folder, at) { + Err(WorldError::ChunkNotGenerated(_)) => { + // This chunk was not generated yet. + todo!("generate chunk here") } - }, - }; + // TODO this doesn't warn the user about the error. fix. + result => result, + }) + .expect("Failed sending ChunkData."); + }) + } - let mut location_table: [u8; 4096] = [0; 4096]; - let mut timestamp_table: [u8; 4096] = [0; 4096]; + fn read_chunk(region_folder: &Path, at: ChunkCoordinates) -> Result { + let region = ( + ((at.x as f32) / 32.0).floor() as i32, + ((at.z as f32) / 32.0).floor() as i32, + ); - // fill the location and timestamp tables - { - match region_file.read_exact(&mut location_table) { - Ok(_) => {} - Err(err) => { - let _ = - channel.blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; - } - } - match region_file.read_exact(&mut timestamp_table) { - Ok(_) => {} - Err(err) => { - let _ = - channel.blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; - } + let mut region_file = OpenOptions::new() + .read(true) + .open(region_folder.join(format!("r.{}.{}.mca", region.0, region.1))) + .map_err(|err| match err.kind() { + std::io::ErrorKind::NotFound => { + WorldError::ChunkNotGenerated(ChunkNotGeneratedError::RegionFileMissing) } - } - - let modulus = |a: i32, b: i32| ((a % b) + b) % b; - let chunk_x = modulus(chunk.x, 32) as u32; - let chunk_z = modulus(chunk.z, 32) as u32; - let channel = channel.clone(); - let table_entry = (chunk_x + chunk_z * 32) * 4; - - let mut offset = vec![0u8]; - offset - .extend_from_slice(&location_table[table_entry as usize..table_entry as usize + 3]); - let offset = u32::from_be_bytes(offset.try_into().unwrap()) as u64 * 4096; - let size = location_table[table_entry as usize + 3] as usize * 4096; + kind => WorldError::IoError(kind), + })?; + + let mut location_table: [u8; 4096] = [0; 4096]; + let mut timestamp_table: [u8; 4096] = [0; 4096]; + + // fill the location and timestamp tables + region_file + .read_exact(&mut location_table) + .map_err(|err| WorldError::IoError(err.kind()))?; + region_file + .read_exact(&mut timestamp_table) + .map_err(|err| WorldError::IoError(err.kind()))?; + + let modulus = |a: i32, b: i32| ((a % b) + b) % b; + let chunk_x = modulus(at.x, 32) as u32; + let chunk_z = modulus(at.z, 32) as u32; + let table_entry = (chunk_x + chunk_z * 32) * 4; + + let mut offset = vec![0u8]; + offset.extend_from_slice(&location_table[table_entry as usize..table_entry as usize + 3]); + let offset = u32::from_be_bytes(offset.try_into().unwrap()) as u64 * 4096; + let size = location_table[table_entry as usize + 3] as usize * 4096; + + if offset == 0 && size == 0 { + return Err(WorldError::ChunkNotGenerated( + ChunkNotGeneratedError::NotFound, + )); + } - if offset == 0 && size == 0 { - let _ = channel.blocking_send((chunk, Err(WorldError::ChunkNotFound))); - return; + // Read the file using the offset and size + let mut file_buf = { + let seek_result = region_file.seek(std::io::SeekFrom::Start(offset)); + if seek_result.is_err() { + return Err(WorldError::RegionIsInvalid); } - // Read the file using the offset and size - let mut file_buf = { - let seek_result = region_file.seek(std::io::SeekFrom::Start(offset)); - if seek_result.is_err() { - let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); - return; - } - let mut out = vec![0; size]; - let read_result = region_file.read_exact(&mut out); - if read_result.is_err() { - let _ = channel.blocking_send((chunk, Err(WorldError::RegionIsInvalid))); - return; - } - out - }; - - // TODO: check checksum to make sure chunk is not corrupted - let header = file_buf.drain(0..5).collect_vec(); + let mut out = vec![0; size]; + let read_result = region_file.read_exact(&mut out); + if read_result.is_err() { + return Err(WorldError::RegionIsInvalid); + } + out + }; + + // TODO: check checksum to make sure chunk is not corrupted + let header = file_buf.drain(0..5).collect_vec(); + + let compression = match Compression::from_byte(header[4]) { + Some(c) => c, + None => { + return Err(WorldError::Compression( + CompressionError::UnknownCompression, + )) + } + }; - let compression = match Compression::from_byte(header[4]) { - Some(c) => c, - None => { - let _ = channel.blocking_send(( - chunk, - Err(WorldError::Compression( - CompressionError::UnknownCompression, - )), - )); - return; - } - }; + let size = u32::from_be_bytes(header[..4].try_into().unwrap()); - let size = u32::from_be_bytes(header[..4].try_into().unwrap()); + // size includes the compression scheme byte, so we need to subtract 1 + let chunk_data = file_buf.drain(0..size as usize - 1).collect_vec(); + let decompressed_chunk = + Self::decompress_data(compression, chunk_data).map_err(WorldError::Compression)?; - // size includes the compression scheme byte, so we need to subtract 1 - let chunk_data = file_buf.drain(0..size as usize - 1).collect_vec(); - let decompressed_chunk = match Self::decompress_data(compression, chunk_data) { - Ok(data) => data, - Err(e) => { - channel - .blocking_send((chunk, Err(WorldError::Compression(e)))) - .expect("Failed to send Compression error"); - return; - } - }; - - channel - .blocking_send((chunk, ChunkData::from_bytes(decompressed_chunk, chunk))) - .expect("Error sending decompressed chunk"); - }) + ChunkData::from_bytes(decompressed_chunk, at) } fn decompress_data( diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index 7bd254c26..4ed5a94b8 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -353,13 +353,10 @@ impl Server { let inst = std::time::Instant::now(); let (sender, mut chunk_receiver) = mpsc::channel(distance as usize); let world = self.world.clone(); + + let chunks: Vec<_> = RadialIterator::new(distance).collect(); tokio::spawn(async move { - world - .lock() - .await - .level - .read_chunks(RadialIterator::new(distance).collect(), sender) - .await; + world.lock().await.level.fetch_chunks(&chunks, sender).await; }); player.client.send_packet(&CCenterChunk { @@ -367,14 +364,15 @@ impl Server { chunk_z: 0.into(), }); - while let Some((_chunk_pos, chunk_data)) = chunk_receiver.recv().await { + while let Some(chunk_data) = chunk_receiver.recv().await { // dbg!(chunk_pos); let chunk_data = match chunk_data { Ok(d) => d, Err(_) => continue, }; #[cfg(debug_assertions)] - if _chunk_pos == (pumpkin_world::coordinates::ChunkCoordinates { x: 0, z: 0 }) { + if chunk_data.position == (pumpkin_world::coordinates::ChunkCoordinates { x: 0, z: 0 }) + { use pumpkin_protocol::bytebuf::ByteBuffer; let mut test = ByteBuffer::empty(); CChunkData(&chunk_data).write(&mut test); From b5b27885c767430bb66f9036a2de41f921a06bc8 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Tue, 27 Aug 2024 22:45:55 +0200 Subject: [PATCH 16/23] Added ChunkStatus to check if a chunk is fully generated yet. When loading a partially generated world there's some incomplete chunks that might not have a heightmap yet. If that is the case we return a ChunkNotGeneratedError to indicate that it's ok to (re-)generate it. --- pumpkin-world/src/chunk.rs | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index 677990594..d57b1ae18 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ block::BlockId, coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates, Height}, - level::WorldError, + level::{ChunkNotGeneratedError, WorldError}, WORLD_HEIGHT, }; @@ -70,6 +70,37 @@ struct ChunkNbt { heightmaps: ChunkHeightmaps, } +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(tag = "Status")] +enum ChunkStatus { + #[serde(rename = "minecraft:empty")] + Empty, + #[serde(rename = "minecraft:structure_starts")] + StructureStarts, + #[serde(rename = "minecraft:structure_references")] + StructureReferences, + #[serde(rename = "minecraft:biomes")] + Biomes, + #[serde(rename = "minecraft:noise")] + Noise, + #[serde(rename = "minecraft:surface")] + Surface, + #[serde(rename = "minecraft:carvers")] + Carvers, + #[serde(rename = "minecraft:liquid_carvers")] + LiquidCarvers, + #[serde(rename = "minecraft:features")] + Features, + #[serde(rename = "minecraft:initialize_light")] + Light, + #[serde(rename = "minecraft:spawn")] + Spawn, + #[serde(rename = "minecraft:heightmaps")] + Heightmaps, + #[serde(rename = "minecraft:full")] + Full, +} + /// The Heightmap for a completely empty chunk impl Default for ChunkHeightmaps { fn default() -> Self { @@ -150,6 +181,14 @@ impl Index for ChunkBlocks { impl ChunkData { pub fn from_bytes(chunk_data: Vec, at: ChunkCoordinates) -> Result { + if fastnbt::from_bytes::(&chunk_data).expect("Failed reading chunk status.") + != ChunkStatus::Full + { + return Err(WorldError::ChunkNotGenerated( + ChunkNotGeneratedError::IncompleteGeneration, + )); + } + let chunk_data = match fastnbt::from_bytes::(chunk_data.as_slice()) { Ok(v) => v, Err(err) => return Err(WorldError::ErrorDeserializingChunk(err.to_string())), From eb4d47aa7450907d0a42aff9c9c665c45304c81f Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Tue, 27 Aug 2024 23:05:52 +0200 Subject: [PATCH 17/23] Added an Enum for Biomes --- pumpkin-world/src/biome.rs | 9 +++++++++ pumpkin-world/src/lib.rs | 1 + 2 files changed, 10 insertions(+) create mode 100644 pumpkin-world/src/biome.rs diff --git a/pumpkin-world/src/biome.rs b/pumpkin-world/src/biome.rs new file mode 100644 index 000000000..3196e6ed6 --- /dev/null +++ b/pumpkin-world/src/biome.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +// TODO make this work with the protocol +#[derive(Serialize, Deserialize, Clone, Copy)] +#[non_exhaustive] +pub enum Biome { + Plains, + // TODO list all Biomes +} diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index b204eef2b..56462faf8 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -1,5 +1,6 @@ use level::Level; +pub mod biome; pub mod block; pub mod chunk; pub mod coordinates; From 4031c4dc1378f117502896db2cf10b027efe6f75 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Tue, 27 Aug 2024 23:12:02 +0200 Subject: [PATCH 18/23] Added `Seed` Type. --- pumpkin-world/src/lib.rs | 1 + pumpkin-world/src/world_gen/mod.rs | 1 + pumpkin-world/src/world_gen/seed.rs | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 pumpkin-world/src/world_gen/mod.rs create mode 100644 pumpkin-world/src/world_gen/seed.rs diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 56462faf8..bad06b925 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -10,6 +10,7 @@ pub mod item; mod level; pub mod radial_chunk_iterator; pub mod vector3; +mod world_gen; pub const WORLD_HEIGHT: usize = 384; pub const WORLD_LOWEST_Y: i16 = -64; diff --git a/pumpkin-world/src/world_gen/mod.rs b/pumpkin-world/src/world_gen/mod.rs new file mode 100644 index 000000000..1db8d3c21 --- /dev/null +++ b/pumpkin-world/src/world_gen/mod.rs @@ -0,0 +1 @@ +mod seed; diff --git a/pumpkin-world/src/world_gen/seed.rs b/pumpkin-world/src/world_gen/seed.rs new file mode 100644 index 000000000..a3245c2b6 --- /dev/null +++ b/pumpkin-world/src/world_gen/seed.rs @@ -0,0 +1,16 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +#[allow(dead_code)] +#[derive(Clone, Copy)] +pub struct Seed(pub i64); + +impl From<&str> for Seed { + fn from(value: &str) -> Self { + // TODO replace with a deterministic hasher (the same as vanilla?) + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + + // TODO use cast_signed once the feature is stabilized. + Self(hasher.finish() as i64) + } +} From 9149e82508515bbfc56e005bfef1a97f68d597f5 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Wed, 28 Aug 2024 10:49:05 +0200 Subject: [PATCH 19/23] Added WorldGenerator traits and a GenericWorldGenerator. --- Cargo.lock | 8 +++ pumpkin-world/Cargo.toml | 2 + pumpkin-world/src/world_gen/generator.rs | 25 +++++++ .../src/world_gen/generic_generator.rs | 66 +++++++++++++++++++ pumpkin-world/src/world_gen/mod.rs | 10 +++ 5 files changed, 111 insertions(+) create mode 100644 pumpkin-world/src/world_gen/generator.rs create mode 100644 pumpkin-world/src/world_gen/generic_generator.rs diff --git a/Cargo.lock b/Cargo.lock index 01b46a6d3..c7ce270de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1980,11 +1980,13 @@ dependencies = [ "futures", "itertools 0.13.0", "lazy_static", + "log", "num-derive", "num-traits", "rayon", "serde", "serde_json", + "static_assertions", "thiserror", "tokio", ] @@ -2565,6 +2567,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index c158369b1..516711d8d 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -16,6 +16,8 @@ flate2 = "1.0.33" serde = { version = "1.0", features = ["derive"] } lazy_static = "1.5.0" serde_json = "1.0" +static_assertions = "1.1.0" +log.workspace = true num-traits = "0.2" num-derive = "0.4" diff --git a/pumpkin-world/src/world_gen/generator.rs b/pumpkin-world/src/world_gen/generator.rs new file mode 100644 index 000000000..c5a10480c --- /dev/null +++ b/pumpkin-world/src/world_gen/generator.rs @@ -0,0 +1,25 @@ +use static_assertions::assert_obj_safe; + +use crate::biome::Biome; +use crate::block::BlockId; +use crate::chunk::ChunkData; +use crate::coordinates::{BlockCoordinates, ChunkCoordinates, XZBlockCoordinates}; +use crate::world_gen::Seed; + +pub trait GeneratorInit { + fn new(seed: Seed) -> Self; +} + +pub trait WorldGenerator: Sync + Send { + #[allow(dead_code)] + fn generate_chunk(&self, at: ChunkCoordinates) -> ChunkData; +} +assert_obj_safe! {WorldGenerator} + +pub(crate) trait BiomeGenerator: Sync + Send { + fn generate_biome(&self, at: XZBlockCoordinates) -> Biome; +} + +pub(crate) trait TerrainGenerator: Sync + Send { + fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockId; +} diff --git a/pumpkin-world/src/world_gen/generic_generator.rs b/pumpkin-world/src/world_gen/generic_generator.rs new file mode 100644 index 000000000..d6a60ea5c --- /dev/null +++ b/pumpkin-world/src/world_gen/generic_generator.rs @@ -0,0 +1,66 @@ +use crate::{ + chunk::{ChunkBlocks, ChunkData}, + coordinates::{ + ChunkCoordinates, ChunkRelativeBlockCoordinates, ChunkRelativeXZBlockCoordinates, + }, + WORLD_LOWEST_Y, WORLD_MAX_Y, +}; + +use super::{ + generator::{BiomeGenerator, GeneratorInit, TerrainGenerator, WorldGenerator}, + Seed, +}; + +pub struct GenericGenerator { + biome_generator: B, + terrain_generator: T, +} + +impl GeneratorInit + for GenericGenerator +{ + fn new(seed: Seed) -> Self { + Self { + biome_generator: B::new(seed), + terrain_generator: T::new(seed), + } + } +} + +impl WorldGenerator for GenericGenerator { + fn generate_chunk(&self, at: ChunkCoordinates) -> ChunkData { + let mut blocks = ChunkBlocks::default(); + + for x in 0..16u8 { + for z in 0..16u8 { + let biome = self.biome_generator.generate_biome( + ChunkRelativeXZBlockCoordinates { + x: x.into(), + z: z.into(), + } + .with_chunk_coordinates(at), + ); + + // Iterate from the highest block to the lowest, in order to minimize the heightmap updates + for y in (WORLD_LOWEST_Y..WORLD_MAX_Y).rev() { + let coordinates = ChunkRelativeBlockCoordinates { + x: x.into(), + y: y.into(), + z: z.into(), + }; + + blocks.set_block( + coordinates, + self.terrain_generator + .generate_block(coordinates.with_chunk_coordinates(at), biome), + ); + } + } + } + + ChunkData { + blocks, + position: at, + } + } +} diff --git a/pumpkin-world/src/world_gen/mod.rs b/pumpkin-world/src/world_gen/mod.rs index 1db8d3c21..43a5ca1a6 100644 --- a/pumpkin-world/src/world_gen/mod.rs +++ b/pumpkin-world/src/world_gen/mod.rs @@ -1 +1,11 @@ +mod generator; +mod generic_generator; mod seed; + +pub use generator::WorldGenerator; +pub use seed::Seed; + +#[allow(dead_code)] +pub fn get_world_gen(_: Seed) -> Box { + todo!() +} From 43c724dec77898210fcb8c506af1b9a8f44d6f75 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Wed, 28 Aug 2024 11:27:06 +0200 Subject: [PATCH 20/23] Added Superflat implementation. --- .../src/world_gen/implementations/mod.rs | 1 + .../world_gen/implementations/superflat.rs | 48 +++++++++++++++++++ pumpkin-world/src/world_gen/mod.rs | 9 +++- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 pumpkin-world/src/world_gen/implementations/mod.rs create mode 100644 pumpkin-world/src/world_gen/implementations/superflat.rs diff --git a/pumpkin-world/src/world_gen/implementations/mod.rs b/pumpkin-world/src/world_gen/implementations/mod.rs new file mode 100644 index 000000000..716121f9d --- /dev/null +++ b/pumpkin-world/src/world_gen/implementations/mod.rs @@ -0,0 +1 @@ +pub mod superflat; diff --git a/pumpkin-world/src/world_gen/implementations/superflat.rs b/pumpkin-world/src/world_gen/implementations/superflat.rs new file mode 100644 index 000000000..edbbcf4ba --- /dev/null +++ b/pumpkin-world/src/world_gen/implementations/superflat.rs @@ -0,0 +1,48 @@ +use crate::{ + biome::Biome, + block::BlockId, + coordinates::{BlockCoordinates, XZBlockCoordinates}, + world_gen::{ + generator::{BiomeGenerator, GeneratorInit, TerrainGenerator}, + generic_generator::GenericGenerator, + Seed, + }, +}; + +#[allow(dead_code)] +pub type SuperflatGenerator = GenericGenerator; + +pub(crate) struct SuperflatBiomeGenerator {} + +impl GeneratorInit for SuperflatBiomeGenerator { + fn new(_: Seed) -> Self { + Self {} + } +} + +impl BiomeGenerator for SuperflatBiomeGenerator { + // TODO make generic over Biome and allow changing the Biome in the config. + fn generate_biome(&self, _: XZBlockCoordinates) -> Biome { + Biome::Plains + } +} + +pub(crate) struct SuperflatTerrainGenerator {} + +impl GeneratorInit for SuperflatTerrainGenerator { + fn new(_: Seed) -> Self { + Self {} + } +} + +impl TerrainGenerator for SuperflatTerrainGenerator { + // TODO allow specifying which blocks should be at which height in the config. + fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockId { + match *at.y { + -64 => BlockId::from_id(79), // Bedrock + -63..=-62 => BlockId::from_id(10), // Dirt + -61 => BlockId::from_id(9), // Grass + _ => BlockId::AIR, + } + } +} diff --git a/pumpkin-world/src/world_gen/mod.rs b/pumpkin-world/src/world_gen/mod.rs index 43a5ca1a6..9ab676e24 100644 --- a/pumpkin-world/src/world_gen/mod.rs +++ b/pumpkin-world/src/world_gen/mod.rs @@ -1,11 +1,16 @@ mod generator; mod generic_generator; +mod implementations; mod seed; pub use generator::WorldGenerator; pub use seed::Seed; +use generator::GeneratorInit; +use implementations::superflat::SuperflatGenerator; + #[allow(dead_code)] -pub fn get_world_gen(_: Seed) -> Box { - todo!() +pub fn get_world_gen(seed: Seed) -> Box { + // TODO decide which WorldGenerator to pick based on config. + Box::new(SuperflatGenerator::new(seed)) } From 112a899559805162b966d3c063c3f6ed97a5a8ef Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Wed, 28 Aug 2024 11:39:07 +0200 Subject: [PATCH 21/23] Added logic to generate chunks if they can't be read from the world file. Moved warning about ./world folder to only appear when necessary + Updated warning contents to reflect changes Made the save_file in the `Level` struct optional in case someone wants an entirely generated World. Removed now redundant asserts. --- pumpkin-world/src/level.rs | 78 ++++++++++++++++++++++++++++---------- pumpkin/src/server.rs | 1 - 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index c95eec840..e066c464c 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,7 +1,7 @@ use std::{ fs::OpenOptions, io::{Read, Seek}, - path::{Path, PathBuf}, + path::PathBuf, }; use flate2::{bufread::ZlibDecoder, read::GzDecoder}; @@ -10,11 +10,20 @@ use rayon::prelude::*; use thiserror::Error; use tokio::sync::mpsc; -use crate::{chunk::ChunkData, coordinates::ChunkCoordinates}; +use crate::{ + chunk::ChunkData, + coordinates::ChunkCoordinates, + world_gen::{get_world_gen, Seed, WorldGenerator}, +}; -#[allow(dead_code)] -/// The Level represents a +/// The Level represents a single Dimension. pub struct Level { + save_file: Option, + world_gen: Box, +} + +struct SaveFile { + #[allow(dead_code)] root_folder: PathBuf, region_folder: PathBuf, } @@ -84,16 +93,31 @@ impl Compression { impl Level { pub fn from_root_folder(root_folder: PathBuf) -> Self { - assert!(root_folder.exists(), "World root folder does not exist!"); - let region_folder = root_folder.join("region"); - assert!( - region_folder.exists(), - "World region folder does not exist!" - ); + let world_gen = get_world_gen(Seed(0)); // TODO Read Seed from config. + + if root_folder.exists() { + let region_folder = root_folder.join("region"); + assert!( + region_folder.exists(), + "World region folder does not exist, despite there being a root folder." + ); - Level { - root_folder, - region_folder, + Self { + world_gen, + save_file: Some(SaveFile { + root_folder, + region_folder, + }), + } + } else { + log::warn!( + "Pumpkin currently only supports Superflat World generation. Use a vanilla ./world folder to play in a normal world." + ); + + Self { + world_gen, + save_file: None, + } } } @@ -121,19 +145,27 @@ impl Level { let channel = channel.clone(); channel - .blocking_send(match Self::read_chunk(&self.region_folder, at) { - Err(WorldError::ChunkNotGenerated(_)) => { - // This chunk was not generated yet. - todo!("generate chunk here") + .blocking_send(match &self.save_file { + Some(save_file) => { + match Self::read_chunk(save_file, at) { + Err(WorldError::ChunkNotGenerated(_)) => { + // This chunk was not generated yet. + Ok(self.world_gen.generate_chunk(at)) + } + // TODO this doesn't warn the user about the error. fix. + result => result, + } + } + None => { + // There is no savefile yet -> generate the chunks + Ok(self.world_gen.generate_chunk(at)) } - // TODO this doesn't warn the user about the error. fix. - result => result, }) .expect("Failed sending ChunkData."); }) } - fn read_chunk(region_folder: &Path, at: ChunkCoordinates) -> Result { + fn read_chunk(save_file: &SaveFile, at: ChunkCoordinates) -> Result { let region = ( ((at.x as f32) / 32.0).floor() as i32, ((at.z as f32) / 32.0).floor() as i32, @@ -141,7 +173,11 @@ impl Level { let mut region_file = OpenOptions::new() .read(true) - .open(region_folder.join(format!("r.{}.{}.mca", region.0, region.1))) + .open( + save_file + .region_folder + .join(format!("r.{}.{}.mca", region.0, region.1)), + ) .map_err(|err| match err.kind() { std::io::ErrorKind::NotFound => { WorldError::ChunkNotGenerated(ChunkNotGeneratedError::RegionFileMissing) diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index 4ed5a94b8..f8e329bba 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -102,7 +102,6 @@ impl Server { log::info!("Loading Plugins"); let plugin_loader = PluginLoader::load(); - log::warn!("Pumpkin does currently not have World or Chunk generation, Using ../world folder with vanilla pregenerated chunks"); let world = World::load(Dimension::OverWorld.into_level( // TODO: load form config "./world".parse().unwrap(), From e2d2b53f4106056a027a7620ae7c11e6379cc181 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Wed, 28 Aug 2024 11:49:36 +0200 Subject: [PATCH 22/23] Fixed bug where whole chunk can break in one mined block --- pumpkin-protocol/src/client/play/c_chunk_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pumpkin-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 0c640c85c..2fbbb571c 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -23,7 +23,7 @@ impl<'a> ClientPacket for CChunkData<'a> { let mut data_buf = ByteBuffer::empty(); self.0.blocks.iter_subchunks().for_each(|chunk| { - let block_count = chunk.iter().dedup().filter(|block| !block.is_air()).count() as i16; + let block_count = chunk.iter().filter(|block| !block.is_air()).count() as i16; // Block count data_buf.put_i16(block_count); //// Block states From ff06d05487046d45fdacc6e9136a5d3a7634e129 Mon Sep 17 00:00:00 2001 From: DaniD3v Date: Wed, 28 Aug 2024 11:50:11 +0200 Subject: [PATCH 23/23] Made ChunkNBT struct slightly more readable --- pumpkin-world/src/chunk.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk.rs index d57b1ae18..8bf95e63c 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -61,12 +61,14 @@ struct ChunkSection { } #[derive(Deserialize, Debug)] -#[allow(dead_code)] +#[serde(rename_all = "PascalCase")] struct ChunkNbt { - #[serde(rename = "DataVersion")] + #[allow(dead_code)] data_version: usize, + + #[serde(rename = "sections")] sections: Vec, - #[serde(rename = "Heightmaps")] + heightmaps: ChunkHeightmaps, }