refactor level.rs

This commit is contained in:
2025-08-21 14:40:10 +02:00
parent a9dfdf74af
commit a55fe08a93
3 changed files with 450 additions and 87 deletions

View File

@@ -1,5 +1,5 @@
[package]
name = "worldgen"
name = "world"
version = "0.1.0"
edition = "2024"
@@ -9,5 +9,5 @@ rand = "0.9.2"
rand_chacha = "0.9.0"
nbt = { git = "https://github.com/Cactus-minecraft-server/nbt.git" }
[lib]
name = "worldgen"
name = "world"
path = "src/lib.rs"

View File

@@ -1,79 +1,385 @@
use nbt::{Tag, write_nbt};
use std::collections::HashMap;
use std::fs::File;
use nbt::{Tag, write_nbt};
pub struct LevelDat {
pub custom_boss_events: CustomBossEvents,
pub data_packs: DataPacks,
pub dragon_fight: DragonFight,
pub game_rules: GameRules,
pub version: VersionInfo,
pub world_gen_settings: WorldGenSettings,
pub scheduled_events: ScheduledEvents,
pub server_brands: ServerBrands,
pub fn create_nbt(
seed: i64,
is_hardcore: bool,
structures: bool,
raining: bool,
thundering: bool,
game_type: i32,
generator_version: i32,
raintime: i32,
spawnx: i32,
spawny: i32,
spawnz: i32,
thundertime: i32,
version: i32,
last_played: i64,
disk_size: i64,
time: i64,
generator_name: String,
level_name: String,
path: String,
) -> std::io::Result<()> {
pub allow_commands: bool,
pub border_center_x: f64,
pub border_center_z: f64,
pub border_damage_per_block: f64,
pub border_safe_zone: f64,
pub border_size: f64,
pub border_size_lerp_target: f64,
pub border_size_lerp_time: i64,
pub border_warning_blocks: i32,
pub border_warning_time: i32,
pub clear_weather_time: i32,
pub data_version: i32,
pub day_time: i64,
pub difficulty: i8,
pub difficulty_locked: bool,
pub game_type: i32,
pub hardcore: bool,
pub initialized: bool,
pub last_played: i64,
pub level_name: String,
pub raining: bool,
pub rain_time: i32,
pub spawn_angle: f32,
pub spawn_x: i32,
pub spawn_y: i32,
pub spawn_z: i32,
pub thundering: bool,
pub thunder_time: i32,
pub time: i64,
pub version_id: i32,
pub wandering_trader_spawn_chance: i32,
pub wandering_trader_spawn_delay: i32,
pub was_modded: bool,
}
pub struct CustomBossEvents {}
pub struct DataPacks {
pub disabled: Vec<String>,
pub enabled: Vec<String>,
}
pub struct DragonFight {
pub gateways: Vec<i32>,
pub dragon_killed: bool,
pub needs_state_scanning: bool,
pub previously_killed: bool,
}
pub type GameRules = HashMap<String, String>;
pub struct VersionInfo {
pub id: i32,
pub name: String,
pub series: String,
pub snapshot: i8,
}
pub enum Dimension {
Overworld,
End,
Nether,
}
pub struct WorldGenSettings {
pub dimensions: HashMap<String, Dimension>,
pub bonus_chest: bool,
pub generate_features: bool,
pub seed: i64,
}
pub struct ScheduledEvents {}
pub type ServerBrands = Vec<String>;
fn dim_to_str(d: &Dimension) -> &'static str {
match d {
Dimension::Overworld => "overworld",
Dimension::End => "the_end",
Dimension::Nether => "the_nether",
}
}
pub fn create_nbt(level: &LevelDat, path: &str) -> std::io::Result<()> {
let mut root = Tag::new_compound("Data");
root.insert("RandomSeed".to_string(), Tag::new_long("RandomSeed", seed));
// --- primitives (root) ---
root.insert(
"allowCommands".to_string(),
Tag::new_byte("allowCommands", i8::from(level.allow_commands)),
);
root.insert(
"BorderCenterX".to_string(),
Tag::new_double("BorderCenterX", level.border_center_x),
);
root.insert(
"BorderCenterZ".to_string(),
Tag::new_double("BorderCenterZ", level.border_center_z),
);
root.insert(
"BorderDamagePerBlock".to_string(),
Tag::new_double("BorderDamagePerBlock", level.border_damage_per_block),
);
root.insert(
"BorderSafeZone".to_string(),
Tag::new_double("BorderSafeZone", level.border_safe_zone),
);
root.insert(
"BorderSize".to_string(),
Tag::new_double("BorderSize", level.border_size),
);
root.insert(
"BorderSizeLerpTarget".to_string(),
Tag::new_double("BorderSizeLerpTarget", level.border_size_lerp_target),
);
root.insert(
"BorderSizeLerpTime".to_string(),
Tag::new_long("BorderSizeLerpTime", level.border_size_lerp_time),
);
root.insert(
"BorderWarningBlocks".to_string(),
Tag::new_int("BorderWarningBlocks", level.border_warning_blocks),
);
root.insert(
"BorderWarningTime".to_string(),
Tag::new_int("BorderWarningTime", level.border_warning_time),
);
root.insert(
"clearWeatherTime".to_string(),
Tag::new_int("clearWeatherTime", level.clear_weather_time),
);
root.insert(
"DataVersion".to_string(),
Tag::new_int("DataVersion", level.data_version),
);
root.insert(
"DayTime".to_string(),
Tag::new_long("DayTime", level.day_time),
);
root.insert(
"Difficulty".to_string(),
Tag::new_byte("Difficulty", level.difficulty),
);
root.insert(
"DifficultyLocked".to_string(),
Tag::new_byte("DifficultyLocked", i8::from(level.difficulty_locked)),
);
root.insert(
"GameType".to_string(),
Tag::new_int("GameType", level.game_type),
);
root.insert(
"hardcore".to_string(),
Tag::new_byte("hardcore", i8::from(is_hardcore)),
Tag::new_byte("hardcore", i8::from(level.hardcore)),
);
root.insert(
"MapFeatures".to_string(),
Tag::new_byte("MapFeatures", i8::from(structures)),
"initialized".to_string(),
Tag::new_byte("initialized", i8::from(level.initialized)),
);
root.insert(
"raining".to_string(),
Tag::new_byte("raining", i8::from(raining)),
);
root.insert(
"thundering".to_string(),
Tag::new_byte("thundering", i8::from(thundering)),
);
root.insert("GameType".to_string(), Tag::new_int("GameType", game_type));
root.insert(
"GeneratorVersion".to_string(),
Tag::new_int("GeneratorVersion", generator_version),
);
root.insert("RainTime".to_string(), Tag::new_int("RainTime", raintime));
root.insert("SpawnX".to_string(), Tag::new_int("SpawnX", spawnx));
root.insert("SpawnY".to_string(), Tag::new_int("SpawnY", spawny));
root.insert("SpawnZ".to_string(), Tag::new_int("SpawnZ", spawnz));
root.insert(
"ThunderTime".to_string(),
Tag::new_int("ThunderTime", thundertime),
);
root.insert("Version".to_string(), Tag::new_int("Version", version));
root.insert(
"LastPlayed".to_string(),
Tag::new_long("LastPlayed", last_played),
); // Not sure if I should keep LastPlayed field because of the fact that this isn't for a
// client world but for a server world
root.insert(
"SizeOnDisk".to_string(),
Tag::new_long("SizeOnDisk", disk_size),
);
root.insert("Time".to_string(), Tag::new_long("Time", time));
root.insert(
"GeneratorName".to_string(),
Tag::new_string("GeneratorName", generator_name),
Tag::new_long("LastPlayed", level.last_played),
);
root.insert(
"LevelName".to_string(),
Tag::new_string("LevelName", level_name),
Tag::new_string("LevelName", level.level_name.clone()),
);
let file = File::create(path)?;
root.insert(
"raining".to_string(),
Tag::new_byte("raining", i8::from(level.raining)),
);
root.insert(
"RainTime".to_string(),
Tag::new_int("RainTime", level.rain_time),
);
root.insert(
"SpawnAngle".to_string(),
Tag::new_float("SpawnAngle", level.spawn_angle),
);
root.insert("SpawnX".to_string(), Tag::new_int("SpawnX", level.spawn_x));
root.insert("SpawnY".to_string(), Tag::new_int("SpawnY", level.spawn_y));
root.insert("SpawnZ".to_string(), Tag::new_int("SpawnZ", level.spawn_z));
root.insert(
"thundering".to_string(),
Tag::new_byte("thundering", i8::from(level.thundering)),
);
root.insert(
"ThunderTime".to_string(),
Tag::new_int("ThunderTime", level.thunder_time),
);
root.insert("Time".to_string(), Tag::new_long("Time", level.time));
root.insert(
"version".to_string(),
Tag::new_int("version", level.version_id),
);
root.insert(
"WanderingTraderSpawnChance".to_string(),
Tag::new_int(
"WanderingTraderSpawnChance",
level.wandering_trader_spawn_chance,
),
);
root.insert(
"WanderingTraderSpawnDelay".to_string(),
Tag::new_int(
"WanderingTraderSpawnDelay",
level.wandering_trader_spawn_delay,
),
);
root.insert(
"WasModded".to_string(),
Tag::new_byte("WasModded", i8::from(level.was_modded)),
);
// Mirror of older key from your example, sourced from struct:
root.insert(
"RandomSeed".to_string(),
Tag::new_long("RandomSeed", level.world_gen_settings.seed),
);
root.insert(
"MapFeatures".to_string(),
Tag::new_byte(
"MapFeatures",
i8::from(level.world_gen_settings.generate_features),
),
);
// --- GameRules ---
let mut gr = Tag::new_compound("GameRules");
for (k, v) in &level.game_rules {
gr.insert(k.clone(), Tag::new_string(k, v.clone()));
}
root.insert("GameRules".to_string(), gr);
// --- Version (compound) ---
let mut ver = Tag::new_compound("Version");
ver.insert("Id".to_string(), Tag::new_int("Id", level.version.id));
ver.insert(
"Name".to_string(),
Tag::new_string("Name", level.version.name.clone()),
);
ver.insert(
"Series".to_string(),
Tag::new_string("Series", level.version.series.clone()),
);
ver.insert(
"Snapshot".to_string(),
Tag::new_byte("Snapshot", level.version.snapshot),
);
root.insert("Version".to_string(), ver);
// --- DataPacks ---
let mut dp = Tag::new_compound("DataPacks");
let enabled_list: Vec<Tag> = level
.data_packs
.enabled
.iter()
.cloned()
.map(|s| Tag::new_string("", s))
.collect();
let disabled_list: Vec<Tag> = level
.data_packs
.disabled
.iter()
.cloned()
.map(|s| Tag::new_string("", s))
.collect();
dp.insert(
"Enabled".to_string(),
Tag::new_list("Enabled", 8, enabled_list),
);
dp.insert(
"Disabled".to_string(),
Tag::new_list("Disabled", 8, disabled_list),
);
root.insert("DataPacks".to_string(), dp);
// --- DragonFight ---
let mut df = Tag::new_compound("DragonFight");
let gateways_list: Vec<Tag> = level
.dragon_fight
.gateways
.iter()
.copied()
.map(|n| Tag::new_int("", n))
.collect();
df.insert(
"Gateways".to_string(),
Tag::new_list("Gateways", 3, gateways_list),
);
df.insert(
"DragonKilled".to_string(),
Tag::new_byte("DragonKilled", i8::from(level.dragon_fight.dragon_killed)),
);
df.insert(
"NeedsStateScanning".to_string(),
Tag::new_byte(
"NeedsStateScanning",
i8::from(level.dragon_fight.needs_state_scanning),
),
);
df.insert(
"PreviouslyKilled".to_string(),
Tag::new_byte(
"PreviouslyKilled",
i8::from(level.dragon_fight.previously_killed),
),
);
root.insert("DragonFight".to_string(), df);
// --- WorldGenSettings ---
let mut dims = Tag::new_compound("dimensions");
for (name, dim) in &level.world_gen_settings.dimensions {
dims.insert(
name.clone(),
Tag::new_string(name, dim_to_str(dim).to_string()),
);
}
let mut wgs = Tag::new_compound("WorldGenSettings");
wgs.insert(
"seed".to_string(),
Tag::new_long("seed", level.world_gen_settings.seed),
);
wgs.insert(
"generate_features".to_string(),
Tag::new_byte(
"generate_features",
i8::from(level.world_gen_settings.generate_features),
),
);
wgs.insert(
"bonus_chest".to_string(),
Tag::new_byte(
"bonus_chest",
i8::from(level.world_gen_settings.bonus_chest),
),
);
wgs.insert("dimensions".to_string(), dims);
root.insert("WorldGenSettings".to_string(), wgs);
// --- ServerBrands ---
let brands_list: Vec<Tag> = level
.server_brands
.iter()
.cloned()
.map(|s| Tag::new_string("", s))
.collect();
root.insert(
"ServerBrands".to_string(),
Tag::new_list("ServerBrands", 8, brands_list),
);
// --- CustomBossEvents / ScheduledEvents (empty compounds for now) ---
root.insert(
"CustomBossEvents".to_string(),
Tag::new_compound("CustomBossEvents"),
);
root.insert(
"ScheduledEvents".to_string(),
Tag::new_compound("ScheduledEvents"),
);
// --- write ---
let file = File::create(format!("{path}/level.dat"))?;
write_nbt(&root, file)?;
Ok(())
}

View File

@@ -48,33 +48,90 @@ mod perlin_test {
/// Test for level.rs
#[cfg(test)]
mod level_file_test {
use crate::level::create_nbt;
use crate::level::{
CustomBossEvents, DataPacks, Dimension, DragonFight, LevelDat, ScheduledEvents,
ServerBrands, VersionInfo, WorldGenSettings, create_nbt,
};
use std::collections::HashMap;
#[test]
fn test_creation_of_file() -> () {
let result = create_nbt(
1234,
true,
true,
true,
true,
1,
1,
1,
0,
0,
0,
213,
1235,
55555,
90000,
900,
"test".to_string(),
"test".to_string(),
"target/level.dat".to_string(),
);
assert_eq!(result.is_ok(), true);
fn test_creation_of_file() {
let mut game_rules: HashMap<String, String> = HashMap::new();
game_rules.insert("doDaylightCycle".into(), "true".into());
let mut dimensions: HashMap<String, Dimension> = HashMap::new();
dimensions.insert("minecraft:overworld".into(), Dimension::Overworld);
let level = LevelDat {
custom_boss_events: CustomBossEvents {},
data_packs: DataPacks {
disabled: vec![],
enabled: vec![],
},
dragon_fight: DragonFight {
gateways: vec![0, 1, 2],
dragon_killed: false,
needs_state_scanning: false,
previously_killed: false,
},
game_rules,
version: VersionInfo {
id: 3465,
name: "1.20.1".into(),
series: "main".into(),
snapshot: 0,
},
world_gen_settings: WorldGenSettings {
dimensions,
bonus_chest: false,
generate_features: true,
seed: 1234,
},
scheduled_events: ScheduledEvents {},
server_brands: ServerBrands::from(vec!["vanilla".to_string()]),
allow_commands: true,
border_center_x: 0.0,
border_center_z: 0.0,
border_damage_per_block: 0.0,
border_safe_zone: 0.0,
border_size: 60_000_000.0,
border_size_lerp_target: 60_000_000.0,
border_size_lerp_time: 0,
border_warning_blocks: 5,
border_warning_time: 15,
clear_weather_time: 0,
data_version: 3465,
day_time: 0,
difficulty: 2,
difficulty_locked: false,
game_type: 0,
hardcore: false,
initialized: true,
last_played: 0,
level_name: "test".into(),
raining: false,
rain_time: 0,
spawn_angle: 0.0,
spawn_x: 0,
spawn_y: 64,
spawn_z: 0,
thundering: false,
thunder_time: 0,
time: 0,
version_id: 3465,
wandering_trader_spawn_chance: 25,
wandering_trader_spawn_delay: 1200,
was_modded: false,
};
let result = create_nbt(&level, "target");
assert!(result.is_ok());
}
}
/// Test for player.rs
#[cfg(test)]
mod player_data_test {