diff --git a/Cargo.lock b/Cargo.lock index 73b534e16..9411b8069 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,7 +2032,6 @@ dependencies = [ "rayon", "serde", "serde_json", - "static_assertions", "thiserror", "tokio", ] @@ -2656,12 +2655,6 @@ 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/Cargo.toml b/Cargo.toml index 8577de730..1e1d7ff1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ version = "0.1.0" edition = "2021" [profile.release] -debug = 1 lto = true codegen-units = 1 diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index 453842903..519cb081d 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -17,7 +17,6 @@ futures = "0.3" flate2 = "1.0" serde.workspace = true serde_json = "1.0" -static_assertions = "1.1.0" log.workspace = true parking_lot.workspace = true diff --git a/pumpkin-world/src/block/block_state.rs b/pumpkin-world/src/block/block_state.rs index 437349df2..df79018c7 100644 --- a/pumpkin-world/src/block/block_state.rs +++ b/pumpkin-world/src/block/block_state.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::level::WorldError; +use thiserror::Error; use super::block_registry::{Block, BlockCategory, BLOCKS}; @@ -21,16 +21,16 @@ impl BlockState { pub fn new( registry_id: &str, properties: Option<&HashMap>, - ) -> Result { + ) -> Result { let block_registry = BLOCKS .get(registry_id) - .ok_or(WorldError::BlockIdentifierNotFound)?; + .ok_or(BlockStateError::BlockIdentifierNotFound)?; let mut block_states = block_registry.states.iter(); let block_state = match properties { Some(properties) => block_states .find(|state| &state.properties == properties) - .ok_or_else(|| WorldError::BlockStateIdNotFound)?, + .ok_or(BlockStateError::BlockStateIdNotFound)?, None => block_states .find(|state| state.is_default) .expect("Every Block should have at least 1 default state"), @@ -71,3 +71,11 @@ impl BlockState { self.category == category } } + +#[derive(Error, Debug)] +pub enum BlockStateError { + #[error("The requested block identifier does not exist")] + BlockIdentifierNotFound, + #[error("The requested block state id does not exist")] + BlockStateIdNotFound, +} diff --git a/pumpkin-world/src/chunk/anvil.rs b/pumpkin-world/src/chunk/anvil.rs new file mode 100644 index 000000000..07a96df7b --- /dev/null +++ b/pumpkin-world/src/chunk/anvil.rs @@ -0,0 +1,144 @@ +use std::{ + fs::OpenOptions, + io::{Read, Seek}, +}; + +use flate2::bufread::{GzDecoder, ZlibDecoder}; +use itertools::Itertools; + +use crate::level::SaveFile; + +use super::{ChunkData, ChunkReader, ChunkReadingError, CompressionError}; + +pub struct AnvilChunkReader {} + +impl Default for AnvilChunkReader { + fn default() -> Self { + Self::new() + } +} + +impl AnvilChunkReader { + pub fn new() -> Self { + Self {} + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Compression { + Gzip, + Zlib, + None, + LZ4, +} + +impl Compression { + pub fn from_byte(byte: u8) -> Option { + match byte { + 1 => Some(Self::Gzip), + 2 => Some(Self::Zlib), + 3 => Some(Self::None), + 4 => Some(Self::LZ4), + _ => None, + } + } + + fn decompress_data(&self, compressed_data: Vec) -> Result, CompressionError> { + match self { + Compression::Gzip => { + let mut z = GzDecoder::new(&compressed_data[..]); + let mut chunk_data = Vec::with_capacity(compressed_data.len()); + z.read_to_end(&mut chunk_data) + .map_err(CompressionError::GZipError)?; + Ok(chunk_data) + } + Compression::Zlib => { + let mut z = ZlibDecoder::new(&compressed_data[..]); + let mut chunk_data = Vec::with_capacity(compressed_data.len()); + z.read_to_end(&mut chunk_data) + .map_err(CompressionError::ZlibError)?; + Ok(chunk_data) + } + Compression::None => Ok(compressed_data), + Compression::LZ4 => todo!(), + } + } +} + +impl ChunkReader for AnvilChunkReader { + fn read_chunk( + &self, + save_file: &SaveFile, + at: pumpkin_core::math::vector2::Vector2, + ) -> Result { + let region = ( + ((at.x as f32) / 32.0).floor() as i32, + ((at.z as f32) / 32.0).floor() as i32, + ); + + 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 => ChunkReadingError::ChunkNotExist, + kind => ChunkReadingError::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| ChunkReadingError::IoError(err.kind()))?; + region_file + .read_exact(&mut timestamp_table) + .map_err(|err| ChunkReadingError::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(ChunkReadingError::ChunkNotExist); + } + + // Read the file using the offset and size + let mut file_buf = { + region_file + .seek(std::io::SeekFrom::Start(offset)) + .map_err(|_| ChunkReadingError::RegionIsInvalid)?; + let mut out = vec![0; size]; + region_file + .read_exact(&mut out) + .map_err(|_| ChunkReadingError::RegionIsInvalid)?; + out + }; + + // TODO: check checksum to make sure chunk is not corrupted + let header = file_buf.drain(0..5).collect_vec(); + + let compression = Compression::from_byte(header[4]) + .ok_or_else(|| ChunkReadingError::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 = compression + .decompress_data(chunk_data) + .map_err(ChunkReadingError::Compression)?; + + ChunkData::from_bytes(decompressed_chunk, at).map_err(ChunkReadingError::ParsingError) + } +} diff --git a/pumpkin-world/src/chunk.rs b/pumpkin-world/src/chunk/mod.rs similarity index 84% rename from pumpkin-world/src/chunk.rs rename to pumpkin-world/src/chunk/mod.rs index 2037bf5fa..6deb462eb 100644 --- a/pumpkin-world/src/chunk.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -5,18 +5,53 @@ use std::ops::Index; use fastnbt::LongArray; use pumpkin_core::math::vector2::Vector2; use serde::{Deserialize, Serialize}; +use thiserror::Error; use crate::{ - block::{BlockId, BlockState}, + block::{block_state::BlockStateError, BlockId, BlockState}, coordinates::{ChunkRelativeBlockCoordinates, Height}, - level::{ChunkNotGeneratedError, WorldError}, + level::SaveFile, WORLD_HEIGHT, }; +pub mod anvil; + const CHUNK_AREA: usize = 16 * 16; const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; +pub trait ChunkReader: Sync + Send { + fn read_chunk( + &self, + save_file: &SaveFile, + at: Vector2, + ) -> Result; +} + +#[derive(Error, Debug)] +pub enum ChunkReadingError { + #[error("Io error: {0}")] + IoError(std::io::ErrorKind), + #[error("Region is invalid")] + RegionIsInvalid, + #[error("Compression error {0}")] + Compression(CompressionError), + #[error("Tried to read chunk which does not exist")] + ChunkNotExist, + #[error("Failed to parse Chunk from bytes: {0}")] + ParsingError(ChunkParsingError), +} + +#[derive(Error, Debug)] +pub enum CompressionError { + #[error("Compression scheme not recognised")] + UnknownCompression, + #[error("Error while working with zlib compression: {0}")] + ZlibError(std::io::Error), + #[error("Error while working with Gzip compression: {0}")] + GZipError(std::io::Error), +} + pub struct ChunkData { pub blocks: ChunkBlocks, pub position: Vector2, @@ -188,18 +223,16 @@ impl Index for ChunkBlocks { } impl ChunkData { - pub fn from_bytes(chunk_data: Vec, at: Vector2) -> Result { + pub fn from_bytes(chunk_data: Vec, at: Vector2) -> Result { if fastnbt::from_bytes::(&chunk_data).expect("Failed reading chunk status.") != ChunkStatus::Full { - return Err(WorldError::ChunkNotGenerated( - ChunkNotGeneratedError::IncompleteGeneration, - )); + return Err(ChunkParsingError::ChunkNotGenerated); } let chunk_data = match fastnbt::from_bytes::(chunk_data.as_slice()) { Ok(v) => v, - Err(err) => return Err(WorldError::ErrorDeserializingChunk(err.to_string())), + Err(err) => return Err(ChunkParsingError::ErrorDeserializingChunk(err.to_string())), }; // this needs to be boxed, otherwise it will cause a stack-overflow @@ -221,7 +254,8 @@ impl ChunkData { Ok(state) => Ok(state.into()), }, ) - .collect::, _>>()?; + .collect::, _>>() + .map_err(ChunkParsingError::BlockStateError)?; let block_data = match block_states.data { None => { @@ -277,3 +311,13 @@ impl ChunkData { }) } } + +#[derive(Error, Debug)] +pub enum ChunkParsingError { + #[error("BlockState error: {0}")] + BlockStateError(BlockStateError), + #[error("The chunk isn't generated yet")] + ChunkNotGenerated, + #[error("Error deserializing chunk: {0}")] + ErrorDeserializingChunk(String), +} diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 28c991124..d4024fb0c 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,13 +1,5 @@ -use std::{ - collections::HashMap, - fs::OpenOptions, - io::{Read, Seek}, - path::PathBuf, - sync::Arc, -}; +use std::{collections::HashMap, path::PathBuf, sync::Arc}; -use flate2::{bufread::ZlibDecoder, read::GzDecoder}; -use itertools::Itertools; use parking_lot::Mutex; use pumpkin_core::math::vector2::Vector2; use rayon::prelude::*; @@ -15,7 +7,7 @@ use thiserror::Error; use tokio::sync::mpsc; use crate::{ - chunk::ChunkData, + chunk::{anvil::AnvilChunkReader, ChunkData, ChunkReader, ChunkReadingError}, world_gen::{get_world_gen, Seed, WorldGenerator}, }; @@ -23,7 +15,7 @@ use crate::{ /// /// Key features include: /// -/// - **Chunk Loading:** Efficiently loads chunks from disk (Anvil format). +/// - **Chunk Loading:** Efficiently loads chunks from disk. /// - **Chunk Caching:** Stores accessed chunks in memory for faster access. /// - **Chunk Generation:** Generates new chunks on-demand using a specified `WorldGenerator`. /// @@ -31,77 +23,18 @@ use crate::{ pub struct Level { save_file: Option, loaded_chunks: Arc, Arc>>>, + chunk_reader: Box, world_gen: Box, } -struct SaveFile { +pub struct SaveFile { #[expect(dead_code)] root_folder: PathBuf, - region_folder: PathBuf, -} - -#[derive(Error, Debug)] -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 is invalid")] - RegionIsInvalid, - #[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, + pub region_folder: PathBuf, } #[derive(Error, Debug)] -pub enum CompressionError { - #[error("Compression scheme not recognised")] - UnknownCompression, - #[error("Error while working with zlib compression: {0}")] - ZlibError(std::io::Error), - #[error("Error while working with Gzip compression: {0}")] - GZipError(std::io::Error), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Compression { - Gzip, - Zlib, - None, - LZ4, -} - -impl Compression { - pub fn from_byte(byte: u8) -> Option { - match byte { - 1 => Some(Self::Gzip), - 2 => Some(Self::Zlib), - 3 => Some(Self::None), - 4 => Some(Self::LZ4), - _ => None, - } - } -} +pub enum WorldError {} impl Level { pub fn from_root_folder(root_folder: PathBuf) -> Self { @@ -120,6 +53,7 @@ impl Level { root_folder, region_folder, }), + chunk_reader: Box::new(AnvilChunkReader::new()), loaded_chunks: Arc::new(Mutex::new(HashMap::new())), } } else { @@ -130,6 +64,7 @@ impl Level { Self { world_gen, save_file: None, + chunk_reader: Box::new(AnvilChunkReader::new()), loaded_chunks: Arc::new(Mutex::new(HashMap::new())), } } @@ -164,8 +99,8 @@ impl Level { let at = *at; let data = match &self.save_file { Some(save_file) => { - match Self::read_chunk(save_file, at) { - Err(WorldError::ChunkNotGenerated(_)) => { + match self.chunk_reader.read_chunk(save_file, at) { + Err(ChunkReadingError::ChunkNotExist) => { // This chunk was not generated yet. Ok(self.world_gen.generate_chunk(at)) } @@ -186,103 +121,4 @@ impl Level { loaded_chunks.insert(at, data); }) } - - fn read_chunk(save_file: &SaveFile, at: Vector2) -> Result { - let region = ( - ((at.x as f32) / 32.0).floor() as i32, - ((at.z as f32) / 32.0).floor() as i32, - ); - - 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) - } - 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, - )); - } - - // Read the file using the offset and size - let mut file_buf = { - region_file - .seek(std::io::SeekFrom::Start(offset)) - .map_err(|_| WorldError::RegionIsInvalid)?; - let mut out = vec![0; size]; - region_file - .read_exact(&mut out) - .map_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 = Compression::from_byte(header[4]) - .ok_or_else(|| 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)?; - - ChunkData::from_bytes(decompressed_chunk, at) - } - - fn decompress_data( - compression: Compression, - compressed_data: Vec, - ) -> Result, CompressionError> { - match compression { - Compression::Gzip => { - let mut z = GzDecoder::new(&compressed_data[..]); - let mut chunk_data = Vec::with_capacity(compressed_data.len()); - z.read_to_end(&mut chunk_data) - .map_err(CompressionError::GZipError)?; - Ok(chunk_data) - } - Compression::Zlib => { - let mut z = ZlibDecoder::new(&compressed_data[..]); - let mut chunk_data = Vec::with_capacity(compressed_data.len()); - z.read_to_end(&mut chunk_data) - .map_err(CompressionError::ZlibError)?; - Ok(chunk_data) - } - Compression::None => Ok(compressed_data), - Compression::LZ4 => todo!(), - } - } } diff --git a/pumpkin-world/src/world_gen/generator.rs b/pumpkin-world/src/world_gen/generator.rs index 36f3f9553..eb7467723 100644 --- a/pumpkin-world/src/world_gen/generator.rs +++ b/pumpkin-world/src/world_gen/generator.rs @@ -1,6 +1,5 @@ use noise::Perlin; use pumpkin_core::math::vector2::Vector2; -use static_assertions::assert_obj_safe; use crate::biome::Biome; use crate::block::block_state::BlockState; @@ -15,7 +14,6 @@ pub trait GeneratorInit { pub trait WorldGenerator: Sync + Send { fn generate_chunk(&self, at: Vector2) -> ChunkData; } -assert_obj_safe! {WorldGenerator} pub(crate) trait BiomeGenerator: Sync + Send { fn generate_biome(&self, at: XZBlockCoordinates) -> Biome;