Skip to main content

steel_registry/
structure.rs

1use std::sync::OnceLock;
2
3use rustc_hash::FxHashMap;
4use steel_utils::Identifier;
5
6/// A registered worldgen structure definition.
7///
8/// Mirrors vanilla's `Structure`: common settings are stored here, while
9/// [`StructureConfigData`] carries the type-specific codec payload.
10#[derive(Debug)]
11pub struct StructureData {
12    /// Registry key, e.g. `minecraft:village_plains`.
13    pub key: Identifier,
14    /// Cached registry ID, set during registration for O(1) lookup on hot paths.
15    pub id: OnceLock<usize>,
16    /// Structure type, e.g. `minecraft:jigsaw` or `minecraft:mineshaft`.
17    pub structure_type: Identifier,
18    /// Biomes this structure can generate in. Tags are resolved at build time.
19    pub allowed_biomes: Vec<Identifier>,
20    /// Structure-specific mob spawn overrides.
21    pub spawn_overrides: Vec<StructureSpawnOverrideData>,
22    /// Generation decoration step from the structure JSON.
23    pub step: StructureGenerationStep,
24    /// Terrain adaptation used by reference inflation and Beardifier.
25    pub terrain_adjustment: TerrainAdjustment,
26    /// Type-specific structure config.
27    pub config: StructureConfigData,
28}
29
30impl StructureData {
31    /// Vanilla inflates the structure start bounding box by 12 for every terrain
32    /// adaptation mode except `none`.
33    #[must_use]
34    pub const fn bb_inflate(&self) -> i32 {
35        self.terrain_adjustment.bb_inflate()
36    }
37}
38
39pub type StructureRef = &'static StructureData;
40
41/// Registry of worldgen structure definitions.
42pub struct StructureRegistry {
43    structures_by_id: Vec<StructureRef>,
44    structures_by_key: FxHashMap<Identifier, usize>,
45    tags: FxHashMap<Identifier, Vec<Identifier>>,
46    allows_registering: bool,
47}
48
49impl StructureRegistry {
50    #[must_use]
51    pub fn new() -> Self {
52        Self {
53            structures_by_id: Vec::new(),
54            structures_by_key: FxHashMap::default(),
55            tags: FxHashMap::default(),
56            allows_registering: true,
57        }
58    }
59
60    pub fn register(&mut self, entry: StructureRef) -> usize {
61        assert!(
62            self.allows_registering,
63            "Cannot register StructureData after registry has been frozen"
64        );
65        let id = self.structures_by_id.len();
66        let cached = entry.id.get_or_init(|| id);
67        assert_eq!(*cached, id, "structure registered with conflicting id");
68        self.structures_by_id.push(entry);
69        self.structures_by_key.insert(entry.key.clone(), id);
70        id
71    }
72
73    pub fn iter(&self) -> impl Iterator<Item = (usize, StructureRef)> + '_ {
74        self.structures_by_id
75            .iter()
76            .enumerate()
77            .map(|(id, &entry)| (id, entry))
78    }
79}
80
81impl Default for StructureRegistry {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87crate::impl_registry_ext!(
88    StructureRegistry,
89    StructureData,
90    structures_by_id,
91    structures_by_key
92);
93crate::impl_tagged_registry!(StructureRegistry, structures_by_key, "structure");
94
95impl crate::RegistryEntry for StructureData {
96    fn key(&self) -> &Identifier {
97        &self.key
98    }
99
100    fn try_id(&self) -> Option<usize> {
101        self.id.get().copied()
102    }
103}
104
105/// Structure generation step.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum StructureGenerationStep {
108    /// `surface_structures`.
109    SurfaceStructures,
110    /// `underground_structures`.
111    UndergroundStructures,
112    /// `underground_decoration`.
113    UndergroundDecoration,
114}
115
116impl StructureGenerationStep {
117    /// Decoration-stage ordinal used by vanilla `GenerationStep.Decoration`.
118    ///
119    /// Structure JSON only names the three structure-capable decoration stages;
120    /// feature generation still runs all eleven decoration stages, so these
121    /// values intentionally leave the vanilla gaps intact.
122    #[must_use]
123    pub const fn decoration_ordinal(self) -> usize {
124        match self {
125            Self::UndergroundStructures => 3,
126            Self::SurfaceStructures => 4,
127            Self::UndergroundDecoration => 7,
128        }
129    }
130}
131
132/// How a structure modifies surrounding terrain.
133///
134/// Corresponds to vanilla's `TerrainAdjustment` enum.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum TerrainAdjustment {
137    /// No terrain adaptation.
138    None,
139    /// Fill in terrain around and above the structure.
140    Bury,
141    /// Carve thin beard below structure.
142    BeardThin,
143    /// Carve box-shaped beard below structure.
144    BeardBox,
145    /// Encapsulate structure in terrain.
146    Encapsulate,
147}
148
149impl TerrainAdjustment {
150    /// Bounding-box inflation used by vanilla's `Structure.adjustBoundingBox`.
151    #[must_use]
152    pub const fn bb_inflate(self) -> i32 {
153        match self {
154            Self::None => 0,
155            Self::Bury | Self::BeardThin | Self::BeardBox | Self::Encapsulate => 12,
156        }
157    }
158}
159
160/// Spawn override bounding-box mode.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum StructureSpawnBoundingBox {
163    /// Applies to the whole structure start bounding box.
164    Full,
165    /// Applies only when inside one of the pieces.
166    Piece,
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::{Registry, TaggedRegistryExt};
172
173    use super::*;
174
175    #[test]
176    fn vanilla_structure_tags_are_registered() {
177        let registry = Registry::new_vanilla();
178        let village_tag = Identifier::vanilla_static("village");
179        let villages = registry.structures.get_tag(&village_tag);
180        assert!(villages.as_ref().is_some_and(|entries| {
181            entries
182                .iter()
183                .any(|structure| structure.key == Identifier::vanilla_static("village_plains"))
184        }));
185    }
186
187    #[test]
188    fn structure_generation_steps_use_vanilla_decoration_ordinals() {
189        assert_eq!(
190            StructureGenerationStep::UndergroundStructures.decoration_ordinal(),
191            3
192        );
193        assert_eq!(
194            StructureGenerationStep::SurfaceStructures.decoration_ordinal(),
195            4
196        );
197        assert_eq!(
198            StructureGenerationStep::UndergroundDecoration.decoration_ordinal(),
199            7
200        );
201    }
202}
203
204/// A structure mob spawn override for one mob category.
205#[derive(Debug, Clone)]
206pub struct StructureSpawnOverrideData {
207    /// Mob category name, e.g. `monster`.
208    pub category: String,
209    /// Bounding box mode.
210    pub bounding_box: StructureSpawnBoundingBox,
211    /// Weighted spawns for this override.
212    pub spawns: Vec<StructureSpawnerData>,
213}
214
215/// Spawn entry inside a structure spawn override.
216#[derive(Debug, Clone)]
217pub struct StructureSpawnerData {
218    /// Entity type id.
219    pub entity_type: Identifier,
220    /// Spawn weight.
221    pub weight: i32,
222    /// Minimum group size.
223    pub min_count: i32,
224    /// Maximum group size.
225    pub max_count: i32,
226}
227
228/// Type-specific structure config.
229#[derive(Debug, Clone)]
230pub enum StructureConfigData {
231    /// `minecraft:jigsaw`.
232    Jigsaw(JigsawConfig),
233    /// `minecraft:mineshaft`.
234    Mineshaft { mineshaft_type: MineshaftTypeData },
235    /// `minecraft:shipwreck`.
236    Shipwreck { is_beached: bool },
237    /// `minecraft:ocean_ruin`.
238    OceanRuin {
239        biome_temp: OceanRuinBiomeTempData,
240        large_probability: f32,
241        cluster_probability: f32,
242    },
243    /// `minecraft:ruined_portal`.
244    RuinedPortal { setups: Vec<RuinedPortalSetupData> },
245    /// `minecraft:nether_fossil`.
246    NetherFossil { height: HeightProviderData },
247    /// Structure types with only common settings, or whose config is still unused.
248    Empty,
249}
250
251impl StructureConfigData {
252    #[must_use]
253    pub fn as_jigsaw(&self) -> Option<&JigsawConfig> {
254        match self {
255            Self::Jigsaw(config) => Some(config),
256            _ => None,
257        }
258    }
259}
260
261/// Jigsaw-specific configuration parsed from structure JSON.
262#[derive(Debug, Clone)]
263pub struct JigsawConfig {
264    /// Starting template pool.
265    pub start_pool: Identifier,
266    /// Maximum recursion depth (vanilla calls this `size`).
267    pub max_depth: i32,
268    /// Whether the expansion hack is enabled.
269    pub use_expansion_hack: bool,
270    /// If set, project the start piece to this heightmap type.
271    pub project_start_to_heightmap: Option<String>,
272    /// Start height provider type and value.
273    pub start_height: StartHeight,
274    /// Maximum distance from center for piece placement.
275    pub max_distance_from_center: i32,
276    /// Optional named jigsaw to anchor the start piece to.
277    pub start_jigsaw_name: Option<Identifier>,
278    /// Dimension padding (min distance from world height limits).
279    pub dimension_padding: DimensionPadding,
280    /// Pool alias configurations.
281    pub pool_aliases: Vec<PoolAlias>,
282    /// Liquid handling mode.
283    pub liquid_settings: LiquidSettingsData,
284}
285
286/// Start height configuration used by currently-generated jigsaw structures.
287#[derive(Debug, Clone)]
288pub enum StartHeight {
289    /// Fixed absolute Y.
290    Constant(i32),
291    /// Uniform random between min and max (inclusive).
292    Uniform { min: i32, max: i32 },
293}
294
295/// Dimension padding (how close pieces can be to world height limits).
296#[derive(Debug, Clone, Copy)]
297pub struct DimensionPadding {
298    /// Bottom padding.
299    pub bottom: i32,
300    /// Top padding.
301    pub top: i32,
302}
303
304/// A pool alias remapping.
305#[derive(Debug, Clone)]
306pub enum PoolAlias {
307    /// Direct remapping: alias -> target.
308    Direct {
309        alias: Identifier,
310        target: Identifier,
311    },
312    /// Random selection from weighted targets.
313    Random {
314        alias: Identifier,
315        targets: Vec<(Identifier, i32)>,
316    },
317    /// Random group: pick one group, apply all bindings in it.
318    RandomGroup {
319        groups: Vec<(Vec<(Identifier, Identifier)>, i32)>,
320    },
321}
322
323/// Jigsaw liquid handling mode.
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum LiquidSettingsData {
326    /// Default vanilla behavior.
327    ApplyWaterlogging,
328    /// Do not apply waterlogging from surrounding fluids.
329    IgnoreWaterlogging,
330}
331
332/// Mineshaft variant type.
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum MineshaftTypeData {
335    /// Standard mineshaft.
336    Normal,
337    /// Badlands mineshaft.
338    Mesa,
339}
340
341/// Ocean ruin temperature variant.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum OceanRuinBiomeTempData {
344    /// Warm ruin pools.
345    Warm,
346    /// Cold ruin pools.
347    Cold,
348}
349
350/// Ruined portal vertical placement type.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum RuinedPortalPlacementData {
353    OnLandSurface,
354    PartlyBuried,
355    Underground,
356    InMountain,
357    OnOceanFloor,
358    InNether,
359}
360
361/// One weighted ruined portal setup entry.
362#[derive(Debug, Clone)]
363pub struct RuinedPortalSetupData {
364    pub placement: RuinedPortalPlacementData,
365    pub weight: f32,
366    pub air_pocket_probability: f32,
367    pub can_be_cold: bool,
368    pub mossiness: f32,
369    pub overgrown: bool,
370    pub replace_with_blackstone: bool,
371    pub vines: bool,
372}
373
374/// Generic vertical anchor used by non-jigsaw height providers.
375#[derive(Debug, Clone)]
376pub enum VerticalAnchorData {
377    Absolute(i32),
378    AboveBottom(i32),
379    BelowTop(i32),
380}
381
382/// Height provider subset used by vanilla structures in this version.
383#[derive(Debug, Clone)]
384pub enum HeightProviderData {
385    Constant(VerticalAnchorData),
386    Uniform {
387        min_inclusive: VerticalAnchorData,
388        max_inclusive: VerticalAnchorData,
389    },
390}