diff --git a/crates/horizon-world/src/worldgen/structure/fortress.rs b/crates/horizon-world/src/worldgen/structure/fortress.rs index 4d3704413..521dac872 100644 --- a/crates/horizon-world/src/worldgen/structure/fortress.rs +++ b/crates/horizon-world/src/worldgen/structure/fortress.rs @@ -871,6 +871,29 @@ impl LegacyPiece for FortressPiece { fn bounding_box(&self) -> BoundingBox { self.frame.bounding_box } + fn save(&self) -> Option { + use crate::nbt::Tag; + let mut tag = super::legacy::piece_tag( + self.type_name(), + self.frame.bounding_box, + self.frame.orientation, + self.depth, + ); + let flag = Tag::Byte(i8::from(*self.flag.lock().unwrap())); + match self.kind { + Kind::BridgeEndFiller => { + tag.insert("Seed".into(), Tag::Int(self.seed)); + } + Kind::SmallCorridorLeftTurn | Kind::SmallCorridorRightTurn => { + tag.insert("Chest".into(), flag); + } + Kind::MonsterThrone => { + tag.insert("Mob".into(), flag); + } + _ => {} + } + Some(tag) + } fn type_name(&self) -> &'static str { match self.kind { // StartPiece is built through the BridgeCrossing constructor. diff --git a/crates/horizon-world/src/worldgen/structure/mineshaft.rs b/crates/horizon-world/src/worldgen/structure/mineshaft.rs index 1173c03bf..0c2d32c08 100644 --- a/crates/horizon-world/src/worldgen/structure/mineshaft.rs +++ b/crates/horizon-world/src/worldgen/structure/mineshaft.rs @@ -732,6 +732,57 @@ impl LegacyPiece for MineshaftPiece { fn bounding_box(&self) -> BoundingBox { self.frame.bounding_box } + fn save(&self) -> Option { + use crate::nbt::Tag; + let flag = |b: bool| Tag::Byte(i8::from(b)); + let mut tag = super::legacy::piece_tag( + self.type_name(), + self.frame.bounding_box, + self.frame.orientation, + self.depth, + ); + // MineshaftStructure.Type's ordinal: NORMAL, MESA. + let mesa = self.wood.log == MESA.log; + let mut entries = vec![("MST", Tag::Int(i32::from(mesa)))]; + match &self.kind { + Kind::Room(entrances) => entries.push(( + "Entrances", + Tag::List( + entrances + .iter() + .map(|b| { + Tag::IntArray(vec![ + b.min[0], b.min[1], b.min[2], b.max[0], b.max[1], b.max[2], + ]) + }) + .collect(), + ), + )), + Kind::Corridor { + rails, + spider, + sections, + placed_spider, + } => entries.extend([ + ("hr", flag(*rails)), + ("sc", flag(*spider)), + ("hps", flag(*placed_spider.lock().unwrap())), + ("Num", Tag::Int(*sections)), + ]), + Kind::Crossing { + direction, + two_floored, + } => { + let d = crate::level::data_2d(*direction) as i8; + entries.extend([("tf", flag(*two_floored)), ("D", Tag::Byte(d))]); + } + Kind::Stairs => {} + } + for (key, value) in entries { + tag.insert(key.into(), value); + } + Some(tag) + } fn type_name(&self) -> &'static str { match self.kind { Kind::Room(_) => "minecraft:msroom", diff --git a/crates/horizon-world/src/worldgen/structure/monument.rs b/crates/horizon-world/src/worldgen/structure/monument.rs index 39959557e..ababac969 100644 --- a/crates/horizon-world/src/worldgen/structure/monument.rs +++ b/crates/horizon-world/src/worldgen/structure/monument.rs @@ -463,6 +463,16 @@ impl LegacyPiece for MonumentBuilding { fn type_name(&self) -> &'static str { "minecraft:omb" } + /// `MonumentBuilding`: the base fields only; vanilla builds its rooms + /// again after loading (`OceanMonumentStructure.regeneratePiecesAfterLoad`). + fn save(&self) -> Option { + Some(super::legacy::piece_tag( + "minecraft:omb", + self.frame.bounding_box, + self.frame.orientation, + 0, + )) + } fn post_process(&self, c: &mut PieceContext<'_>) -> Result<()> { let sea = c.world.sea_level()?; let water = c.state("water"); diff --git a/crates/horizon-world/src/worldgen/structure/simple.rs b/crates/horizon-world/src/worldgen/structure/simple.rs index d69314c13..d05aab20f 100644 --- a/crates/horizon-world/src/worldgen/structure/simple.rs +++ b/crates/horizon-world/src/worldgen/structure/simple.rs @@ -18,11 +18,22 @@ fn empty_processors() -> Arc { #[derive(Debug)] pub struct BuriedTreasurePiece { pub position: [i32; 3], + /// Where the chest went: the box vanilla saves once placed. + pub found: Mutex>, } impl LegacyPiece for BuriedTreasurePiece { fn bounding_box(&self) -> BoundingBox { BoundingBox::new(self.position, self.position) } + fn save(&self) -> Option { + let at = self.found.lock().unwrap().unwrap_or(self.position); + Some(super::legacy::piece_tag( + "minecraft:btp", + BoundingBox::new(at, at), + None, + 0, + )) + } fn type_name(&self) -> &'static str { "minecraft:btp" } @@ -58,6 +69,7 @@ impl LegacyPiece for BuriedTreasurePiece { } } } + *self.found.lock().unwrap() = Some(pos); create_chest(c, pos, "minecraft:chests/buried_treasure", None)?; return Ok(()); } @@ -441,6 +453,21 @@ impl Scattered { pub fn frame(&self) -> PieceFrame { *self.frame.lock().unwrap() } + /// `ScatteredFeaturePiece`'s tag: the box and the height it settled on. + pub fn saved(&self, id: &str) -> crate::nbt::Compound { + use crate::nbt::Tag; + let f = self.frame(); + let mut tag = super::legacy::piece_tag(id, f.bounding_box, f.orientation, 0); + for (key, value) in [ + ("Width", self.width), + ("Height", self.height), + ("Depth", self.depth), + ("HPos", *self.height_position.lock().unwrap()), + ] { + tag.insert(key.into(), Tag::Int(value)); + } + tag + } /// updateAverageGroundHeight over the part of the box inside the chunk. pub fn average_ground(&self, c: &mut PieceContext<'_>, offset: i32) -> Result { if *self.height_position.lock().unwrap() >= 0 { @@ -498,12 +525,15 @@ impl Scattered { #[derive(Debug)] pub struct SwampHutPiece { pub base: Scattered, + /// spawnedWitch and spawnedCat: both spawn in the same chunk. + spawned: Mutex, } impl SwampHutPiece { pub fn new(random: &mut impl RandomSource, west: i32, north: i32) -> Self { let direction = random_horizontal(random); Self { base: Scattered::new(west, 64, north, 7, 7, 9, direction), + spawned: Mutex::new(false), } } } @@ -511,6 +541,13 @@ impl LegacyPiece for SwampHutPiece { fn bounding_box(&self) -> BoundingBox { self.base.frame().bounding_box } + fn save(&self) -> Option { + let mut tag = self.base.saved("minecraft:tesh"); + let spawned = crate::nbt::Tag::Byte(i8::from(*self.spawned.lock().unwrap())); + tag.insert("Witch".into(), spawned.clone()); + tag.insert("Cat".into(), spawned); + Some(tag) + } fn type_name(&self) -> &'static str { "minecraft:tesh" } @@ -579,6 +616,7 @@ impl LegacyPiece for SwampHutPiece { c.world .entity(super::legacy::spawn_nbt(id, pos, true, true))?; } + *self.spawned.lock().unwrap() = true; } Ok(()) } diff --git a/crates/horizon-world/src/worldgen/structure/start.rs b/crates/horizon-world/src/worldgen/structure/start.rs index 862423de7..810332c61 100644 --- a/crates/horizon-world/src/worldgen/structure/start.rs +++ b/crates/horizon-world/src/worldgen/structure/start.rs @@ -962,6 +962,7 @@ fn try_generate( let stub = top_of_chunk_center(context, chunk, HeightmapType::OceanFloorWg)?; let piece = super::simple::BuriedTreasurePiece { position: [chunk.x * 16 + 9, 90, chunk.z * 16 + 9], + found: std::sync::Mutex::new(None), }; (stub, legacy(vec![piece])) } diff --git a/crates/horizon-world/src/worldgen/structure/stronghold.rs b/crates/horizon-world/src/worldgen/structure/stronghold.rs index 94042ba6f..29f7cd149 100644 --- a/crates/horizon-world/src/worldgen/structure/stronghold.rs +++ b/crates/horizon-world/src/worldgen/structure/stronghold.rs @@ -1075,6 +1075,48 @@ impl LegacyPiece for StrongholdPiece { fn bounding_box(&self) -> BoundingBox { self.frame.bounding_box } + fn save(&self) -> Option { + use crate::nbt::Tag; + let flag = |b: bool| Tag::Byte(i8::from(b)); + let mut tag = super::legacy::piece_tag( + self.type_name(), + self.frame.bounding_box, + self.frame.orientation, + self.depth, + ); + // SmallDoorType.LEGACY_CODEC: the enum name. + let door = match self.door { + Door::Opening => "OPENING", + Door::Wood => "WOOD_DOOR", + Door::Grates => "GRATES", + Door::Iron => "IRON_DOOR", + }; + let placed = *self.placed.lock().unwrap(); + let [a, b, c, d] = self.flags; + let mut entries = vec![("EntryDoor", Tag::String(door.into()))]; + match self.kind { + Kind::ChestCorridor => entries.push(("Chest", flag(placed))), + Kind::Filler => entries.push(("Steps", Tag::Int(self.value))), + Kind::FiveCrossing => entries.extend([ + ("leftLow", flag(a)), + ("leftHigh", flag(b)), + ("rightLow", flag(c)), + ("rightHigh", flag(d)), + ]), + Kind::Library => entries.push(("Tall", flag(a))), + Kind::PortalRoom => entries.push(("Mob", flag(placed))), + Kind::RoomCrossing => entries.push(("Type", Tag::Int(self.value))), + // StartPiece is a StairsDown that is the source. + Kind::StairsDown => entries.push(("Source", flag(false))), + Kind::Start => entries.push(("Source", flag(true))), + Kind::Straight => entries.extend([("Left", flag(a)), ("Right", flag(b))]), + _ => {} + } + for (key, value) in entries { + tag.insert(key.into(), value); + } + Some(tag) + } fn type_name(&self) -> &'static str { match self.kind { Kind::Straight => "minecraft:shs", diff --git a/crates/horizon-world/src/worldgen/structure/temple.rs b/crates/horizon-world/src/worldgen/structure/temple.rs index 195f37791..c328f0f55 100644 --- a/crates/horizon-world/src/worldgen/structure/temple.rs +++ b/crates/horizon-world/src/worldgen/structure/temple.rs @@ -82,6 +82,16 @@ impl LegacyPiece for DesertPyramidPiece { fn bounding_box(&self) -> BoundingBox { self.base.frame().bounding_box } + fn save(&self) -> Option { + let mut tag = self.base.saved("minecraft:tedp"); + for (i, placed) in self.placed_chest.lock().unwrap().iter().enumerate() { + tag.insert( + format!("hasPlacedChest{i}").as_str().into(), + crate::nbt::Tag::Byte(i8::from(*placed)), + ); + } + Some(tag) + } fn type_name(&self) -> &'static str { "minecraft:tedp" } @@ -448,6 +458,22 @@ impl LegacyPiece for JungleTemplePiece { fn bounding_box(&self) -> BoundingBox { self.base.frame().bounding_box } + fn save(&self) -> Option { + let mut tag = self.base.saved("minecraft:tejp"); + let placed = *self.placed.lock().unwrap(); + for (key, value) in [ + "placedMainChest", + "placedHiddenChest", + "placedTrap1", + "placedTrap2", + ] + .into_iter() + .zip(placed) + { + tag.insert(key.into(), crate::nbt::Tag::Byte(i8::from(value))); + } + Some(tag) + } fn type_name(&self) -> &'static str { "minecraft:tejp" } diff --git a/tools/test_structure_start_nbt_parity.py b/tools/test_structure_start_nbt_parity.py index c222ad127..00ff7f9d5 100755 --- a/tools/test_structure_start_nbt_parity.py +++ b/tools/test_structure_start_nbt_parity.py @@ -4,8 +4,8 @@ The oracle (VanillaStructureStartOracle with HORIZON_START_NBT) generates the start of each case's chunk with the pinned server's own classes and prints its tag; the probe (world_probe structure_start_nbt) prints the tag Horizon saves for the same chunk. Both print sorted keys and typed numbers, and the lines must be identical. -The cases are the start chunks of one site of every structure kind built from jigsaw pools or templates (seed -12345). +The cases are the start chunks of one site of every structure kind (seed 12345, and a badlands mineshaft of seed +-7777), which between them use every StructurePieceType. """ import argparse import collections @@ -31,6 +31,9 @@ CASES = [ (12345, 'overworld', 114, -133), (12345, 'overworld', -16, 5), (12345, 'overworld', -109, 91), (12345, 'overworld', 0, -18), (12345, 'overworld', -150, 51), (12345, 'overworld', 61, 49), (12345, 'the_nether', 0, -18), (12345, 'the_nether', -22, 6), (12345, 'the_end', -15, -76), + (12345, 'overworld', 38, -115), (12345, 'overworld', -238, -313), (12345, 'overworld', -95, 2), + (12345, 'overworld', -126, -106), (12345, 'overworld', 47, 25), (12345, 'overworld', 110, 23), + (12345, 'overworld', 4, -1), (12345, 'the_nether', -13, -37), (-7777, 'overworld', 75, 84), ]