Skip to main content

steel_worldgen/structure/
nether_fossil.rs

1//! Nether fossil: sample a random (x, z) in the chunk and a uniform Y, then walk
2//! the base-noise column down until we find air over solid. Fails if the walk
3//! reaches sea level.
4
5use glam::IVec3;
6use steel_registry::structure::{
7    HeightProviderData, LiquidSettingsData, StructureConfigData, StructureData, VerticalAnchorData,
8};
9use steel_utils::Direction;
10use steel_utils::Identifier;
11use steel_utils::Rotation;
12use steel_utils::random::Random;
13use steel_utils::random::legacy_random::LegacyRandom;
14
15use crate::structure::{
16    GenerationStub, Structure, StructureBlockIgnore, StructureGenerationContext, StructureMirror,
17    StructurePiece, StructurePiecePayload, TemplateMarkerHandling, TemplatePieceData,
18    TemplatePlacementAdjustment, TemplatePlacementClip, TemplatePostProcess, TemplateProcessorList,
19};
20
21/// Fossil templates count (`minecraft:nether_fossils/fossil_N`).
22pub const FOSSIL_COUNT: i32 = 14;
23const SEA_LEVEL: i32 = 32;
24
25/// Result of [`find_generation_point`].
26pub struct FossilResult {
27    /// Template path relative to `minecraft:` (e.g. `"nether_fossils/fossil_3"`).
28    pub template_name: String,
29    /// World-space solid-block position.
30    pub position: IVec3,
31    /// Piece rotation.
32    pub rotation: Rotation,
33    /// Position used for the biome check.
34    pub biome_check_pos: IVec3,
35}
36
37const fn resolve_vertical_anchor(
38    anchor: &VerticalAnchorData,
39    min_gen_y: i32,
40    gen_depth: i32,
41) -> i32 {
42    match anchor {
43        VerticalAnchorData::Absolute(y) => *y,
44        VerticalAnchorData::AboveBottom(offset) => min_gen_y + *offset,
45        VerticalAnchorData::BelowTop(offset) => min_gen_y + gen_depth - 1 - *offset,
46    }
47}
48
49fn sample_height(
50    height: &HeightProviderData,
51    rng: &mut LegacyRandom,
52    min_gen_y: i32,
53    gen_depth: i32,
54) -> i32 {
55    match height {
56        HeightProviderData::Constant(anchor) => {
57            resolve_vertical_anchor(anchor, min_gen_y, gen_depth)
58        }
59        HeightProviderData::Uniform {
60            min_inclusive,
61            max_inclusive,
62        } => {
63            let min = resolve_vertical_anchor(min_inclusive, min_gen_y, gen_depth);
64            let max = resolve_vertical_anchor(max_inclusive, min_gen_y, gen_depth);
65            if min > max {
66                min
67            } else {
68                min + rng.next_i32_bounded(max - min + 1)
69            }
70        }
71    }
72}
73
74/// Vanilla's RNG sequence. Returns `None` if no air-over-solid transition above sea level.
75pub fn find_generation_point<F>(
76    rng: &mut LegacyRandom,
77    chunk_x: i32,
78    chunk_z: i32,
79    height: &HeightProviderData,
80    min_gen_y: i32,
81    gen_depth: i32,
82    mut solid_block_below_air: F,
83) -> Option<FossilResult>
84where
85    F: FnMut(i32, i32, i32, i32) -> Option<i32>,
86{
87    let block_x = (chunk_x << 4) + rng.next_i32_bounded(16);
88    let block_z = (chunk_z << 4) + rng.next_i32_bounded(16);
89
90    let start_y = sample_height(height, rng, min_gen_y, gen_depth);
91    let y = solid_block_below_air(block_x, block_z, start_y, SEA_LEVEL + 1)?;
92
93    let rotation = Rotation::get_random(rng);
94    let fossil_idx = rng.next_i32_bounded(FOSSIL_COUNT) + 1;
95    Some(FossilResult {
96        template_name: format!("nether_fossils/fossil_{fossil_idx}"),
97        position: IVec3::new(block_x, y, block_z),
98        rotation,
99        biome_check_pos: IVec3::new(block_x, y, block_z),
100    })
101}
102
103fn make_nether_fossil_piece(
104    template_id: Identifier,
105    position: IVec3,
106    rotation: Rotation,
107    size: IVec3,
108) -> StructurePiece {
109    StructurePiece {
110        piece_type: Identifier::new_static("minecraft", "nefos"),
111        bounding_box: rotation.get_bounding_box(position, size),
112        gen_depth: 0,
113        orientation: Some(Direction::North),
114        payload: StructurePiecePayload::Template(TemplatePieceData {
115            template_id,
116            template_position: position,
117            rotation,
118            mirror: StructureMirror::None,
119            rotation_pivot: IVec3::ZERO,
120            block_ignore: StructureBlockIgnore::StructureAndAir,
121            late_block_ignore: StructureBlockIgnore::None,
122            processors: TemplateProcessorList::Empty,
123            liquid_settings: LiquidSettingsData::ApplyWaterlogging,
124            marker_handling: TemplateMarkerHandling::Ignore,
125            placement_adjustment: TemplatePlacementAdjustment::None,
126            placement_clip: TemplatePlacementClip::CenterChunkExpandedToTemplate,
127            post_process: TemplatePostProcess::NetherFossil,
128        }),
129        ground_level_delta: 0,
130        junctions: Vec::new(),
131        projection: None,
132    }
133}
134
135/// Entry point used by `VanillaGenerator`.
136pub struct NetherFossilStructure;
137
138impl Structure for NetherFossilStructure {
139    fn find_generation_point(
140        &self,
141        ctx: &mut dyn StructureGenerationContext,
142        structure: &StructureData,
143        rng: &mut LegacyRandom,
144    ) -> Option<GenerationStub> {
145        let min_gen_y = ctx.min_y();
146        let gen_depth = ctx.height();
147        let (chunk_x, chunk_z) = (ctx.chunk_x(), ctx.chunk_z());
148        let StructureConfigData::NetherFossil { height } = &structure.config else {
149            return None;
150        };
151
152        let result = find_generation_point(
153            rng,
154            chunk_x,
155            chunk_z,
156            height,
157            min_gen_y,
158            gen_depth,
159            |x, z, start_y, min_solid_y| ctx.solid_block_below_air(x, z, start_y, min_solid_y),
160        )?;
161
162        let biome = ctx.biome_at(
163            result.biome_check_pos.x,
164            result.biome_check_pos.y,
165            result.biome_check_pos.z,
166        );
167        if !structure.allowed_biomes.contains(&biome.key) {
168            return None;
169        }
170
171        let template_id = Identifier::vanilla(result.template_name);
172        let tmpl_size = ctx.templates().get(&template_id)?.size;
173        Some(GenerationStub {
174            position: (result.position.x, result.position.y, result.position.z),
175            pieces: vec![make_nether_fossil_piece(
176                template_id,
177                result.position,
178                result.rotation,
179                IVec3::from(tmpl_size),
180            )],
181        })
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn nether_fossil_piece_uses_full_template_payload() {
191        let position = IVec3::new(10, 45, -20);
192        let size = IVec3::new(7, 5, 9);
193        let piece = make_nether_fossil_piece(
194            Identifier::vanilla_static("nether_fossils/fossil_1"),
195            position,
196            Rotation::Clockwise90,
197            size,
198        );
199
200        assert_eq!(
201            piece.piece_type,
202            Identifier::new_static("minecraft", "nefos")
203        );
204        assert_eq!(piece.gen_depth, 0);
205        assert_eq!(piece.orientation, Some(Direction::North));
206        assert_eq!(
207            piece.bounding_box,
208            Rotation::Clockwise90.get_bounding_box(position, size),
209        );
210
211        let StructurePiecePayload::Template(data) = piece.payload else {
212            panic!("nether fossil piece should be template-backed");
213        };
214        assert_eq!(
215            data.template_id,
216            Identifier::vanilla_static("nether_fossils/fossil_1")
217        );
218        assert_eq!(data.template_position, position);
219        assert_eq!(data.rotation, Rotation::Clockwise90);
220        assert_eq!(data.mirror, StructureMirror::None);
221        assert_eq!(data.rotation_pivot, IVec3::ZERO);
222        assert_eq!(data.block_ignore, StructureBlockIgnore::StructureAndAir);
223        assert_eq!(data.late_block_ignore, StructureBlockIgnore::None);
224        assert_eq!(data.processors, TemplateProcessorList::Empty);
225        assert_eq!(data.liquid_settings, LiquidSettingsData::ApplyWaterlogging);
226        assert_eq!(data.marker_handling, TemplateMarkerHandling::Ignore);
227        assert_eq!(data.placement_adjustment, TemplatePlacementAdjustment::None);
228        assert_eq!(
229            data.placement_clip,
230            TemplatePlacementClip::CenterChunkExpandedToTemplate
231        );
232        assert_eq!(data.post_process, TemplatePostProcess::NetherFossil);
233    }
234}