Skip to main content

steel_registry/
biome.rs

1use std::sync::OnceLock;
2
3use rustc_hash::FxHashMap;
4use simdnbt::ToNbtTag;
5use simdnbt::owned::NbtTag;
6use steel_utils::Identifier;
7
8use crate::TaggedRegistryExt;
9use crate::sound_event::SoundEventRef;
10use crate::{REGISTRY, RegistryTags};
11
12#[derive(Debug)]
13pub struct Biome {
14    pub key: Identifier,
15    pub has_precipitation: bool,
16    pub temperature: f32,
17    pub downfall: f32,
18    pub temperature_modifier: TemperatureModifier,
19    pub effects: BiomeEffects,
20    pub creature_spawn_probability: f32,
21    pub spawners: FxHashMap<String, Vec<SpawnerData>>,
22    pub spawn_costs: FxHashMap<Identifier, SpawnCost>,
23    pub carvers: Vec<Identifier>,
24    pub features: Vec<Vec<Identifier>>,
25    /// Cached registry ID, set during registration for O(1) lookup on hot paths.
26    pub id: OnceLock<usize>,
27}
28
29impl Biome {
30    /// Returns `true` if this biome is tagged with the given tag.
31    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
32        REGISTRY.biomes.is_in_tag(self, tag)
33    }
34}
35
36#[derive(Debug)]
37pub struct BiomeEffects {
38    pub fog_color: i32,
39    pub sky_color: i32,
40    pub water_color: i32,
41    pub water_fog_color: i32,
42    pub foliage_color: Option<i32>,
43    pub grass_color: Option<i32>,
44    pub dry_foliage_color: Option<i32>,
45    pub grass_color_modifier: GrassColorModifier,
46    pub music: Option<Vec<WeightedMusic>>,
47    pub ambient_sound: Option<SoundEventRef>,
48    pub additions_sound: Option<AdditionsSound>,
49    pub mood_sound: Option<MoodSound>,
50    pub particle: Option<Particle>,
51}
52
53#[derive(Debug)]
54pub struct SpawnerData {
55    pub entity_type: Identifier,
56    pub weight: i32,
57    pub min_count: i32,
58    pub max_count: i32,
59}
60
61#[derive(Debug)]
62pub struct SpawnCost {
63    pub energy_budget: f64,
64    pub charge: f64,
65}
66
67#[derive(Debug, Default)]
68pub enum TemperatureModifier {
69    #[default]
70    None,
71    Frozen,
72}
73
74#[derive(Debug)]
75pub enum GrassColorModifier {
76    None,
77    DarkForest,
78    Swamp,
79}
80
81#[derive(Debug)]
82pub struct WeightedMusic {
83    pub data: Music,
84    pub weight: i32,
85}
86
87#[derive(Debug)]
88pub struct Music {
89    pub replace_current_music: bool,
90    pub max_delay: i32,
91    pub min_delay: i32,
92    pub sound: SoundEventRef,
93}
94
95#[derive(Debug)]
96pub struct AdditionsSound {
97    pub sound: SoundEventRef,
98    pub tick_chance: f64,
99}
100
101#[derive(Debug)]
102pub struct MoodSound {
103    pub sound: SoundEventRef,
104    pub tick_delay: i32,
105    pub block_search_extent: i32,
106    pub offset: f64,
107}
108
109#[derive(Debug)]
110pub struct Particle {
111    pub options: ParticleOptions,
112    pub probability: f32,
113}
114
115#[derive(Debug)]
116pub struct ParticleOptions {
117    pub particle_type: Identifier,
118}
119
120impl ToNbtTag for &Biome {
121    fn to_nbt_tag(self) -> NbtTag {
122        use simdnbt::owned::{NbtCompound, NbtList};
123        let mut compound = NbtCompound::new();
124        compound.insert("has_precipitation", self.has_precipitation);
125        compound.insert("temperature", self.temperature);
126        compound.insert("downfall", self.downfall);
127        compound.insert(
128            "temperature_modifier",
129            match self.temperature_modifier {
130                TemperatureModifier::None => "none",
131                TemperatureModifier::Frozen => "frozen",
132            },
133        );
134        compound.insert(
135            "creature_spawn_probability",
136            self.creature_spawn_probability,
137        );
138
139        // Effects
140        let mut effects = NbtCompound::new();
141        effects.insert("fog_color", self.effects.fog_color);
142        effects.insert("sky_color", self.effects.sky_color);
143        effects.insert("water_color", self.effects.water_color);
144        effects.insert("water_fog_color", self.effects.water_fog_color);
145        if let Some(fc) = self.effects.foliage_color {
146            effects.insert("foliage_color", fc);
147        }
148        if let Some(gc) = self.effects.grass_color {
149            effects.insert("grass_color", gc);
150        }
151        if let Some(dfc) = self.effects.dry_foliage_color {
152            effects.insert("dry_foliage_color", dfc);
153        }
154        match self.effects.grass_color_modifier {
155            GrassColorModifier::None => {}
156            GrassColorModifier::DarkForest => {
157                effects.insert("grass_color_modifier", "dark_forest");
158            }
159            GrassColorModifier::Swamp => {
160                effects.insert("grass_color_modifier", "swamp");
161            }
162        }
163        if let Some(ambient_sound) = &self.effects.ambient_sound {
164            let s = ambient_sound.key.to_string();
165            effects.insert("ambient_sound", s.as_str());
166        }
167        if let Some(additions) = &self.effects.additions_sound {
168            let mut a = NbtCompound::new();
169            let s = additions.sound.key.to_string();
170            a.insert("sound", s.as_str());
171            a.insert("tick_chance", additions.tick_chance);
172            effects.insert("additions_sound", NbtTag::Compound(a));
173        }
174        if let Some(mood) = &self.effects.mood_sound {
175            let mut m = NbtCompound::new();
176            let s = mood.sound.key.to_string();
177            m.insert("sound", s.as_str());
178            m.insert("tick_delay", mood.tick_delay);
179            m.insert("block_search_extent", mood.block_search_extent);
180            m.insert("offset", mood.offset);
181            effects.insert("mood_sound", NbtTag::Compound(m));
182        }
183        if let Some(particle) = &self.effects.particle {
184            let mut p = NbtCompound::new();
185            let mut opts = NbtCompound::new();
186            let s = particle.options.particle_type.to_string();
187            opts.insert("type", s.as_str());
188            p.insert("options", NbtTag::Compound(opts));
189            p.insert("probability", particle.probability);
190            effects.insert("particle", NbtTag::Compound(p));
191        }
192        if let Some(music_list) = &self.effects.music {
193            let music_nbt: Vec<NbtCompound> = music_list
194                .iter()
195                .map(|wm| {
196                    let mut wmc = NbtCompound::new();
197                    let mut data = NbtCompound::new();
198                    data.insert("replace_current_music", wm.data.replace_current_music);
199                    data.insert("max_delay", wm.data.max_delay);
200                    data.insert("min_delay", wm.data.min_delay);
201                    let s = wm.data.sound.key.to_string();
202                    data.insert("sound", s.as_str());
203                    wmc.insert("data", NbtTag::Compound(data));
204                    wmc.insert("weight", wm.weight);
205                    wmc
206                })
207                .collect();
208            effects.insert("music", NbtTag::List(NbtList::Compound(music_nbt)));
209        }
210        compound.insert("effects", NbtTag::Compound(effects));
211
212        // Spawners
213        let mut spawners_compound = NbtCompound::new();
214        for (category, entries) in &self.spawners {
215            let category_entries: Vec<NbtCompound> = entries
216                .iter()
217                .map(|sd| {
218                    let mut e = NbtCompound::new();
219                    let s = sd.entity_type.to_string();
220                    e.insert("type", s.as_str());
221                    e.insert("weight", sd.weight);
222                    e.insert("minCount", sd.min_count);
223                    e.insert("maxCount", sd.max_count);
224                    e
225                })
226                .collect();
227            spawners_compound.insert(
228                category.as_str(),
229                NbtTag::List(NbtList::Compound(category_entries)),
230            );
231        }
232        compound.insert("spawners", NbtTag::Compound(spawners_compound));
233
234        // Spawn costs
235        let mut spawn_costs_compound = NbtCompound::new();
236        for (entity_type, cost) in &self.spawn_costs {
237            let mut cost_compound = NbtCompound::new();
238            cost_compound.insert("charge", cost.charge);
239            cost_compound.insert("energy_budget", cost.energy_budget);
240            let s = entity_type.to_string();
241            spawn_costs_compound.insert(s.as_str(), NbtTag::Compound(cost_compound));
242        }
243        compound.insert("spawn_costs", NbtTag::Compound(spawn_costs_compound));
244
245        // Carvers (all treated as "air" step)
246        let mut carvers_compound = NbtCompound::new();
247        let air_carvers: Vec<String> = self
248            .carvers
249            .iter()
250            .map(std::string::ToString::to_string)
251            .collect();
252        carvers_compound.insert("air", NbtTag::List(NbtList::from(air_carvers)));
253        compound.insert("carvers", NbtTag::Compound(carvers_compound));
254
255        // Features (list of lists)
256        let features_nbt: Vec<NbtList> = self
257            .features
258            .iter()
259            .map(|step| {
260                let step_strings: Vec<String> =
261                    step.iter().map(std::string::ToString::to_string).collect();
262                NbtList::from(step_strings)
263            })
264            .collect();
265        compound.insert("features", NbtTag::List(NbtList::List(features_nbt)));
266
267        NbtTag::Compound(compound)
268    }
269}
270
271pub type BiomeRef = &'static Biome;
272
273pub struct BiomeRegistry {
274    biomes_by_id: Vec<BiomeRef>,
275    biomes_by_key: FxHashMap<Identifier, usize>,
276    tags: RegistryTags,
277    allows_registering: bool,
278}
279
280impl BiomeRegistry {
281    #[must_use]
282    pub fn new() -> Self {
283        Self {
284            biomes_by_id: Vec::new(),
285            biomes_by_key: FxHashMap::default(),
286            tags: RegistryTags::default(),
287            allows_registering: true,
288        }
289    }
290}
291
292impl BiomeRegistry {
293    pub fn register(&mut self, entry: BiomeRef) -> usize {
294        assert!(
295            self.allows_registering,
296            "Cannot register Biome after registry has been frozen"
297        );
298        let id = self.biomes_by_id.len();
299        let cached = entry.id.get_or_init(|| id);
300        assert_eq!(*cached, id, "biome registered with conflicting id");
301        self.biomes_by_id.push(entry);
302        self.biomes_by_key.insert(entry.key.clone(), id);
303        id
304    }
305
306    pub fn iter(&self) -> impl Iterator<Item = (usize, BiomeRef)> + '_ {
307        self.biomes_by_id
308            .iter()
309            .enumerate()
310            .map(|(id, &entry)| (id, entry))
311    }
312}
313
314impl Default for BiomeRegistry {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320crate::impl_registry_ext!(BiomeRegistry, Biome, biomes_by_id, biomes_by_key);
321crate::impl_tagged_registry!(BiomeRegistry, biomes_by_key, "biome");
322
323crate::impl_registry_entry_eq!(Biome);
324
325impl crate::RegistryEntry for Biome {
326    fn key(&self) -> &Identifier {
327        &self.key
328    }
329
330    fn try_id(&self) -> Option<usize> {
331        self.id.get().copied()
332    }
333}