Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix world.dat serialization #543

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions pumpkin-world/src/level.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::{path::PathBuf, sync::Arc};
use std::{
fs::{self},
path::PathBuf,
sync::Arc,
};

use dashmap::{DashMap, Entry};
use num_traits::Zero;
Expand All @@ -17,7 +21,10 @@ use crate::{
},
generation::{get_world_gen, Seed, WorldGenerator},
lock::{anvil::AnvilLevelLocker, LevelLocker},
world_info::{anvil::AnvilLevelInfo, LevelData, WorldInfoReader, WorldInfoWriter},
world_info::{
anvil::{AnvilLevelInfo, LEVEL_DAT_BACKUP_FILE_NAME, LEVEL_DAT_FILE_NAME},
LevelData, WorldInfoReader, WorldInfoWriter,
},
};

/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
Expand Down Expand Up @@ -67,9 +74,25 @@ impl Level {
let locker = AnvilLevelLocker::look(&level_folder).expect("Failed to lock level");

// 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 level_info = AnvilLevelInfo.read_world_info(&level_folder);
if let Err(error) = &level_info {
log::warn!("Failed to load world info!");
log::warn!("{:?}", error);
log::warn!("Using default world options!");
} else {
let dat_path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
if dat_path.exists() {
let backup_path = level_folder.root_folder.join(LEVEL_DAT_BACKUP_FILE_NAME);
fs::copy(dat_path, backup_path).unwrap();
}
}

let level_info = level_info.unwrap_or_default(); // TODO: Improve error handling
log::info!(
"Loading world with seed: {}",
level_info.world_gen_settings.seed
);

let seed = Seed(level_info.world_gen_settings.seed as u64);
let world_gen = get_world_gen(seed).into();

Expand Down Expand Up @@ -97,6 +120,8 @@ impl Level {
log::info!("Saving level...");

// chunks are automatically saved when all players get removed
// TODO: ^This^ isn't true because they are saved in tokio threads. We need to explicitly
// await the handles here.

// then lets save the world info
self.world_info_writer
Expand Down Expand Up @@ -210,13 +235,16 @@ impl Level {
}

pub async fn write_chunk(&self, chunk_to_write: (Vector2<i32>, Arc<RwLock<ChunkData>>)) {
let data = chunk_to_write.1.read().await;
if let Err(error) =
self.chunk_writer
.write_chunk(&data, &self.level_folder, &chunk_to_write.0)
{
log::error!("Failed writing Chunk to disk {}", error.to_string());
}
let chunk_writer = self.chunk_writer.clone();
let level_folder = self.level_folder.clone();

// TODO: Save the join handles to await them when stopping the server
tokio::spawn(async move {
let data = chunk_to_write.1.read().await;
if let Err(error) = chunk_writer.write_chunk(&data, &level_folder, &chunk_to_write.0) {
log::error!("Failed writing Chunk to disk {}", error.to_string());
}
});
}

fn load_chunk_from_save(
Expand Down
86 changes: 54 additions & 32 deletions pumpkin-world/src/world_info/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,18 @@ use crate::level::LevelFolder;

use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};

const LEVEL_DAT_FILE_NAME: &str = "level.dat";
pub const LEVEL_DAT_FILE_NAME: &str = "level.dat";
pub const LEVEL_DAT_BACKUP_FILE_NAME: &str = "level.dat_old";

pub struct AnvilLevelInfo;

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)?;

let world_info_file = OpenOptions::new().read(true).open(path)?;
// try to decompress using GZip
let mut decoder = GzDecoder::new(&buffer[..]);
let mut decoder = GzDecoder::new(world_info_file);
let mut decompressed_data = Vec::new();
decoder.read_to_end(&mut decompressed_data)?;

Expand All @@ -41,45 +38,35 @@ impl WorldInfoReader for AnvilLevelInfo {
impl WorldInfoWriter for AnvilLevelInfo {
fn write_world_info(
&self,
info: LevelData,
data: 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,
spawn_angle: info.spawn_angle,
nbt_version: info.nbt_version,
version: info.version,
},
};

let mut data = data.clone();
data.last_played = since_the_epoch.as_millis() as i64;
let level_dat = LevelDat { data };

// 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)?;
// TODO: Doesn't seem like pumpkin_nbt is working
// TODO: fastnbt doesnt serialize bools
let nbt = fastnbt::to_bytes(&level_dat).unwrap();

// open file
let path = level_folder.root_folder.join(LEVEL_DAT_FILE_NAME);
let mut world_info_file = OpenOptions::new()
let world_info_file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.write(true)
.open(path)?;
// 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(world_info_file, Compression::best());

// write compressed data into file
world_info_file.write_all(&compressed_data).unwrap();
encoder.write_all(&nbt)?;

Ok(())
}
Expand All @@ -91,3 +78,38 @@ pub struct LevelDat {
#[serde(rename = "Data")]
pub data: LevelData,
}

#[cfg(test)]
mod test {
use std::{fs, path::PathBuf};

use crate::{level::LevelFolder, world_info::LevelData};

use super::{AnvilLevelInfo, WorldInfoReader, WorldInfoWriter};

#[test]
fn test_perserve_level_dat_seed() {
let seed = 1337;

let mut data = LevelData::default();
data.world_gen_settings.seed = seed;

let level_folder = LevelFolder {
root_folder: PathBuf::from("./tmp_Info"),
region_folder: PathBuf::from("./tmp_Info/region"),
};
if fs::exists(&level_folder.root_folder).unwrap() {
fs::remove_dir_all(&level_folder.root_folder).expect("Could not delete directory");
}
fs::create_dir_all(&level_folder.region_folder).expect("Could not create directory");

AnvilLevelInfo
.write_world_info(data, &level_folder)
.unwrap();
let data = AnvilLevelInfo.read_world_info(&level_folder).unwrap();

fs::remove_dir_all(&level_folder.root_folder).expect("Could not delete directory");

assert_eq!(data.world_gen_settings.seed, seed);
}
}
11 changes: 4 additions & 7 deletions pumpkin-world/src/world_info/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ pub(crate) trait WorldInfoWriter: Sync + Send {

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
#[serde(default)]
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.
Expand Down Expand Up @@ -58,7 +55,7 @@ pub struct WorldGenSettings {
}

fn get_or_create_seed() -> Seed {
// TODO: if there is a seed in the config (!= 0) use it. Otherwise make a random one
// TODO: if there is a seed in the config (!= "") use it. Otherwise make a random one
Seed::from(BASIC_CONFIG.seed.as_str())
}

Expand All @@ -78,7 +75,9 @@ pub struct WorldVersion {
// An integer displaying the data version.
pub id: i32,
// Whether the version is a snapshot or not.
pub snapshot: bool,
// TODO: fast_nbt doesnt support bools
// pub snapshot: bool,

// Developing series. In 1.18 experimental snapshots, it was set to "ccpreview". In others, set to "main".
pub series: String,
}
Expand All @@ -88,7 +87,6 @@ impl Default for WorldVersion {
Self {
name: "1.24.4".to_string(),
id: -1,
snapshot: false,
series: "main".to_string(),
}
}
Expand All @@ -97,7 +95,6 @@ impl Default for WorldVersion {
impl Default for LevelData {
fn default() -> Self {
Self {
allow_commands: true,
// TODO
data_version: -1,
difficulty: Difficulty::Normal as u8,
Expand Down
4 changes: 2 additions & 2 deletions pumpkin/src/command/commands/stop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ impl CommandExecutor for StopExecutor {
)
.await;

// TODO: Gracefully stop

let kick_message = TextComponent::text("Server stopped");
for player in server.get_all_players().await {
player.kick(kick_message.clone()).await;
}
server.save().await;

// TODO: Gracefully stop
std::process::exit(0)
}
}
Expand Down
4 changes: 2 additions & 2 deletions pumpkin/src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,10 @@ impl PluginManager {
// Take a snapshot of handlers to avoid lifetime issues
let handlers = self.handlers.read().await;

log::debug!("Firing event: {}", E::get_name_static());
log::trace!("Firing event: {}", E::get_name_static());

if let Some(handlers_vec) = handlers.get(&E::get_name_static()) {
log::debug!(
log::trace!(
"Found {} handlers for event: {}",
handlers_vec.len(),
E::get_name_static()
Expand Down
2 changes: 2 additions & 0 deletions pumpkin/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ impl Server {
for world in self.worlds.read().await.iter() {
world.save().await;
}

log::info!("Completed world save");
}

/// Adds a new living entity to the server. This does not Spawn the entity
Expand Down