Pumpkin/pumpkin-world/src/world_info/anvil.rs
2026-08-02 08:31:01 +02:00

374 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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