diff --git a/Cargo.toml b/Cargo.toml index 4c6d95c..d22095b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/level.rs b/src/level.rs index d47ff3e..9b018ec 100644 --- a/src/level.rs +++ b/src/level.rs @@ -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, + pub enabled: Vec, +} + +pub struct DragonFight { + pub gateways: Vec, + pub dragon_killed: bool, + pub needs_state_scanning: bool, + pub previously_killed: bool, +} + +pub type GameRules = HashMap; + +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, + pub bonus_chest: bool, + pub generate_features: bool, + pub seed: i64, +} + +pub struct ScheduledEvents {} + +pub type ServerBrands = Vec; +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 = level + .data_packs + .enabled + .iter() + .cloned() + .map(|s| Tag::new_string("", s)) + .collect(); + let disabled_list: Vec = 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 = 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 = 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(()) } diff --git a/src/test.rs b/src/test.rs index d54ef03..d36592f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -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 = HashMap::new(); + game_rules.insert("doDaylightCycle".into(), "true".into()); + + let mut dimensions: HashMap = 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 {