Add abstraction to support multiple world formats
This commit is contained in:
parent
41e5355223
commit
df80ac2837
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -2032,7 +2032,6 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"static_assertions",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -2656,12 +2655,6 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|||||||
@ -18,7 +18,6 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = 1
|
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,6 @@ futures = "0.3"
|
|||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
static_assertions = "1.1.0"
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::level::WorldError;
|
use thiserror::Error;
|
||||||
|
|
||||||
use super::block_registry::{Block, BlockCategory, BLOCKS};
|
use super::block_registry::{Block, BlockCategory, BLOCKS};
|
||||||
|
|
||||||
@ -21,16 +21,16 @@ impl BlockState {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
registry_id: &str,
|
registry_id: &str,
|
||||||
properties: Option<&HashMap<String, String>>,
|
properties: Option<&HashMap<String, String>>,
|
||||||
) -> Result<Self, WorldError> {
|
) -> Result<Self, BlockStateError> {
|
||||||
let block_registry = BLOCKS
|
let block_registry = BLOCKS
|
||||||
.get(registry_id)
|
.get(registry_id)
|
||||||
.ok_or(WorldError::BlockIdentifierNotFound)?;
|
.ok_or(BlockStateError::BlockIdentifierNotFound)?;
|
||||||
let mut block_states = block_registry.states.iter();
|
let mut block_states = block_registry.states.iter();
|
||||||
|
|
||||||
let block_state = match properties {
|
let block_state = match properties {
|
||||||
Some(properties) => block_states
|
Some(properties) => block_states
|
||||||
.find(|state| &state.properties == properties)
|
.find(|state| &state.properties == properties)
|
||||||
.ok_or_else(|| WorldError::BlockStateIdNotFound)?,
|
.ok_or(BlockStateError::BlockStateIdNotFound)?,
|
||||||
None => block_states
|
None => block_states
|
||||||
.find(|state| state.is_default)
|
.find(|state| state.is_default)
|
||||||
.expect("Every Block should have at least 1 default state"),
|
.expect("Every Block should have at least 1 default state"),
|
||||||
@ -71,3 +71,11 @@ impl BlockState {
|
|||||||
self.category == category
|
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,
|
||||||
|
}
|
||||||
|
|||||||
144
pumpkin-world/src/chunk/anvil.rs
Normal file
144
pumpkin-world/src/chunk/anvil.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,18 +5,53 @@ use std::ops::Index;
|
|||||||
use fastnbt::LongArray;
|
use fastnbt::LongArray;
|
||||||
use pumpkin_core::math::vector2::Vector2;
|
use pumpkin_core::math::vector2::Vector2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
block::{BlockId, BlockState},
|
block::{block_state::BlockStateError, BlockId, BlockState},
|
||||||
coordinates::{ChunkRelativeBlockCoordinates, Height},
|
coordinates::{ChunkRelativeBlockCoordinates, Height},
|
||||||
level::{ChunkNotGeneratedError, WorldError},
|
level::SaveFile,
|
||||||
WORLD_HEIGHT,
|
WORLD_HEIGHT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod anvil;
|
||||||
|
|
||||||
const CHUNK_AREA: usize = 16 * 16;
|
const CHUNK_AREA: usize = 16 * 16;
|
||||||
const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16;
|
const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16;
|
||||||
const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT;
|
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 struct ChunkData {
|
||||||
pub blocks: ChunkBlocks,
|
pub blocks: ChunkBlocks,
|
||||||
pub position: Vector2<i32>,
|
pub position: Vector2<i32>,
|
||||||
@ -188,18 +223,16 @@ impl Index<ChunkRelativeBlockCoordinates> for ChunkBlocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChunkData {
|
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.")
|
if fastnbt::from_bytes::<ChunkStatus>(&chunk_data).expect("Failed reading chunk status.")
|
||||||
!= ChunkStatus::Full
|
!= ChunkStatus::Full
|
||||||
{
|
{
|
||||||
return Err(WorldError::ChunkNotGenerated(
|
return Err(ChunkParsingError::ChunkNotGenerated);
|
||||||
ChunkNotGeneratedError::IncompleteGeneration,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let chunk_data = match fastnbt::from_bytes::<ChunkNbt>(chunk_data.as_slice()) {
|
let chunk_data = match fastnbt::from_bytes::<ChunkNbt>(chunk_data.as_slice()) {
|
||||||
Ok(v) => v,
|
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
|
// this needs to be boxed, otherwise it will cause a stack-overflow
|
||||||
@ -221,7 +254,8 @@ impl ChunkData {
|
|||||||
Ok(state) => Ok(state.into()),
|
Ok(state) => Ok(state.into()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(ChunkParsingError::BlockStateError)?;
|
||||||
|
|
||||||
let block_data = match block_states.data {
|
let block_data = match block_states.data {
|
||||||
None => {
|
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),
|
||||||
|
}
|
||||||
@ -1,13 +1,5 @@
|
|||||||
use std::{
|
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||||
collections::HashMap,
|
|
||||||
fs::OpenOptions,
|
|
||||||
io::{Read, Seek},
|
|
||||||
path::PathBuf,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use flate2::{bufread::ZlibDecoder, read::GzDecoder};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use pumpkin_core::math::vector2::Vector2;
|
use pumpkin_core::math::vector2::Vector2;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
@ -15,7 +7,7 @@ use thiserror::Error;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
chunk::ChunkData,
|
chunk::{anvil::AnvilChunkReader, ChunkData, ChunkReader, ChunkReadingError},
|
||||||
world_gen::{get_world_gen, Seed, WorldGenerator},
|
world_gen::{get_world_gen, Seed, WorldGenerator},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,7 +15,7 @@ use crate::{
|
|||||||
///
|
///
|
||||||
/// Key features include:
|
/// 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 Caching:** Stores accessed chunks in memory for faster access.
|
||||||
/// - **Chunk Generation:** Generates new chunks on-demand using a specified `WorldGenerator`.
|
/// - **Chunk Generation:** Generates new chunks on-demand using a specified `WorldGenerator`.
|
||||||
///
|
///
|
||||||
@ -31,77 +23,18 @@ use crate::{
|
|||||||
pub struct Level {
|
pub struct Level {
|
||||||
save_file: Option<SaveFile>,
|
save_file: Option<SaveFile>,
|
||||||
loaded_chunks: Arc<Mutex<HashMap<Vector2<i32>, Arc<ChunkData>>>>,
|
loaded_chunks: Arc<Mutex<HashMap<Vector2<i32>, Arc<ChunkData>>>>,
|
||||||
|
chunk_reader: Box<dyn ChunkReader>,
|
||||||
world_gen: Box<dyn WorldGenerator>,
|
world_gen: Box<dyn WorldGenerator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SaveFile {
|
pub struct SaveFile {
|
||||||
#[expect(dead_code)]
|
#[expect(dead_code)]
|
||||||
root_folder: PathBuf,
|
root_folder: PathBuf,
|
||||||
region_folder: PathBuf,
|
pub region_folder: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum WorldError {
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Level {
|
impl Level {
|
||||||
pub fn from_root_folder(root_folder: PathBuf) -> Self {
|
pub fn from_root_folder(root_folder: PathBuf) -> Self {
|
||||||
@ -120,6 +53,7 @@ impl Level {
|
|||||||
root_folder,
|
root_folder,
|
||||||
region_folder,
|
region_folder,
|
||||||
}),
|
}),
|
||||||
|
chunk_reader: Box::new(AnvilChunkReader::new()),
|
||||||
loaded_chunks: Arc::new(Mutex::new(HashMap::new())),
|
loaded_chunks: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -130,6 +64,7 @@ impl Level {
|
|||||||
Self {
|
Self {
|
||||||
world_gen,
|
world_gen,
|
||||||
save_file: None,
|
save_file: None,
|
||||||
|
chunk_reader: Box::new(AnvilChunkReader::new()),
|
||||||
loaded_chunks: Arc::new(Mutex::new(HashMap::new())),
|
loaded_chunks: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,8 +99,8 @@ impl Level {
|
|||||||
let at = *at;
|
let at = *at;
|
||||||
let data = match &self.save_file {
|
let data = match &self.save_file {
|
||||||
Some(save_file) => {
|
Some(save_file) => {
|
||||||
match Self::read_chunk(save_file, at) {
|
match self.chunk_reader.read_chunk(save_file, at) {
|
||||||
Err(WorldError::ChunkNotGenerated(_)) => {
|
Err(ChunkReadingError::ChunkNotExist) => {
|
||||||
// This chunk was not generated yet.
|
// This chunk was not generated yet.
|
||||||
Ok(self.world_gen.generate_chunk(at))
|
Ok(self.world_gen.generate_chunk(at))
|
||||||
}
|
}
|
||||||
@ -186,103 +121,4 @@ impl Level {
|
|||||||
loaded_chunks.insert(at, data);
|
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!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use noise::Perlin;
|
use noise::Perlin;
|
||||||
use pumpkin_core::math::vector2::Vector2;
|
use pumpkin_core::math::vector2::Vector2;
|
||||||
use static_assertions::assert_obj_safe;
|
|
||||||
|
|
||||||
use crate::biome::Biome;
|
use crate::biome::Biome;
|
||||||
use crate::block::block_state::BlockState;
|
use crate::block::block_state::BlockState;
|
||||||
@ -15,7 +14,6 @@ pub trait GeneratorInit {
|
|||||||
pub trait WorldGenerator: Sync + Send {
|
pub trait WorldGenerator: Sync + Send {
|
||||||
fn generate_chunk(&self, at: Vector2<i32>) -> ChunkData;
|
fn generate_chunk(&self, at: Vector2<i32>) -> ChunkData;
|
||||||
}
|
}
|
||||||
assert_obj_safe! {WorldGenerator}
|
|
||||||
|
|
||||||
pub(crate) trait BiomeGenerator: Sync + Send {
|
pub(crate) trait BiomeGenerator: Sync + Send {
|
||||||
fn generate_biome(&self, at: XZBlockCoordinates) -> Biome;
|
fn generate_biome(&self, at: XZBlockCoordinates) -> Biome;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user