Skip to main content

steel_registry/
shared_structs.rs

1use 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/// Block state data as encoded by vanilla registry JSON.
13#[derive(Debug, Clone, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct BlockStateData {
16    /// Block identifier.
17    #[serde(rename = "Name")]
18    pub name: Identifier,
19    /// String-valued block-state properties.
20    #[serde(rename = "Properties", default)]
21    pub properties: BTreeMap<String, String>,
22}
23
24/// Fluid state data as encoded by vanilla registry JSON.
25#[derive(Debug, Clone, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct FluidStateData {
28    /// Fluid identifier.
29    #[serde(rename = "Name")]
30    pub name: Identifier,
31    /// String-valued fluid-state properties.
32    #[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/// A single entry in the list of spawn conditions.
57#[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/// Defines a condition based on a biome or list of biomes.
84#[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/// Vanilla spawn-condition biome target.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum BiomeConditionTarget {
104    /// A biome tag target encoded as `#namespace:path` in registry data.
105    Tag(Identifier),
106    /// A direct biome target encoded as `namespace:path` in registry data.
107    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
136/// Picks entries using vanilla `PriorityProvider.pick` semantics.
137pub 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
173/// Serialize a `spawn_conditions` list into the enclosing compound.
174/// Matches vanilla's `[{priority, condition?}, …]` shape exactly.
175pub 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}