Save level.dat

This commit is contained in:
Alexander Medvedev 2024-12-24 17:48:59 +01:00
parent cc83e090f9
commit fcf85df518
13 changed files with 250 additions and 171 deletions

View File

@ -7,7 +7,7 @@ pub use gamemode::GameMode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(PartialEq, Serialize, Deserialize)] #[derive(PartialEq, Serialize, Deserialize, Clone)]
pub enum Difficulty { pub enum Difficulty {
Peaceful, Peaceful,
Easy, Easy,

View File

@ -4,6 +4,7 @@ version.workspace = true
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]
pumpkin-nbt = { path = "../pumpkin-nbt" }
pumpkin-core = { path = "../pumpkin-core" } pumpkin-core = { path = "../pumpkin-core" }
pumpkin-config = { path = "../pumpkin-config" } pumpkin-config = { path = "../pumpkin-config" }
pumpkin-macros = { path = "../pumpkin-macros" } pumpkin-macros = { path = "../pumpkin-macros" }

View File

@ -5,7 +5,7 @@ use std::{
use flate2::bufread::{GzDecoder, ZlibDecoder}; use flate2::bufread::{GzDecoder, ZlibDecoder};
use crate::level::SaveFile; use crate::level::LevelFolder;
use super::{ChunkData, ChunkReader, ChunkReadingError, CompressionError}; use super::{ChunkData, ChunkReader, ChunkReadingError, CompressionError};
@ -87,7 +87,7 @@ impl Compression {
impl ChunkReader for AnvilChunkReader { impl ChunkReader for AnvilChunkReader {
fn read_chunk( fn read_chunk(
&self, &self,
save_file: &SaveFile, save_file: &LevelFolder,
at: &pumpkin_core::math::vector2::Vector2<i32>, at: &pumpkin_core::math::vector2::Vector2<i32>,
) -> Result<super::ChunkData, ChunkReadingError> { ) -> Result<super::ChunkData, ChunkReadingError> {
let region = (at.x >> 5, at.z >> 5); let region = (at.x >> 5, at.z >> 5);
@ -168,14 +168,14 @@ mod tests {
use crate::{ use crate::{
chunk::{anvil::AnvilChunkReader, ChunkReader, ChunkReadingError}, chunk::{anvil::AnvilChunkReader, ChunkReader, ChunkReadingError},
level::SaveFile, level::LevelFolder,
}; };
#[test] #[test]
fn not_existing() { fn not_existing() {
let region_path = PathBuf::from("not_existing"); let region_path = PathBuf::from("not_existing");
let result = AnvilChunkReader::new().read_chunk( let result = AnvilChunkReader::new().read_chunk(
&SaveFile { &LevelFolder {
root_folder: PathBuf::from(""), root_folder: PathBuf::from(""),
region_folder: region_path, region_folder: region_path,
}, },

View File

@ -9,7 +9,7 @@ use thiserror::Error;
use crate::{ use crate::{
block::BlockState, block::BlockState,
coordinates::{ChunkRelativeBlockCoordinates, Height}, coordinates::{ChunkRelativeBlockCoordinates, Height},
level::SaveFile, level::LevelFolder,
WORLD_HEIGHT, WORLD_HEIGHT,
}; };
@ -22,7 +22,7 @@ const CHUNK_VOLUME: usize = CHUNK_AREA * WORLD_HEIGHT;
pub trait ChunkReader: Sync + Send { pub trait ChunkReader: Sync + Send {
fn read_chunk( fn read_chunk(
&self, &self,
save_file: &SaveFile, save_file: &LevelFolder,
at: &Vector2<i32>, at: &Vector2<i32>,
) -> Result<ChunkData, ChunkReadingError>; ) -> Result<ChunkData, ChunkReadingError>;
} }

View File

@ -2,7 +2,6 @@ use std::{path::PathBuf, sync::Arc};
use dashmap::{DashMap, Entry}; use dashmap::{DashMap, Entry};
use num_traits::Zero; use num_traits::Zero;
use pumpkin_config::BASIC_CONFIG;
use pumpkin_core::math::vector2::Vector2; use pumpkin_core::math::vector2::Vector2;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tokio::{ use tokio::{
@ -15,7 +14,7 @@ use crate::{
anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError, anvil::AnvilChunkReader, ChunkData, ChunkParsingError, ChunkReader, ChunkReadingError,
}, },
generation::{get_world_gen, Seed, WorldGenerator}, 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. /// 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. /// For more details on world generation, refer to the `WorldGenerator` module.
pub struct Level { pub struct Level {
pub seed: Seed, pub seed: Seed,
pub world_info: Option<WorldInfo>, pub level_info: LevelData,
save_file: Option<SaveFile>, world_info_writer: Arc<dyn WorldInfoWriter>,
level_folder: LevelFolder,
loaded_chunks: Arc<DashMap<Vector2<i32>, Arc<RwLock<ChunkData>>>>, loaded_chunks: Arc<DashMap<Vector2<i32>, Arc<RwLock<ChunkData>>>>,
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>, chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
chunk_reader: Arc<dyn ChunkReader>, chunk_reader: Arc<dyn ChunkReader>,
@ -38,60 +38,53 @@ pub struct Level {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct SaveFile { pub struct LevelFolder {
pub root_folder: PathBuf, pub root_folder: PathBuf,
pub region_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 { impl Level {
pub fn from_root_folder(root_folder: PathBuf) -> Self { 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 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"); let region_folder = root_folder.join("region");
assert!( if !region_folder.exists() {
region_folder.exists(), std::fs::create_dir_all(&region_folder).expect("Failed to create Region folder");
"World region folder does not exist, despite there being a root folder." }
); let level_folder = LevelFolder {
let save_file = SaveFile {
root_folder, root_folder,
region_folder, region_folder,
}; };
// TODO: Load info correctly based on world format type // TODO: Load info correctly based on world format type
let world_info_reader = AnvilInfoReader::new(); let level_info = AnvilLevelInfo
let info = world_info_reader .read_world_info(&level_folder)
.read_world_info(&save_file) .unwrap_or_default(); // TODO: Improve error handling
.expect("Unable to get world info!"); // TODO: Improve error handling let seed = Seed(level_info.world_gen_settings.seed as u64);
let seed = Seed(info.seed as u64); let world_gen = get_world_gen(seed).into();
let world_gen = get_world_gen(seed).into(); // TODO Read Seed from config.
Self { Self {
seed, seed,
world_gen, world_gen,
save_file: Some(save_file), world_info_writer: Arc::new(AnvilLevelInfo),
level_folder,
chunk_reader: Arc::new(AnvilChunkReader::new()), chunk_reader: Arc::new(AnvilChunkReader::new()),
loaded_chunks: Arc::new(DashMap::new()), loaded_chunks: Arc::new(DashMap::new()),
chunk_watchers: Arc::new(DashMap::new()), chunk_watchers: Arc::new(DashMap::new()),
world_info: Some(info), level_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,
} }
} }
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() {} pub fn get_block() {}
@ -205,10 +198,10 @@ impl Level {
fn load_chunk_from_save( fn load_chunk_from_save(
chunk_reader: Arc<dyn ChunkReader>, chunk_reader: Arc<dyn ChunkReader>,
save_file: SaveFile, save_file: &LevelFolder,
chunk_pos: Vector2<i32>, chunk_pos: Vector2<i32>,
) -> Result<Option<Arc<RwLock<ChunkData>>>, ChunkReadingError> { ) -> Result<Option<Arc<RwLock<ChunkData>>>, 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)))), Ok(data) => Ok(Some(Arc::new(RwLock::new(data)))),
Err( Err(
ChunkReadingError::ChunkNotExist ChunkReadingError::ChunkNotExist
@ -233,7 +226,7 @@ impl Level {
let channel = channel.clone(); let channel = channel.clone();
let loaded_chunks = self.loaded_chunks.clone(); let loaded_chunks = self.loaded_chunks.clone();
let chunk_reader = self.chunk_reader.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 world_gen = self.world_gen.clone();
let chunk_pos = *at; let chunk_pos = *at;
@ -241,9 +234,8 @@ impl Level {
.get(&chunk_pos) .get(&chunk_pos)
.map(|entry| entry.value().clone()) .map(|entry| entry.value().clone())
.unwrap_or_else(|| { .unwrap_or_else(|| {
let loaded_chunk = save_file let loaded_chunk =
.and_then(|save_file| { match Self::load_chunk_from_save(chunk_reader, &level_info, chunk_pos) {
match Self::load_chunk_from_save(chunk_reader, save_file, chunk_pos) {
Ok(chunk) => chunk, Ok(chunk) => chunk,
Err(err) => { Err(err) => {
log::error!( log::error!(
@ -254,7 +246,6 @@ impl Level {
None None
} }
} }
})
.unwrap_or_else(|| { .unwrap_or_else(|| {
Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos))) Arc::new(RwLock::new(world_gen.generate_chunk(chunk_pos)))
}); });

View File

@ -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 flate2::{read::GzDecoder, write::GzEncoder, Compression};
use serde::Deserialize; 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 struct AnvilLevelInfo;
pub fn new() -> Self {
Self {}
}
}
impl WorldInfoReader for AnvilInfoReader { impl WorldInfoReader for AnvilLevelInfo {
fn read_world_info(&self, save_file: &SaveFile) -> Result<WorldInfo, WorldInfoError> { fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError> {
let path = save_file.root_folder.join("level.dat"); let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
let mut world_info_file = OpenOptions::new().read(true).open(path)?; let mut world_info_file = OpenOptions::new().read(true).open(path)?;
let mut buffer = Vec::new(); let mut buffer = Vec::new();
world_info_file.read_to_end(&mut buffer)?; world_info_file.read_to_end(&mut buffer)?;
// try to decompress using GZip
let mut decoder = GzDecoder::new(&buffer[..]); let mut decoder = GzDecoder::new(&buffer[..]);
let mut decompressed_data = Vec::new(); let mut decompressed_data = Vec::new();
decoder.read_to_end(&mut decompressed_data)?; decoder.read_to_end(&mut decompressed_data)?;
@ -31,63 +32,57 @@ impl WorldInfoReader for AnvilInfoReader {
let info = fastnbt::from_bytes::<LevelDat>(&decompressed_data) let info = fastnbt::from_bytes::<LevelDat>(&decompressed_data)
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?; .map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
Ok(WorldInfo { // todo check version
seed: info.data.world_gen_settings.seed,
}) Ok(info.data)
} }
} }
impl Default for AnvilInfoReader { impl WorldInfoWriter for AnvilLevelInfo {
fn default() -> Self { fn write_world_info(
Self::new() &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 { pub struct LevelDat {
// No idea why its formatted like this // This tag contains all the level data.
#[serde(rename = "Data")] #[serde(rename = "Data")]
pub data: WorldData, pub data: LevelData,
}
#[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);
}
} }

View File

@ -1,17 +1,113 @@
use pumpkin_config::BASIC_CONFIG;
use pumpkin_core::Difficulty;
use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::level::SaveFile; use crate::{generation::Seed, level::LevelFolder};
pub mod anvil; pub mod anvil;
pub(crate) trait WorldInfoReader { pub(crate) trait WorldInfoReader {
fn read_world_info(&self, save_file: &SaveFile) -> Result<WorldInfo, WorldInfoError>; fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError>;
} }
#[derive(Debug, PartialEq)] pub(crate) trait WorldInfoWriter: Sync + Send {
pub struct WorldInfo { 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, 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)] #[derive(Error, Debug)]

View File

@ -32,7 +32,7 @@ impl CommandExecutor for StopExecutor {
for player in server.get_all_players().await { for player in server.get_all_players().await {
player.kick(kick_message.clone()).await; player.kick(kick_message.clone()).await;
} }
server.save().await;
std::process::exit(0) std::process::exit(0)
} }
} }

View File

@ -220,18 +220,18 @@ impl Player {
); );
// Decrement value of watched chunks // 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 // 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 // Remove left over entries from all possiblily loaded chunks
world.clean_memory(&radial_chunks); world.level.clean_memory(&radial_chunks);
log::debug!( log::debug!(
"Removed player id {} ({}) ({} chunks remain cached)", "Removed player id {} ({}) ({} chunks remain cached)",
self.gameprofile.name, self.gameprofile.name,
self.client.id, self.client.id,
self.world().get_cached_chunk_len() self.world().level.loaded_chunk_count()
); );
//self.world().level.list_cached(); //self.world().level.list_cached();

View File

@ -174,6 +174,12 @@ impl Server {
self.server_listing.lock().await.remove_player(); 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( pub async fn try_get_container(
&self, &self,
player_id: EntityId, player_id: EntityId,

View File

@ -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. /// Broadcasts a packet to all connected players within the world.
/// ///
/// Sends the specified packet to every player currently logged in to 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; player.set_health(20.0, 20, 20.0).await;
} }
pub fn mark_chunks_as_not_watched(&self, chunks: &[Vector2<i32>]) -> Vec<Vector2<i32>> {
self.level.mark_chunks_as_not_watched(chunks)
}
pub fn mark_chunks_as_watched(&self, chunks: &[Vector2<i32>]) {
self.level.mark_chunks_as_newly_watched(chunks);
}
pub fn clean_chunks(&self, chunks: &[Vector2<i32>]) {
self.level.clean_chunks(chunks);
}
pub fn clean_memory(&self, chunks_to_check: &[Vector2<i32>]) {
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 /// IMPORTANT: Chunks have to be non-empty
fn spawn_world_chunks( fn spawn_world_chunks(
&self, &self,

View File

@ -79,12 +79,18 @@ pub async fn update_position(player: &Arc<Player>) {
// Make sure the watched section and the chunk watcher updates are async atomic. We want to // 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 // ensure what we unload when the player disconnects is correct
entity.world.mark_chunks_as_watched(&loading_chunks); entity
let chunks_to_clean = entity.world.mark_chunks_as_not_watched(&unloading_chunks); .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); player.watched_section.store(new_cylindrical);
if !chunks_to_clean.is_empty() { 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 // This can take a little if we are sending a bunch of packets, queue it up :p
let client = player.client.clone(); let client = player.client.clone();