steel_registry/
shared_structs.rs1use std::{cmp::Ordering, collections::BTreeMap, str::FromStr};
2
3use serde::{Deserialize, Deserializer, de::Error as _};
4use simdnbt::ToNbtTag;
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use steel_utils::Identifier;
7use steel_utils::random::Random;
8
9use crate::biome::BiomeRef;
10use crate::{REGISTRY, TaggedRegistryExt};
11
12#[derive(Debug, Clone, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct BlockStateData {
16 #[serde(rename = "Name")]
18 pub name: Identifier,
19 #[serde(rename = "Properties", default)]
21 pub properties: BTreeMap<String, String>,
22}
23
24#[derive(Debug, Clone, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct FluidStateData {
28 #[serde(rename = "Name")]
30 pub name: Identifier,
31 #[serde(rename = "Properties", default)]
33 pub properties: BTreeMap<String, String>,
34}
35
36pub fn deserialize_tag_identifier<'de, D: Deserializer<'de>>(
37 deserializer: D,
38) -> Result<Identifier, D::Error> {
39 let value = String::deserialize(deserializer)?;
40 let tag = value.strip_prefix('#').unwrap_or(&value);
41 Identifier::from_str(tag).map_err(D::Error::custom)
42}
43
44pub fn deserialize_optional_tag_identifier<'de, D: Deserializer<'de>>(
45 deserializer: D,
46) -> Result<Option<Identifier>, D::Error> {
47 let Some(value) = Option::<String>::deserialize(deserializer)? else {
48 return Ok(None);
49 };
50 let tag = value.strip_prefix('#').unwrap_or(&value);
51 Identifier::from_str(tag)
52 .map(Some)
53 .map_err(D::Error::custom)
54}
55
56#[derive(Debug)]
58pub struct SpawnConditionEntry {
59 pub priority: i32,
60 pub condition: Option<BiomeCondition>,
61}
62
63impl SpawnConditionEntry {
64 #[must_use]
65 pub fn matches_biome(&self, biome: BiomeRef) -> bool {
66 self.condition
67 .as_ref()
68 .is_none_or(|condition| condition.matches_biome(biome))
69 }
70}
71
72impl ToNbtTag for &SpawnConditionEntry {
73 fn to_nbt_tag(self) -> NbtTag {
74 let mut e = NbtCompound::new();
75 e.insert("priority", self.priority);
76 if let Some(cond) = &self.condition {
77 e.insert("condition", cond.to_nbt_tag());
78 }
79 NbtTag::Compound(e)
80 }
81}
82
83#[derive(Debug)]
85pub struct BiomeCondition {
86 pub condition_type: &'static str,
87 pub biomes: BiomeConditionTarget,
88}
89
90impl BiomeCondition {
91 #[must_use]
92 pub fn matches_biome(&self, biome: BiomeRef) -> bool {
93 if self.condition_type != "minecraft:biome" {
94 return false;
95 }
96
97 self.biomes.matches_biome(biome)
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum BiomeConditionTarget {
104 Tag(Identifier),
106 Direct(Identifier),
108}
109
110impl BiomeConditionTarget {
111 #[must_use]
112 pub fn matches_biome(&self, biome: BiomeRef) -> bool {
113 match self {
114 Self::Tag(tag) => REGISTRY.biomes.is_in_tag(biome, tag),
115 Self::Direct(key) => &biome.key == key,
116 }
117 }
118
119 fn to_vanilla_string(&self) -> String {
120 match self {
121 Self::Tag(tag) => format!("#{tag}"),
122 Self::Direct(key) => key.to_string(),
123 }
124 }
125}
126
127impl ToNbtTag for &BiomeCondition {
128 fn to_nbt_tag(self) -> NbtTag {
129 let mut c = NbtCompound::new();
130 c.insert("type", self.condition_type);
131 c.insert("biomes", self.biomes.to_vanilla_string());
132 NbtTag::Compound(c)
133 }
134}
135
136pub fn pick_spawn_conditioned_entry<T: Copy>(
138 entries: impl IntoIterator<Item = T>,
139 selectors: impl Fn(T) -> &'static [SpawnConditionEntry],
140 biome: BiomeRef,
141 random: &mut impl Random,
142) -> Option<T> {
143 let mut selected = Vec::new();
144 let mut highest_priority = i32::MIN;
145
146 for entry in entries {
147 for selector in selectors(entry) {
148 if !selector.matches_biome(biome) {
149 continue;
150 }
151
152 match selector.priority.cmp(&highest_priority) {
153 Ordering::Greater => {
154 selected.clear();
155 selected.push(entry);
156 highest_priority = selector.priority;
157 }
158 Ordering::Equal => selected.push(entry),
159 Ordering::Less => {}
160 }
161 }
162 }
163
164 let bound = i32::try_from(selected.len()).ok()?;
165 if bound == 0 {
166 return None;
167 }
168
169 let index = random.next_i32_bounded(bound) as usize;
170 selected.get(index).copied()
171}
172
173pub fn insert_spawn_conditions(compound: &mut NbtCompound, entries: &[SpawnConditionEntry]) {
176 let list: Vec<NbtCompound> = entries
177 .iter()
178 .map(|entry| {
179 let mut e = NbtCompound::new();
180 e.insert("priority", entry.priority);
181 if let Some(cond) = &entry.condition {
182 e.insert("condition", cond.to_nbt_tag());
183 }
184 e
185 })
186 .collect();
187 compound.insert("spawn_conditions", NbtTag::List(NbtList::Compound(list)));
188}
189
190#[cfg(test)]
191mod tests {
192 use steel_utils::random::{Random, RandomSplitter};
193
194 use crate::{init_vanilla_registry, vanilla_biomes};
195
196 use super::{SpawnConditionEntry, pick_spawn_conditioned_entry};
197
198 static HIGH_DUPLICATE_SELECTORS: [SpawnConditionEntry; 2] = [
199 SpawnConditionEntry {
200 priority: 2,
201 condition: None,
202 },
203 SpawnConditionEntry {
204 priority: 2,
205 condition: None,
206 },
207 ];
208 static HIGH_SINGLE_SELECTOR: [SpawnConditionEntry; 1] = [SpawnConditionEntry {
209 priority: 2,
210 condition: None,
211 }];
212 static LOWER_SELECTOR: [SpawnConditionEntry; 1] = [SpawnConditionEntry {
213 priority: 1,
214 condition: None,
215 }];
216
217 struct IndexRandom {
218 index: i32,
219 }
220
221 impl Random for IndexRandom {
222 fn fork(&mut self) -> Self {
223 unreachable!("selector tests do not fork random")
224 }
225
226 fn next_i32(&mut self) -> i32 {
227 unreachable!("selector tests only use bounded random")
228 }
229
230 fn next_i32_bounded(&mut self, bound: i32) -> i32 {
231 assert!(self.index < bound);
232 self.index
233 }
234
235 fn next_i64(&mut self) -> i64 {
236 unreachable!("selector tests only use bounded random")
237 }
238
239 fn next_f32(&mut self) -> f32 {
240 unreachable!("selector tests only use bounded random")
241 }
242
243 fn next_f64(&mut self) -> f64 {
244 unreachable!("selector tests only use bounded random")
245 }
246
247 fn next_bool(&mut self) -> bool {
248 unreachable!("selector tests only use bounded random")
249 }
250
251 fn next_gaussian(&mut self) -> f64 {
252 unreachable!("selector tests only use bounded random")
253 }
254
255 fn next_positional(&mut self) -> RandomSplitter {
256 unreachable!("selector tests only use bounded random")
257 }
258 }
259
260 #[test]
261 fn pick_spawn_conditioned_entry_keeps_duplicate_highest_priority_matches() {
262 init_vanilla_registry();
263
264 let mut random = IndexRandom { index: 1 };
265 let selected = pick_spawn_conditioned_entry(
266 [1, 2, 3],
267 |entry| match entry {
268 1 => &HIGH_DUPLICATE_SELECTORS,
269 2 => &HIGH_SINGLE_SELECTOR,
270 _ => &LOWER_SELECTOR,
271 },
272 &vanilla_biomes::PLAINS,
273 &mut random,
274 );
275
276 assert_eq!(selected, Some(1));
277 }
278}