chunkedge_registry/
biome.rs

1//! Contains biomes and the biome registry. Minecraft's default biomes are added
2//! to the registry by default.
3//!
4//! ### **NOTE:**
5//! - Modifying the biome registry after the server has started can break
6//!   invariants within instances and clients! Make sure there are no instances
7//!   or clients spawned before mutating.
8//! - A biome named "minecraft:plains" must exist. Otherwise, vanilla clients
9//!   will be disconnected.
10
11use std::ops::{Deref, DerefMut};
12
13use bevy_app::prelude::*;
14use bevy_ecs::prelude::*;
15use chunkedge_ident::{ident, Ident};
16use chunkedge_nbt::serde::ser::CompoundSerializer;
17use serde::{Deserialize, Serialize};
18use tracing::error;
19
20use crate::codec::{RegistryCodec, RegistryValue};
21use crate::{Registry, RegistryIdx, RegistrySet};
22
23pub struct BiomePlugin;
24
25impl Plugin for BiomePlugin {
26    fn build(&self, app: &mut App) {
27        app.init_resource::<BiomeRegistry>()
28            .add_systems(PreStartup, load_default_biomes)
29            .add_systems(PostUpdate, update_biome_registry.before(RegistrySet));
30    }
31}
32
33fn load_default_biomes(mut reg: ResMut<BiomeRegistry>, codec: Res<RegistryCodec>) {
34    let mut helper = move || -> anyhow::Result<()> {
35        for value in codec.registry(BiomeRegistry::KEY) {
36            let biome = Biome::deserialize(value.element.clone())?;
37
38            reg.insert(value.name.clone(), biome);
39        }
40
41        // Move "plains" to the front so that `BiomeId::default()` is the ID of plains.
42        reg.swap_to_front(ident!("plains"));
43
44        Ok(())
45    };
46
47    if let Err(e) = helper() {
48        error!("failed to load default biomes from registry codec: {e:#}");
49    }
50}
51
52fn update_biome_registry(reg: Res<BiomeRegistry>, mut codec: ResMut<RegistryCodec>) {
53    if reg.is_changed() {
54        let biomes = codec.registry_mut(BiomeRegistry::KEY);
55
56        biomes.clear();
57
58        biomes.extend(reg.iter().map(|(_, name, biome)| {
59            RegistryValue {
60                name: name.into(),
61                element: biome
62                    .serialize(CompoundSerializer)
63                    .expect("failed to serialize biome"),
64            }
65        }));
66    }
67}
68
69#[derive(Resource, Default, Debug)]
70pub struct BiomeRegistry {
71    reg: Registry<BiomeId, Biome>,
72}
73
74impl BiomeRegistry {
75    pub const KEY: Ident<&'static str> = ident!("worldgen/biome");
76}
77
78impl Deref for BiomeRegistry {
79    type Target = Registry<BiomeId, Biome>;
80
81    fn deref(&self) -> &Self::Target {
82        &self.reg
83    }
84}
85
86impl DerefMut for BiomeRegistry {
87    fn deref_mut(&mut self) -> &mut Self::Target {
88        &mut self.reg
89    }
90}
91
92#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
93pub struct BiomeId(u32);
94
95impl BiomeId {
96    pub const DEFAULT: Self = BiomeId(0);
97}
98
99impl RegistryIdx for BiomeId {
100    const MAX: usize = u32::MAX as usize;
101
102    #[inline]
103    fn to_index(self) -> usize {
104        self.0 as usize
105    }
106
107    #[inline]
108    fn from_index(idx: usize) -> Self {
109        Self(idx as u32)
110    }
111}
112
113#[derive(Serialize, Deserialize, Clone, Debug)]
114pub struct Biome {
115    pub has_precipitation: bool,
116    pub temperature: f32,
117    pub downfall: f32,
118    pub effects: BiomeEffects,
119}
120
121impl Default for Biome {
122    /// Default will be the same as the `minecraft:plains` biome.
123    fn default() -> Self {
124        Self {
125            has_precipitation: true,
126            temperature: 0.8,
127            downfall: 0.4,
128            effects: BiomeEffects::default(),
129        }
130    }
131}
132
133#[derive(Serialize, Deserialize, Clone, Debug)]
134pub struct BiomeEffects {
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub mood_sound: Option<BiomeMoodSound>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub additions_sound: Option<BiomeAdditionsSound>,
139    #[serde(skip_serializing_if = "Vec::is_empty", default)]
140    pub music: Vec<BiomeMusic>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub music_volume: Option<f32>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub particle: Option<BiomeParticle>,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub sky_color: Option<u32>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub foliage_color: Option<u32>,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub grass_color: Option<u32>,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub fog_color: Option<u32>,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub water_color: Option<u32>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub water_fog_color: Option<u32>,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub grass_color_modifier: Option<String>,
159}
160#[derive(Serialize, Deserialize, Clone, Debug)]
161pub struct BiomeMoodSound {
162    pub sound: Ident<String>,
163    pub tick_delay: u32,
164    pub block_search_extent: u32,
165    pub offset: f32,
166}
167
168#[derive(Serialize, Deserialize, Clone, Debug)]
169pub struct BiomeMusic {
170    pub data: BiomeMusicData,
171    pub weight: u32,
172}
173
174#[derive(Serialize, Deserialize, Clone, Debug)]
175pub struct BiomeMusicData {
176    pub sound: Ident<String>,
177    pub min_delay: u32,
178    pub max_delay: u32,
179    pub replace_current_music: bool,
180}
181
182#[derive(Serialize, Deserialize, Clone, Debug)]
183pub struct BiomeAdditionsSound {
184    pub sound: Ident<String>,
185    pub tick_chance: f32,
186}
187
188#[derive(Serialize, Deserialize, Clone, Debug)]
189pub struct BiomeParticle {
190    pub options: BiomeParticleOptions,
191    pub probability: f32,
192}
193
194#[derive(Serialize, Deserialize, Clone, Debug)]
195pub struct BiomeParticleOptions {
196    #[serde(rename = "type")]
197    pub kind: Ident<String>,
198}
199
200impl Default for BiomeEffects {
201    /// Default will be the same as the `minecraft:plains` biome.
202    fn default() -> Self {
203        Self {
204            mood_sound: Some(BiomeMoodSound {
205                sound: ident!("minecraft:ambient.cave").into(),
206                tick_delay: 6000,
207                block_search_extent: 8,
208                offset: 2.0,
209            }),
210            music_volume: Some(1.0),
211            sky_color: Some(7907327),
212            fog_color: Some(12638463),
213            water_color: Some(4159204),
214            water_fog_color: Some(329011),
215            additions_sound: None,
216            music: Vec::new(),
217            particle: None,
218            foliage_color: None,
219            grass_color: None,
220            grass_color_modifier: None,
221        }
222    }
223}