From ad50996f7c087500de2d52dc366e268a9d400e99 Mon Sep 17 00:00:00 2001 From: FusselTV Date: Mon, 26 Oct 2026 00:09:45 +0100 Subject: [PATCH] Structures: save the remaining legacy pieces as vanilla does Desert and jungle temples, swamp huts, buried treasure, ocean monuments, mineshafts, fortresses and strongholds save the tag their StructurePiece subclass writes, with the state placement leaves: the height a scattered feature settled on, the buried treasure chest, placed chests, traps and spawners, and the swamp hut's witch and cat. With these every StructurePieceType is saved. test_structure_start_nbt_parity gets one site of each of these kinds, and a badlands mineshaft. Co-Authored-By: Claude Opus 5.5 --- .../src/worldgen/structure/fortress.rs | 23 +++++++++ .../src/worldgen/structure/mineshaft.rs | 51 +++++++++++++++++++ .../src/worldgen/structure/monument.rs | 10 ++++ .../src/worldgen/structure/simple.rs | 38 ++++++++++++++ .../src/worldgen/structure/start.rs | 1 + .../src/worldgen/structure/stronghold.rs | 42 +++++++++++++++ .../src/worldgen/structure/temple.rs | 26 ++++++++++ tools/test_structure_start_nbt_parity.py | 7 ++- 8 files changed, 196 insertions(+), 2 deletions(-) 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), ]