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::REGISTRY;
9use crate::TaggedRegistryExt;
10use crate::sound_event::SoundEventRef;
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.carvers.iter().map(|id| id.to_string()).collect();
248        carvers_compound.insert("air", NbtTag::List(NbtList::from(air_carvers)));
249        compound.insert("carvers", NbtTag::Compound(carvers_compound));
250
251        // Features (list of lists)
252        let features_nbt: Vec<NbtList> = self
253            .features
254            .iter()
255            .map(|step| {
256                let step_strings: Vec<String> = step.iter().map(|id| id.to_string()).collect();
257                NbtList::from(step_strings)
258            })
259            .collect();
260        compound.insert("features", NbtTag::List(NbtList::List(features_nbt)));
261
262        NbtTag::Compound(compound)
263    }
264}
265
266pub type BiomeRef = &'static Biome;
267
268pub struct BiomeRegistry {
269    biomes_by_id: Vec<BiomeRef>,
270    biomes_by_key: FxHashMap<Identifier, usize>,
271    tags: FxHashMap<Identifier, Vec<Identifier>>,
272    allows_registering: bool,
273}
274
275impl BiomeRegistry {
276    #[must_use]
277    pub fn new() -> Self {
278        Self {
279            biomes_by_id: Vec::new(),
280            biomes_by_key: FxHashMap::default(),
281            tags: FxHashMap::default(),
282            allows_registering: true,
283        }
284    }
285}
286
287impl BiomeRegistry {
288    pub fn register(&mut self, entry: BiomeRef) -> usize {
289        assert!(
290            self.allows_registering,
291            "Cannot register Biome after registry has been frozen"
292        );
293        let id = self.biomes_by_id.len();
294        let cached = entry.id.get_or_init(|| id);
295        assert_eq!(*cached, id, "biome registered with conflicting id");
296        self.biomes_by_id.push(entry);
297        self.biomes_by_key.insert(entry.key.clone(), id);
298        id
299    }
300
301    pub fn iter(&self) -> impl Iterator<Item = (usize, BiomeRef)> + '_ {
302        self.biomes_by_id
303            .iter()
304            .enumerate()
305            .map(|(id, &entry)| (id, entry))
306    }
307}
308
309impl Default for BiomeRegistry {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315crate::impl_registry_ext!(BiomeRegistry, Biome, biomes_by_id, biomes_by_key);
316crate::impl_tagged_registry!(BiomeRegistry, biomes_by_key, "biome");
317
318impl crate::RegistryEntry for Biome {
319    fn key(&self) -> &Identifier {
320        &self.key
321    }
322
323    fn try_id(&self) -> Option<usize> {
324        self.id.get().copied()
325    }
326}