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-protocol/src/client/play/c_chunk_data.rs b/pumpkin-protocol/src/client/play/c_chunk_data.rs index 484be4fd6..2fbbb571c 100644 --- a/pumpkin-protocol/src/client/play/c_chunk_data.rs +++ b/pumpkin-protocol/src/client/play/c_chunk_data.rs @@ -11,23 +11,19 @@ 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()) + 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| { - let block_count = chunk - .iter() - .dedup() - .filter(|block| **block != 0 && **block != 12959 && **block != 12958) - .count() as i16; + self.0.blocks.iter_subchunks().for_each(|chunk| { + let block_count = chunk.iter().filter(|block| !block.is_air()).count() as i16; // Block count data_buf.put_i16(block_count); //// Block states @@ -63,7 +59,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 +79,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/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/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/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 fa2b790b6..c6c9b96f4 100644 --- a/pumpkin-world/src/block/block_registry.rs +++ b/pumpkin-world/src/block/block_registry.rs @@ -1,58 +1,52 @@ 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"); +lazy_static! { + pub static ref BLOCKS: HashMap = + serde_json::from_str(include_str!("../../assets/blocks.json")) + .expect("Could not parse block.json registry."); +} -#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlockDefinition { +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct RegistryBlockDefinition { + /// e.g. minecraft:door or minecraft:button #[serde(rename = "type")] - kind: String, - block_set_type: Option, -} + pub category: String, -#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlockState { - default: Option, - id: i64, - properties: Option>, + /// Specifies the variant of the blocks category. + /// e.g. minecraft:iron_door has the variant iron + #[serde(rename = "block_set_type")] + pub variant: Option, } -#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct BlocksElement { - definition: BlockDefinition, - properties: Option>>, - states: Vec, -} +/// One possible state of a Block. +/// This could e.g. be an extended piston facing left. +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct RegistryBlockState { + pub id: BlockId, -lazy_static! { - pub static ref BLOCKS: HashMap = - serde_json::from_str(BLOCKS_JSON).expect("Could not parse block.json registry."); + /// Whether this is the default state of the Block + #[serde(default, rename = "default")] + pub is_default: bool, + + /// The propertise active for this `BlockState`. + #[serde(default)] + pub properties: HashMap, } -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.default.unwrap_or(false)) - .expect("Each block should have at least one default state") - .id), - Some(properties) => block - .states - .iter() - .find(|state| state.properties.as_ref() == Some(properties)) - .map(|state| state.id) - .ok_or(WorldError::BlockStateIdNotFound), - }; - block_state_id +/// 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 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>, } 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 c3fe94ff7..8bf95e63c 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk.rs @@ -1,36 +1,58 @@ +use std::cmp::max; use std::collections::HashMap; +use std::ops::Index; use fastnbt::LongArray; +use serde::{Deserialize, Serialize}; -use crate::{level::WorldError, vector3::Vector3, WORLD_HEIGHT, WORLD_Y_START_AT}; +use crate::{ + block::BlockId, + coordinates::{ChunkCoordinates, ChunkRelativeBlockCoordinates, Height}, + level::{ChunkNotGeneratedError, WorldError}, + 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<[i32; 16 * 16 * WORLD_HEIGHT]>, - pub position: (i32, i32), - pub heightmaps: ChunkHeightmaps, + pub blocks: ChunkBlocks, + pub position: ChunkCoordinates, +} + +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(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")] @@ -38,68 +60,202 @@ struct ChunkSection { block_states: Option, } -#[derive(serde::Deserialize, Debug)] -#[allow(dead_code)] +#[derive(Deserialize, Debug)] +#[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, } +#[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 { + 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 { + 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: (i32, i32)) -> Result { + 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())), }; // 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 = ChunkBlocks::empty_with_heightmap(chunk_data.heightmaps); + 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 .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, + None => { + // We skipped placing an empty subchunk. + // We need to increase the y coordinate of the next subchunk being placed. + block_index += SUBCHUNK_VOLUME; + continue; + } Some(d) => d, } .into_inner(); - let block_size = { + + // 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; + '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]; + + // 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, + ); - let mask = (1 << block_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 { - if blocks_left <= 0 { + 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 % SUBCHUNK_VOLUME) == 0 { break 'block_loop; } - let index = (block >> (i * block_size)) & mask; - let block = palette[index as usize]; - blocks[k * 16 * 16 * 16 + j * ((64 / block_size) as usize) + i as usize] = - block; - blocks_left -= 1; } } } @@ -107,24 +263,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: 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); - } - - Ok(std::mem::replace( - &mut self.blocks[(y * 16 * 16 + z * 16 + x) as usize], - block_id, - )) - } } diff --git a/pumpkin-world/src/coordinates.rs b/pumpkin-world/src/coordinates.rs new file mode 100644 index 000000000..93056f531 --- /dev/null +++ b/pumpkin-world/src/coordinates.rs @@ -0,0 +1,138 @@ +use std::ops::Deref; + +use num_traits::{PrimInt, Signed, Unsigned}; +use serde::{Deserialize, Serialize}; + +use crate::{WORLD_LOWEST_Y, WORLD_MAX_Y}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(transparent)] +pub struct Height { + height: i16, +} + +impl Height { + pub fn from_absolute(height: u16) -> Self { + (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 { + (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; + + fn deref(&self) -> &Self::Target { + &self.height + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkRelativeScalar { + scalar: u8, +} + +impl From for ChunkRelativeScalar { + fn from(scalar: T) -> Self { + let scalar = scalar.to_u8().unwrap(); + + assert!(scalar < 16); + Self { scalar } + } +} + +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, +} + +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 { + 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, + } + } + + pub fn with_y(self, height: Height) -> ChunkRelativeBlockCoordinates { + ChunkRelativeBlockCoordinates { + x: self.x, + y: height, + z: self.z, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkCoordinates { + pub x: i32, + pub z: i32, +} diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 3ccff077d..e066c464c 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -10,11 +10,20 @@ use rayon::prelude::*; use thiserror::Error; use tokio::sync::mpsc; -use crate::chunk::ChunkData; +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, } @@ -24,22 +33,34 @@ 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}")] 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")] 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")] @@ -72,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, + } } } @@ -96,137 +132,120 @@ 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<(i32, i32)>, - channel: mpsc::Sender<((i32, i32), Result)>, + chunks: &[ChunkCoordinates], + channel: mpsc::Sender>, ) { - 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, - ); - 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; - } - }, - }; - - 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; + chunks.into_par_iter().copied().for_each(|at| { + let channel = channel.clone(); + + channel + .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, } } - match region_file.read_exact(&mut timestamp_table) { - Ok(_) => {} - Err(err) => { - let _ = channel - .blocking_send((chunk, Err(WorldError::IoError(err.kind())))); - return; - } + None => { + // There is no savefile yet -> generate the chunks + Ok(self.world_gen.generate_chunk(at)) } - } + }) + .expect("Failed sending ChunkData."); + }) + } + + 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, + ); - 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 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; - - if offset == 0 && size == 0 { - let _ = - channel.blocking_send(((chunk.0, chunk.1), Err(WorldError::ChunkNotFound))); - return; + let mut region_file = OpenOptions::new() + .read(true) + .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) } - // 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))); - 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))); - return; - } - out - }; + kind => WorldError::IoError(kind), + })?; - // TODO: check checksum to make sure chunk is not corrupted - let header = file_buf.drain(0..5).collect_vec(); + let mut location_table: [u8; 4096] = [0; 4096]; + let mut timestamp_table: [u8; 4096] = [0; 4096]; - let compression = match Compression::from_byte(header[4]) { - Some(c) => c, - None => { - let _ = channel.blocking_send(( - (chunk.0, chunk.1), - Err(WorldError::Compression( - CompressionError::UnknownCompression, - )), - )); - return; - } - }; + // 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 size = u32::from_be_bytes(header[..4].try_into().unwrap()); + 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; - // 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.0, chunk.1), Err(WorldError::Compression(e)))) - .expect("Failed to send Compression error"); - return; - } - }; + 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, + )); + } + + // 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); + } + 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 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)?; - channel - .blocking_send((chunk, ChunkData::from_bytes(decompressed_chunk, chunk))) - .expect("Error sending decompressed chunk"); - }) - .collect::>(); + ChunkData::from_bytes(decompressed_chunk, at) } fn decompress_data( diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 143942f5e..bad06b925 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -1,16 +1,21 @@ use level::Level; +pub mod biome; +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; +mod world_gen; + +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, 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-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/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 new file mode 100644 index 000000000..9ab676e24 --- /dev/null +++ b/pumpkin-world/src/world_gen/mod.rs @@ -0,0 +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: Seed) -> Box { + // TODO decide which WorldGenerator to pick based on config. + Box::new(SuperflatGenerator::new(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) + } +} diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 5cee5a1ae..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::BlockFace; +use pumpkin_world::block::{BlockFace, BlockId}; 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(), ), ); } diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index 76ab4029b..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(), @@ -353,13 +352,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 +363,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 == (0, 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);