Skip to main content

steel_registry/
dimension_type.rs

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