From a967ac4e8711c11a78a0aded8210ff1a218b6a53 Mon Sep 17 00:00:00 2001 From: suprohub Date: Fri, 10 Jan 2025 15:43:48 +0400 Subject: [PATCH 01/13] first non-error version --- pumpkin-world/src/chunk/db/compression.rs | 112 ++++++++ .../src/chunk/db/informative_table.rs | 269 ++++++++++++++++++ pumpkin-world/src/chunk/db/mod.rs | 64 +++++ pumpkin-world/src/chunk/format/anvil.rs | 195 +++++++++++++ pumpkin-world/src/chunk/format/mod.rs | 60 ++++ pumpkin-world/src/chunk/mod.rs | 62 +--- pumpkin-world/src/level.rs | 22 +- 7 files changed, 714 insertions(+), 70 deletions(-) create mode 100644 pumpkin-world/src/chunk/db/compression.rs create mode 100644 pumpkin-world/src/chunk/db/informative_table.rs create mode 100644 pumpkin-world/src/chunk/db/mod.rs create mode 100644 pumpkin-world/src/chunk/format/anvil.rs create mode 100644 pumpkin-world/src/chunk/format/mod.rs diff --git a/pumpkin-world/src/chunk/db/compression.rs b/pumpkin-world/src/chunk/db/compression.rs new file mode 100644 index 000000000..ba53fd0a8 --- /dev/null +++ b/pumpkin-world/src/chunk/db/compression.rs @@ -0,0 +1,112 @@ +use std::io::{Read, Write}; + +use flate2::bufread::{GzDecoder, GzEncoder, ZlibDecoder, ZlibEncoder}; + +use super::CompressionError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Compression { + /// GZip Compression + GZip = 1, + /// ZLib Compression + ZLib = 2, + /// LZ4 Compression (since 24w04a) + LZ4 = 4, + /// Custom compression algorithm (since 24w05a) + Custom = 127, +} + +impl Compression { + /// Returns Ok when a compression is found otherwise an Err + #[allow(clippy::result_unit_err)] + pub fn from_byte(byte: u8) -> Result, ()> { + match byte { + 1 => Ok(Some(Self::GZip)), + 2 => Ok(Some(Self::ZLib)), + // Uncompressed (since a version before 1.15.1) + 3 => Ok(None), + 4 => Ok(Some(Self::LZ4)), + 127 => Ok(Some(Self::Custom)), + // Unknown format + _ => Err(()), + } + } + + pub fn decompress_data(&self, compressed_data: &[u8]) -> Result, CompressionError> { + match self { + Compression::GZip => { + let mut decoder = GzDecoder::new(compressed_data); + let mut chunk_data = Vec::new(); + decoder + .read_to_end(&mut chunk_data) + .map_err(CompressionError::GZipError)?; + Ok(chunk_data) + } + Compression::ZLib => { + let mut decoder = ZlibDecoder::new(compressed_data); + let mut chunk_data = Vec::new(); + decoder + .read_to_end(&mut chunk_data) + .map_err(CompressionError::ZlibError)?; + Ok(chunk_data) + } + Compression::LZ4 => { + let mut decoder = + lz4::Decoder::new(compressed_data).map_err(CompressionError::LZ4Error)?; + let mut decompressed_data = Vec::new(); + decoder + .read_to_end(&mut decompressed_data) + .map_err(CompressionError::LZ4Error)?; + Ok(decompressed_data) + } + Compression::Custom => todo!(), + } + } + + pub fn compress_data( + &self, + uncompressed_data: &[u8], + compression_level: u32, + ) -> Result, CompressionError> { + match self { + Compression::GZip => { + let mut encoder = GzEncoder::new( + uncompressed_data, + flate2::Compression::new(compression_level), + ); + let mut chunk_data = Vec::new(); + encoder + .read_to_end(&mut chunk_data) + .map_err(CompressionError::GZipError)?; + Ok(chunk_data) + } + Compression::ZLib => { + let mut encoder = ZlibEncoder::new( + uncompressed_data, + flate2::Compression::new(compression_level), + ); + let mut chunk_data = Vec::new(); + encoder + .read_to_end(&mut chunk_data) + .map_err(CompressionError::ZlibError)?; + Ok(chunk_data) + } + Compression::LZ4 => { + let mut compressed_data = Vec::new(); + let mut encoder = lz4::EncoderBuilder::new() + .level(compression_level) + .build(&mut compressed_data) + .map_err(CompressionError::LZ4Error)?; + if let Err(err) = encoder.write_all(uncompressed_data) { + return Err(CompressionError::LZ4Error(err)); + } + if let (_output, Err(err)) = encoder.finish() { + return Err(CompressionError::LZ4Error(err)); + } + Ok(compressed_data) + } + Compression::Custom => todo!(), + } + } +} diff --git a/pumpkin-world/src/chunk/db/informative_table.rs b/pumpkin-world/src/chunk/db/informative_table.rs new file mode 100644 index 000000000..7d46a6893 --- /dev/null +++ b/pumpkin-world/src/chunk/db/informative_table.rs @@ -0,0 +1,269 @@ +use std::{ + fs::OpenOptions, + io::{Read, Seek, SeekFrom, Write}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +use crate::chunk::db::{compression::Compression, RawChunkWritingError}; + +use super::{CompressionError, RawChunkReader, RawChunkReadingError, RawChunkWriter}; + +pub struct InformativeTableDB; + +impl RawChunkReader for InformativeTableDB { + fn read_raw_chunk( + &self, + save_file: &crate::level::LevelFolder, + at: &pumpkin_util::math::vector2::Vector2, + ) -> Result, RawChunkReadingError> { + let region = (at.x >> 5, at.z >> 5); + + 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 => RawChunkReadingError::ChunkNotExist, + kind => RawChunkReadingError::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| RawChunkReadingError::IoError(err.kind()))?; + region_file + .read_exact(&mut timestamp_table) + .map_err(|err| RawChunkReadingError::IoError(err.kind()))?; + + let chunk_x = at.x & 0x1F; + let chunk_z = at.z & 0x1F; + let table_entry = (chunk_x + chunk_z * 32) * 4; + + let mut offset = BytesMut::new(); + offset.put_u8(0); + offset.extend_from_slice(&location_table[table_entry as usize..table_entry as usize + 3]); + let offset_at = offset.get_u32() as u64 * 4096; + let size_at = location_table[table_entry as usize + 3] as usize * 4096; + + if offset_at == 0 && size_at == 0 { + return Err(RawChunkReadingError::ChunkNotExist); + } + + // Read the file using the offset and size + let mut file_buf = { + region_file + .seek(std::io::SeekFrom::Start(offset_at)) + .map_err(|_| RawChunkReadingError::RegionIsInvalid)?; + let mut out = vec![0; size_at]; + region_file + .read_exact(&mut out) + .map_err(|_| RawChunkReadingError::RegionIsInvalid)?; + out + }; + + let mut header: Bytes = file_buf.drain(0..5).collect(); + if header.remaining() != 5 { + return Err(RawChunkReadingError::InvalidHeader); + } + + let size = header.get_u32(); + let compression = header.get_u8(); + + let compression = Compression::from_byte(compression) + .map_err(|_| RawChunkReadingError::Compression(CompressionError::UnknownCompression))?; + + // size includes the compression scheme byte, so we need to subtract 1 + let chunk_data: Vec = file_buf.drain(0..size as usize - 1).collect(); + + let decompressed_chunk = if let Some(compression) = compression { + compression + .decompress_data(&chunk_data) + .map_err(RawChunkReadingError::Compression)? + } else { + chunk_data + }; + + Ok(decompressed_chunk) + } +} + +impl RawChunkWriter for InformativeTableDB { + fn write_raw_chunk( + &self, + chunk: Vec, + level_folder: &crate::level::LevelFolder, + at: &pumpkin_util::math::vector2::Vector2, + ) -> Result<(), super::RawChunkWritingError> { + let region = (at.x >> 5, at.z >> 5); + + let mut region_file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open( + level_folder + .region_folder + .join(format!("./r.{}.{}.mca", region.0, region.1)), + ) + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + + // Compress chunk data + let compression = Compression::ZLib; + let compressed_data = compression + .compress_data(&chunk, 6) + .map_err(RawChunkWritingError::Compression)?; + + // Length of compressed data + compression type + let length = compressed_data.len() as u32 + 1; + + // | 0 1 2 3 | 4 | 5.. | + // | length | compression type | compressed data | + let mut chunk_payload = BytesMut::with_capacity(5); + // Payload Header + Body + chunk_payload.put_u32(length); + chunk_payload.put_u8(compression as u8); + chunk_payload.put_slice(&compressed_data); + + // Calculate sector size + let sector_size = chunk_payload.len().div_ceil(4096); + + // Region file header tables + let mut location_table = [0u8; 4096]; + let mut timestamp_table = [0u8; 4096]; + + let file_meta = region_file + .metadata() + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + + // The header consists of 8 KiB of data + // Try to fill the location and timestamp tables if they already exist + if file_meta.len() >= 8192 { + region_file + .read_exact(&mut location_table) + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + region_file + .read_exact(&mut timestamp_table) + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + } + + // Get location table index + let chunk_x = at.x & 0x1F; + let chunk_z = at.z & 0x1F; + let table_index = (chunk_x as usize + chunk_z as usize * 32) * 4; + + // | 0 1 2 | 3 | + // | offset | sector count | + // Get the entry from the current location table and check + // if the new chunk fits in the space of the old chunk + let chunk_location = &location_table[table_index..table_index + 4]; + let chunk_data_location: u64 = if chunk_location[3] >= sector_size as u8 { + // Return old chunk location + u32::from_be_bytes([0, chunk_location[0], chunk_location[1], chunk_location[2]]) as u64 + } else { + // Retrieve next writable sector + self.find_free_sector(&location_table, sector_size) as u64 + }; + + assert!( + chunk_data_location > 1, + "This should never happen. The header would be corrupted" + ); + + // Construct location header + location_table[table_index] = (chunk_data_location >> 16) as u8; + location_table[table_index + 1] = (chunk_data_location >> 8) as u8; + location_table[table_index + 2] = chunk_data_location as u8; + location_table[table_index + 3] = sector_size as u8; + + // Get epoch may result in errors if after the year 2106 :( + let epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as u32; + + // Construct timestamp header + timestamp_table[table_index] = (epoch >> 24) as u8; + timestamp_table[table_index + 1] = (epoch >> 16) as u8; + timestamp_table[table_index + 2] = (epoch >> 8) as u8; + timestamp_table[table_index + 3] = epoch as u8; + + // Write new location and timestamp table + region_file.seek(SeekFrom::Start(0)).unwrap(); + region_file + .write_all(&[location_table, timestamp_table].concat()) + .map_err(|e| RawChunkWritingError::IoError(e.kind()))?; + + // Seek to where the chunk is located + region_file + .seek(SeekFrom::Start(chunk_data_location * 4096)) + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + + // Write header and payload + region_file + .write_all(&chunk_payload) + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + + // Calculate padding to fill the sectors + // (length + 4) 3 bits for length and 1 for compression type + payload length + let padding = ((sector_size * 4096) as u32 - ((length + 4) & 0xFFF)) & 0xFFF; + + // Write padding + region_file + .write_all(&vec![0u8; padding as usize]) + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + + region_file + .flush() + .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + + Ok(()) + } +} + +impl InformativeTableDB { + /// Returns the next free writable sector + /// The sector is absolute which means it always has a spacing of 2 sectors + fn find_free_sector(&self, location_table: &[u8; 4096], sector_size: usize) -> usize { + let mut used_sectors: Vec = Vec::new(); + for i in 0..1024 { + let entry_offset = i * 4; + let location_offset = u32::from_be_bytes([ + 0, + location_table[entry_offset], + location_table[entry_offset + 1], + location_table[entry_offset + 2], + ]) as u64; + let length = location_table[entry_offset + 3] as u64; + let sector_count = location_offset; + for used_sector in sector_count..sector_count + length { + used_sectors.push(used_sector as u16); + } + } + + if used_sectors.is_empty() { + return 2; + } + + used_sectors.sort(); + + let mut prev_sector = &used_sectors[0]; + for sector in used_sectors[1..].iter() { + // Iterate over consecutive pairs + if sector - prev_sector > sector_size as u16 { + return (prev_sector + 1) as usize; + } + prev_sector = sector; + } + + (*used_sectors.last().unwrap() + 1) as usize + } +} diff --git a/pumpkin-world/src/chunk/db/mod.rs b/pumpkin-world/src/chunk/db/mod.rs new file mode 100644 index 000000000..83ed967da --- /dev/null +++ b/pumpkin-world/src/chunk/db/mod.rs @@ -0,0 +1,64 @@ +pub mod compression; +pub mod informative_table; + +use pumpkin_util::math::vector2::Vector2; +use thiserror::Error; + +use crate::level::LevelFolder; + +use super::ChunkParsingError; + +pub trait RawChunkReader: Sync + Send { + fn read_raw_chunk( + &self, + save_file: &LevelFolder, + at: &Vector2, + ) -> Result, RawChunkReadingError>; +} + +pub trait RawChunkWriter: Send + Sync { + fn write_raw_chunk( + &self, + chunk: Vec, + level_folder: &LevelFolder, + at: &Vector2, + ) -> Result<(), RawChunkWritingError>; +} + +#[derive(Error, Debug)] +pub enum RawChunkReadingError { + #[error("Io error: {0}")] + IoError(std::io::ErrorKind), + #[error("Invalid header")] + InvalidHeader, + #[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 RawChunkWritingError { + #[error("Io error: {0}")] + IoError(std::io::ErrorKind), + #[error("Compression error {0}")] + Compression(CompressionError), + #[error("Chunk serializing error: {0}")] + ChunkSerializingError(String), +} + +#[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), + #[error("Error while working with LZ4 compression: {0}")] + LZ4Error(std::io::Error), +} diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs new file mode 100644 index 000000000..8ab8b00eb --- /dev/null +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -0,0 +1,195 @@ +use fastnbt::LongArray; +use indexmap::IndexMap; +use pumpkin_util::math::ceil_log2; +use pumpkin_util::math::vector2::Vector2; +use std::collections::HashSet; + +use crate::block::block_registry::BLOCK_ID_TO_REGISTRY_ID; +use crate::chunk::{ + ChunkData, ChunkNbt, ChunkSection, ChunkSectionBlockStates, ChunkStatus, PaletteEntry, + WORLD_DATA_VERSION, +}; + +use super::{ChunkReader, ChunkReadingError, ChunkWriter, ChunkWritingError}; + +#[derive(Clone, Default)] +pub struct AnvilChunkFormat; + +impl ChunkReader for AnvilChunkFormat { + fn read_chunk( + &self, + chunk_bytes: Vec, + at: &Vector2, + ) -> Result { + ChunkData::from_bytes(&chunk_bytes, *at).map_err(ChunkReadingError::ParsingError) + } +} + +impl ChunkWriter for AnvilChunkFormat { + fn write_chunk( + &self, + chunk_data: &ChunkData, + _at: &Vector2, + ) -> Result, super::ChunkWritingError> { + let mut sections = Vec::new(); + + for (i, blocks) in chunk_data.subchunks.array_iter().enumerate() { + // get unique blocks + let unique_blocks: HashSet<_> = blocks.iter().collect(); + + let palette: IndexMap<_, _> = unique_blocks + .into_iter() + .enumerate() + .map(|(i, block)| { + let name = BLOCK_ID_TO_REGISTRY_ID.get(block).unwrap().as_str(); + (block, (name, i)) + }) + .collect(); + + // Determine the number of bits needed to represent the largest index in the palette + let block_bit_size = if palette.len() < 16 { + 4 + } else { + ceil_log2(palette.len() as u32).max(4) + }; + + let mut section_longs = Vec::new(); + let mut current_pack_long: i64 = 0; + let mut bits_used_in_pack: u32 = 0; + + // Empty data if the palette only contains one index https://minecraft.fandom.com/wiki/Chunk_format + // if palette.len() > 1 {} + // TODO: Update to write empty data. Rn or read does not handle this elegantly + for block in blocks.iter() { + // Push if next bit does not fit + if bits_used_in_pack + block_bit_size as u32 > 64 { + section_longs.push(current_pack_long); + current_pack_long = 0; + bits_used_in_pack = 0; + } + let index = palette.get(block).expect("Just added all unique").1; + current_pack_long |= (index as i64) << bits_used_in_pack; + bits_used_in_pack += block_bit_size as u32; + + assert!(bits_used_in_pack <= 64); + + // If the current 64-bit integer is full, push it to the section_longs and start a new one + if bits_used_in_pack >= 64 { + section_longs.push(current_pack_long); + current_pack_long = 0; + bits_used_in_pack = 0; + } + } + + // Push the last 64-bit integer if it contains any data + if bits_used_in_pack > 0 { + section_longs.push(current_pack_long); + } + + sections.push(ChunkSection { + y: i as i8 - 4, + block_states: Some(ChunkSectionBlockStates { + data: Some(LongArray::new(section_longs)), + palette: palette + .into_iter() + .map(|entry| PaletteEntry { + name: entry.1 .0.to_owned(), + properties: None, + }) + .collect(), + }), + }); + } + + let nbt = ChunkNbt { + data_version: WORLD_DATA_VERSION, + x_pos: chunk_data.position.x, + z_pos: chunk_data.position.z, + status: ChunkStatus::Full, + heightmaps: chunk_data.heightmap.clone(), + sections, + }; + + fastnbt::to_bytes(&nbt).map_err(|e| ChunkWritingError::ChunkSerializingError(e.to_string())) + } +} + +/*#[cfg(test)] +mod tests { + use pumpkin_util::math::vector2::Vector2; + use std::fs; + use std::path::PathBuf; + + use crate::chunk::ChunkWriter; + use crate::generation::{get_world_gen, Seed}; + use crate::{ + chunk::{format::anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError}, + level::LevelFolder, + }; + + #[test] + fn not_existing() { + let region_path = PathBuf::from("not_existing"); + let result = AnvilChunkFormat.read_chunk( + &LevelFolder { + root_folder: PathBuf::from(""), + region_folder: region_path, + }, + &Vector2::new(0, 0), + ); + assert!(matches!(result, Err(ChunkReadingError::ChunkNotExist))); + } + + #[test] + fn test_writing() { + let generator = get_world_gen(Seed(0)); + let level_folder = LevelFolder { + root_folder: PathBuf::from("./tmp"), + region_folder: PathBuf::from("./tmp/region"), + }; + if fs::exists(&level_folder.root_folder).unwrap() { + fs::remove_dir_all(&level_folder.root_folder).expect("Could not delete directory"); + } + + fs::create_dir_all(&level_folder.region_folder).expect("Could not create directory"); + + // Generate chunks + let mut chunks = vec![]; + for x in -5..5 { + for y in -5..5 { + let position = Vector2::new(x, y); + chunks.push((position, generator.generate_chunk(position))); + } + } + + for i in 0..5 { + println!("Iteration {}", i + 1); + for (at, chunk) in &chunks { + AnvilChunkFormat + .write_chunk(chunk, &level_folder, at) + .expect("Failed to write chunk"); + } + + let mut read_chunks = vec![]; + for (at, _chunk) in &chunks { + read_chunks.push( + AnvilChunkFormat + .read_chunk(&level_folder, at) + .expect("Could not read chunk"), + ); + } + + for (at, chunk) in &chunks { + let read_chunk = read_chunks + .iter() + .find(|chunk| chunk.position == *at) + .expect("Missing chunk"); + assert_eq!(chunk.subchunks, read_chunk.subchunks, "Chunks don't match"); + } + } + + fs::remove_dir_all(&level_folder.root_folder).expect("Could not delete directory"); + + println!("Checked chunks successfully"); + } +}*/ diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs new file mode 100644 index 000000000..06d4e45df --- /dev/null +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -0,0 +1,60 @@ +pub mod anvil; + +use pumpkin_util::math::vector2::Vector2; +use thiserror::Error; + +use super::{ChunkData, ChunkParsingError}; + +pub trait ChunkReader: Sync + Send { + fn read_chunk( + &self, + chunk_bytes: Vec, + at: &Vector2, + ) -> Result; +} + +pub trait ChunkWriter: Send + Sync { + fn write_chunk( + &self, + chunk_data: &ChunkData, + at: &Vector2, + ) -> Result, ChunkWritingError>; +} + +#[derive(Error, Debug)] +pub enum ChunkReadingError { + #[error("Io error: {0}")] + IoError(std::io::ErrorKind), + #[error("Invalid header")] + InvalidHeader, + #[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 ChunkWritingError { + #[error("Io error: {0}")] + IoError(std::io::ErrorKind), + #[error("Compression error {0}")] + Compression(CompressionError), + #[error("Chunk serializing error: {0}")] + ChunkSerializingError(String), +} + +#[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), + #[error("Error while working with LZ4 compression: {0}")] + LZ4Error(std::io::Error), +} diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 53bce75e0..dc66f40b5 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -8,72 +8,20 @@ use thiserror::Error; use crate::{ block::BlockState, coordinates::{ChunkRelativeBlockCoordinates, Height}, - level::LevelFolder, WORLD_HEIGHT, }; -pub mod anvil; +pub mod db; +pub mod format; + +// 1.21.4 +const WORLD_DATA_VERSION: i32 = 4189; pub const CHUNK_AREA: usize = 16 * 16; pub const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; pub const SUBCHUNKS_COUNT: usize = WORLD_HEIGHT / 16; pub const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; -pub trait ChunkReader: Sync + Send { - fn read_chunk( - &self, - save_file: &LevelFolder, - at: &Vector2, - ) -> Result; -} - -pub trait ChunkWriter: Send + Sync { - fn write_chunk( - &self, - chunk: &ChunkData, - level_folder: &LevelFolder, - at: &Vector2, - ) -> Result<(), ChunkWritingError>; -} - -#[derive(Error, Debug)] -pub enum ChunkReadingError { - #[error("Io error: {0}")] - IoError(std::io::ErrorKind), - #[error("Invalid header")] - InvalidHeader, - #[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 ChunkWritingError { - #[error("Io error: {0}")] - IoError(std::io::ErrorKind), - #[error("Compression error {0}")] - Compression(CompressionError), - #[error("Chunk serializing error: {0}")] - ChunkSerializingError(String), -} - -#[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), - #[error("Error while working with LZ4 compression: {0}")] - LZ4Error(std::io::Error), -} - pub struct ChunkData { /// See description in `Subchunks` pub subchunks: Subchunks, diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 0f705129b..863d041b7 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -11,8 +11,8 @@ use tokio::{ use crate::{ chunk::{ - anvil::AnvilChunkFormat, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError, - ChunkWriter, + format::{anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError, ChunkWriter}, + ChunkData, }, generation::{get_world_gen, Seed, WorldGenerator}, lock::{anvil::AnvilLevelLocker, LevelLocker}, @@ -206,10 +206,7 @@ impl Level { pub async fn write_chunk(&self, chunk_to_write: (Vector2, Arc>)) { let data = chunk_to_write.1.read().await; - if let Err(error) = - self.chunk_writer - .write_chunk(&data, &self.level_folder, &chunk_to_write.0) - { + if let Err(error) = self.chunk_writer.write_chunk(&data, &chunk_to_write.0) { log::error!("Failed writing Chunk to disk {}", error.to_string()); } } @@ -219,7 +216,8 @@ impl Level { save_file: &LevelFolder, chunk_pos: Vector2, ) -> Result>>, ChunkReadingError> { - match chunk_reader.read_chunk(save_file, &chunk_pos) { + todo!() + /*match chunk_reader.read_chunk(save_file, &chunk_pos) { Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), Err( ChunkReadingError::ChunkNotExist @@ -229,7 +227,7 @@ impl Level { Ok(None) } Err(err) => Err(err), - } + }*/ } /// Reads/Generates many chunks in a world @@ -258,11 +256,9 @@ impl Level { Ok(chunk) => { // Save new Chunk if let Some(chunk) = &chunk { - if let Err(error) = chunk_writer.write_chunk( - &chunk.blocking_read(), - &level_folder, - &chunk_pos, - ) { + if let Err(error) = + chunk_writer.write_chunk(&chunk.blocking_read(), &chunk_pos) + { log::error!( "Failed writing Chunk to disk {}", error.to_string() From dc78db4b2d6b1a13120ce8fed7aa575172329ba6 Mon Sep 17 00:00:00 2001 From: suprohub Date: Fri, 10 Jan 2025 15:54:32 +0400 Subject: [PATCH 02/13] first working version --- pumpkin-world/src/level.rs | 90 ++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 863d041b7..1aae80ee3 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -11,8 +11,11 @@ use tokio::{ use crate::{ chunk::{ + db::{ + informative_table::InformativeTableDB, RawChunkReader, RawChunkWriter, + }, format::{anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError, ChunkWriter}, - ChunkData, + ChunkData, ChunkParsingError, }, generation::{get_world_gen, Seed, WorldGenerator}, lock::{anvil::AnvilLevelLocker, LevelLocker}, @@ -37,6 +40,8 @@ pub struct Level { chunk_watchers: Arc, usize>>, chunk_reader: Arc, chunk_writer: Arc, + raw_chunk_reader: Arc, + raw_chunk_writer: Arc, world_gen: Arc, // Gets unlocked when dropped // TODO: Make this a trait @@ -79,6 +84,8 @@ impl Level { level_folder, chunk_reader: Arc::new(AnvilChunkFormat), chunk_writer: Arc::new(AnvilChunkFormat), + raw_chunk_reader: Arc::new(InformativeTableDB), + raw_chunk_writer: Arc::new(InformativeTableDB), loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), level_info, @@ -212,12 +219,17 @@ impl Level { } fn load_chunk_from_save( + raw_chunk_reader: Arc, chunk_reader: Arc, save_file: &LevelFolder, chunk_pos: Vector2, ) -> Result>>, ChunkReadingError> { - todo!() - /*match chunk_reader.read_chunk(save_file, &chunk_pos) { + match chunk_reader.read_chunk( + raw_chunk_reader + .read_raw_chunk(save_file, &chunk_pos) + .map_err(|_| ChunkReadingError::RegionIsInvalid)?, + &chunk_pos, + ) { Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), Err( ChunkReadingError::ChunkNotExist @@ -227,7 +239,7 @@ impl Level { Ok(None) } Err(err) => Err(err), - }*/ + } } /// Reads/Generates many chunks in a world @@ -243,6 +255,8 @@ impl Level { let loaded_chunks = self.loaded_chunks.clone(); let chunk_reader = self.chunk_reader.clone(); let chunk_writer = self.chunk_writer.clone(); + let raw_chunk_reader = self.raw_chunk_reader.clone(); + let raw_chunk_writer = self.raw_chunk_writer.clone(); let level_folder = self.level_folder.clone(); let world_gen = self.world_gen.clone(); let chunk_pos = *at; @@ -251,38 +265,46 @@ impl Level { .get(&chunk_pos) .map(|entry| entry.value().clone()) .unwrap_or_else(|| { - let loaded_chunk = - match Self::load_chunk_from_save(chunk_reader, &level_folder, chunk_pos) { - Ok(chunk) => { - // Save new Chunk - if let Some(chunk) = &chunk { - if let Err(error) = - chunk_writer.write_chunk(&chunk.blocking_read(), &chunk_pos) - { - log::error!( - "Failed writing Chunk to disk {}", - error.to_string() - ); - }; - } - chunk - } - Err(err) => { - log::error!( - "Failed to read chunk (regenerating) {:?}: {:?}", - chunk_pos, - err - ); - None + let loaded_chunk = match Self::load_chunk_from_save( + raw_chunk_reader, + chunk_reader, + &level_folder, + chunk_pos, + ) { + Ok(chunk) => { + // Save new Chunk + if let Some(chunk) = &chunk { + if let Err(error) = raw_chunk_writer.write_raw_chunk( + chunk_writer + .write_chunk(&chunk.blocking_read(), &chunk_pos) + .unwrap(), + &level_folder, + &chunk_pos, + ) { + log::error!( + "Failed writing Chunk to disk {}", + error.to_string() + ); + }; } + chunk } - .unwrap_or_else(|| { - Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos))) - // Arc::new(RwLock::new(ChunkData { - // blocks: ChunkBlocks::default(), - // position: chunk_pos, - // })) - }); + Err(err) => { + log::error!( + "Failed to read chunk (regenerating) {:?}: {:?}", + chunk_pos, + err + ); + None + } + } + .unwrap_or_else(|| { + Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos))) + // Arc::new(RwLock::new(ChunkData { + // blocks: ChunkBlocks::default(), + // position: chunk_pos, + // })) + }); if let Some(data) = loaded_chunks.get(&chunk_pos) { // Another thread populated in between the previous check and now From 1415079bd758d4eb5dc2bfc9a9b8ba892a50ea44 Mon Sep 17 00:00:00 2001 From: suprohub Date: Fri, 10 Jan 2025 16:59:17 +0400 Subject: [PATCH 03/13] separate anvil stuff from mod.rs in chunk/ --- pumpkin-world/src/chunk/format/anvil.rs | 185 +++++++++++++++++++++++- pumpkin-world/src/chunk/mod.rs | 165 +-------------------- pumpkin-world/src/level.rs | 6 +- 3 files changed, 188 insertions(+), 168 deletions(-) diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 8ab8b00eb..aba96c6a3 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -2,26 +2,205 @@ use fastnbt::LongArray; use indexmap::IndexMap; use pumpkin_util::math::ceil_log2; use pumpkin_util::math::vector2::Vector2; -use std::collections::HashSet; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use thiserror::Error; use crate::block::block_registry::BLOCK_ID_TO_REGISTRY_ID; +use crate::block::BlockState; use crate::chunk::{ - ChunkData, ChunkNbt, ChunkSection, ChunkSectionBlockStates, ChunkStatus, PaletteEntry, + ChunkData, ChunkHeightmaps, ChunkParsingError, Subchunks, CHUNK_AREA, SUBCHUNK_VOLUME, WORLD_DATA_VERSION, }; +use crate::coordinates::{ChunkRelativeBlockCoordinates, Height}; use super::{ChunkReader, ChunkReadingError, ChunkWriter, ChunkWritingError}; #[derive(Clone, Default)] pub struct AnvilChunkFormat; +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "PascalCase")] +struct PaletteEntry { + // block name + name: String, + properties: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +struct ChunkSection { + #[serde(rename = "Y")] + y: i8, + block_states: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct ChunkSectionBlockStates { + // #[serde(with = "LongArray")] + data: Option, + palette: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +struct ChunkNbt { + data_version: i32, + #[serde(rename = "xPos")] + x_pos: i32, + // #[serde(rename = "yPos")] + //y_pos: i32, + #[serde(rename = "zPos")] + z_pos: i32, + status: ChunkStatus, + #[serde(rename = "sections")] + sections: Vec, + heightmaps: ChunkHeightmaps, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +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:features")] + Features, + #[serde(rename = "minecraft:initialize_light")] + InitLight, + #[serde(rename = "minecraft:light")] + Light, + #[serde(rename = "minecraft:spawn")] + Spawn, + #[serde(rename = "minecraft:full")] + Full, +} + +// I can't use an tag because it will break ChunkNBT, but status need to have a big S, so "Status" +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +pub struct ChunkStatusWrapper { + status: ChunkStatus, +} + +#[derive(Error, Debug)] +pub enum ChunkSerializingError { + #[error("Error serializing chunk: {0}")] + ErrorSerializingChunk(fastnbt::error::Error), +} + impl ChunkReader for AnvilChunkFormat { fn read_chunk( &self, chunk_bytes: Vec, at: &Vector2, ) -> Result { - ChunkData::from_bytes(&chunk_bytes, *at).map_err(ChunkReadingError::ParsingError) + if fastnbt::from_bytes::(&chunk_bytes) + .map_err(|_| ChunkReadingError::InvalidHeader)? + .status + != ChunkStatus::Full + { + return Err(ChunkReadingError::ChunkNotExist); + } + + let chunk_data = fastnbt::from_bytes::(&chunk_bytes).map_err(|e| { + ChunkReadingError::ParsingError(ChunkParsingError::ErrorDeserializingChunk( + e.to_string(), + )) + })?; + + if chunk_data.x_pos != at.x || chunk_data.z_pos != at.z { + log::error!( + "Expected chunk at {}:{}, but got {}:{}", + at.x, + at.z, + chunk_data.x_pos, + chunk_data.z_pos + ); + // lets still continue + } + + let mut subchunks = Subchunks::Single(0); + let mut block_index = 0; // which block we're currently at + + for section in chunk_data.sections.into_iter() { + let block_states = match section.block_states { + Some(states) => states, + 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| match BlockState::new(&entry.name) { + // Block not found, Often the case when World has an newer or older version then block registry + None => BlockState::AIR, + Some(state) => state, + }) + .collect::>(); + + let block_data = match block_states.data { + 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, + }; + + // How many bits each block has in one of the palette u64s + let block_bit_size = if palette.len() < 16 { + 4 + } else { + ceil_log2(palette.len() as u32).max(4) + }; + // How many blocks there are in one of the palettes u64s + let blocks_in_palette = 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_palette { + 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 initialized the heightmap of `blocks` + // from the cached value in the world file + subchunks.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.get_id(), + ); + + block_index += 1; + + // if `SUBCHUNK_VOLUME `is not divisible by `blocks_in_palette` the block_data + // can sometimes spill into other subchunks. We avoid that by aborting early + if (block_index % SUBCHUNK_VOLUME) == 0 { + break 'block_loop; + } + } + } + } + + Ok(ChunkData { + subchunks, + heightmap: chunk_data.heightmaps, + position: *at, + }) } } diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index dc66f40b5..fc8de86d8 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -1,15 +1,10 @@ use fastnbt::LongArray; -use pumpkin_data::chunk::ChunkStatus; -use pumpkin_util::math::{ceil_log2, vector2::Vector2}; +use pumpkin_util::math::vector2::Vector2; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, iter::repeat_with}; +use std::iter::repeat_with; use thiserror::Error; -use crate::{ - block::BlockState, - coordinates::{ChunkRelativeBlockCoordinates, Height}, - WORLD_HEIGHT, -}; +use crate::{coordinates::ChunkRelativeBlockCoordinates, WORLD_HEIGHT}; pub mod db; pub mod format; @@ -63,14 +58,7 @@ pub enum Subchunk { Multi(Box<[u16; SUBCHUNK_VOLUME]>), } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "PascalCase")] -struct PaletteEntry { - // block name - name: String, - properties: Option>, -} - +// TODO: Separate heightmaps from anvil saving and ram, because its not optimized #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "UPPERCASE")] pub struct ChunkHeightmaps { @@ -80,36 +68,6 @@ pub struct ChunkHeightmaps { world_surface: LongArray, } -#[derive(Serialize, Deserialize, Debug)] -struct ChunkSection { - #[serde(rename = "Y")] - y: i8, - block_states: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -struct ChunkSectionBlockStates { - // #[serde(with = "LongArray")] - data: Option, - palette: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "PascalCase")] -struct ChunkNbt { - data_version: i32, - #[serde(rename = "xPos")] - x_pos: i32, - // #[serde(rename = "yPos")] - //y_pos: i32, - #[serde(rename = "zPos")] - z_pos: i32, - status: ChunkStatus, - #[serde(rename = "sections")] - sections: Vec, - heightmaps: ChunkHeightmaps, -} - /// The Heightmap for a completely empty chunk impl Default for ChunkHeightmaps { fn default() -> Self { @@ -271,116 +229,6 @@ impl ChunkData { } } -// I can't use an tag because it will break ChunkNBT, but status need to have a big S, so "Status" -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "PascalCase")] -pub struct ChunkStatusWrapper { - status: ChunkStatus, -} - -impl ChunkData { - pub fn from_bytes( - chunk_data: &[u8], - position: Vector2, - ) -> Result { - if fastnbt::from_bytes::(chunk_data) - .map_err(|_| ChunkParsingError::FailedReadStatus)? - .status - != ChunkStatus::Full - { - return Err(ChunkParsingError::ChunkNotGenerated); - } - - let chunk_data = fastnbt::from_bytes::(chunk_data) - .map_err(|e| ChunkParsingError::ErrorDeserializingChunk(e.to_string()))?; - - if chunk_data.x_pos != position.x || chunk_data.z_pos != position.z { - log::error!( - "Expected chunk at {}:{}, but got {}:{}", - position.x, - position.z, - chunk_data.x_pos, - chunk_data.z_pos - ); - // lets still continue - } - - // this needs to be boxed, otherwise it will cause a stack-overflow - let mut subchunks = Subchunks::Single(0); - let mut block_index = 0; // which block we're currently at - - for section in chunk_data.sections.into_iter() { - let block_states = match section.block_states { - Some(states) => states, - 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| match BlockState::new(&entry.name) { - // Block not found, Often the case when World has an newer or older version then block registry - None => BlockState::AIR, - Some(state) => state, - }) - .collect::>(); - - let block_data = match block_states.data { - 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, - }; - - // How many bits each block has in one of the palette u64s - let block_bit_size = if palette.len() < 16 { - 4 - } else { - ceil_log2(palette.len() as u32).max(4) - }; - // How many blocks there are in one of the palettes u64s - let blocks_in_palette = 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_palette { - 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 initialized the heightmap of `blocks` - // from the cached value in the world file - subchunks.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.get_id(), - ); - - block_index += 1; - - // if `SUBCHUNK_VOLUME `is not divisible by `blocks_in_palette` the block_data - // can sometimes spill into other subchunks. We avoid that by aborting early - if (block_index % SUBCHUNK_VOLUME) == 0 { - break 'block_loop; - } - } - } - } - - Ok(ChunkData { - subchunks, - heightmap: chunk_data.heightmaps, - position, - }) - } -} - #[derive(Error, Debug)] pub enum ChunkParsingError { #[error("Failed reading chunk status")] @@ -395,8 +243,3 @@ fn convert_index(index: ChunkRelativeBlockCoordinates) -> usize { // % works for negative numbers as intended. (index.y.get_absolute() % 16) as usize * CHUNK_AREA + *index.z as usize * 16 + *index.x as usize } -#[derive(Error, Debug)] -pub enum ChunkSerializingError { - #[error("Error serializing chunk: {0}")] - ErrorSerializingChunk(fastnbt::error::Error), -} diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 1aae80ee3..0c88138c8 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -11,9 +11,7 @@ use tokio::{ use crate::{ chunk::{ - db::{ - informative_table::InformativeTableDB, RawChunkReader, RawChunkWriter, - }, + db::{informative_table::InformativeTableDB, RawChunkReader, RawChunkWriter}, format::{anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError, ChunkWriter}, ChunkData, ChunkParsingError, }, @@ -227,7 +225,7 @@ impl Level { match chunk_reader.read_chunk( raw_chunk_reader .read_raw_chunk(save_file, &chunk_pos) - .map_err(|_| ChunkReadingError::RegionIsInvalid)?, + .map_err(|_| ChunkReadingError::ChunkNotExist)?, &chunk_pos, ) { Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), From 9f29e4072ead5ce532495f678b41265a2eeddff9 Mon Sep 17 00:00:00 2001 From: suprohub Date: Fri, 10 Jan 2025 17:09:02 +0400 Subject: [PATCH 04/13] move compression to chunks --- pumpkin-world/src/chunk/{db => }/compression.rs | 13 ++++++++++++- pumpkin-world/src/chunk/db/informative_table.rs | 2 +- pumpkin-world/src/chunk/db/mod.rs | 15 +-------------- pumpkin-world/src/chunk/format/mod.rs | 14 +------------- pumpkin-world/src/chunk/mod.rs | 1 + 5 files changed, 16 insertions(+), 29 deletions(-) rename pumpkin-world/src/chunk/{db => }/compression.rs (90%) diff --git a/pumpkin-world/src/chunk/db/compression.rs b/pumpkin-world/src/chunk/compression.rs similarity index 90% rename from pumpkin-world/src/chunk/db/compression.rs rename to pumpkin-world/src/chunk/compression.rs index ba53fd0a8..9ff8dfdbb 100644 --- a/pumpkin-world/src/chunk/db/compression.rs +++ b/pumpkin-world/src/chunk/compression.rs @@ -1,8 +1,19 @@ use std::io::{Read, Write}; use flate2::bufread::{GzDecoder, GzEncoder, ZlibDecoder, ZlibEncoder}; +use thiserror::Error; -use super::CompressionError; +#[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), + #[error("Error while working with LZ4 compression: {0}")] + LZ4Error(std::io::Error), +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] diff --git a/pumpkin-world/src/chunk/db/informative_table.rs b/pumpkin-world/src/chunk/db/informative_table.rs index 7d46a6893..5a1968f89 100644 --- a/pumpkin-world/src/chunk/db/informative_table.rs +++ b/pumpkin-world/src/chunk/db/informative_table.rs @@ -6,7 +6,7 @@ use std::{ use bytes::{Buf, BufMut, Bytes, BytesMut}; -use crate::chunk::db::{compression::Compression, RawChunkWritingError}; +use crate::chunk::{compression::Compression, db::RawChunkWritingError}; use super::{CompressionError, RawChunkReader, RawChunkReadingError, RawChunkWriter}; diff --git a/pumpkin-world/src/chunk/db/mod.rs b/pumpkin-world/src/chunk/db/mod.rs index 83ed967da..762e4da43 100644 --- a/pumpkin-world/src/chunk/db/mod.rs +++ b/pumpkin-world/src/chunk/db/mod.rs @@ -1,4 +1,3 @@ -pub mod compression; pub mod informative_table; use pumpkin_util::math::vector2::Vector2; @@ -6,7 +5,7 @@ use thiserror::Error; use crate::level::LevelFolder; -use super::ChunkParsingError; +use super::{compression::CompressionError, ChunkParsingError}; pub trait RawChunkReader: Sync + Send { fn read_raw_chunk( @@ -50,15 +49,3 @@ pub enum RawChunkWritingError { #[error("Chunk serializing error: {0}")] ChunkSerializingError(String), } - -#[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), - #[error("Error while working with LZ4 compression: {0}")] - LZ4Error(std::io::Error), -} diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index 06d4e45df..6a9e62643 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -3,7 +3,7 @@ pub mod anvil; use pumpkin_util::math::vector2::Vector2; use thiserror::Error; -use super::{ChunkData, ChunkParsingError}; +use super::{compression::CompressionError, ChunkData, ChunkParsingError}; pub trait ChunkReader: Sync + Send { fn read_chunk( @@ -46,15 +46,3 @@ pub enum ChunkWritingError { #[error("Chunk serializing error: {0}")] ChunkSerializingError(String), } - -#[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), - #[error("Error while working with LZ4 compression: {0}")] - LZ4Error(std::io::Error), -} diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index fc8de86d8..3651bc734 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -6,6 +6,7 @@ use thiserror::Error; use crate::{coordinates::ChunkRelativeBlockCoordinates, WORLD_HEIGHT}; +pub mod compression; pub mod db; pub mod format; From bfd6cad35a2de23c064cf42776a0b4dcd35fb896 Mon Sep 17 00:00:00 2001 From: suprohub Date: Fri, 10 Jan 2025 17:11:59 +0400 Subject: [PATCH 05/13] remove `?` --- pumpkin-world/src/level.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 0c88138c8..1006d1d64 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -222,13 +222,23 @@ impl Level { save_file: &LevelFolder, chunk_pos: Vector2, ) -> Result>>, ChunkReadingError> { - match chunk_reader.read_chunk( - raw_chunk_reader - .read_raw_chunk(save_file, &chunk_pos) - .map_err(|_| ChunkReadingError::ChunkNotExist)?, - &chunk_pos, - ) { - Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), + match raw_chunk_reader + .read_raw_chunk(save_file, &chunk_pos) + .map_err(|_| ChunkReadingError::ChunkNotExist) + { + Ok(chunk_bytes) => { + match chunk_reader.read_chunk(chunk_bytes, &chunk_pos) { + Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), + Err( + ChunkReadingError::ChunkNotExist + | ChunkReadingError::ParsingError(ChunkParsingError::ChunkNotGenerated), + ) => { + // This chunk was not generated yet. + Ok(None) + } + Err(err) => Err(err), + } + } Err( ChunkReadingError::ChunkNotExist | ChunkReadingError::ParsingError(ChunkParsingError::ChunkNotGenerated), From 8ceccfb831c96d1a1b121daa6b8a70a58ba3dc90 Mon Sep 17 00:00:00 2001 From: suprohub Date: Fri, 10 Jan 2025 17:25:11 +0400 Subject: [PATCH 06/13] saving issue fix --- pumpkin-world/src/level.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 1006d1d64..8556080d8 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -210,10 +210,15 @@ impl Level { } pub async fn write_chunk(&self, chunk_to_write: (Vector2, Arc>)) { - let data = chunk_to_write.1.read().await; - if let Err(error) = self.chunk_writer.write_chunk(&data, &chunk_to_write.0) { + if let Err(error) = self.raw_chunk_writer.write_raw_chunk( + self.chunk_writer + .write_chunk(&*chunk_to_write.1.read().await, &chunk_to_write.0) + .unwrap(), + &self.level_folder, + &chunk_to_write.0, + ) { log::error!("Failed writing Chunk to disk {}", error.to_string()); - } + }; } fn load_chunk_from_save( From 5bcb5ada342eaf5644f60664b1dca00fb1835810 Mon Sep 17 00:00:00 2001 From: suprohub Date: Sun, 12 Jan 2025 10:43:55 +0400 Subject: [PATCH 07/13] fixes --- .../src/chunk/db/informative_table.rs | 59 +++++++++---------- pumpkin-world/src/chunk/db/mod.rs | 12 ++-- pumpkin-world/src/chunk/format/anvil.rs | 8 +-- pumpkin-world/src/chunk/format/mod.rs | 6 +- pumpkin-world/src/level.rs | 42 ++++++------- 5 files changed, 57 insertions(+), 70 deletions(-) diff --git a/pumpkin-world/src/chunk/db/informative_table.rs b/pumpkin-world/src/chunk/db/informative_table.rs index 5a1968f89..6efa8b58e 100644 --- a/pumpkin-world/src/chunk/db/informative_table.rs +++ b/pumpkin-world/src/chunk/db/informative_table.rs @@ -6,18 +6,18 @@ use std::{ use bytes::{Buf, BufMut, Bytes, BytesMut}; -use crate::chunk::{compression::Compression, db::RawChunkWritingError}; +use crate::chunk::{compression::Compression, db::ChunkStorageWritingError}; -use super::{CompressionError, RawChunkReader, RawChunkReadingError, RawChunkWriter}; +use super::{ChunkStorage, ChunkStorageReadingError, CompressionError}; -pub struct InformativeTableDB; +pub struct InformativeTable; -impl RawChunkReader for InformativeTableDB { +impl ChunkStorage for InformativeTable { fn read_raw_chunk( &self, save_file: &crate::level::LevelFolder, at: &pumpkin_util::math::vector2::Vector2, - ) -> Result, RawChunkReadingError> { + ) -> Result, ChunkStorageReadingError> { let region = (at.x >> 5, at.z >> 5); let mut region_file = OpenOptions::new() @@ -28,8 +28,8 @@ impl RawChunkReader for InformativeTableDB { .join(format!("r.{}.{}.mca", region.0, region.1)), ) .map_err(|err| match err.kind() { - std::io::ErrorKind::NotFound => RawChunkReadingError::ChunkNotExist, - kind => RawChunkReadingError::IoError(kind), + std::io::ErrorKind::NotFound => ChunkStorageReadingError::ChunkNotExist, + kind => ChunkStorageReadingError::IoError(kind), })?; let mut location_table: [u8; 4096] = [0; 4096]; @@ -38,10 +38,10 @@ impl RawChunkReader for InformativeTableDB { // fill the location and timestamp tables region_file .read_exact(&mut location_table) - .map_err(|err| RawChunkReadingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageReadingError::IoError(err.kind()))?; region_file .read_exact(&mut timestamp_table) - .map_err(|err| RawChunkReadingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageReadingError::IoError(err.kind()))?; let chunk_x = at.x & 0x1F; let chunk_z = at.z & 0x1F; @@ -54,31 +54,32 @@ impl RawChunkReader for InformativeTableDB { let size_at = location_table[table_entry as usize + 3] as usize * 4096; if offset_at == 0 && size_at == 0 { - return Err(RawChunkReadingError::ChunkNotExist); + return Err(ChunkStorageReadingError::ChunkNotExist); } // Read the file using the offset and size let mut file_buf = { region_file .seek(std::io::SeekFrom::Start(offset_at)) - .map_err(|_| RawChunkReadingError::RegionIsInvalid)?; + .map_err(|_| ChunkStorageReadingError::RegionIsInvalid)?; let mut out = vec![0; size_at]; region_file .read_exact(&mut out) - .map_err(|_| RawChunkReadingError::RegionIsInvalid)?; + .map_err(|_| ChunkStorageReadingError::RegionIsInvalid)?; out }; let mut header: Bytes = file_buf.drain(0..5).collect(); if header.remaining() != 5 { - return Err(RawChunkReadingError::InvalidHeader); + return Err(ChunkStorageReadingError::InvalidHeader); } let size = header.get_u32(); let compression = header.get_u8(); - let compression = Compression::from_byte(compression) - .map_err(|_| RawChunkReadingError::Compression(CompressionError::UnknownCompression))?; + let compression = Compression::from_byte(compression).map_err(|_| { + ChunkStorageReadingError::Compression(CompressionError::UnknownCompression) + })?; // size includes the compression scheme byte, so we need to subtract 1 let chunk_data: Vec = file_buf.drain(0..size as usize - 1).collect(); @@ -86,22 +87,20 @@ impl RawChunkReader for InformativeTableDB { let decompressed_chunk = if let Some(compression) = compression { compression .decompress_data(&chunk_data) - .map_err(RawChunkReadingError::Compression)? + .map_err(ChunkStorageReadingError::Compression)? } else { chunk_data }; Ok(decompressed_chunk) } -} -impl RawChunkWriter for InformativeTableDB { fn write_raw_chunk( &self, chunk: Vec, level_folder: &crate::level::LevelFolder, at: &pumpkin_util::math::vector2::Vector2, - ) -> Result<(), super::RawChunkWritingError> { + ) -> Result<(), super::ChunkStorageWritingError> { let region = (at.x >> 5, at.z >> 5); let mut region_file = OpenOptions::new() @@ -114,13 +113,13 @@ impl RawChunkWriter for InformativeTableDB { .region_folder .join(format!("./r.{}.{}.mca", region.0, region.1)), ) - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; // Compress chunk data let compression = Compression::ZLib; let compressed_data = compression .compress_data(&chunk, 6) - .map_err(RawChunkWritingError::Compression)?; + .map_err(ChunkStorageWritingError::Compression)?; // Length of compressed data + compression type let length = compressed_data.len() as u32 + 1; @@ -142,17 +141,17 @@ impl RawChunkWriter for InformativeTableDB { let file_meta = region_file .metadata() - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; // The header consists of 8 KiB of data // Try to fill the location and timestamp tables if they already exist if file_meta.len() >= 8192 { region_file .read_exact(&mut location_table) - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; region_file .read_exact(&mut timestamp_table) - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; } // Get location table index @@ -200,17 +199,17 @@ impl RawChunkWriter for InformativeTableDB { region_file.seek(SeekFrom::Start(0)).unwrap(); region_file .write_all(&[location_table, timestamp_table].concat()) - .map_err(|e| RawChunkWritingError::IoError(e.kind()))?; + .map_err(|e| ChunkStorageWritingError::IoError(e.kind()))?; // Seek to where the chunk is located region_file .seek(SeekFrom::Start(chunk_data_location * 4096)) - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; // Write header and payload region_file .write_all(&chunk_payload) - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; // Calculate padding to fill the sectors // (length + 4) 3 bits for length and 1 for compression type + payload length @@ -219,17 +218,17 @@ impl RawChunkWriter for InformativeTableDB { // Write padding region_file .write_all(&vec![0u8; padding as usize]) - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; region_file .flush() - .map_err(|err| RawChunkWritingError::IoError(err.kind()))?; + .map_err(|err| ChunkStorageWritingError::IoError(err.kind()))?; Ok(()) } } -impl InformativeTableDB { +impl InformativeTable { /// Returns the next free writable sector /// The sector is absolute which means it always has a spacing of 2 sectors fn find_free_sector(&self, location_table: &[u8; 4096], sector_size: usize) -> usize { diff --git a/pumpkin-world/src/chunk/db/mod.rs b/pumpkin-world/src/chunk/db/mod.rs index 762e4da43..e07292812 100644 --- a/pumpkin-world/src/chunk/db/mod.rs +++ b/pumpkin-world/src/chunk/db/mod.rs @@ -7,25 +7,23 @@ use crate::level::LevelFolder; use super::{compression::CompressionError, ChunkParsingError}; -pub trait RawChunkReader: Sync + Send { +pub trait ChunkStorage: Sync + Send { fn read_raw_chunk( &self, save_file: &LevelFolder, at: &Vector2, - ) -> Result, RawChunkReadingError>; -} + ) -> Result, ChunkStorageReadingError>; -pub trait RawChunkWriter: Send + Sync { fn write_raw_chunk( &self, chunk: Vec, level_folder: &LevelFolder, at: &Vector2, - ) -> Result<(), RawChunkWritingError>; + ) -> Result<(), ChunkStorageWritingError>; } #[derive(Error, Debug)] -pub enum RawChunkReadingError { +pub enum ChunkStorageReadingError { #[error("Io error: {0}")] IoError(std::io::ErrorKind), #[error("Invalid header")] @@ -41,7 +39,7 @@ pub enum RawChunkReadingError { } #[derive(Error, Debug)] -pub enum RawChunkWritingError { +pub enum ChunkStorageWritingError { #[error("Io error: {0}")] IoError(std::io::ErrorKind), #[error("Compression error {0}")] diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index aba96c6a3..f4a17e7d6 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -14,7 +14,7 @@ use crate::chunk::{ }; use crate::coordinates::{ChunkRelativeBlockCoordinates, Height}; -use super::{ChunkReader, ChunkReadingError, ChunkWriter, ChunkWritingError}; +use super::{ChunkFormat, ChunkReadingError, ChunkWritingError}; #[derive(Clone, Default)] pub struct AnvilChunkFormat; @@ -98,7 +98,7 @@ pub enum ChunkSerializingError { ErrorSerializingChunk(fastnbt::error::Error), } -impl ChunkReader for AnvilChunkFormat { +impl ChunkFormat for AnvilChunkFormat { fn read_chunk( &self, chunk_bytes: Vec, @@ -202,10 +202,8 @@ impl ChunkReader for AnvilChunkFormat { position: *at, }) } -} -impl ChunkWriter for AnvilChunkFormat { - fn write_chunk( + fn save_chunk( &self, chunk_data: &ChunkData, _at: &Vector2, diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index 6a9e62643..ce6d99102 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -5,16 +5,14 @@ use thiserror::Error; use super::{compression::CompressionError, ChunkData, ChunkParsingError}; -pub trait ChunkReader: Sync + Send { +pub trait ChunkFormat: Sync + Send { fn read_chunk( &self, chunk_bytes: Vec, at: &Vector2, ) -> Result; -} -pub trait ChunkWriter: Send + Sync { - fn write_chunk( + fn save_chunk( &self, chunk_data: &ChunkData, at: &Vector2, diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 8556080d8..4f7da083e 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -11,8 +11,8 @@ use tokio::{ use crate::{ chunk::{ - db::{informative_table::InformativeTableDB, RawChunkReader, RawChunkWriter}, - format::{anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError, ChunkWriter}, + db::{informative_table::InformativeTable, ChunkStorage}, + format::{anvil::AnvilChunkFormat, ChunkFormat, ChunkReadingError}, ChunkData, ChunkParsingError, }, generation::{get_world_gen, Seed, WorldGenerator}, @@ -36,10 +36,8 @@ pub struct Level { level_folder: LevelFolder, loaded_chunks: Arc, Arc>>>, chunk_watchers: Arc, usize>>, - chunk_reader: Arc, - chunk_writer: Arc, - raw_chunk_reader: Arc, - raw_chunk_writer: Arc, + chunk_format: Arc, + chunk_storage: Arc, world_gen: Arc, // Gets unlocked when dropped // TODO: Make this a trait @@ -80,10 +78,8 @@ impl Level { world_gen, world_info_writer: Arc::new(AnvilLevelInfo), level_folder, - chunk_reader: Arc::new(AnvilChunkFormat), - chunk_writer: Arc::new(AnvilChunkFormat), - raw_chunk_reader: Arc::new(InformativeTableDB), - raw_chunk_writer: Arc::new(InformativeTableDB), + chunk_format: Arc::new(AnvilChunkFormat), + chunk_storage: Arc::new(InformativeTable), loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), level_info, @@ -210,9 +206,9 @@ impl Level { } pub async fn write_chunk(&self, chunk_to_write: (Vector2, Arc>)) { - if let Err(error) = self.raw_chunk_writer.write_raw_chunk( - self.chunk_writer - .write_chunk(&*chunk_to_write.1.read().await, &chunk_to_write.0) + if let Err(error) = self.chunk_storage.write_raw_chunk( + self.chunk_format + .save_chunk(&*chunk_to_write.1.read().await, &chunk_to_write.0) .unwrap(), &self.level_folder, &chunk_to_write.0, @@ -222,8 +218,8 @@ impl Level { } fn load_chunk_from_save( - raw_chunk_reader: Arc, - chunk_reader: Arc, + raw_chunk_reader: Arc, + chunk_reader: Arc, save_file: &LevelFolder, chunk_pos: Vector2, ) -> Result>>, ChunkReadingError> { @@ -266,10 +262,8 @@ impl Level { chunks.par_iter().for_each(|at| { let channel = channel.clone(); let loaded_chunks = self.loaded_chunks.clone(); - let chunk_reader = self.chunk_reader.clone(); - let chunk_writer = self.chunk_writer.clone(); - let raw_chunk_reader = self.raw_chunk_reader.clone(); - let raw_chunk_writer = self.raw_chunk_writer.clone(); + let chunk_format = self.chunk_format.clone(); + let chunk_storage = self.chunk_storage.clone(); let level_folder = self.level_folder.clone(); let world_gen = self.world_gen.clone(); let chunk_pos = *at; @@ -279,17 +273,17 @@ impl Level { .map(|entry| entry.value().clone()) .unwrap_or_else(|| { let loaded_chunk = match Self::load_chunk_from_save( - raw_chunk_reader, - chunk_reader, + chunk_storage.clone(), + chunk_format.clone(), &level_folder, chunk_pos, ) { Ok(chunk) => { // Save new Chunk if let Some(chunk) = &chunk { - if let Err(error) = raw_chunk_writer.write_raw_chunk( - chunk_writer - .write_chunk(&chunk.blocking_read(), &chunk_pos) + if let Err(error) = chunk_storage.write_raw_chunk( + chunk_format + .save_chunk(&chunk.blocking_read(), &chunk_pos) .unwrap(), &level_folder, &chunk_pos, From 100c0f9e45935cd351cb10e3d55b2a9297a96fa0 Mon Sep 17 00:00:00 2001 From: Suprohub Date: Fri, 24 Jan 2025 17:06:17 +0400 Subject: [PATCH 08/13] fmt clippy --- pumpkin-world/src/chunk/format/anvil.rs | 32 ++----------------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index f4a17e7d6..3f9bb8ecc 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -1,7 +1,7 @@ use fastnbt::LongArray; use indexmap::IndexMap; -use pumpkin_util::math::ceil_log2; -use pumpkin_util::math::vector2::Vector2; +use pumpkin_data::chunk::ChunkStatus; +use pumpkin_util::math::{ceil_log2, vector2::Vector2}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use thiserror::Error; @@ -57,34 +57,6 @@ struct ChunkNbt { heightmaps: ChunkHeightmaps, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] -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:features")] - Features, - #[serde(rename = "minecraft:initialize_light")] - InitLight, - #[serde(rename = "minecraft:light")] - Light, - #[serde(rename = "minecraft:spawn")] - Spawn, - #[serde(rename = "minecraft:full")] - Full, -} - // I can't use an tag because it will break ChunkNBT, but status need to have a big S, so "Status" #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] From 42242f6e584e6bef5425501e74016687fed059a1 Mon Sep 17 00:00:00 2001 From: suprohub <125716028+suprohub@users.noreply.github.com> Date: Mon, 27 Jan 2025 00:05:40 +0400 Subject: [PATCH 09/13] Update anvil.rs --- pumpkin-world/src/chunk/format/anvil.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 3f9bb8ecc..b38c957dc 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -263,7 +263,7 @@ impl ChunkFormat for AnvilChunkFormat { } } -/*#[cfg(test)] +#[cfg(test)] mod tests { use pumpkin_util::math::vector2::Vector2; use std::fs; @@ -341,4 +341,4 @@ mod tests { println!("Checked chunks successfully"); } -}*/ +} From 85f556c4a2e5f9248bfb6661f1a176b7d189d4e7 Mon Sep 17 00:00:00 2001 From: suprohub Date: Mon, 27 Jan 2025 16:13:52 +0400 Subject: [PATCH 10/13] Add test --- pumpkin-world/src/chunk/anvil.rs | 577 ------------------------ pumpkin-world/src/chunk/format/anvil.rs | 38 +- 2 files changed, 28 insertions(+), 587 deletions(-) delete mode 100644 pumpkin-world/src/chunk/anvil.rs diff --git a/pumpkin-world/src/chunk/anvil.rs b/pumpkin-world/src/chunk/anvil.rs deleted file mode 100644 index 66898f090..000000000 --- a/pumpkin-world/src/chunk/anvil.rs +++ /dev/null @@ -1,577 +0,0 @@ -use bytes::*; -use fastnbt::LongArray; -use flate2::bufread::{GzDecoder, GzEncoder, ZlibDecoder, ZlibEncoder}; -use indexmap::IndexMap; -use pumpkin_config::ADVANCED_CONFIG; -use pumpkin_util::math::ceil_log2; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{ - collections::HashSet, - fs::OpenOptions, - io::{Read, Seek, SeekFrom, Write}, -}; - -use crate::{ - block::block_registry::BLOCK_ID_TO_REGISTRY_ID, chunk::ChunkWritingError, level::LevelFolder, -}; - -use super::{ - ChunkData, ChunkNbt, ChunkReader, ChunkReadingError, ChunkSection, ChunkSectionBlockStates, - ChunkSerializingError, ChunkWriter, CompressionError, PaletteEntry, -}; - -// 1.21.4 -const WORLD_DATA_VERSION: i32 = 4189; - -#[derive(Clone, Default)] -pub struct AnvilChunkFormat; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum Compression { - /// GZip Compression - GZip = 1, - /// ZLib Compression - ZLib = 2, - /// LZ4 Compression (since 24w04a) - LZ4 = 4, - /// Custom compression algorithm (since 24w05a) - Custom = 127, -} - -impl From for Compression { - fn from(value: pumpkin_config::chunk::Compression) -> Self { - // :c - match value { - pumpkin_config::chunk::Compression::GZip => Self::GZip, - pumpkin_config::chunk::Compression::ZLib => Self::ZLib, - pumpkin_config::chunk::Compression::LZ4 => Self::LZ4, - pumpkin_config::chunk::Compression::Custom => Self::Custom, - } - } -} - -impl Compression { - /// Returns Ok when a compression is found otherwise an Err - #[allow(clippy::result_unit_err)] - pub fn from_byte(byte: u8) -> Result, ()> { - match byte { - 1 => Ok(Some(Self::GZip)), - 2 => Ok(Some(Self::ZLib)), - // Uncompressed (since a version before 1.15.1) - 3 => Ok(None), - 4 => Ok(Some(Self::LZ4)), - 127 => Ok(Some(Self::Custom)), - // Unknown format - _ => Err(()), - } - } - - fn decompress_data(&self, compressed_data: &[u8]) -> Result, CompressionError> { - match self { - Compression::GZip => { - let mut decoder = GzDecoder::new(compressed_data); - let mut chunk_data = Vec::new(); - decoder - .read_to_end(&mut chunk_data) - .map_err(CompressionError::GZipError)?; - Ok(chunk_data) - } - Compression::ZLib => { - let mut decoder = ZlibDecoder::new(compressed_data); - let mut chunk_data = Vec::new(); - decoder - .read_to_end(&mut chunk_data) - .map_err(CompressionError::ZlibError)?; - Ok(chunk_data) - } - Compression::LZ4 => { - let mut decoder = - lz4::Decoder::new(compressed_data).map_err(CompressionError::LZ4Error)?; - let mut decompressed_data = Vec::new(); - decoder - .read_to_end(&mut decompressed_data) - .map_err(CompressionError::LZ4Error)?; - Ok(decompressed_data) - } - Compression::Custom => todo!(), - } - } - fn compress_data( - &self, - uncompressed_data: &[u8], - compression_level: u32, - ) -> Result, CompressionError> { - match self { - Compression::GZip => { - let mut encoder = GzEncoder::new( - uncompressed_data, - flate2::Compression::new(compression_level), - ); - let mut chunk_data = Vec::new(); - encoder - .read_to_end(&mut chunk_data) - .map_err(CompressionError::GZipError)?; - Ok(chunk_data) - } - Compression::ZLib => { - let mut encoder = ZlibEncoder::new( - uncompressed_data, - flate2::Compression::new(compression_level), - ); - let mut chunk_data = Vec::new(); - encoder - .read_to_end(&mut chunk_data) - .map_err(CompressionError::ZlibError)?; - Ok(chunk_data) - } - Compression::LZ4 => { - let mut compressed_data = Vec::new(); - let mut encoder = lz4::EncoderBuilder::new() - .level(compression_level) - .build(&mut compressed_data) - .map_err(CompressionError::LZ4Error)?; - if let Err(err) = encoder.write_all(uncompressed_data) { - return Err(CompressionError::LZ4Error(err)); - } - if let (_output, Err(err)) = encoder.finish() { - return Err(CompressionError::LZ4Error(err)); - } - Ok(compressed_data) - } - Compression::Custom => todo!(), - } - } -} - -impl ChunkReader for AnvilChunkFormat { - fn read_chunk( - &self, - save_file: &LevelFolder, - at: &pumpkin_util::math::vector2::Vector2, - ) -> Result { - let region = (at.x >> 5, at.z >> 5); - - 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 chunk_x = at.x & 0x1F; - let chunk_z = at.z & 0x1F; - let table_entry = (chunk_x + chunk_z * 32) * 4; - - let mut offset = BytesMut::new(); - offset.put_u8(0); - offset.extend_from_slice(&location_table[table_entry as usize..table_entry as usize + 3]); - let offset_at = offset.get_u32() as u64 * 4096; - let size_at = location_table[table_entry as usize + 3] as usize * 4096; - - if offset_at == 0 && size_at == 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_at)) - .map_err(|_| ChunkReadingError::RegionIsInvalid)?; - let mut out = vec![0; size_at]; - region_file - .read_exact(&mut out) - .map_err(|_| ChunkReadingError::RegionIsInvalid)?; - out - }; - - let mut header: Bytes = file_buf.drain(0..5).collect(); - if header.remaining() != 5 { - return Err(ChunkReadingError::InvalidHeader); - } - - let size = header.get_u32(); - let compression = header.get_u8(); - - let compression = Compression::from_byte(compression) - .map_err(|_| ChunkReadingError::Compression(CompressionError::UnknownCompression))?; - - // size includes the compression scheme byte, so we need to subtract 1 - let chunk_data: Vec = file_buf.drain(0..size as usize - 1).collect(); - - let decompressed_chunk = if let Some(compression) = compression { - compression - .decompress_data(&chunk_data) - .map_err(ChunkReadingError::Compression)? - } else { - chunk_data - }; - - ChunkData::from_bytes(&decompressed_chunk, *at).map_err(ChunkReadingError::ParsingError) - } -} - -impl ChunkWriter for AnvilChunkFormat { - fn write_chunk( - &self, - chunk_data: &ChunkData, - level_folder: &LevelFolder, - at: &pumpkin_util::math::vector2::Vector2, - ) -> Result<(), super::ChunkWritingError> { - let region = (at.x >> 5, at.z >> 5); - - let mut region_file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open( - level_folder - .region_folder - .join(format!("./r.{}.{}.mca", region.0, region.1)), - ) - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - - // Serialize chunk data - let raw_bytes = self - .to_bytes(chunk_data) - .map_err(|err| ChunkWritingError::ChunkSerializingError(err.to_string()))?; - - // Compress chunk data - let compression: Compression = ADVANCED_CONFIG - .chunk - .compression - .compression_algorithm - .clone() - .into(); - let compressed_data = compression - .compress_data( - &raw_bytes, - ADVANCED_CONFIG.chunk.compression.compression_level, - ) - .map_err(ChunkWritingError::Compression)?; - - // Length of compressed data + compression type - let length = compressed_data.len() as u32 + 1; - - // | 0 1 2 3 | 4 | 5.. | - // | length | compression type | compressed data | - let mut chunk_payload = BytesMut::with_capacity(5); - // Payload Header + Body - chunk_payload.put_u32(length); - chunk_payload.put_u8(compression as u8); - chunk_payload.put_slice(&compressed_data); - - // Calculate sector size - let sector_size = chunk_payload.len().div_ceil(4096); - - // Region file header tables - let mut location_table = [0u8; 4096]; - let mut timestamp_table = [0u8; 4096]; - - let file_meta = region_file - .metadata() - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - - // The header consists of 8 KiB of data - // Try to fill the location and timestamp tables if they already exist - if file_meta.len() >= 8192 { - region_file - .read_exact(&mut location_table) - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - region_file - .read_exact(&mut timestamp_table) - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - } - - // Get location table index - let chunk_x = at.x & 0x1F; - let chunk_z = at.z & 0x1F; - let table_index = (chunk_x as usize + chunk_z as usize * 32) * 4; - - // | 0 1 2 | 3 | - // | offset | sector count | - // Get the entry from the current location table and check - // if the new chunk fits in the space of the old chunk - let chunk_location = &location_table[table_index..table_index + 4]; - let chunk_data_location: u64 = if chunk_location[3] >= sector_size as u8 { - // Return old chunk location - u32::from_be_bytes([0, chunk_location[0], chunk_location[1], chunk_location[2]]) as u64 - } else { - // Retrieve next writable sector - self.find_free_sector(&location_table, sector_size) as u64 - }; - - assert!( - chunk_data_location > 1, - "This should never happen. The header would be corrupted" - ); - - // Construct location header - location_table[table_index] = (chunk_data_location >> 16) as u8; - location_table[table_index + 1] = (chunk_data_location >> 8) as u8; - location_table[table_index + 2] = chunk_data_location as u8; - location_table[table_index + 3] = sector_size as u8; - - // Get epoch may result in errors if after the year 2106 :( - let epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as u32; - - // Construct timestamp header - timestamp_table[table_index] = (epoch >> 24) as u8; - timestamp_table[table_index + 1] = (epoch >> 16) as u8; - timestamp_table[table_index + 2] = (epoch >> 8) as u8; - timestamp_table[table_index + 3] = epoch as u8; - - // Write new location and timestamp table - region_file.seek(SeekFrom::Start(0)).unwrap(); - region_file - .write_all(&[location_table, timestamp_table].concat()) - .map_err(|e| ChunkWritingError::IoError(e.kind()))?; - - // Seek to where the chunk is located - region_file - .seek(SeekFrom::Start(chunk_data_location * 4096)) - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - - // Write header and payload - region_file - .write_all(&chunk_payload) - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - - // Calculate padding to fill the sectors - // (length + 4) 3 bits for length and 1 for compression type + payload length - let padding = ((sector_size * 4096) as u32 - ((length + 4) & 0xFFF)) & 0xFFF; - - // Write padding - region_file - .write_all(&vec![0u8; padding as usize]) - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - - region_file - .flush() - .map_err(|err| ChunkWritingError::IoError(err.kind()))?; - - Ok(()) - } -} - -impl AnvilChunkFormat { - pub fn to_bytes(&self, chunk_data: &ChunkData) -> Result, ChunkSerializingError> { - let mut sections = Vec::new(); - - for (i, blocks) in chunk_data.subchunks.array_iter().enumerate() { - // get unique blocks - let unique_blocks: HashSet<_> = blocks.iter().collect(); - - let palette: IndexMap<_, _> = unique_blocks - .into_iter() - .enumerate() - .map(|(i, block)| { - let name = BLOCK_ID_TO_REGISTRY_ID.get(block).unwrap().as_str(); - (block, (name, i)) - }) - .collect(); - - // Determine the number of bits needed to represent the largest index in the palette - let block_bit_size = if palette.len() < 16 { - 4 - } else { - ceil_log2(palette.len() as u32).max(4) - }; - - let mut section_longs = Vec::new(); - let mut current_pack_long: i64 = 0; - let mut bits_used_in_pack: u32 = 0; - - // Empty data if the palette only contains one index https://minecraft.fandom.com/wiki/Chunk_format - // if palette.len() > 1 {} - // TODO: Update to write empty data. Rn or read does not handle this elegantly - for block in blocks.iter() { - // Push if next bit does not fit - if bits_used_in_pack + block_bit_size as u32 > 64 { - section_longs.push(current_pack_long); - current_pack_long = 0; - bits_used_in_pack = 0; - } - let index = palette.get(block).expect("Just added all unique").1; - current_pack_long |= (index as i64) << bits_used_in_pack; - bits_used_in_pack += block_bit_size as u32; - - assert!(bits_used_in_pack <= 64); - - // If the current 64-bit integer is full, push it to the section_longs and start a new one - if bits_used_in_pack >= 64 { - section_longs.push(current_pack_long); - current_pack_long = 0; - bits_used_in_pack = 0; - } - } - - // Push the last 64-bit integer if it contains any data - if bits_used_in_pack > 0 { - section_longs.push(current_pack_long); - } - - sections.push(ChunkSection { - y: i as i8 - 4, - block_states: Some(ChunkSectionBlockStates { - data: Some(LongArray::new(section_longs)), - palette: palette - .into_iter() - .map(|entry| PaletteEntry { - name: entry.1 .0.to_owned(), - properties: None, - }) - .collect(), - }), - }); - } - - let nbt = ChunkNbt { - data_version: WORLD_DATA_VERSION, - x_pos: chunk_data.position.x, - z_pos: chunk_data.position.z, - status: super::ChunkStatus::Full, - heightmaps: chunk_data.heightmap.clone(), - sections, - }; - - fastnbt::to_bytes(&nbt).map_err(ChunkSerializingError::ErrorSerializingChunk) - } - - /// Returns the next free writable sector - /// The sector is absolute which means it always has a spacing of 2 sectors - fn find_free_sector(&self, location_table: &[u8; 4096], sector_size: usize) -> usize { - let mut used_sectors: Vec = Vec::new(); - for i in 0..1024 { - let entry_offset = i * 4; - let location_offset = u32::from_be_bytes([ - 0, - location_table[entry_offset], - location_table[entry_offset + 1], - location_table[entry_offset + 2], - ]) as u64; - let length = location_table[entry_offset + 3] as u64; - let sector_count = location_offset; - for used_sector in sector_count..sector_count + length { - used_sectors.push(used_sector as u16); - } - } - - if used_sectors.is_empty() { - return 2; - } - - used_sectors.sort(); - - let mut prev_sector = &used_sectors[0]; - for sector in used_sectors[1..].iter() { - // Iterate over consecutive pairs - if sector - prev_sector > sector_size as u16 { - return (prev_sector + 1) as usize; - } - prev_sector = sector; - } - - (*used_sectors.last().unwrap() + 1) as usize - } -} - -#[cfg(test)] -mod tests { - use pumpkin_util::math::vector2::Vector2; - use std::fs; - use std::path::PathBuf; - - use crate::chunk::ChunkWriter; - use crate::generation::{get_world_gen, Seed}; - use crate::{ - chunk::{anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError}, - level::LevelFolder, - }; - - #[test] - fn not_existing() { - let region_path = PathBuf::from("not_existing"); - let result = AnvilChunkFormat.read_chunk( - &LevelFolder { - root_folder: PathBuf::from(""), - region_folder: region_path, - }, - &Vector2::new(0, 0), - ); - assert!(matches!(result, Err(ChunkReadingError::ChunkNotExist))); - } - - #[test] - fn test_writing() { - let generator = get_world_gen(Seed(0)); - let level_folder = LevelFolder { - root_folder: PathBuf::from("./tmp"), - region_folder: PathBuf::from("./tmp/region"), - }; - if fs::exists(&level_folder.root_folder).unwrap() { - fs::remove_dir_all(&level_folder.root_folder).expect("Could not delete directory"); - } - - fs::create_dir_all(&level_folder.region_folder).expect("Could not create directory"); - - // Generate chunks - let mut chunks = vec![]; - for x in -5..5 { - for y in -5..5 { - let position = Vector2::new(x, y); - chunks.push((position, generator.generate_chunk(position))); - } - } - - for i in 0..5 { - println!("Iteration {}", i + 1); - for (at, chunk) in &chunks { - AnvilChunkFormat - .write_chunk(chunk, &level_folder, at) - .expect("Failed to write chunk"); - } - - let mut read_chunks = vec![]; - for (at, _chunk) in &chunks { - read_chunks.push( - AnvilChunkFormat - .read_chunk(&level_folder, at) - .expect("Could not read chunk"), - ); - } - - for (at, chunk) in &chunks { - let read_chunk = read_chunks - .iter() - .find(|chunk| chunk.position == *at) - .expect("Missing chunk"); - assert_eq!(chunk.subchunks, read_chunk.subchunks, "Chunks don't match"); - } - } - - fs::remove_dir_all(&level_folder.root_folder).expect("Could not delete directory"); - - println!("Checked chunks successfully"); - } -} diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index b38c957dc..072cd4e27 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -269,10 +269,12 @@ mod tests { use std::fs; use std::path::PathBuf; - use crate::chunk::ChunkWriter; + use crate::chunk::db::informative_table::InformativeTable; + use crate::chunk::db::ChunkStorage; + use crate::chunk::format::ChunkReadingError; use crate::generation::{get_world_gen, Seed}; use crate::{ - chunk::{format::anvil::AnvilChunkFormat, ChunkReader, ChunkReadingError}, + chunk::format::{anvil::AnvilChunkFormat, ChunkFormat}, level::LevelFolder, }; @@ -280,10 +282,15 @@ mod tests { fn not_existing() { let region_path = PathBuf::from("not_existing"); let result = AnvilChunkFormat.read_chunk( - &LevelFolder { - root_folder: PathBuf::from(""), - region_folder: region_path, - }, + InformativeTable + .read_raw_chunk( + &LevelFolder { + root_folder: PathBuf::from(""), + region_folder: region_path, + }, + &Vector2::new(0, 0), + ) + .expect("Failed to read raw chunk"), &Vector2::new(0, 0), ); assert!(matches!(result, Err(ChunkReadingError::ChunkNotExist))); @@ -314,8 +321,14 @@ mod tests { for i in 0..5 { println!("Iteration {}", i + 1); for (at, chunk) in &chunks { - AnvilChunkFormat - .write_chunk(chunk, &level_folder, at) + InformativeTable + .write_raw_chunk( + AnvilChunkFormat + .save_chunk(chunk, at) + .expect("Failed to write raw chunk"), + &level_folder, + at, + ) .expect("Failed to write chunk"); } @@ -323,8 +336,13 @@ mod tests { for (at, _chunk) in &chunks { read_chunks.push( AnvilChunkFormat - .read_chunk(&level_folder, at) - .expect("Could not read chunk"), + .read_chunk( + InformativeTable + .read_raw_chunk(&level_folder, at) + .expect("Failed to read raw chunk"), + at, + ) + .expect("Failed to read chunk"), ); } From d2b038271d86f4c430fbfec47816ff64d20de499 Mon Sep 17 00:00:00 2001 From: suprohub Date: Mon, 27 Jan 2025 16:25:20 +0400 Subject: [PATCH 11/13] Fixes --- pumpkin-world/src/chunk/format/anvil.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 072cd4e27..c3018f407 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -270,8 +270,8 @@ mod tests { use std::path::PathBuf; use crate::chunk::db::informative_table::InformativeTable; - use crate::chunk::db::ChunkStorage; - use crate::chunk::format::ChunkReadingError; + use crate::chunk::db::{ChunkStorage, ChunkStorageReadingError}; + use crate::generation::{get_world_gen, Seed}; use crate::{ chunk::format::{anvil::AnvilChunkFormat, ChunkFormat}, @@ -281,19 +281,17 @@ mod tests { #[test] fn not_existing() { let region_path = PathBuf::from("not_existing"); - let result = AnvilChunkFormat.read_chunk( - InformativeTable - .read_raw_chunk( - &LevelFolder { - root_folder: PathBuf::from(""), - region_folder: region_path, - }, - &Vector2::new(0, 0), - ) - .expect("Failed to read raw chunk"), + let result = InformativeTable.read_raw_chunk( + &LevelFolder { + root_folder: PathBuf::from(""), + region_folder: region_path, + }, &Vector2::new(0, 0), ); - assert!(matches!(result, Err(ChunkReadingError::ChunkNotExist))); + assert!(matches!( + result, + Err(ChunkStorageReadingError::ChunkNotExist) + )); } #[test] From 7a2ac3da959bad149cacb41bcbd4c0330544dc2f Mon Sep 17 00:00:00 2001 From: suprohub Date: Mon, 27 Jan 2025 16:37:35 +0400 Subject: [PATCH 12/13] fmt --- pumpkin-world/src/chunk/format/anvil.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index c3018f407..53beb5af7 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -271,7 +271,7 @@ mod tests { use crate::chunk::db::informative_table::InformativeTable; use crate::chunk::db::{ChunkStorage, ChunkStorageReadingError}; - + use crate::generation::{get_world_gen, Seed}; use crate::{ chunk::format::{anvil::AnvilChunkFormat, ChunkFormat}, From c08325ecd733f15d7a3f7e4f758bd8d145341523 Mon Sep 17 00:00:00 2001 From: suprohub Date: Tue, 28 Jan 2025 16:32:06 +0400 Subject: [PATCH 13/13] Fixes --- .../db/{informative_table.rs => anvil_saver.rs} | 6 +++--- pumpkin-world/src/chunk/db/mod.rs | 2 +- pumpkin-world/src/chunk/format/anvil.rs | 12 +++++++----- pumpkin-world/src/chunk/mod.rs | 3 --- pumpkin-world/src/level.rs | 4 ++-- 5 files changed, 13 insertions(+), 14 deletions(-) rename pumpkin-world/src/chunk/db/{informative_table.rs => anvil_saver.rs} (99%) diff --git a/pumpkin-world/src/chunk/db/informative_table.rs b/pumpkin-world/src/chunk/db/anvil_saver.rs similarity index 99% rename from pumpkin-world/src/chunk/db/informative_table.rs rename to pumpkin-world/src/chunk/db/anvil_saver.rs index 6efa8b58e..d680cd532 100644 --- a/pumpkin-world/src/chunk/db/informative_table.rs +++ b/pumpkin-world/src/chunk/db/anvil_saver.rs @@ -10,9 +10,9 @@ use crate::chunk::{compression::Compression, db::ChunkStorageWritingError}; use super::{ChunkStorage, ChunkStorageReadingError, CompressionError}; -pub struct InformativeTable; +pub struct AnvilSaver; -impl ChunkStorage for InformativeTable { +impl ChunkStorage for AnvilSaver { fn read_raw_chunk( &self, save_file: &crate::level::LevelFolder, @@ -228,7 +228,7 @@ impl ChunkStorage for InformativeTable { } } -impl InformativeTable { +impl AnvilSaver { /// Returns the next free writable sector /// The sector is absolute which means it always has a spacing of 2 sectors fn find_free_sector(&self, location_table: &[u8; 4096], sector_size: usize) -> usize { diff --git a/pumpkin-world/src/chunk/db/mod.rs b/pumpkin-world/src/chunk/db/mod.rs index e07292812..a56cc8c3d 100644 --- a/pumpkin-world/src/chunk/db/mod.rs +++ b/pumpkin-world/src/chunk/db/mod.rs @@ -1,4 +1,4 @@ -pub mod informative_table; +pub mod anvil_saver; use pumpkin_util::math::vector2::Vector2; use thiserror::Error; diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 53beb5af7..b118123c6 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -10,12 +10,14 @@ use crate::block::block_registry::BLOCK_ID_TO_REGISTRY_ID; use crate::block::BlockState; use crate::chunk::{ ChunkData, ChunkHeightmaps, ChunkParsingError, Subchunks, CHUNK_AREA, SUBCHUNK_VOLUME, - WORLD_DATA_VERSION, }; use crate::coordinates::{ChunkRelativeBlockCoordinates, Height}; use super::{ChunkFormat, ChunkReadingError, ChunkWritingError}; +// 1.21.4 +const WORLD_DATA_VERSION: i32 = 4189; + #[derive(Clone, Default)] pub struct AnvilChunkFormat; @@ -269,7 +271,7 @@ mod tests { use std::fs; use std::path::PathBuf; - use crate::chunk::db::informative_table::InformativeTable; + use crate::chunk::db::anvil_saver::AnvilSaver; use crate::chunk::db::{ChunkStorage, ChunkStorageReadingError}; use crate::generation::{get_world_gen, Seed}; @@ -281,7 +283,7 @@ mod tests { #[test] fn not_existing() { let region_path = PathBuf::from("not_existing"); - let result = InformativeTable.read_raw_chunk( + let result = AnvilSaver.read_raw_chunk( &LevelFolder { root_folder: PathBuf::from(""), region_folder: region_path, @@ -319,7 +321,7 @@ mod tests { for i in 0..5 { println!("Iteration {}", i + 1); for (at, chunk) in &chunks { - InformativeTable + AnvilSaver .write_raw_chunk( AnvilChunkFormat .save_chunk(chunk, at) @@ -335,7 +337,7 @@ mod tests { read_chunks.push( AnvilChunkFormat .read_chunk( - InformativeTable + AnvilSaver .read_raw_chunk(&level_folder, at) .expect("Failed to read raw chunk"), at, diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 3651bc734..f3f5e2019 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -10,9 +10,6 @@ pub mod compression; pub mod db; pub mod format; -// 1.21.4 -const WORLD_DATA_VERSION: i32 = 4189; - pub const CHUNK_AREA: usize = 16 * 16; pub const SUBCHUNK_VOLUME: usize = CHUNK_AREA * 16; pub const SUBCHUNKS_COUNT: usize = WORLD_HEIGHT / 16; diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 4f7da083e..0b64033ee 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -11,7 +11,7 @@ use tokio::{ use crate::{ chunk::{ - db::{informative_table::InformativeTable, ChunkStorage}, + db::{anvil_saver::AnvilSaver, ChunkStorage}, format::{anvil::AnvilChunkFormat, ChunkFormat, ChunkReadingError}, ChunkData, ChunkParsingError, }, @@ -79,7 +79,7 @@ impl Level { world_info_writer: Arc::new(AnvilLevelInfo), level_folder, chunk_format: Arc::new(AnvilChunkFormat), - chunk_storage: Arc::new(InformativeTable), + chunk_storage: Arc::new(AnvilSaver), loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), level_info,