Add abstraction to support multiple world formats

This commit is contained in:
Snowiiii
2024-10-14 15:57:37 +02:00
parent 41e5355223
commit df80ac2837
8 changed files with 219 additions and 198 deletions

7
Cargo.lock generated
View File

@@ -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"

View File

@@ -18,7 +18,6 @@ version = "0.1.0"
edition = "2021"
[profile.release]
debug = 1
lto = true
codegen-units = 1

View File

@@ -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

View File

@@ -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<String, String>>,
) -> Result<Self, WorldError> {
) -> Result<Self, BlockStateError> {
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,
}

View File

@@ -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<Self> {
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<u8>) -> Result<Vec<u8>, 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<i32>,
) -> Result<super::ChunkData, ChunkReadingError> {
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)
}
}

View File

@@ -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<i32>,
) -> Result<ChunkData, ChunkReadingError>;
}
#[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<i32>,
@@ -188,18 +223,16 @@ impl Index<ChunkRelativeBlockCoordinates> for ChunkBlocks {
}
impl ChunkData {
pub fn from_bytes(chunk_data: Vec<u8>, at: Vector2<i32>) -> Result<Self, WorldError> {
pub fn from_bytes(chunk_data: Vec<u8>, at: Vector2<i32>) -> Result<Self, ChunkParsingError> {
if fastnbt::from_bytes::<ChunkStatus>(&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::<ChunkNbt>(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::<Result<Vec<_>, _>>()?;
.collect::<Result<Vec<_>, _>>()
.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),
}

View File

@@ -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<SaveFile>,
loaded_chunks: Arc<Mutex<HashMap<Vector2<i32>, Arc<ChunkData>>>>,
chunk_reader: Box<dyn ChunkReader>,
world_gen: Box<dyn WorldGenerator>,
}
struct SaveFile {
pub struct SaveFile {
#[expect(dead_code)]
root_folder: PathBuf,
region_folder: PathBuf,
pub 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,
}
#[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<Self> {
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<i32>) -> Result<ChunkData, WorldError> {
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<u8>,
) -> Result<Vec<u8>, 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!(),
}
}
}

View File

@@ -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<i32>) -> ChunkData;
}
assert_obj_safe! {WorldGenerator}
pub(crate) trait BiomeGenerator: Sync + Send {
fn generate_biome(&self, at: XZBlockCoordinates) -> Biome;