Skip to main content

steel_core/block_entity/entities/spawner/
spawn_data.rs

1//! Spawner `SpawnData` and its optional light and equipment rules.
2
3use simdnbt::borrow::NbtCompound as NbtCompoundView;
4use simdnbt::borrow::NbtTag as BorrowedNbtTag;
5use simdnbt::owned::NbtCompound;
6use steel_utils::nbt::NbtNumeric;
7use steel_utils::{BlockPos, Identifier};
8
9use crate::chunk::light::{LightLayer, MAX_LIGHT_LEVEL};
10use crate::inventory::equipment::EquipmentSlot;
11use crate::world::World;
12
13/// Inclusive light range used by custom spawner rules.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct LightRange {
16    /// Lowest allowed light level.
17    pub min: i32,
18    /// Highest allowed light level.
19    pub max: i32,
20}
21
22impl LightRange {
23    pub(super) const DEFAULT: Self = Self {
24        min: 0,
25        max: MAX_LIGHT_LEVEL as i32,
26    };
27    fn from_nbt(tag: Option<BorrowedNbtTag<'_, '_>>) -> Option<Self> {
28        let range = tag
29            .and_then(|tag| {
30                if let Some(value) = tag.codec_i32() {
31                    return Some(Self {
32                        min: value,
33                        max: value,
34                    });
35                }
36                let (min, max) = if let Some(compound) = tag.compound() {
37                    (
38                        compound.get("min_inclusive")?.codec_i32()?,
39                        compound.get("max_inclusive")?.codec_i32()?,
40                    )
41                } else {
42                    let values = if let Some(list) = tag.list() {
43                        list.to_owned()
44                            .as_nbt_tags()
45                            .iter()
46                            .map(NbtNumeric::codec_i32)
47                            .collect::<Option<Vec<_>>>()?
48                    } else if let Some(values) = tag.int_array() {
49                        values
50                    } else if let Some(values) = tag.long_array() {
51                        values.into_iter().map(|value| value as i32).collect()
52                    } else {
53                        tag.byte_array()?
54                            .iter()
55                            .map(|value| i32::from(*value as i8))
56                            .collect()
57                    };
58                    let [min, max] = values.as_slice() else {
59                        return None;
60                    };
61                    (*min, *max)
62                };
63                (min <= max).then_some(Self { min, max })
64            })
65            .unwrap_or(Self::DEFAULT);
66        (range.min >= Self::DEFAULT.min && range.max <= Self::DEFAULT.max).then_some(range)
67    }
68
69    fn to_nbt(self) -> NbtCompound {
70        let mut nbt = NbtCompound::new();
71        nbt.insert("min_inclusive", self.min);
72        nbt.insert("max_inclusive", self.max);
73        nbt
74    }
75
76    /// Returns whether `value` is in this range.
77    #[must_use]
78    pub const fn contains(self, value: u8) -> bool {
79        value as i32 >= self.min && value as i32 <= self.max
80    }
81}
82
83/// Optional light restrictions stored in a spawner's `SpawnData`.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct CustomSpawnRules {
86    /// Allowed block light range.
87    pub block_light_limit: LightRange,
88    /// Allowed sky light range.
89    pub sky_light_limit: LightRange,
90}
91
92impl CustomSpawnRules {
93    pub(super) fn from_nbt(nbt: &NbtCompoundView<'_, '_>) -> Option<Self> {
94        Some(Self {
95            block_light_limit: LightRange::from_nbt(nbt.get("block_light_limit"))?,
96            sky_light_limit: LightRange::from_nbt(nbt.get("sky_light_limit"))?,
97        })
98    }
99
100    fn to_nbt(self) -> NbtCompound {
101        let mut nbt = NbtCompound::new();
102        nbt.insert("block_light_limit", self.block_light_limit.to_nbt());
103        nbt.insert("sky_light_limit", self.sky_light_limit.to_nbt());
104        nbt
105    }
106
107    /// Returns whether the world lighting at `pos` satisfies this rule.
108    #[must_use]
109    pub fn is_valid_position(&self, world: &World, pos: BlockPos) -> bool {
110        self.block_light_limit
111            .contains(world.light_value_at(LightLayer::Block, pos))
112            && self
113                .sky_light_limit
114                .contains(world.effective_sky_brightness(pos))
115    }
116}
117
118/// Equipment loot-table configuration attached to spawner data.
119#[derive(Debug, Clone, PartialEq)]
120pub struct EquipmentTable {
121    /// Loot table used to roll the spawned mob's equipment.
122    pub loot_table: Identifier,
123    /// Per-slot drop chances for the rolled equipment.
124    pub slot_drop_chances: Vec<(EquipmentSlot, f32)>,
125}
126
127impl EquipmentTable {
128    fn from_nbt(nbt: &NbtCompoundView<'_, '_>) -> Option<Self> {
129        let loot_table = nbt.string("loot_table")?.to_str().parse().ok()?;
130        let mut slot_drop_chances = Vec::new();
131        match nbt.get("slot_drop_chances") {
132            Some(tag) if tag.codec_f32().is_some() => {
133                let chance = tag.codec_f32()?;
134                for slot in EquipmentSlot::ALL {
135                    slot_drop_chances.push((slot, chance));
136                }
137            }
138            Some(tag) => {
139                for (name, value) in tag.compound().iter().flat_map(NbtCompoundView::iter) {
140                    let name = name.to_str();
141                    let Some(slot) = EquipmentSlot::by_name(name.as_ref()) else {
142                        continue;
143                    };
144                    let Some(chance) = value.codec_f32() else {
145                        continue;
146                    };
147                    slot_drop_chances.push((slot, chance));
148                }
149            }
150            None => {}
151        }
152
153        Some(Self {
154            loot_table,
155            slot_drop_chances,
156        })
157    }
158
159    fn to_nbt(&self) -> NbtCompound {
160        let mut nbt = NbtCompound::new();
161        nbt.insert("loot_table", self.loot_table.to_string());
162        if !self.slot_drop_chances.is_empty() {
163            let all_slots = EquipmentSlot::ALL;
164            let scalar = self.slot_drop_chances.len() == all_slots.len()
165                && all_slots.iter().all(|slot| {
166                    self.slot_drop_chances
167                        .iter()
168                        .find(|(entry_slot, _)| entry_slot == slot)
169                        .is_some_and(|(_, chance)| {
170                            self.slot_drop_chances
171                                .first()
172                                .is_some_and(|(_, first)| chance.to_bits() == first.to_bits())
173                        })
174                });
175            if scalar {
176                if let Some((_, chance)) = self.slot_drop_chances.first() {
177                    nbt.insert("slot_drop_chances", *chance);
178                }
179            } else {
180                let mut chances = NbtCompound::new();
181                for (slot, chance) in &self.slot_drop_chances {
182                    chances.insert(slot.name(), *chance);
183                }
184                nbt.insert("slot_drop_chances", chances);
185            }
186        }
187        nbt
188    }
189
190    /// Returns the configured drop chance for a slot, if one was supplied.
191    #[must_use]
192    pub fn drop_chance(&self, slot: EquipmentSlot) -> Option<f32> {
193        self.slot_drop_chances
194            .iter()
195            .find(|(entry_slot, _)| *entry_slot == slot)
196            .map(|(_, chance)| *chance)
197    }
198}
199
200/// One weighted `SpawnPotentials` entry.
201#[derive(Debug, Clone, PartialEq)]
202pub struct WeightedSpawnData {
203    /// Relative selection weight of this entry.
204    pub weight: i32,
205    /// Spawn data used when this entry is selected.
206    pub data: SpawnData,
207}
208
209/// Entity and optional rules used by a spawner.
210#[derive(Debug, Clone, PartialEq)]
211pub struct SpawnData {
212    /// Entity NBT, including its `id`.
213    pub entity_to_spawn: NbtCompound,
214    /// Optional light restrictions for the spawn position.
215    pub custom_spawn_rules: Option<CustomSpawnRules>,
216    /// Optional equipment loot table for the spawned mob.
217    pub equipment: Option<EquipmentTable>,
218}
219
220impl Default for SpawnData {
221    fn default() -> Self {
222        Self {
223            entity_to_spawn: NbtCompound::new(),
224            custom_spawn_rules: None,
225            equipment: None,
226        }
227    }
228}
229
230impl SpawnData {
231    pub(super) fn from_nbt(nbt: &NbtCompoundView<'_, '_>) -> Option<Self> {
232        let mut entity_to_spawn = nbt.compound("entity")?.to_owned();
233        normalize_entity_id(&mut entity_to_spawn);
234        let custom_spawn_rules = nbt
235            .compound("custom_spawn_rules")
236            .and_then(|rules| CustomSpawnRules::from_nbt(&rules));
237        let equipment = nbt
238            .compound("equipment")
239            .and_then(|equipment| EquipmentTable::from_nbt(&equipment));
240        Some(Self {
241            entity_to_spawn,
242            custom_spawn_rules,
243            equipment,
244        })
245    }
246
247    pub(super) fn to_nbt(&self) -> NbtCompound {
248        let mut nbt = NbtCompound::new();
249        nbt.insert("entity", self.entity_to_spawn.clone());
250        if let Some(rules) = self.custom_spawn_rules {
251            nbt.insert("custom_spawn_rules", rules.to_nbt());
252        }
253        if let Some(equipment) = &self.equipment {
254            nbt.insert("equipment", equipment.to_nbt());
255        }
256        nbt
257    }
258}
259
260fn normalize_entity_id(entity: &mut NbtCompound) {
261    let id = entity
262        .string("id")
263        .and_then(|id| id.to_str().parse::<Identifier>().ok());
264    while entity.remove("id").is_some() {}
265    if let Some(id) = id {
266        entity.insert("id", id.to_string());
267    }
268}