steel_core/worldgen/feature/
sorter.rs1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3
4use rustc_hash::FxHashMap;
5use steel_registry::biome::BiomeRef;
6use steel_registry::feature::PlacedFeatureEntryRef;
7use steel_registry::{Registry, RegistryEntry as _, RegistryExt as _};
8
9#[derive(Debug)]
11pub(super) struct FeatureSorter {
12 steps: Box<[FeatureStepData]>,
13}
14
15#[derive(Debug)]
16pub(super) struct FeatureStepData {
17 features: Box<[PlacedFeatureEntryRef]>,
18 index_by_placed_feature_id: FxHashMap<usize, usize>,
19 feature_indices_by_biome_id: FxHashMap<usize, Box<[usize]>>,
20}
21
22#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23struct FeatureVertex {
24 step: usize,
25 order: usize,
26 placed_feature_id: usize,
27}
28
29impl Ord for FeatureVertex {
30 fn cmp(&self, other: &Self) -> Ordering {
31 (self.step, self.order, self.placed_feature_id).cmp(&(
32 other.step,
33 other.order,
34 other.placed_feature_id,
35 ))
36 }
37}
38
39impl PartialOrd for FeatureVertex {
40 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
41 Some(self.cmp(other))
42 }
43}
44
45impl FeatureSorter {
46 #[must_use]
47 pub(super) fn build(possible_biomes: &[BiomeRef], registry: &Registry) -> Self {
48 let mut feature_order_by_id = FxHashMap::default();
49 let mut next_feature_order = 0usize;
50 let mut edges = BTreeMap::<FeatureVertex, BTreeSet<FeatureVertex>>::new();
51
52 for biome in possible_biomes {
53 let mut biome_features = Vec::new();
54
55 for (step, feature_stage) in biome.features.iter().enumerate() {
56 for feature_key in feature_stage {
57 let Some(placed_feature_id) = registry.placed_features.id_from_key(feature_key)
58 else {
59 panic!(
60 "biome {} references unknown placed feature {}",
61 biome.key, feature_key
62 );
63 };
64
65 let feature_order =
66 if let Some(&order) = feature_order_by_id.get(&placed_feature_id) {
67 order
68 } else {
69 let order = next_feature_order;
70 next_feature_order += 1;
71 feature_order_by_id.insert(placed_feature_id, order);
72 order
73 };
74
75 let vertex = FeatureVertex {
76 step,
77 order: feature_order,
78 placed_feature_id,
79 };
80 edges.entry(vertex).or_default();
81 biome_features.push(vertex);
82 }
83 }
84
85 for feature_pair in biome_features.windows(2) {
86 edges
87 .entry(feature_pair[0])
88 .or_default()
89 .insert(feature_pair[1]);
90 }
91 }
92
93 let sorted_features = Self::topological_sort(&edges);
94 Self::from_sorted_features(&sorted_features, possible_biomes, registry)
95 }
96
97 #[must_use]
98 pub(super) fn step_count(&self) -> usize {
99 self.steps.len()
100 }
101
102 pub(super) fn step(&self, step: usize) -> Option<&FeatureStepData> {
103 self.steps.get(step)
104 }
105
106 fn topological_sort(
107 edges: &BTreeMap<FeatureVertex, BTreeSet<FeatureVertex>>,
108 ) -> Vec<FeatureVertex> {
109 let mut sorted = Vec::with_capacity(edges.len());
110 let mut discovered = BTreeSet::new();
111 let mut visiting = BTreeSet::new();
112 let vertices = edges.keys().copied().collect::<Vec<_>>();
113
114 for vertex in vertices {
115 assert!(
116 !Self::visit(vertex, edges, &mut discovered, &mut visiting, &mut sorted),
117 "biome decoration placed-feature order contains a cycle"
118 );
119 }
120
121 sorted.reverse();
122 sorted
123 }
124
125 fn visit(
126 vertex: FeatureVertex,
127 edges: &BTreeMap<FeatureVertex, BTreeSet<FeatureVertex>>,
128 discovered: &mut BTreeSet<FeatureVertex>,
129 visiting: &mut BTreeSet<FeatureVertex>,
130 sorted: &mut Vec<FeatureVertex>,
131 ) -> bool {
132 if discovered.contains(&vertex) {
133 return false;
134 }
135 if !visiting.insert(vertex) {
136 return true;
137 }
138
139 if let Some(neighbors) = edges.get(&vertex) {
140 for &neighbor in neighbors {
141 if Self::visit(neighbor, edges, discovered, visiting, sorted) {
142 return true;
143 }
144 }
145 }
146
147 visiting.remove(&vertex);
148 discovered.insert(vertex);
149 sorted.push(vertex);
150 false
151 }
152
153 #[must_use]
154 fn from_sorted_features(
155 sorted_features: &[FeatureVertex],
156 possible_biomes: &[BiomeRef],
157 registry: &Registry,
158 ) -> Self {
159 let Some(max_step) = sorted_features.iter().map(|feature| feature.step).max() else {
160 return Self {
161 steps: Box::new([]),
162 };
163 };
164
165 let mut steps = Vec::with_capacity(max_step + 1);
166 for step in 0..=max_step {
167 let mut features = Vec::new();
168 let mut index_by_placed_feature_id = FxHashMap::default();
169
170 for feature in sorted_features
171 .iter()
172 .filter(|feature| feature.step == step)
173 {
174 let Some(placed_feature) =
175 registry.placed_features.by_id(feature.placed_feature_id)
176 else {
177 panic!(
178 "feature sorter references unknown placed feature id {}",
179 feature.placed_feature_id
180 );
181 };
182 let index = features.len();
183 features.push(placed_feature);
184 index_by_placed_feature_id.insert(feature.placed_feature_id, index);
185 }
186
187 steps.push(FeatureStepData {
188 features: features.into_boxed_slice(),
189 index_by_placed_feature_id,
190 feature_indices_by_biome_id: FxHashMap::default(),
191 });
192 }
193
194 for biome in possible_biomes {
195 let Some(biome_id) = biome.try_id() else {
196 panic!("possible biome {} is not registered", biome.key);
197 };
198
199 for (step, feature_stage) in biome.features.iter().enumerate() {
200 let Some(step_data) = steps.get_mut(step) else {
201 continue;
202 };
203
204 let mut indices = Vec::with_capacity(feature_stage.len());
205 for feature_key in feature_stage {
206 let Some(placed_feature_id) = registry.placed_features.id_from_key(feature_key)
207 else {
208 panic!(
209 "biome {} references unknown placed feature {}",
210 biome.key, feature_key
211 );
212 };
213 let Some(feature_index) = step_data.feature_index(placed_feature_id) else {
214 panic!(
215 "placed feature {} from biome {} was not included in decoration step {}",
216 feature_key, biome.key, step
217 );
218 };
219 indices.push(feature_index);
220 }
221
222 if indices.is_empty() {
223 continue;
224 }
225
226 indices.sort_unstable();
227 indices.dedup();
228 step_data
229 .feature_indices_by_biome_id
230 .insert(biome_id, indices.into_boxed_slice());
231 }
232 }
233
234 Self {
235 steps: steps.into_boxed_slice(),
236 }
237 }
238}
239
240impl FeatureStepData {
241 pub(super) fn feature_index(&self, placed_feature_id: usize) -> Option<usize> {
242 self.index_by_placed_feature_id
243 .get(&placed_feature_id)
244 .copied()
245 }
246
247 pub(super) fn feature(&self, index: usize) -> Option<PlacedFeatureEntryRef> {
248 self.features.get(index).copied()
249 }
250
251 pub(super) fn feature_indices_for_biome(&self, biome_id: usize) -> Option<&[usize]> {
252 self.feature_indices_by_biome_id
253 .get(&biome_id)
254 .map(Box::as_ref)
255 }
256}