Skip to content

Commit

Permalink
Implement level.dat reading and load the seed for use in world genera…
Browse files Browse the repository at this point in the history
…tion (#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 <[email protected]>
  • Loading branch information
neeleshpoli and Snowiiii authored Dec 24, 2024
1 parent 5394910 commit cc83e09
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 7 deletions.
22 changes: 16 additions & 6 deletions pumpkin-world/src/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<WorldInfo>,
save_file: Option<SaveFile>,
loaded_chunks: Arc<DashMap<Vector2<i32>, Arc<RwLock<ChunkData>>>>,
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
Expand Down Expand Up @@ -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();
Expand All @@ -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,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion pumpkin-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
93 changes: 93 additions & 0 deletions pumpkin-world/src/world_info/anvil.rs
Original file line number Diff line number Diff line change
@@ -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<WorldInfo, WorldInfoError> {
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::<LevelDat>(&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);
}
}
34 changes: 34 additions & 0 deletions pumpkin-world/src/world_info/mod.rs
Original file line number Diff line number Diff line change
@@ -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<WorldInfo, WorldInfoError>;
}

#[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<std::io::Error> for WorldInfoError {
fn from(value: std::io::Error) -> Self {
match value.kind() {
std::io::ErrorKind::NotFound => Self::InfoNotFound,
value => Self::IoError(value),
}
}
}
Binary file added pumpkin-world/test-files/sample-1/level.dat
Binary file not shown.

0 comments on commit cc83e09

Please sign in to comment.