374 lines
13 KiB
Rust
374 lines
13 KiB
Rust
use std::{
|
||
fs::File,
|
||
io::{Cursor, Read},
|
||
path::Path,
|
||
time::{SystemTime, UNIX_EPOCH},
|
||
};
|
||
use tracing::error;
|
||
|
||
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::world_info::{
|
||
MAXIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION,
|
||
MINIMUM_SUPPORTED_LEVEL_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION,
|
||
data_files::{
|
||
minecraft_data_dir, read_game_rules, read_wandering_trader, read_weather,
|
||
read_world_clocks, read_world_gen_settings, write_custom_boss_events_stub,
|
||
write_game_rules, write_scheduled_events_stub, write_wandering_trader, write_weather,
|
||
write_world_clocks, write_world_gen_settings,
|
||
},
|
||
};
|
||
|
||
use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};
|
||
|
||
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. This is necessary because if a user tries to
|
||
// load a world with different data, they will get a generic "Failed to deserialize level.dat error".
|
||
// When only checking for the data version, we can determine if we can support the full
|
||
// deserializiation before going through with it.
|
||
#[derive(Deserialize)]
|
||
#[serde(rename_all = "PascalCase")]
|
||
struct LevelData {
|
||
data_version: i32,
|
||
}
|
||
#[derive(Deserialize)]
|
||
#[serde(rename_all = "PascalCase")]
|
||
struct LevelDat {
|
||
data: LevelData,
|
||
}
|
||
|
||
let info: LevelDat = pumpkin_nbt::from_bytes(Cursor::new(raw_nbt))
|
||
.map_err(|e|{
|
||
error!("The level.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)
|
||
{
|
||
Ok(())
|
||
} else {
|
||
Err(WorldInfoError::UnsupportedDataVersion(data_version))
|
||
}
|
||
}
|
||
|
||
fn check_file_level_version(raw_nbt: &[u8]) -> Result<(), WorldInfoError> {
|
||
#[derive(Deserialize)]
|
||
struct LevelData {
|
||
version: i32,
|
||
}
|
||
#[derive(Deserialize)]
|
||
#[serde(rename_all = "PascalCase")]
|
||
struct LevelDat {
|
||
data: LevelData,
|
||
}
|
||
|
||
let info: LevelDat = pumpkin_nbt::from_bytes(Cursor::new(raw_nbt))
|
||
.map_err(|e|{
|
||
error!("The level.dat file does not have a level version! This means it is either corrupt or very old (read unsupported)");
|
||
WorldInfoError::DeserializationError(e.to_string())})?;
|
||
|
||
let level_version = info.data.version;
|
||
|
||
if (MINIMUM_SUPPORTED_LEVEL_VERSION..=MAXIMUM_SUPPORTED_LEVEL_VERSION).contains(&level_version)
|
||
{
|
||
Ok(())
|
||
} else {
|
||
Err(WorldInfoError::UnsupportedLevelVersion(level_version))
|
||
}
|
||
}
|
||
|
||
impl WorldInfoReader for AnvilLevelInfo {
|
||
fn read_world_info(&self, level_folder: &Path) -> Result<LevelData, WorldInfoError> {
|
||
let path = level_folder.join(LEVEL_DAT_FILE_NAME);
|
||
|
||
let world_info_file = File::open(path)?;
|
||
let mut buf = Vec::new();
|
||
GzDecoder::new(world_info_file).read_to_end(&mut buf)?;
|
||
|
||
check_file_data_version(&buf)?;
|
||
check_file_level_version(&buf)?;
|
||
let mut info = pumpkin_nbt::from_bytes::<LevelDat>(Cursor::new(buf))
|
||
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
|
||
|
||
// game_rules.dat – prefer the new file; fall back to level.dat values
|
||
if minecraft_data_dir(level_folder)
|
||
.join("game_rules.dat")
|
||
.exists()
|
||
{
|
||
info.data.game_rules = read_game_rules(level_folder);
|
||
}
|
||
|
||
// world_gen_settings.dat
|
||
if let Some(wgs) = read_world_gen_settings(level_folder) {
|
||
info.data.world_gen_settings = wgs;
|
||
}
|
||
|
||
// world_clocks.dat – read the overworld day_time
|
||
if minecraft_data_dir(level_folder)
|
||
.join("world_clocks.dat")
|
||
.exists()
|
||
{
|
||
let clocks = read_world_clocks(level_folder);
|
||
if let Some(overworld) = clocks.clocks.get("minecraft:overworld") {
|
||
info.data.day_time = overworld.total_ticks;
|
||
}
|
||
}
|
||
|
||
// weather.dat
|
||
if minecraft_data_dir(level_folder)
|
||
.join("weather.dat")
|
||
.exists()
|
||
{
|
||
let weather = read_weather(level_folder);
|
||
info.data.clear_weather_time = weather.clear_weather_time;
|
||
}
|
||
|
||
// (wandering_trader.dat is not part of LevelData; stored separately when needed)
|
||
|
||
Ok(info.data)
|
||
}
|
||
}
|
||
|
||
impl WorldInfoWriter for AnvilLevelInfo {
|
||
fn write_world_info(
|
||
&self,
|
||
info: &LevelData,
|
||
level_folder: &Path,
|
||
) -> Result<(), WorldInfoError> {
|
||
let start = SystemTime::now();
|
||
let since_the_epoch = start
|
||
.duration_since(UNIX_EPOCH)
|
||
.expect("Time went backwards");
|
||
let mut level_data = info.clone();
|
||
level_data.last_played = since_the_epoch.as_millis() as i64;
|
||
let level = LevelDat { data: level_data };
|
||
|
||
// ── Write level.dat ───────────────────────────────────────────────────
|
||
let path = level_folder.join(LEVEL_DAT_FILE_NAME);
|
||
let world_info_file = File::create(path)?;
|
||
|
||
let compression_writer = GzEncoder::new(world_info_file, Compression::best());
|
||
// TODO: Proper error handling
|
||
pumpkin_nbt::to_bytes(&level, compression_writer)
|
||
.expect("Failed to write level.dat to disk");
|
||
|
||
let data_version = info.data_version;
|
||
|
||
// ── Write data/minecraft/*.dat files ─────────────────────────────────
|
||
|
||
// game_rules.dat
|
||
if let Err(e) = write_game_rules(level_folder, &info.game_rules, data_version) {
|
||
error!("Failed to write game_rules.dat: {e}");
|
||
}
|
||
|
||
// world_gen_settings.dat
|
||
if let Err(e) =
|
||
write_world_gen_settings(level_folder, &info.world_gen_settings, data_version)
|
||
{
|
||
error!("Failed to write world_gen_settings.dat: {e}");
|
||
}
|
||
|
||
// world_clocks.dat – persist the overworld day_time; preserve other
|
||
let mut clocks = read_world_clocks(level_folder);
|
||
clocks.data_version = data_version;
|
||
clocks
|
||
.clocks
|
||
.entry("minecraft:overworld".to_string())
|
||
.and_modify(|c| c.total_ticks = info.day_time)
|
||
.or_insert(crate::world_info::data_files::DimensionClock {
|
||
total_ticks: info.day_time,
|
||
});
|
||
|
||
if let Err(e) = write_world_clocks(level_folder, &clocks) {
|
||
error!("Failed to write world_clocks.dat: {e}");
|
||
}
|
||
|
||
// weather.dat
|
||
let mut weather = read_weather(level_folder);
|
||
weather.clear_weather_time = info.clear_weather_time;
|
||
weather.data_version = data_version;
|
||
if let Err(e) = write_weather(level_folder, &weather) {
|
||
error!("Failed to write weather.dat: {e}");
|
||
}
|
||
|
||
// wandering_trader.dat (stub / load-save)
|
||
let mut wandering_trader = read_wandering_trader(level_folder);
|
||
wandering_trader.data_version = data_version;
|
||
if let Err(e) = write_wandering_trader(level_folder, &wandering_trader) {
|
||
error!("Failed to write wandering_trader.dat: {e}");
|
||
}
|
||
|
||
// custom_boss_events.dat
|
||
if let Err(e) = write_custom_boss_events_stub(level_folder, data_version) {
|
||
error!("Failed to write custom_boss_events.dat: {e}");
|
||
}
|
||
|
||
// scheduled_events.dat
|
||
if let Err(e) = write_scheduled_events_stub(level_folder, data_version) {
|
||
error!("Failed to write scheduled_events.dat: {e}");
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||
pub struct LevelDat {
|
||
// This tag contains all the level data.
|
||
#[serde(rename = "Data")]
|
||
pub data: LevelData,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
|
||
use flate2::read::GzDecoder;
|
||
use pumpkin_data::game_rules::GameRuleRegistry;
|
||
use pumpkin_nbt::{deserializer::from_bytes, serializer::to_bytes};
|
||
use pumpkin_util::{Difficulty, world_seed::Seed};
|
||
use std::{
|
||
fs,
|
||
io::{Cursor, Read},
|
||
sync::LazyLock,
|
||
};
|
||
use tempfile::TempDir;
|
||
|
||
use crate::{
|
||
global_path,
|
||
world_info::{DataPacks, LevelData, WorldGenSettings, WorldInfoError, WorldVersion},
|
||
};
|
||
|
||
use super::{AnvilLevelInfo, LEVEL_DAT_FILE_NAME, LevelDat, WorldInfoReader, WorldInfoWriter};
|
||
|
||
#[test]
|
||
fn preserve_level_dat_seed() {
|
||
let seed = 1337;
|
||
|
||
let data = LevelData::default(Seed(1337));
|
||
|
||
let temp_dir = TempDir::new().unwrap();
|
||
|
||
AnvilLevelInfo
|
||
.write_world_info(&data, temp_dir.path())
|
||
.unwrap();
|
||
|
||
let data = AnvilLevelInfo.read_world_info(temp_dir.path()).unwrap();
|
||
|
||
assert_eq!(data.world_gen_settings.seed, seed);
|
||
}
|
||
|
||
static LEVEL_DAT: LazyLock<LevelDat> = LazyLock::new(|| LevelDat {
|
||
data: LevelData {
|
||
allow_commands: true,
|
||
border_center_x: 0.0,
|
||
border_center_z: 0.0,
|
||
border_damage_per_block: 0.2,
|
||
border_size: 59_999_968.0,
|
||
border_safe_zone: 5.0,
|
||
border_size_lerp_target: 59_999_968.0,
|
||
border_size_lerp_time: 0,
|
||
border_warning_blocks: 5.0,
|
||
border_warning_time: 15.0,
|
||
clear_weather_time: 0,
|
||
data_packs: DataPacks {
|
||
disabled: vec![
|
||
"minecart_improvements".to_string(),
|
||
"redstone_experiments".to_string(),
|
||
"trade_rebalance".to_string(),
|
||
],
|
||
enabled: vec!["vanilla".to_string()],
|
||
},
|
||
data_version: 4189,
|
||
day_time: 1727,
|
||
difficulty: Difficulty::Normal,
|
||
difficulty_locked: false,
|
||
game_rules: GameRuleRegistry {
|
||
block_explosion_drop_decay: true,
|
||
command_block_output: true,
|
||
drowning_damage: true,
|
||
ender_pearls_vanish_on_death: true,
|
||
fall_damage: true,
|
||
fire_damage: true,
|
||
forgive_dead_players: true,
|
||
freeze_damage: true,
|
||
global_sound_events: true,
|
||
keep_inventory: false,
|
||
lava_source_conversion: false,
|
||
log_admin_commands: true,
|
||
max_entity_cramming: 24,
|
||
mob_explosion_drop_decay: true,
|
||
mob_griefing: true,
|
||
players_nether_portal_creative_delay: 0,
|
||
players_nether_portal_default_delay: 80,
|
||
players_sleeping_percentage: 100,
|
||
projectiles_can_break_blocks: true,
|
||
random_tick_speed: 3,
|
||
reduced_debug_info: false,
|
||
send_command_feedback: true,
|
||
show_death_messages: true,
|
||
spectators_generate_chunks: true,
|
||
tnt_explosion_drop_decay: false,
|
||
universal_anger: false,
|
||
water_source_conversion: true,
|
||
..Default::default()
|
||
},
|
||
world_gen_settings: WorldGenSettings::new(Seed(1)),
|
||
last_played: 1733847709327,
|
||
level_name: "New World".to_string(),
|
||
spawn_x: 160,
|
||
spawn_y: 70,
|
||
spawn_z: 160,
|
||
spawn_yaw: 0.0,
|
||
spawn_pitch: 0.0,
|
||
level_version: 19133,
|
||
world_version: WorldVersion {
|
||
name: "1.21.4".to_string(),
|
||
id: 4189,
|
||
snapshot: false,
|
||
series: "main".to_string(),
|
||
},
|
||
map_id: 0,
|
||
},
|
||
});
|
||
|
||
// #[test]
|
||
// fn deserialize_level_dat() {
|
||
// let raw_compressed_nbt = fs::read("assets/level_1_21_4.dat").unwrap();
|
||
// assert!(!raw_compressed_nbt.is_empty());
|
||
|
||
// let mut decoder = GzDecoder::new(&raw_compressed_nbt[..]);
|
||
// let mut buf = Vec::new();
|
||
// decoder.read_to_end(&mut buf).unwrap();
|
||
// let level_dat: LevelDat = from_bytes(Cursor::new(buf)).expect("Failed to decode from file");
|
||
|
||
// assert_eq!(level_dat, *LEVEL_DAT);
|
||
// }
|
||
|
||
#[test]
|
||
fn serialize_level_dat() {
|
||
let mut serialized = Vec::new();
|
||
to_bytes(&*LEVEL_DAT, &mut serialized).expect("Failed to encode to bytes");
|
||
|
||
assert!(!serialized.is_empty());
|
||
|
||
let level_dat_again: LevelDat =
|
||
from_bytes(Cursor::new(serialized)).expect("Failed to decode from bytes");
|
||
|
||
let mut expected = (*LEVEL_DAT).clone();
|
||
expected.data.game_rules = GameRuleRegistry::default();
|
||
expected.data.world_gen_settings = WorldGenSettings::default();
|
||
expected.data.day_time = 0;
|
||
expected.data.clear_weather_time = 0;
|
||
|
||
assert_eq!(level_dat_again, expected);
|
||
}
|
||
}
|