From cc83e090f93cf9dbf462539625195dc05688f40d Mon Sep 17 00:00:00 2001 From: we sell insurance <72574589+neeleshpoli@users.noreply.github.com> Date: Tue, 24 Dec 2024 08:25:33 -0600 Subject: [PATCH] Implement level.dat reading and load the seed for use in world generation (#402) * Implement level.dat reading and use the seed in the world for chunk generation * Add tests * Seperate world info loading from chunk reading * Fix cargo fmt --------- Co-authored-by: Alexander Medvedev --- pumpkin-world/src/level.rs | 22 +++-- pumpkin-world/src/lib.rs | 2 +- pumpkin-world/src/world_info/anvil.rs | 93 ++++++++++++++++++++ pumpkin-world/src/world_info/mod.rs | 34 +++++++ pumpkin-world/test-files/sample-1/level.dat | Bin 0 -> 1530 bytes 5 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 pumpkin-world/src/world_info/anvil.rs create mode 100644 pumpkin-world/src/world_info/mod.rs create mode 100644 pumpkin-world/test-files/sample-1/level.dat diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 70dcdc7a..4c06f5a2 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -15,6 +15,7 @@ use crate::{ anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError, }, generation::{get_world_gen, Seed, WorldGenerator}, + world_info::{anvil::AnvilInfoReader, WorldInfo, WorldInfoReader}, }; /// The `Level` module provides functionality for working with chunks within or outside a Minecraft world. @@ -28,6 +29,7 @@ use crate::{ /// For more details on world generation, refer to the `WorldGenerator` module. pub struct Level { pub seed: Seed, + pub world_info: Option, save_file: Option, loaded_chunks: Arc, Arc>>>, chunk_watchers: Arc, usize>>, @@ -55,20 +57,27 @@ impl Level { region_folder.exists(), "World region folder does not exist, despite there being a root folder." ); - // TODO: read seed from level.dat - let seed = get_or_create_seed(); + let save_file = SaveFile { + root_folder, + region_folder, + }; + + // TODO: Load info correctly based on world format type + let world_info_reader = AnvilInfoReader::new(); + let info = world_info_reader + .read_world_info(&save_file) + .expect("Unable to get world info!"); // TODO: Improve error handling + let seed = Seed(info.seed as u64); let world_gen = get_world_gen(seed).into(); // TODO Read Seed from config. Self { seed, world_gen, - save_file: Some(SaveFile { - root_folder, - region_folder, - }), + save_file: Some(save_file), chunk_reader: Arc::new(AnvilChunkReader::new()), loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), + world_info: Some(info), } } else { let seed = get_or_create_seed(); @@ -80,6 +89,7 @@ impl Level { chunk_reader: Arc::new(AnvilChunkReader::new()), loaded_chunks: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()), + world_info: None, } } } diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index 3735dbb5..8e26b106 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -16,7 +16,7 @@ pub mod dimension; mod generation; pub mod item; pub mod level; - +pub mod world_info; pub const WORLD_HEIGHT: usize = 384; pub const WORLD_LOWEST_Y: i16 = -64; pub const WORLD_MAX_Y: i16 = WORLD_HEIGHT as i16 - WORLD_LOWEST_Y.abs(); diff --git a/pumpkin-world/src/world_info/anvil.rs b/pumpkin-world/src/world_info/anvil.rs new file mode 100644 index 00000000..1132d072 --- /dev/null +++ b/pumpkin-world/src/world_info/anvil.rs @@ -0,0 +1,93 @@ +use std::{fs::OpenOptions, io::Read}; + +use flate2::read::GzDecoder; +use serde::Deserialize; + +use crate::level::SaveFile; + +use super::{WorldInfo, WorldInfoError, WorldInfoReader}; + +pub struct AnvilInfoReader {} + +impl AnvilInfoReader { + pub fn new() -> Self { + Self {} + } +} + +impl WorldInfoReader for AnvilInfoReader { + fn read_world_info(&self, save_file: &SaveFile) -> Result { + let path = save_file.root_folder.join("level.dat"); + + let mut world_info_file = OpenOptions::new().read(true).open(path)?; + + let mut buffer = Vec::new(); + world_info_file.read_to_end(&mut buffer)?; + + let mut decoder = GzDecoder::new(&buffer[..]); + let mut decompressed_data = Vec::new(); + decoder.read_to_end(&mut decompressed_data)?; + + let info = fastnbt::from_bytes::(&decompressed_data) + .map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?; + + Ok(WorldInfo { + seed: info.data.world_gen_settings.seed, + }) + } +} + +impl Default for AnvilInfoReader { + fn default() -> Self { + Self::new() + } +} + +#[derive(Deserialize)] +pub struct LevelDat { + // No idea why its formatted like this + #[serde(rename = "Data")] + pub data: WorldData, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct WorldData { + pub world_gen_settings: WorldGenSettings, + // TODO: Implement the rest of the fields + // Fields below this comment are being deserialized, but are not being used + pub spawn_x: i32, + pub spawn_y: i32, + pub spawn_z: i32, +} + +#[derive(Deserialize)] +pub struct WorldGenSettings { + pub seed: i64, +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::{ + level::SaveFile, + world_info::{anvil::AnvilInfoReader, WorldInfo, WorldInfoReader}, + }; + + #[test] + fn test_level_dat_reading() { + let world_info = AnvilInfoReader::new(); + let root_folder = PathBuf::from("test-files").join("sample-1"); + let save_file = SaveFile { + root_folder: root_folder.clone(), + region_folder: root_folder, + }; + let expected = WorldInfo { + seed: -79717552349559436, + }; + let info = world_info.read_world_info(&save_file).unwrap(); + + assert_eq!(info, expected); + } +} diff --git a/pumpkin-world/src/world_info/mod.rs b/pumpkin-world/src/world_info/mod.rs new file mode 100644 index 00000000..a1184635 --- /dev/null +++ b/pumpkin-world/src/world_info/mod.rs @@ -0,0 +1,34 @@ +use thiserror::Error; + +use crate::level::SaveFile; + +pub mod anvil; + +pub(crate) trait WorldInfoReader { + fn read_world_info(&self, save_file: &SaveFile) -> Result; +} + +#[derive(Debug, PartialEq)] +pub struct WorldInfo { + pub seed: i64, + // TODO: Implement all fields +} + +#[derive(Error, Debug)] +pub enum WorldInfoError { + #[error("Io error: {0}")] + IoError(std::io::ErrorKind), + #[error("Info not found!")] + InfoNotFound, + #[error("Deserialization error: {0}")] + DeserializationError(String), +} + +impl From for WorldInfoError { + fn from(value: std::io::Error) -> Self { + match value.kind() { + std::io::ErrorKind::NotFound => Self::InfoNotFound, + value => Self::IoError(value), + } + } +} diff --git a/pumpkin-world/test-files/sample-1/level.dat b/pumpkin-world/test-files/sample-1/level.dat new file mode 100644 index 0000000000000000000000000000000000000000..1a527907bf8616dee934fd9e7c1cdce5087c30e7 GIT binary patch literal 1530 zcmVwogCsOVZ<@@Rej*sCGado)?tL*sYI{=QfzRK;?vKAdA^0ni+7Z>J z71ALvLcE_o{PyY(k1WtIlvfQIMN}|NX2?SNv;o3^`j%urkIZqw4W}V~l6f;^Ej7EK zN^sHJ3uSPrU##cGl|EH0kP65&nH_-ATna+I{=G`(eNcF?r>wh*GcLDi3~HLp_%Xc_ z&gC?z9MA~PhB4TGXUJlf5beSnDIoV$CN=kHSM)+>Ss~RU7(jS}uSxkzs*sgP$pPbj zJETgM{Gbmk34!eoFiTBRkIO*_R9yfy{R~Ryc|G;qq!H~w3nRCwe)axm`}<{@+`ga| zdc@#NQ5&HRrwNF18qVTga(T(k=6MFfGnedx`sQ|h^U?kKsOEG^@g-uzOYCifbhW>JkrRQjueS*Bh=cU@k$Q_ zC~BbHMT8Rt+N+To!w(9Ji>{INhzkf5?V9IUfwll!hYBefOM;1)HB!yKDhtDoG&b8MQ6!PxJ3-sMu!p^?zoq` zn{@X-MnB2jSOOCbFo%_Km_Za7e0=jC;{W(#W6>>)uI-g2Vo3wAzyExH^4EubW97ke zFV(xvI!0n;u|-jXs$_Ow&<-|t#c_LfKm`v&O6Iyega;khjiemgF07JDlk027DXvtR zHjiS3$V5&b))g>qPd`T%iM3Bp-p@=$0Q z2~LZnE-HGeR{7BqlTA7dd9QC8LsYr^{PPL!o6x05snq+_Q*^(`WG0GZkC$~SBZ;`o zQu4Md*yf=`JbZ~lm8olP&t!`lB}1n#RyMMWjflR@<`0Sz7e^p^rccT{j~_qWLHCK^ zc7o6lMIJk?jQLrvZx~_r-^QV|U^kVFn^?JFalMMjL=iBKl5+|!*5a<#%6@k45sK zBN|fV<1vqJMfc@Z(;|KhT4Mxy6r_?rKwyx_x=y3QA&H#%QSv^6_w2vhPKfrE5tUQm*r|&W=G+Q1m*hiQL9+t=~ ziswHgTyqi9cRUF@&_0D9&6A{-0=04Fc!Q2+n{ literal 0 HcmV?d00001