Skip to main content

steel_registry/
dimension_type.rs

1use rustc_hash::FxHashMap;
2use simdnbt::ToNbtTag;
3use simdnbt::owned::NbtTag;
4use steel_utils::Identifier;
5
6use crate::sound_event::SoundEventRef;
7use crate::world_clock::WorldClockRef;
8
9#[derive(Debug)]
10pub struct BedRule {
11    pub can_set_spawn: BedRuleValue,
12    pub can_sleep: BedRuleValue,
13    pub explodes: bool,
14    pub error_message_key: Option<&'static str>,
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum BedRuleValue {
19    Always,
20    WhenDark,
21    Never,
22}
23
24impl BedRuleValue {
25    #[must_use]
26    pub const fn as_str(self) -> &'static str {
27        match self {
28            Self::Always => "always",
29            Self::WhenDark => "when_dark",
30            Self::Never => "never",
31        }
32    }
33}
34
35impl ToNbtTag for BedRuleValue {
36    fn to_nbt_tag(self) -> NbtTag {
37        self.as_str().to_nbt_tag()
38    }
39}
40
41#[derive(Debug)]
42pub struct MoodSound {
43    pub sound: SoundEventRef,
44    pub tick_delay: i32,
45    pub block_search_extent: i32,
46    pub offset: f64,
47}
48
49#[derive(Debug)]
50pub struct MusicEntry {
51    pub sound: SoundEventRef,
52    pub min_delay: i32,
53    pub max_delay: i32,
54    pub replace_current_music: bool,
55}
56
57#[derive(Debug)]
58pub struct BackgroundMusic {
59    pub default: MusicEntry,
60    pub creative: Option<MusicEntry>,
61}
62
63/// Represents a full dimension type definition from a data pack JSON file.
64#[derive(Debug)]
65pub struct DimensionType {
66    pub key: Identifier,
67    pub fixed_time: Option<i64>,
68    pub has_skylight: bool,
69    pub has_ceiling: bool,
70    pub coordinate_scale: f64,
71    pub min_y: i32,
72    pub height: i32,
73    pub logical_height: i32,
74    pub infiniburn: &'static str,
75    pub ambient_light: f32,
76    pub default_clock: Option<WorldClockRef>,
77    pub timelines: Option<&'static str>,
78    pub has_ender_dragon_fight: bool,
79    pub monster_spawn_light_level: MonsterSpawnLightLevel,
80    pub monster_spawn_block_light_limit: i32,
81
82    // Top-level
83    pub skybox: Option<&'static str>,
84    pub cardinal_light: Option<&'static str>,
85
86    // Attributes: visual
87    pub sky_color: Option<&'static str>,
88    pub fog_color: Option<&'static str>,
89    pub cloud_color: Option<&'static str>,
90    pub cloud_height: Option<f32>,
91    pub ambient_light_color: Option<&'static str>,
92    pub sky_light_color: Option<&'static str>,
93    pub sky_light_factor: Option<f32>,
94    pub fog_start_distance: Option<f32>,
95    pub fog_end_distance: Option<f32>,
96    pub default_dripstone_particle: Option<&'static str>,
97
98    // Attributes: gameplay
99    pub respawn_anchor_works: bool,
100    pub can_start_raid: bool,
101    pub fast_lava: bool,
102    pub piglins_zombify: bool,
103    pub sky_light_level: Option<f32>,
104    pub snow_golem_melts: bool,
105    pub water_evaporates: bool,
106    pub nether_portal_spawns_piglin: bool,
107    pub bed_rule: BedRule,
108
109    // Attributes: audio
110    pub mood_sound: Option<MoodSound>,
111    pub background_music: Option<BackgroundMusic>,
112}
113
114impl DimensionType {
115    /// Returns vanilla `DimensionType.getTeleportationScale`.
116    #[must_use]
117    pub fn get_teleportation_scale(last_dimension_type: &Self, new_dimension_type: &Self) -> f64 {
118        last_dimension_type.coordinate_scale / new_dimension_type.coordinate_scale
119    }
120}
121
122/// Represents the complex structure for monster spawn light level.
123#[derive(Debug)]
124pub enum MonsterSpawnLightLevel {
125    Simple(i32),
126    Complex {
127        distribution_type: &'static str,
128        min_inclusive: i32,
129        max_inclusive: i32,
130    },
131}
132
133impl ToNbtTag for &DimensionType {
134    fn to_nbt_tag(self) -> NbtTag {
135        use simdnbt::owned::{NbtCompound, NbtTag};
136        let mut compound = NbtCompound::new();
137
138        // Top-level fields
139        if let Some(fixed_time) = self.fixed_time {
140            compound.insert("fixed_time", fixed_time);
141        }
142        compound.insert("has_skylight", self.has_skylight);
143        compound.insert("has_ceiling", self.has_ceiling);
144        compound.insert("coordinate_scale", self.coordinate_scale);
145        compound.insert("min_y", self.min_y);
146        compound.insert("height", self.height);
147        compound.insert("logical_height", self.logical_height);
148        compound.insert("infiniburn", self.infiniburn);
149        compound.insert("ambient_light", self.ambient_light);
150        compound.insert("has_ender_dragon_fight", self.has_ender_dragon_fight);
151        if let Some(clock) = self.default_clock {
152            compound.insert("default_clock", clock.key.to_string().as_str());
153        }
154        if let Some(timelines) = self.timelines {
155            compound.insert("timelines", timelines);
156        }
157        if let Some(skybox) = self.skybox {
158            compound.insert("skybox", skybox);
159        }
160        if let Some(cardinal_light) = self.cardinal_light {
161            compound.insert("cardinal_light", cardinal_light);
162        }
163        compound.insert(
164            "monster_spawn_light_level",
165            match &self.monster_spawn_light_level {
166                MonsterSpawnLightLevel::Simple(v) => NbtTag::Int(*v),
167                MonsterSpawnLightLevel::Complex {
168                    distribution_type,
169                    min_inclusive,
170                    max_inclusive,
171                } => {
172                    let mut inner = NbtCompound::new();
173                    inner.insert("type", *distribution_type);
174                    inner.insert("min_inclusive", *min_inclusive);
175                    inner.insert("max_inclusive", *max_inclusive);
176                    NbtTag::Compound(inner)
177                }
178            },
179        );
180        compound.insert(
181            "monster_spawn_block_light_limit",
182            self.monster_spawn_block_light_limit,
183        );
184
185        // Attributes compound
186        let mut attributes = NbtCompound::new();
187
188        // Visual attributes
189        if let Some(sky_color) = self.sky_color {
190            attributes.insert("minecraft:visual/sky_color", sky_color);
191        }
192        if let Some(fog_color) = self.fog_color {
193            attributes.insert("minecraft:visual/fog_color", fog_color);
194        }
195        if let Some(cloud_color) = self.cloud_color {
196            attributes.insert("minecraft:visual/cloud_color", cloud_color);
197        }
198        if let Some(cloud_height) = self.cloud_height {
199            attributes.insert("minecraft:visual/cloud_height", cloud_height);
200        }
201        if let Some(ambient_light_color) = self.ambient_light_color {
202            attributes.insert("minecraft:visual/ambient_light_color", ambient_light_color);
203        }
204        if let Some(sky_light_color) = self.sky_light_color {
205            attributes.insert("minecraft:visual/sky_light_color", sky_light_color);
206        }
207        if let Some(sky_light_factor) = self.sky_light_factor {
208            attributes.insert("minecraft:visual/sky_light_factor", sky_light_factor);
209        }
210        if let Some(fog_start_distance) = self.fog_start_distance {
211            attributes.insert("minecraft:visual/fog_start_distance", fog_start_distance);
212        }
213        if let Some(fog_end_distance) = self.fog_end_distance {
214            attributes.insert("minecraft:visual/fog_end_distance", fog_end_distance);
215        }
216        if let Some(particle_type) = self.default_dripstone_particle {
217            let mut particle = NbtCompound::new();
218            particle.insert("type", particle_type);
219            attributes.insert(
220                "minecraft:visual/default_dripstone_particle",
221                NbtTag::Compound(particle),
222            );
223        }
224
225        // Gameplay attributes
226        attributes.insert(
227            "minecraft:gameplay/respawn_anchor_works",
228            self.respawn_anchor_works,
229        );
230        attributes.insert("minecraft:gameplay/can_start_raid", self.can_start_raid);
231        if self.fast_lava {
232            attributes.insert("minecraft:gameplay/fast_lava", self.fast_lava);
233        }
234        if !self.piglins_zombify {
235            attributes.insert("minecraft:gameplay/piglins_zombify", self.piglins_zombify);
236        }
237        if let Some(sky_light_level) = self.sky_light_level {
238            attributes.insert("minecraft:gameplay/sky_light_level", sky_light_level);
239        }
240        if self.snow_golem_melts {
241            attributes.insert("minecraft:gameplay/snow_golem_melts", self.snow_golem_melts);
242        }
243        if self.water_evaporates {
244            attributes.insert("minecraft:gameplay/water_evaporates", self.water_evaporates);
245        }
246        if self.nether_portal_spawns_piglin {
247            attributes.insert(
248                "minecraft:gameplay/nether_portal_spawns_piglin",
249                self.nether_portal_spawns_piglin,
250            );
251        }
252
253        // Bed rule
254        {
255            let mut bed_rule = NbtCompound::new();
256            bed_rule.insert("can_set_spawn", self.bed_rule.can_set_spawn);
257            bed_rule.insert("can_sleep", self.bed_rule.can_sleep);
258            if self.bed_rule.explodes {
259                bed_rule.insert("explodes", self.bed_rule.explodes);
260            }
261            if let Some(key) = self.bed_rule.error_message_key {
262                let mut msg = NbtCompound::new();
263                msg.insert("translate", key);
264                bed_rule.insert("error_message", NbtTag::Compound(msg));
265            }
266            attributes.insert("minecraft:gameplay/bed_rule", NbtTag::Compound(bed_rule));
267        }
268
269        // Audio attributes
270        if let Some(mood) = &self.mood_sound {
271            let mut mood_compound = NbtCompound::new();
272            let sound = mood.sound.key.to_string();
273            mood_compound.insert("sound", sound.as_str());
274            mood_compound.insert("tick_delay", mood.tick_delay);
275            mood_compound.insert("block_search_extent", mood.block_search_extent);
276            mood_compound.insert("offset", mood.offset);
277            let mut ambient_sounds = NbtCompound::new();
278            ambient_sounds.insert("mood", NbtTag::Compound(mood_compound));
279            attributes.insert(
280                "minecraft:audio/ambient_sounds",
281                NbtTag::Compound(ambient_sounds),
282            );
283        }
284        if let Some(bg_music) = &self.background_music {
285            let mut music_compound = NbtCompound::new();
286            let mut default_entry = NbtCompound::new();
287            let sound = bg_music.default.sound.key.to_string();
288            default_entry.insert("sound", sound.as_str());
289            default_entry.insert("min_delay", bg_music.default.min_delay);
290            default_entry.insert("max_delay", bg_music.default.max_delay);
291            if bg_music.default.replace_current_music {
292                default_entry.insert(
293                    "replace_current_music",
294                    bg_music.default.replace_current_music,
295                );
296            }
297            music_compound.insert("default", NbtTag::Compound(default_entry));
298            if let Some(creative) = &bg_music.creative {
299                let mut creative_entry = NbtCompound::new();
300                let sound = creative.sound.key.to_string();
301                creative_entry.insert("sound", sound.as_str());
302                creative_entry.insert("min_delay", creative.min_delay);
303                creative_entry.insert("max_delay", creative.max_delay);
304                if creative.replace_current_music {
305                    creative_entry.insert("replace_current_music", creative.replace_current_music);
306                }
307                music_compound.insert("creative", NbtTag::Compound(creative_entry));
308            }
309            attributes.insert(
310                "minecraft:audio/background_music",
311                NbtTag::Compound(music_compound),
312            );
313        }
314
315        if !attributes.is_empty() {
316            compound.insert("attributes", NbtTag::Compound(attributes));
317        }
318
319        NbtTag::Compound(compound)
320    }
321}
322
323pub type DimensionTypeRef = &'static DimensionType;
324
325pub struct DimensionTypeRegistry {
326    dimension_types_by_id: Vec<DimensionTypeRef>,
327    dimension_types_by_key: FxHashMap<Identifier, usize>,
328    allows_registering: bool,
329}
330
331impl DimensionTypeRegistry {
332    #[must_use]
333    pub fn new() -> Self {
334        Self {
335            dimension_types_by_id: Vec::new(),
336            dimension_types_by_key: FxHashMap::default(),
337            allows_registering: true,
338        }
339    }
340
341    #[must_use]
342    pub fn get_ids(&self) -> Vec<Identifier> {
343        self.dimension_types_by_key.keys().cloned().collect()
344    }
345}
346
347crate::impl_standard_methods!(
348    DimensionTypeRegistry,
349    DimensionTypeRef,
350    dimension_types_by_id,
351    dimension_types_by_key,
352    allows_registering
353);
354
355crate::impl_registry!(
356    DimensionTypeRegistry,
357    DimensionType,
358    dimension_types_by_id,
359    dimension_types_by_key,
360    dimension_types
361);
362
363#[cfg(test)]
364mod tests {
365    use crate::dimension_type::DimensionType;
366    use crate::vanilla_dimension_types::{OVERWORLD, THE_END, THE_NETHER};
367
368    #[test]
369    fn teleportation_scale_matches_vanilla_coordinate_ratio() {
370        assert_eq!(
371            DimensionType::get_teleportation_scale(&OVERWORLD, &THE_NETHER),
372            0.125
373        );
374        assert_eq!(
375            DimensionType::get_teleportation_scale(&THE_NETHER, &OVERWORLD),
376            8.0
377        );
378        assert_eq!(
379            DimensionType::get_teleportation_scale(&THE_END, &THE_NETHER),
380            0.125
381        );
382    }
383}