Skip to main content

steel_registry/structure/
mod.rs

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