steel_core/block_entity/entities/spawner/
spawn_data.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct LightRange {
16 pub min: i32,
18 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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct CustomSpawnRules {
86 pub block_light_limit: LightRange,
88 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 #[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#[derive(Debug, Clone, PartialEq)]
120pub struct EquipmentTable {
121 pub loot_table: Identifier,
123 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 #[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#[derive(Debug, Clone, PartialEq)]
202pub struct WeightedSpawnData {
203 pub weight: i32,
205 pub data: SpawnData,
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct SpawnData {
212 pub entity_to_spawn: NbtCompound,
214 pub custom_spawn_rules: Option<CustomSpawnRules>,
216 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}