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};
#[derive(PartialEq, Serialize, Deserialize)]
#[derive(PartialEq, Serialize, Deserialize, Clone)]
pub enum Difficulty {
Peaceful,
Easy,

View File

@ -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" }

View File

@ -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<i32>,
) -> Result<super::ChunkData, ChunkReadingError> {
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,
},

View File

@ -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<i32>,
) -> Result<ChunkData, ChunkReadingError>;
}

View File

@ -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<WorldInfo>,
save_file: Option<SaveFile>,
pub level_info: LevelData,
world_info_writer: Arc<dyn WorldInfoWriter>,
level_folder: LevelFolder,
loaded_chunks: Arc<DashMap<Vector2<i32>, Arc<RwLock<ChunkData>>>>,
chunk_watchers: Arc<DashMap<Vector2<i32>, usize>>,
chunk_reader: Arc<dyn ChunkReader>,
@ -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 {
if !region_folder.exists() {
std::fs::create_dir_all(&region_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.
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),
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()),
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,
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<dyn ChunkReader>,
save_file: SaveFile,
save_file: &LevelFolder,
chunk_pos: Vector2<i32>,
) -> 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)))),
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,9 +234,8 @@ 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) {
let loaded_chunk =
match Self::load_chunk_from_save(chunk_reader, &level_info, chunk_pos) {
Ok(chunk) => chunk,
Err(err) => {
log::error!(
@ -254,7 +246,6 @@ impl Level {
None
}
}
})
.unwrap_or_else(|| {
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 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<WorldInfo, WorldInfoError> {
let path = save_file.root_folder.join("level.dat");
impl WorldInfoReader for AnvilLevelInfo {
fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError> {
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::<LevelDat>(&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,
}

View File

@ -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<WorldInfo, WorldInfoError>;
fn read_world_info(&self, level_folder: &LevelFolder) -> Result<LevelData, WorldInfoError>;
}
#[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)]

View File

@ -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)
}
}

View File

@ -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();

View File

@ -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,

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.
///
/// 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<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
fn spawn_world_chunks(
&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
// 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();