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 all 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
2 changes: 1 addition & 1 deletion pumpkin-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ enum_dispatch = "0.3"
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }

noise = "0.9"
rand = "0.8"

# Had to use custom, because google's is broken, I made a PR.
serde_json5 = { git = "https://github.com/kralverde/serde_json5.git" }

derive-getters = "0.5.0"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
temp-dir = "0.1.14"

[[bench]]
name = "chunk_noise_populate"
Expand Down
15 changes: 6 additions & 9 deletions pumpkin-world/src/chunk/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ mod tests {
use pumpkin_util::math::vector2::Vector2;
use std::fs;
use std::path::PathBuf;
use temp_dir::TempDir;

use crate::chunk::ChunkWriter;
use crate::generation::{get_world_gen, Seed};
Expand All @@ -516,15 +517,13 @@ mod tests {
#[test]
fn test_writing() {
let generator = get_world_gen(Seed(0));

let temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {
root_folder: PathBuf::from("./tmp_Anvil"),
region_folder: PathBuf::from("./tmp_Anvil/region"),
root_folder: temp_dir.path().to_path_buf(),
region_folder: temp_dir.path().join("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");
fs::create_dir(&level_folder.region_folder).expect("couldn't create region folder");

// Generate chunks
let mut chunks = vec![];
Expand Down Expand Up @@ -561,8 +560,6 @@ mod tests {
}
}

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

println!("Checked chunks successfully");
}
}
15 changes: 6 additions & 9 deletions pumpkin-world/src/chunk/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ mod tests {
use pumpkin_util::math::vector2::Vector2;
use std::fs;
use std::path::PathBuf;
use temp_dir::TempDir;

use crate::chunk::ChunkWriter;
use crate::generation::{get_world_gen, Seed};
Expand All @@ -485,15 +486,13 @@ mod tests {
#[test]
fn test_writing() {
let generator = get_world_gen(Seed(0));

let temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {
root_folder: PathBuf::from("./tmp_Linear"),
region_folder: PathBuf::from("./tmp_Linear/region"),
root_folder: temp_dir.path().to_path_buf(),
region_folder: temp_dir.path().join("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");
fs::create_dir(&level_folder.region_folder).expect("couldn't create region folder");

// Generate chunks
let mut chunks = vec![];
Expand Down Expand Up @@ -530,8 +529,6 @@ mod tests {
}
}

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

println!("Checked chunks successfully");
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use noise::Perlin;
use pumpkin_data::chunk::Biome;
use pumpkin_macros::block_state;
use pumpkin_util::math::vector2::Vector2;
use rand::Rng;
use pumpkin_util::{
math::vector2::Vector2,
random::{self, xoroshiro128::Xoroshiro, RandomImpl},
};

use crate::{
chunk::Subchunks,
Expand Down Expand Up @@ -63,12 +65,15 @@ impl PerlinTerrainGenerator for PlainsTerrainGenerator {
} else if y == chunk_height - 2 {
subchunks.set_block(coordinates, block_state!("grass_block").state_id);
} else if y == chunk_height - 1 {
let seed = random::get_seed();
let mut random = Xoroshiro::from_seed(seed);

// TODO: generate flowers and grass
let grass: u8 = rand::thread_rng().gen_range(0..7);
let grass = random.next_bounded_i32(8);
if grass == 3 {
let flower: u8 = rand::thread_rng().gen_range(0..20);
let flower = random.next_bounded_i32(20);
if flower == 6 {
match rand::thread_rng().gen_range(0..4) {
match random.next_bounded_i32(5) {
0 => {
subchunks.set_block(coordinates, block_state!("dandelion").state_id);
}
Expand Down
58 changes: 46 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, WorldInfoError, WorldInfoReader, WorldInfoWriter,
},
};

/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
Expand Down Expand Up @@ -67,9 +74,31 @@ 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 {
match error {
// If it doesn't exist, just make a new one
WorldInfoError::InfoNotFound => (),
_ => {
log::error!("Failed to load world info!");
log::error!("{}", error);
panic!("Unsupported world data! See the logs for more info.");
}
}
} 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 +126,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 +241,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
117 changes: 84 additions & 33 deletions pumpkin-world/src/world_info/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,59 @@ use std::{
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use serde::{Deserialize, Serialize};

use crate::level::LevelFolder;
use crate::{
level::LevelFolder,
world_info::{MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION},
};

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;

fn check_file_data_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> {
// Define a struct that only has the data version
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct LevelData {
data_version: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct LevelDat {
data: LevelData,
}

let info: LevelDat = fastnbt::from_bytes(raw_nbt)
.map_err(|e|{
log::error!("The world.dat file does not have a data version! This means it is either corrupt or very old (read unsupported)");
WorldInfoError::DeserializationError(e.to_string())})?;

let data_version = info.data.data_version;

if !(MINIMUM_SUPPORTED_WORLD_DATA_VERSION..=MAXIMUM_SUPPORTED_WORLD_DATA_VERSION)
.contains(&data_version)
{
Err(WorldInfoError::UnsupportedVersion(data_version))
} else {
Ok(())
}
}

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

check_file_data_version(&decompressed_data)?;

let info = fastnbt::from_bytes::<LevelDat>(&decompressed_data)
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;

Expand All @@ -41,45 +72,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 +112,33 @@ pub struct LevelDat {
#[serde(rename = "Data")]
pub data: LevelData,
}

#[cfg(test)]
mod test {
use temp_dir::TempDir;

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 temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {
root_folder: temp_dir.path().to_path_buf(),
region_folder: temp_dir.path().join("region"),
};

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

assert_eq!(data.world_gen_settings.seed, seed);
}
}
Loading