Skip to main content

steel_worldgen/structure/
start.rs

1use crate::structure::StructurePiece;
2use core::slice;
3use rustc_hash::FxHashMap;
4use std::vec;
5use steel_registry::structure::TerrainAdjustment;
6use steel_utils::{BlockPos, BoundingBox, ChunkPos, Identifier};
7
8/// A structure start placed in a chunk. Vanilla's `StructureStart` — invalid (empty)
9/// starts are not stored.
10#[derive(Debug, Clone)]
11pub struct StructureStart {
12    /// Structure id (e.g., `minecraft:village`).
13    pub structure: Identifier,
14    /// Origin chunk.
15    pub chunk_pos: ChunkPos,
16    /// Vanilla's map/locate reference counter. This is distinct from
17    /// [`StructureReferenceMap`]; generating per-chunk structure references does
18    /// not increment this counter.
19    pub references: i32,
20    /// Pieces composing this structure.
21    pub pieces: Vec<StructurePiece>,
22    /// Bounding-box inflation applied at construction. Vanilla inflates by 12
23    /// when `terrain_adaptation != NONE`. Stored for serialization parity; the
24    /// inflation is already baked into [`bounding_box`](Self::bounding_box).
25    pub bb_inflate: i32,
26    /// Terrain adaptation mode from the structure registry. Used by Beardifier.
27    pub terrain_adjustment: TerrainAdjustment,
28    /// Cached bounding box matching vanilla's `StructureStart.getBoundingBox()`:
29    /// the union of piece bounding boxes, then `inflatedBy(bb_inflate)`.
30    /// `None` iff `pieces` is empty.
31    pub bounding_box: Option<BoundingBox>,
32}
33
34impl StructureStart {
35    /// Creates a start, computing the inflated piece-union bounding box up-front.
36    #[must_use]
37    pub fn new(
38        structure: Identifier,
39        chunk_pos: ChunkPos,
40        pieces: Vec<StructurePiece>,
41        terrain_adjustment: TerrainAdjustment,
42    ) -> Self {
43        let bb_inflate = terrain_adjustment.bb_inflate();
44        let bounding_box = Self::compute_bounding_box(&pieces, bb_inflate);
45        Self {
46            structure,
47            chunk_pos,
48            references: 0,
49            pieces,
50            bb_inflate,
51            terrain_adjustment,
52            bounding_box,
53        }
54    }
55
56    /// Union of all pieces' bounding boxes, inflated by `bb_inflate` on every
57    /// axis. Returns `None` if `pieces` is empty. Mirrors vanilla's
58    /// `StructureStart.getBoundingBox()` (= `adjustBoundingBox(union)`).
59    #[must_use]
60    pub fn compute_bounding_box(pieces: &[StructurePiece], bb_inflate: i32) -> Option<BoundingBox> {
61        let (first, rest) = pieces.split_first()?;
62        let mut bb = first.bounding_box;
63        for piece in rest {
64            bb = BoundingBox::encapsulating(&bb, &piece.bounding_box);
65        }
66        Some(bb.inflate_xyz(bb_inflate, bb_inflate, bb_inflate))
67    }
68
69    /// Vanilla `StructureStart.placeInChunk` reference position: the first
70    /// piece center X/Z and first piece minimum Y.
71    #[must_use]
72    pub fn placement_reference_pos(&self) -> Option<BlockPos> {
73        let first_piece = self.pieces.first()?;
74        let center = first_piece.bounding_box.center();
75        Some(BlockPos::new(
76            center.x,
77            first_piece.bounding_box.min_y(),
78            center.z,
79        ))
80    }
81}
82
83/// Structure starts keyed by structure id.
84pub type StructureStartMap = FxHashMap<Identifier, StructureStart>;
85
86/// Structure references → origin chunk positions.
87///
88/// Vanilla stores these as a fastutil `LongOpenHashSet`, so duplicates are
89/// ignored and feature-stage iteration follows that table order.
90pub type StructureReferenceMap = FxHashMap<Identifier, StructureReferenceSet>;
91
92/// Set of structure-start chunk positions with vanilla iteration order.
93///
94/// Reference generation discovers sources in a stable scan order, but vanilla
95/// stores the packed chunk longs in fastutil's `LongOpenHashSet`. Feature-stage
96/// placement consumes the set through that table iteration order, so Steel keeps
97/// the insertion order for persistence and exposes the vanilla iteration order
98/// for worldgen.
99#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub struct StructureReferenceSet {
101    insertion_order: Vec<ChunkPos>,
102    iteration_order: Vec<ChunkPos>,
103}
104
105impl StructureReferenceSet {
106    /// Inserts a chunk position if it was not already present.
107    pub fn insert(&mut self, pos: ChunkPos) -> bool {
108        if self.insertion_order.contains(&pos) {
109            return false;
110        }
111        self.insertion_order.push(pos);
112        self.rebuild_iteration_order();
113        true
114    }
115
116    /// Extends this set with insertion-order duplicate removal.
117    pub fn extend(&mut self, positions: impl IntoIterator<Item = ChunkPos>) {
118        for pos in positions {
119            self.insert(pos);
120        }
121    }
122
123    /// Returns an iterator over positions in vanilla `LongOpenHashSet` order.
124    pub fn iter(&self) -> slice::Iter<'_, ChunkPos> {
125        self.iteration_order.iter()
126    }
127
128    /// Returns an iterator over positions in discovery order.
129    pub fn insertion_order_iter(&self) -> slice::Iter<'_, ChunkPos> {
130        self.insertion_order.iter()
131    }
132
133    /// Returns `true` when no positions are stored.
134    #[must_use]
135    pub const fn is_empty(&self) -> bool {
136        self.insertion_order.is_empty()
137    }
138
139    fn rebuild_iteration_order(&mut self) {
140        self.iteration_order = Self::vanilla_long_open_hash_set_order(&self.insertion_order);
141    }
142
143    fn vanilla_long_open_hash_set_order(insertion_order: &[ChunkPos]) -> Vec<ChunkPos> {
144        let Some(table_size) = Self::vanilla_long_open_hash_set_table_size(insertion_order.len())
145        else {
146            return Vec::new();
147        };
148        let mask = (table_size - 1) as u64;
149        let mut table = vec![None; table_size];
150        let mut zero_key = None;
151
152        for &pos in insertion_order {
153            let packed = Self::pack_chunk_pos(pos);
154            if packed == 0 {
155                zero_key = Some(pos);
156                continue;
157            }
158
159            let mut slot = (Self::fastutil_mix(packed) & mask) as usize;
160            loop {
161                if table[slot].is_none() {
162                    table[slot] = Some(pos);
163                    break;
164                }
165                slot = (slot + 1) & (table_size - 1);
166            }
167        }
168
169        let mut ordered = Vec::with_capacity(insertion_order.len());
170        if let Some(pos) = zero_key {
171            ordered.push(pos);
172        }
173        for slot in (0..table_size).rev() {
174            if let Some(pos) = table[slot] {
175                ordered.push(pos);
176            }
177        }
178        ordered
179    }
180
181    fn vanilla_long_open_hash_set_table_size(len: usize) -> Option<usize> {
182        if len == 0 {
183            return None;
184        }
185
186        let mut table_size = Self::fastutil_array_size(16);
187        let mut max_fill = Self::fastutil_max_fill(table_size);
188        let mut size = 0;
189        for _ in 0..len {
190            let old_size = size;
191            size += 1;
192            if old_size >= max_fill {
193                table_size = Self::fastutil_array_size(size + 1);
194                max_fill = Self::fastutil_max_fill(table_size);
195            }
196        }
197        Some(table_size)
198    }
199
200    fn fastutil_array_size(expected: usize) -> usize {
201        let needed = ((expected as f64) / 0.75).ceil() as usize;
202        needed.max(2).next_power_of_two()
203    }
204
205    const fn fastutil_max_fill(table_size: usize) -> usize {
206        let fill = table_size - table_size / 4;
207        if fill < table_size {
208            fill
209        } else {
210            table_size - 1
211        }
212    }
213
214    const fn pack_chunk_pos(pos: ChunkPos) -> u64 {
215        (pos.0.x as u32 as u64) | ((pos.0.y as u32 as u64) << 32)
216    }
217
218    const fn fastutil_mix(value: u64) -> u64 {
219        let mixed = value.wrapping_mul(0x9E37_79B9_7F4A_7C15);
220        let mixed = mixed ^ (mixed >> 32);
221        mixed ^ (mixed >> 16)
222    }
223}
224
225impl FromIterator<ChunkPos> for StructureReferenceSet {
226    fn from_iter<T: IntoIterator<Item = ChunkPos>>(iter: T) -> Self {
227        let mut set = Self::default();
228        set.extend(iter);
229        set
230    }
231}
232
233impl<'a> IntoIterator for &'a StructureReferenceSet {
234    type IntoIter = slice::Iter<'a, ChunkPos>;
235    type Item = &'a ChunkPos;
236
237    fn into_iter(self) -> Self::IntoIter {
238        self.iter()
239    }
240}
241
242impl IntoIterator for StructureReferenceSet {
243    type IntoIter = vec::IntoIter<ChunkPos>;
244    type Item = ChunkPos;
245
246    fn into_iter(self) -> Self::IntoIter {
247        self.iteration_order.into_iter()
248    }
249}