diff --git a/pumpkin-core/src/lib.rs b/pumpkin-core/src/lib.rs index 09f36f9b..8ffd25b2 100644 --- a/pumpkin-core/src/lib.rs +++ b/pumpkin-core/src/lib.rs @@ -7,7 +7,7 @@ pub use gamemode::GameMode; use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Serialize, Deserialize)] +#[derive(PartialEq, Serialize, Deserialize, Clone)] pub enum Difficulty { Peaceful, Easy, diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index fcbc14a5..495ee3ff 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true [dependencies] +pumpkin-nbt = { path = "../pumpkin-nbt" } pumpkin-core = { path = "../pumpkin-core" } pumpkin-config = { path = "../pumpkin-config" } pumpkin-macros = { path = "../pumpkin-macros" } diff --git a/pumpkin-world/src/chunk/anvil.rs b/pumpkin-world/src/chunk/anvil.rs index 86389be6..2391537f 100644 --- a/pumpkin-world/src/chunk/anvil.rs +++ b/pumpkin-world/src/chunk/anvil.rs @@ -5,7 +5,7 @@ use std::{ use flate2::bufread::{GzDecoder, ZlibDecoder}; -use crate::level::SaveFile; +use crate::level::LevelFolder; use super::{ChunkData, ChunkReader, ChunkReadingError, CompressionError}; @@ -87,7 +87,7 @@ impl Compression { impl ChunkReader for AnvilChunkReader { fn read_chunk( &self, - save_file: &SaveFile, + save_file: &LevelFolder, at: &pumpkin_core::math::vector2::Vector2, ) -> Result { let region = (at.x >> 5, at.z >> 5); @@ -168,14 +168,14 @@ mod tests { use crate::{ chunk::{anvil::AnvilChunkReader, ChunkReader, ChunkReadingError}, - level::SaveFile, + level::LevelFolder, }; #[test] fn not_existing() { let region_path = PathBuf::from("not_existing"); let result = AnvilChunkReader::new().read_chunk( - &SaveFile { + &LevelFolder { root_folder: PathBuf::from(""), region_folder: region_path, }, diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index fcbb53f1..8473e030 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::{ block::BlockState, coordinates::{ChunkRelativeBlockCoordinates, Height}, - level::SaveFile, + level::LevelFolder, WORLD_HEIGHT, }; @@ -22,7 +22,7 @@ const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT; pub trait ChunkReader: Sync + Send { fn read_chunk( &self, - save_file: &SaveFile, + save_file: &LevelFolder, at: &Vector2, ) -> Result; } diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index 4c06f5a2..930363ec 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -2,7 +2,6 @@ use std::{path::PathBuf, sync::Arc}; use dashmap::{DashMap, Entry}; use num_traits::Zero; -use pumpkin_config::BASIC_CONFIG; use pumpkin_core::math::vector2::Vector2; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use tokio::{ @@ -15,7 +14,7 @@ use crate::{ anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError, }, generation::{get_world_gen, Seed, WorldGenerator}, - world_info::{anvil::AnvilInfoReader, WorldInfo, WorldInfoReader}, + world_info::{anvil::AnvilLevelInfo, LevelData, WorldInfoReader, WorldInfoWriter}, }; /// The `Level` module provides functionality for working with chunks within or outside a Minecraft world. @@ -29,8 +28,9 @@ 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, + pub level_info: LevelData, + world_info_writer: Arc, + level_folder: LevelFolder, loaded_chunks: Arc, Arc>>>, chunk_watchers: Arc, usize>>, chunk_reader: Arc, @@ -38,60 +38,53 @@ pub struct Level { } #[derive(Clone)] -pub struct SaveFile { +pub struct LevelFolder { pub root_folder: PathBuf, pub region_folder: PathBuf, } -fn get_or_create_seed() -> Seed { - // TODO: if there is a seed in the config (!= 0) use it. Otherwise make a random one - Seed::from(BASIC_CONFIG.seed.as_str()) -} - impl Level { pub fn from_root_folder(root_folder: PathBuf) -> Self { // If we are using an already existing world we want to read the seed from the level.dat, If not we want to check if there is a seed in the config, if not lets create a random one - if root_folder.exists() { - let region_folder = root_folder.join("region"); - assert!( - region_folder.exists(), - "World region folder does not exist, despite there being a root folder." - ); - let save_file = SaveFile { - root_folder, - region_folder, - }; + let region_folder = root_folder.join("region"); + if !region_folder.exists() { + std::fs::create_dir_all(®ion_folder).expect("Failed to create Region folder"); + } + let level_folder = LevelFolder { + 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. + // TODO: Load info correctly based on world format type + let level_info = AnvilLevelInfo + .read_world_info(&level_folder) + .unwrap_or_default(); // TODO: Improve error handling + let seed = Seed(level_info.world_gen_settings.seed as u64); + let world_gen = get_world_gen(seed).into(); - Self { - seed, - world_gen, - 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(); - let world_gen = get_world_gen(seed).into(); - Self { - seed, - world_gen, - save_file: None, - chunk_reader: Arc::new(AnvilChunkReader::new()), - loaded_chunks: Arc::new(DashMap::new()), - chunk_watchers: Arc::new(DashMap::new()), - world_info: None, - } + Self { + seed, + world_gen, + world_info_writer: Arc::new(AnvilLevelInfo), + level_folder, + chunk_reader: Arc::new(AnvilChunkReader::new()), + loaded_chunks: Arc::new(DashMap::new()), + chunk_watchers: Arc::new(DashMap::new()), + level_info, + } + } + + pub async fn save(&self) { + log::info!("Saving level..."); + // lets first save all chunks + for chunk in self.loaded_chunks.iter() { + let chunk = chunk.read().await; + self.clean_chunk(&chunk.position); } + // then lets save the world info + self.world_info_writer + .write_world_info(self.level_info.clone(), &self.level_folder) + .expect("Failed to save world info"); } pub fn get_block() {} @@ -205,10 +198,10 @@ impl Level { fn load_chunk_from_save( chunk_reader: Arc, - save_file: SaveFile, + save_file: &LevelFolder, chunk_pos: Vector2, ) -> Result>>, ChunkReadingError> { - match chunk_reader.read_chunk(&save_file, &chunk_pos) { + match chunk_reader.read_chunk(save_file, &chunk_pos) { Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))), Err( ChunkReadingError::ChunkNotExist @@ -233,7 +226,7 @@ impl Level { let channel = channel.clone(); let loaded_chunks = self.loaded_chunks.clone(); let chunk_reader = self.chunk_reader.clone(); - let save_file = self.save_file.clone(); + let level_info = self.level_folder.clone(); let world_gen = self.world_gen.clone(); let chunk_pos = *at; @@ -241,20 +234,18 @@ impl Level { .get(&chunk_pos) .map(|entry| entry.value().clone()) .unwrap_or_else(|| { - let loaded_chunk = save_file - .and_then(|save_file| { - match Self::load_chunk_from_save(chunk_reader, save_file, chunk_pos) { - Ok(chunk) => chunk, - Err(err) => { - log::error!( - "Failed to read chunk (regenerating) {:?}: {:?}", - chunk_pos, - err - ); - None - } + let loaded_chunk = + match Self::load_chunk_from_save(chunk_reader, &level_info, chunk_pos) { + Ok(chunk) => chunk, + 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))) }); diff --git a/pumpkin-world/src/world_info/anvil.rs b/pumpkin-world/src/world_info/anvil.rs index 1132d072..080d61e1 100644 --- a/pumpkin-world/src/world_info/anvil.rs +++ b/pumpkin-world/src/world_info/anvil.rs @@ -1,29 +1,30 @@ -use std::{fs::OpenOptions, io::Read}; +use std::{ + fs::OpenOptions, + io::{Read, Write}, + time::{SystemTime, UNIX_EPOCH}, +}; -use flate2::read::GzDecoder; -use serde::Deserialize; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use serde::{Deserialize, Serialize}; -use crate::level::SaveFile; +use crate::level::LevelFolder; -use super::{WorldInfo, WorldInfoError, WorldInfoReader}; +use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter}; -pub struct AnvilInfoReader {} +const LEVEL_DAT_FILE_NAME: &str = "level.dat"; -impl AnvilInfoReader { - pub fn new() -> Self { - Self {} - } -} +pub struct AnvilLevelInfo; -impl WorldInfoReader for AnvilInfoReader { - fn read_world_info(&self, save_file: &SaveFile) -> Result { - let path = save_file.root_folder.join("level.dat"); +impl WorldInfoReader for AnvilLevelInfo { + fn read_world_info(&self, level_folder: &LevelFolder) -> Result { + let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME); let mut world_info_file = OpenOptions::new().read(true).open(path)?; let mut buffer = Vec::new(); world_info_file.read_to_end(&mut buffer)?; + // try to decompress using GZip let mut decoder = GzDecoder::new(&buffer[..]); let mut decompressed_data = Vec::new(); decoder.read_to_end(&mut decompressed_data)?; @@ -31,63 +32,57 @@ impl WorldInfoReader for AnvilInfoReader { let info = fastnbt::from_bytes::(&decompressed_data) .map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?; - Ok(WorldInfo { - seed: info.data.world_gen_settings.seed, - }) + // todo check version + + Ok(info.data) } } -impl Default for AnvilInfoReader { - fn default() -> Self { - Self::new() +impl WorldInfoWriter for AnvilLevelInfo { + fn write_world_info( + &self, + info: LevelData, + level_folder: &LevelFolder, + ) -> Result<(), WorldInfoError> { + let start = SystemTime::now(); + let since_the_epoch = start + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + let level = LevelDat { + data: LevelData { + allow_commands: info.allow_commands, + data_version: info.data_version, + difficulty: info.difficulty, + world_gen_settings: info.world_gen_settings, + last_played: since_the_epoch.as_millis() as i64, + level_name: info.level_name, + spawn_x: info.spawn_x, + spawn_y: info.spawn_y, + spawn_z: info.spawn_z, + nbt_version: info.nbt_version, + version: info.version, + }, + }; + // convert it into nbt + let nbt = pumpkin_nbt::serializer::to_bytes_unnamed(&level).unwrap(); + // now compress using GZip, TODO: im not sure about the to_vec, but writer is not implemented for BytesMut, see https://github.com/tokio-rs/bytes/pull/478 + let mut encoder = GzEncoder::new(nbt.to_vec(), Compression::best()); + let compressed_data = Vec::new(); + encoder.write_all(&compressed_data)?; + + // open file + let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME); + let mut world_info_file = OpenOptions::new().write(true).open(path)?; + // write compressed data into file + world_info_file.write_all(&compressed_data).unwrap(); + + Ok(()) } } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct LevelDat { - // No idea why its formatted like this + // This tag contains all the level data. #[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); - } + pub data: LevelData, } diff --git a/pumpkin-world/src/world_info/mod.rs b/pumpkin-world/src/world_info/mod.rs index a1184635..d06be497 100644 --- a/pumpkin-world/src/world_info/mod.rs +++ b/pumpkin-world/src/world_info/mod.rs @@ -1,17 +1,113 @@ +use pumpkin_config::BASIC_CONFIG; +use pumpkin_core::Difficulty; +use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::level::SaveFile; +use crate::{generation::Seed, level::LevelFolder}; pub mod anvil; pub(crate) trait WorldInfoReader { - fn read_world_info(&self, save_file: &SaveFile) -> Result; + fn read_world_info(&self, level_folder: &LevelFolder) -> Result; } -#[derive(Debug, PartialEq)] -pub struct WorldInfo { +pub(crate) trait WorldInfoWriter: Sync + Send { + fn write_world_info( + &self, + info: LevelData, + level_folder: &LevelFolder, + ) -> Result<(), WorldInfoError>; +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct LevelData { + // true if cheats are enabled. + pub allow_commands: bool, + // An integer displaying the data version. + pub data_version: i32, + // The current difficulty setting. + pub difficulty: Difficulty, + // the generation settings for each dimension. + pub world_gen_settings: WorldGenSettings, + // The Unix time in milliseconds when the level was last loaded. + pub last_played: i64, + // The name of the level. + pub level_name: String, + // The X coordinate of the world spawn. + pub spawn_x: i32, + // The Y coordinate of the world spawn. + pub spawn_y: i32, + // The Z coordinate of the world spawn. + pub spawn_z: i32, + #[serde(rename = "version")] + // The NBT version of the level + pub nbt_version: i32, + #[serde(rename = "Version")] + pub version: WorldVersion, + // TODO: Implement the rest of the fields +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct WorldGenSettings { + // the numerical seed of the world pub seed: i64, - // TODO: Implement all fields +} + +fn get_or_create_seed() -> Seed { + // TODO: if there is a seed in the config (!= 0) use it. Otherwise make a random one + Seed::from(BASIC_CONFIG.seed.as_str()) +} + +impl Default for WorldGenSettings { + fn default() -> Self { + Self { + seed: get_or_create_seed().0 as i64, + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "PascalCase")] +pub struct WorldVersion { + // The version name as a string, e.g. "15w32b". + pub name: String, + // An integer displaying the data version. + pub id: i32, + // Whether the version is a snapshot or not. + pub snapshot: bool, + // Developing series. In 1.18 experimental snapshots, it was set to "ccpreview". In others, set to "main". + pub series: String, +} + +impl Default for WorldVersion { + fn default() -> Self { + Self { + name: "1.24.4".to_string(), + id: -1, + snapshot: false, + series: "main".to_string(), + } + } +} + +impl Default for LevelData { + fn default() -> Self { + Self { + allow_commands: true, + // TODO + data_version: -1, + difficulty: Difficulty::Normal, + world_gen_settings: Default::default(), + last_played: -1, + level_name: "world".to_string(), + spawn_x: 0, + spawn_y: 200, + spawn_z: 0, + nbt_version: -1, + version: Default::default(), + } + } } #[derive(Error, Debug)] diff --git a/pumpkin-world/test-files/sample-1/level.dat b/pumpkin-world/test-files/sample-1/level.dat deleted file mode 100644 index 1a527907..00000000 Binary files a/pumpkin-world/test-files/sample-1/level.dat and /dev/null differ diff --git a/pumpkin/src/command/commands/cmd_stop.rs b/pumpkin/src/command/commands/cmd_stop.rs index 0f1bc7af..de7803b1 100644 --- a/pumpkin/src/command/commands/cmd_stop.rs +++ b/pumpkin/src/command/commands/cmd_stop.rs @@ -32,7 +32,7 @@ impl CommandExecutor for StopExecutor { for player in server.get_all_players().await { player.kick(kick_message.clone()).await; } - + server.save().await; std::process::exit(0) } } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index bc275f8b..dbc84e74 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -220,18 +220,18 @@ impl Player { ); // Decrement value of watched chunks - let chunks_to_clean = world.mark_chunks_as_not_watched(&radial_chunks); + let chunks_to_clean = world.level.mark_chunks_as_not_watched(&radial_chunks); // Remove chunks with no watchers from the cache - world.clean_chunks(&chunks_to_clean); + world.level.clean_chunks(&chunks_to_clean); // Remove left over entries from all possiblily loaded chunks - world.clean_memory(&radial_chunks); + world.level.clean_memory(&radial_chunks); log::debug!( "Removed player id {} ({}) ({} chunks remain cached)", self.gameprofile.name, self.client.id, - self.world().get_cached_chunk_len() + self.world().level.loaded_chunk_count() ); //self.world().level.list_cached(); diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index d8f0fc83..c672eb2c 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -174,6 +174,12 @@ impl Server { self.server_listing.lock().await.remove_player(); } + pub async fn save(&self) { + for world in &self.worlds { + world.save().await; + } + } + pub async fn try_get_container( &self, player_id: EntityId, diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 9380f1bb..02c13b10 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -118,6 +118,10 @@ impl World { } } + pub async fn save(&self) { + self.level.save().await; + } + /// Broadcasts a packet to all connected players within the world. /// /// Sends the specified packet to every player currently logged in to the world. @@ -523,26 +527,6 @@ impl World { player.set_health(20.0, 20, 20.0).await; } - pub fn mark_chunks_as_not_watched(&self, chunks: &[Vector2]) -> Vec> { - self.level.mark_chunks_as_not_watched(chunks) - } - - pub fn mark_chunks_as_watched(&self, chunks: &[Vector2]) { - self.level.mark_chunks_as_newly_watched(chunks); - } - - pub fn clean_chunks(&self, chunks: &[Vector2]) { - self.level.clean_chunks(chunks); - } - - pub fn clean_memory(&self, chunks_to_check: &[Vector2]) { - self.level.clean_memory(chunks_to_check); - } - - pub fn get_cached_chunk_len(&self) -> usize { - self.level.loaded_chunk_count() - } - /// IMPORTANT: Chunks have to be non-empty fn spawn_world_chunks( &self, diff --git a/pumpkin/src/world/player_chunker.rs b/pumpkin/src/world/player_chunker.rs index c0ba854e..98e1157d 100644 --- a/pumpkin/src/world/player_chunker.rs +++ b/pumpkin/src/world/player_chunker.rs @@ -79,12 +79,18 @@ pub async fn update_position(player: &Arc) { // Make sure the watched section and the chunk watcher updates are async atomic. We want to // ensure what we unload when the player disconnects is correct - entity.world.mark_chunks_as_watched(&loading_chunks); - let chunks_to_clean = entity.world.mark_chunks_as_not_watched(&unloading_chunks); + entity + .world + .level + .mark_chunks_as_newly_watched(&loading_chunks); + let chunks_to_clean = entity + .world + .level + .mark_chunks_as_not_watched(&unloading_chunks); player.watched_section.store(new_cylindrical); if !chunks_to_clean.is_empty() { - entity.world.clean_chunks(&chunks_to_clean); + entity.world.level.clean_chunks(&chunks_to_clean); // This can take a little if we are sending a bunch of packets, queue it up :p let client = player.client.clone();