Skip to main content

basalt_mc_protocol/
registry_data.rs

1//! Registry data required for the Configuration state.
2//!
3//! The Minecraft client expects registry data for several registries
4//! before it will accept a FinishConfiguration packet. This module
5//! provides builders for all required registries:
6//!
7//! - `minecraft:dimension_type` — world properties (height, light)
8//! - `minecraft:worldgen/biome` — biome rendering (colors, sky, fog)
9//! - `minecraft:damage_type` — damage source definitions (49 entries)
10//! - `minecraft:painting_variant` — required since 1.21+
11//! - `minecraft:wolf_variant` — required since 1.21+
12//! - `minecraft:chat_type` — chat message formatting (chat + msg_command)
13//! - `minecraft:trim_pattern` — armor trim patterns
14//! - `minecraft:trim_material` — armor trim materials
15//! - `minecraft:banner_pattern` — banner pattern definitions
16//! - `minecraft:enchantment` — enchantment definitions
17//! - `minecraft:jukebox_song` — music disc definitions
18//! - `minecraft:instrument` — goat horn instrument definitions
19
20use std::sync::OnceLock;
21
22use crate::packets::configuration::{
23    ClientboundConfigurationRegistryData, ClientboundConfigurationRegistryDataEntries,
24};
25use basalt_types::nbt::{NbtCompound, NbtTag};
26use basalt_types::{Encode, EncodedSize};
27
28/// Returns the pre-encoded payloads of every default registry packet,
29/// suitable for direct write via a `RawSlice`-style wrapper.
30///
31/// `build_default_registries` produces identical content for every
32/// login (six static registries: dimension type, biome, damage type,
33/// painting variant, wolf variant, chat type), so encoding it fresh
34/// per connection is pure waste — most visible on cold-start mass
35/// joins. This function encodes each registry once on first call,
36/// caches the byte vectors in a `OnceLock`, and returns the slice
37/// for every subsequent caller. The packet ID is unchanged across
38/// payloads (`ClientboundConfigurationRegistryData::PACKET_ID`); the
39/// caller frames each slice with that id.
40///
41/// Order matches `build_default_registries` exactly — keeping the
42/// cache and the builder bytewise comparable in tests.
43pub fn cached_registry_payloads() -> &'static [Vec<u8>] {
44    static CACHE: OnceLock<Vec<Vec<u8>>> = OnceLock::new();
45    CACHE.get_or_init(|| {
46        build_default_registries()
47            .into_iter()
48            .map(|reg| {
49                let mut buf = Vec::with_capacity(reg.encoded_size());
50                reg.encode(&mut buf)
51                    .expect("registry data encoding cannot fail");
52                buf
53            })
54            .collect()
55    })
56}
57
58/// Builds all required registry data packets for the Configuration state.
59///
60/// Returns a list of `ClientboundConfigurationRegistryData` packets,
61/// one per registry. These should all be sent before `FinishConfiguration`.
62pub fn build_default_registries() -> Vec<ClientboundConfigurationRegistryData> {
63    vec![
64        build_dimension_type_registry(),
65        build_biome_registry(),
66        build_damage_type_registry(),
67        build_painting_variant_registry(),
68        build_wolf_variant_registry(),
69        build_chat_type_registry(),
70    ]
71}
72
73/// Builds the `minecraft:dimension_type` registry with a single
74/// overworld dimension type.
75///
76/// The dimension type defines world properties like height range,
77/// ambient light, natural spawning, and coordinate scale.
78fn build_dimension_type_registry() -> ClientboundConfigurationRegistryData {
79    let mut overworld = NbtCompound::new();
80    overworld.insert("has_skylight", NbtTag::Byte(1));
81    overworld.insert("has_ceiling", NbtTag::Byte(0));
82    overworld.insert("ultrawarm", NbtTag::Byte(0));
83    overworld.insert("natural", NbtTag::Byte(1));
84    overworld.insert("coordinate_scale", NbtTag::Double(1.0));
85    overworld.insert("bed_works", NbtTag::Byte(1));
86    overworld.insert("respawn_anchor_works", NbtTag::Byte(0));
87    overworld.insert("min_y", NbtTag::Int(-64));
88    overworld.insert("height", NbtTag::Int(384));
89    overworld.insert("logical_height", NbtTag::Int(384));
90    overworld.insert(
91        "infiniburn",
92        NbtTag::String("#minecraft:infiniburn_overworld".into()),
93    );
94    overworld.insert("effects", NbtTag::String("minecraft:overworld".into()));
95    overworld.insert("ambient_light", NbtTag::Float(0.0));
96    overworld.insert("piglin_safe", NbtTag::Byte(0));
97    overworld.insert("has_raids", NbtTag::Byte(1));
98    overworld.insert("monster_spawn_light_level", NbtTag::Int(0));
99    overworld.insert("monster_spawn_block_light_limit", NbtTag::Int(0));
100
101    ClientboundConfigurationRegistryData {
102        id: "minecraft:dimension_type".into(),
103        entries: vec![ClientboundConfigurationRegistryDataEntries {
104            key: "minecraft:overworld".into(),
105            value: Some(overworld),
106        }],
107    }
108}
109
110/// Builds the `minecraft:worldgen/biome` registry with a single
111/// plains biome.
112///
113/// The biome defines rendering properties: sky color, fog color,
114/// water color, grass/foliage modifiers, and weather.
115fn build_biome_registry() -> ClientboundConfigurationRegistryData {
116    let mut effects = NbtCompound::new();
117    effects.insert("sky_color", NbtTag::Int(7907327));
118    effects.insert("water_fog_color", NbtTag::Int(329011));
119    effects.insert("fog_color", NbtTag::Int(12638463));
120    effects.insert("water_color", NbtTag::Int(4159204));
121
122    let mut plains = NbtCompound::new();
123    plains.insert("has_precipitation", NbtTag::Byte(1));
124    plains.insert("temperature", NbtTag::Float(0.8));
125    plains.insert("downfall", NbtTag::Float(0.4));
126    plains.insert("effects", NbtTag::Compound(effects));
127
128    ClientboundConfigurationRegistryData {
129        id: "minecraft:worldgen/biome".into(),
130        entries: vec![ClientboundConfigurationRegistryDataEntries {
131            key: "minecraft:plains".into(),
132            value: Some(plains),
133        }],
134    }
135}
136
137/// Definition of a damage type for the registry data table.
138///
139/// Each entry maps to one `minecraft:damage_type` registry entry
140/// that the client needs during `DamageSources` initialization.
141struct DamageTypeDef {
142    /// Registry key (e.g., "in_fire").
143    key: &'static str,
144    /// Death message translation key (e.g., "inFire").
145    message_id: &'static str,
146    /// Damage scaling rule: "never", "when_caused_by_living_non_player", or "always".
147    scaling: &'static str,
148    /// Hunger exhaustion applied when this damage is taken.
149    exhaustion: f32,
150    /// Optional visual/sound effect: "burning", "drowning", "freezing", "poking", "thorns".
151    effects: Option<&'static str>,
152    /// Optional death message variant: "fall_variants", "intentional_game_design".
153    death_message_type: Option<&'static str>,
154}
155
156/// All damage types required by the Minecraft 1.21.4 client.
157///
158/// The `DamageSources` class looks up these types during world
159/// initialization via `getOrThrow` — any missing entry crashes
160/// the client. This list covers all types from the vanilla data
161/// generator plus 1.21+ additions (wind_charge, mace_smash).
162const DAMAGE_TYPES: &[DamageTypeDef] = &[
163    // -- Environment --
164    DamageTypeDef {
165        key: "in_fire",
166        message_id: "inFire",
167        scaling: "when_caused_by_living_non_player",
168        exhaustion: 0.1,
169        effects: Some("burning"),
170        death_message_type: None,
171    },
172    DamageTypeDef {
173        key: "campfire",
174        message_id: "inFire",
175        scaling: "when_caused_by_living_non_player",
176        exhaustion: 0.1,
177        effects: Some("burning"),
178        death_message_type: None,
179    },
180    DamageTypeDef {
181        key: "on_fire",
182        message_id: "onFire",
183        scaling: "when_caused_by_living_non_player",
184        exhaustion: 0.0,
185        effects: Some("burning"),
186        death_message_type: None,
187    },
188    DamageTypeDef {
189        key: "lava",
190        message_id: "lava",
191        scaling: "when_caused_by_living_non_player",
192        exhaustion: 0.1,
193        effects: Some("burning"),
194        death_message_type: None,
195    },
196    DamageTypeDef {
197        key: "hot_floor",
198        message_id: "hotFloor",
199        scaling: "when_caused_by_living_non_player",
200        exhaustion: 0.1,
201        effects: Some("burning"),
202        death_message_type: None,
203    },
204    DamageTypeDef {
205        key: "in_wall",
206        message_id: "inWall",
207        scaling: "when_caused_by_living_non_player",
208        exhaustion: 0.0,
209        effects: None,
210        death_message_type: None,
211    },
212    DamageTypeDef {
213        key: "cramming",
214        message_id: "cramming",
215        scaling: "when_caused_by_living_non_player",
216        exhaustion: 0.0,
217        effects: None,
218        death_message_type: None,
219    },
220    DamageTypeDef {
221        key: "drown",
222        message_id: "drown",
223        scaling: "when_caused_by_living_non_player",
224        exhaustion: 0.0,
225        effects: Some("drowning"),
226        death_message_type: None,
227    },
228    DamageTypeDef {
229        key: "starve",
230        message_id: "starve",
231        scaling: "when_caused_by_living_non_player",
232        exhaustion: 0.0,
233        effects: None,
234        death_message_type: None,
235    },
236    DamageTypeDef {
237        key: "cactus",
238        message_id: "cactus",
239        scaling: "when_caused_by_living_non_player",
240        exhaustion: 0.1,
241        effects: Some("poking"),
242        death_message_type: None,
243    },
244    DamageTypeDef {
245        key: "sweet_berry_bush",
246        message_id: "sweetBerryBush",
247        scaling: "when_caused_by_living_non_player",
248        exhaustion: 0.1,
249        effects: Some("poking"),
250        death_message_type: None,
251    },
252    DamageTypeDef {
253        key: "freeze",
254        message_id: "freeze",
255        scaling: "when_caused_by_living_non_player",
256        exhaustion: 0.0,
257        effects: Some("freezing"),
258        death_message_type: None,
259    },
260    DamageTypeDef {
261        key: "lightning_bolt",
262        message_id: "lightningBolt",
263        scaling: "when_caused_by_living_non_player",
264        exhaustion: 0.1,
265        effects: None,
266        death_message_type: None,
267    },
268    DamageTypeDef {
269        key: "dry_out",
270        message_id: "dryOut",
271        scaling: "when_caused_by_living_non_player",
272        exhaustion: 0.1,
273        effects: None,
274        death_message_type: None,
275    },
276    // -- Physics --
277    DamageTypeDef {
278        key: "fall",
279        message_id: "fall",
280        scaling: "when_caused_by_living_non_player",
281        exhaustion: 0.0,
282        effects: None,
283        death_message_type: Some("fall_variants"),
284    },
285    DamageTypeDef {
286        key: "fly_into_wall",
287        message_id: "flyIntoWall",
288        scaling: "when_caused_by_living_non_player",
289        exhaustion: 0.0,
290        effects: None,
291        death_message_type: None,
292    },
293    DamageTypeDef {
294        key: "stalagmite",
295        message_id: "stalagmite",
296        scaling: "when_caused_by_living_non_player",
297        exhaustion: 0.0,
298        effects: None,
299        death_message_type: Some("fall_variants"),
300    },
301    DamageTypeDef {
302        key: "falling_anvil",
303        message_id: "anvil",
304        scaling: "when_caused_by_living_non_player",
305        exhaustion: 0.1,
306        effects: None,
307        death_message_type: None,
308    },
309    DamageTypeDef {
310        key: "falling_block",
311        message_id: "fallingBlock",
312        scaling: "when_caused_by_living_non_player",
313        exhaustion: 0.1,
314        effects: None,
315        death_message_type: None,
316    },
317    DamageTypeDef {
318        key: "falling_stalactite",
319        message_id: "fallingStalactite",
320        scaling: "when_caused_by_living_non_player",
321        exhaustion: 0.1,
322        effects: None,
323        death_message_type: None,
324    },
325    // -- System --
326    DamageTypeDef {
327        key: "out_of_world",
328        message_id: "outOfWorld",
329        scaling: "when_caused_by_living_non_player",
330        exhaustion: 0.0,
331        effects: None,
332        death_message_type: None,
333    },
334    DamageTypeDef {
335        key: "generic",
336        message_id: "generic",
337        scaling: "when_caused_by_living_non_player",
338        exhaustion: 0.0,
339        effects: None,
340        death_message_type: None,
341    },
342    DamageTypeDef {
343        key: "generic_kill",
344        message_id: "genericKill",
345        scaling: "when_caused_by_living_non_player",
346        exhaustion: 0.0,
347        effects: None,
348        death_message_type: None,
349    },
350    DamageTypeDef {
351        key: "outside_border",
352        message_id: "outsideBorder",
353        scaling: "when_caused_by_living_non_player",
354        exhaustion: 0.0,
355        effects: None,
356        death_message_type: None,
357    },
358    DamageTypeDef {
359        key: "bad_respawn_point",
360        message_id: "badRespawnPoint",
361        scaling: "always",
362        exhaustion: 0.1,
363        effects: None,
364        death_message_type: Some("intentional_game_design"),
365    },
366    // -- Magic / status effects --
367    DamageTypeDef {
368        key: "magic",
369        message_id: "magic",
370        scaling: "when_caused_by_living_non_player",
371        exhaustion: 0.0,
372        effects: None,
373        death_message_type: None,
374    },
375    DamageTypeDef {
376        key: "indirect_magic",
377        message_id: "indirectMagic",
378        scaling: "when_caused_by_living_non_player",
379        exhaustion: 0.0,
380        effects: None,
381        death_message_type: None,
382    },
383    DamageTypeDef {
384        key: "wither",
385        message_id: "wither",
386        scaling: "when_caused_by_living_non_player",
387        exhaustion: 0.0,
388        effects: None,
389        death_message_type: None,
390    },
391    DamageTypeDef {
392        key: "dragon_breath",
393        message_id: "dragonBreath",
394        scaling: "when_caused_by_living_non_player",
395        exhaustion: 0.0,
396        effects: None,
397        death_message_type: None,
398    },
399    DamageTypeDef {
400        key: "sonic_boom",
401        message_id: "sonic_boom",
402        scaling: "always",
403        exhaustion: 0.0,
404        effects: None,
405        death_message_type: None,
406    },
407    // -- Combat --
408    DamageTypeDef {
409        key: "mob_attack",
410        message_id: "mob_attack",
411        scaling: "when_caused_by_living_non_player",
412        exhaustion: 0.1,
413        effects: None,
414        death_message_type: None,
415    },
416    DamageTypeDef {
417        key: "mob_attack_no_aggro",
418        message_id: "mob_attack_no_aggro",
419        scaling: "when_caused_by_living_non_player",
420        exhaustion: 0.1,
421        effects: None,
422        death_message_type: None,
423    },
424    DamageTypeDef {
425        key: "mob_projectile",
426        message_id: "mob_projectile",
427        scaling: "when_caused_by_living_non_player",
428        exhaustion: 0.1,
429        effects: None,
430        death_message_type: None,
431    },
432    DamageTypeDef {
433        key: "player_attack",
434        message_id: "player_attack",
435        scaling: "when_caused_by_living_non_player",
436        exhaustion: 0.1,
437        effects: None,
438        death_message_type: None,
439    },
440    DamageTypeDef {
441        key: "player_explosion",
442        message_id: "player_explosion",
443        scaling: "always",
444        exhaustion: 0.1,
445        effects: None,
446        death_message_type: None,
447    },
448    DamageTypeDef {
449        key: "explosion",
450        message_id: "explosion",
451        scaling: "always",
452        exhaustion: 0.1,
453        effects: None,
454        death_message_type: None,
455    },
456    DamageTypeDef {
457        key: "thorns",
458        message_id: "thorns",
459        scaling: "when_caused_by_living_non_player",
460        exhaustion: 0.1,
461        effects: Some("thorns"),
462        death_message_type: None,
463    },
464    DamageTypeDef {
465        key: "sting",
466        message_id: "sting",
467        scaling: "when_caused_by_living_non_player",
468        exhaustion: 0.1,
469        effects: None,
470        death_message_type: None,
471    },
472    DamageTypeDef {
473        key: "spit",
474        message_id: "mob_attack",
475        scaling: "when_caused_by_living_non_player",
476        exhaustion: 0.1,
477        effects: None,
478        death_message_type: None,
479    },
480    // -- Projectiles --
481    DamageTypeDef {
482        key: "arrow",
483        message_id: "arrow",
484        scaling: "when_caused_by_living_non_player",
485        exhaustion: 0.1,
486        effects: None,
487        death_message_type: None,
488    },
489    DamageTypeDef {
490        key: "trident",
491        message_id: "trident",
492        scaling: "when_caused_by_living_non_player",
493        exhaustion: 0.1,
494        effects: None,
495        death_message_type: None,
496    },
497    DamageTypeDef {
498        key: "thrown",
499        message_id: "thrown",
500        scaling: "when_caused_by_living_non_player",
501        exhaustion: 0.1,
502        effects: None,
503        death_message_type: None,
504    },
505    DamageTypeDef {
506        key: "fireball",
507        message_id: "fireball",
508        scaling: "when_caused_by_living_non_player",
509        exhaustion: 0.1,
510        effects: Some("burning"),
511        death_message_type: None,
512    },
513    DamageTypeDef {
514        key: "unattributed_fireball",
515        message_id: "onFire",
516        scaling: "when_caused_by_living_non_player",
517        exhaustion: 0.1,
518        effects: Some("burning"),
519        death_message_type: None,
520    },
521    DamageTypeDef {
522        key: "fireworks",
523        message_id: "fireworks",
524        scaling: "when_caused_by_living_non_player",
525        exhaustion: 0.1,
526        effects: None,
527        death_message_type: None,
528    },
529    DamageTypeDef {
530        key: "wither_skull",
531        message_id: "witherSkull",
532        scaling: "when_caused_by_living_non_player",
533        exhaustion: 0.1,
534        effects: None,
535        death_message_type: None,
536    },
537    DamageTypeDef {
538        key: "wind_charge",
539        message_id: "wind_charge",
540        scaling: "when_caused_by_living_non_player",
541        exhaustion: 0.1,
542        effects: None,
543        death_message_type: None,
544    },
545    DamageTypeDef {
546        key: "mace_smash",
547        message_id: "mace_smash",
548        scaling: "when_caused_by_living_non_player",
549        exhaustion: 0.1,
550        effects: None,
551        death_message_type: None,
552    },
553    DamageTypeDef {
554        key: "ender_pearl",
555        message_id: "fall",
556        scaling: "when_caused_by_living_non_player",
557        exhaustion: 0.0,
558        effects: None,
559        death_message_type: Some("fall_variants"),
560    },
561];
562
563/// Builds the `minecraft:damage_type` registry with all vanilla
564/// damage types.
565///
566/// The client's `DamageSources` class looks up specific damage
567/// types via `getOrThrow` during world initialization — any
568/// missing entry causes an immediate crash. This sends the
569/// complete set from the 1.21.4 data generator.
570fn build_damage_type_registry() -> ClientboundConfigurationRegistryData {
571    let entries = DAMAGE_TYPES
572        .iter()
573        .map(|def| {
574            let mut nbt = NbtCompound::new();
575            nbt.insert("message_id", NbtTag::String(def.message_id.into()));
576            nbt.insert("scaling", NbtTag::String(def.scaling.into()));
577            nbt.insert("exhaustion", NbtTag::Float(def.exhaustion));
578            if let Some(effects) = def.effects {
579                nbt.insert("effects", NbtTag::String(effects.into()));
580            }
581            if let Some(dmt) = def.death_message_type {
582                nbt.insert("death_message_type", NbtTag::String(dmt.into()));
583            }
584            ClientboundConfigurationRegistryDataEntries {
585                key: format!("minecraft:{}", def.key),
586                value: Some(nbt),
587            }
588        })
589        .collect();
590
591    ClientboundConfigurationRegistryData {
592        id: "minecraft:damage_type".into(),
593        entries,
594    }
595}
596
597/// Builds the `minecraft:painting_variant` registry with a single
598/// painting variant.
599///
600/// Required since 1.21+ — the client crashes without it.
601fn build_painting_variant_registry() -> ClientboundConfigurationRegistryData {
602    let mut kebab = NbtCompound::new();
603    kebab.insert("asset_id", NbtTag::String("minecraft:kebab".into()));
604    kebab.insert("width", NbtTag::Int(1));
605    kebab.insert("height", NbtTag::Int(1));
606
607    ClientboundConfigurationRegistryData {
608        id: "minecraft:painting_variant".into(),
609        entries: vec![ClientboundConfigurationRegistryDataEntries {
610            key: "minecraft:kebab".into(),
611            value: Some(kebab),
612        }],
613    }
614}
615
616/// Builds the `minecraft:wolf_variant` registry with a single
617/// wolf variant.
618///
619/// Required since 1.21+ — the client crashes without it.
620fn build_wolf_variant_registry() -> ClientboundConfigurationRegistryData {
621    let mut pale = NbtCompound::new();
622    pale.insert(
623        "wild_texture",
624        NbtTag::String("minecraft:entity/wolf/wolf".into()),
625    );
626    pale.insert(
627        "tame_texture",
628        NbtTag::String("minecraft:entity/wolf/wolf_tame".into()),
629    );
630    pale.insert(
631        "angry_texture",
632        NbtTag::String("minecraft:entity/wolf/wolf_angry".into()),
633    );
634    pale.insert("biomes", NbtTag::String("minecraft:plains".into()));
635
636    ClientboundConfigurationRegistryData {
637        id: "minecraft:wolf_variant".into(),
638        entries: vec![ClientboundConfigurationRegistryDataEntries {
639            key: "minecraft:pale".into(),
640            value: Some(pale),
641        }],
642    }
643}
644
645/// Builds the `minecraft:chat_type` registry.
646///
647/// Defines how chat messages are formatted on the client. Each entry
648/// has a `chat` section (for the chat window) and a `narration` section
649/// (for accessibility narration). The `chat` type uses `chat.type.text`
650/// which formats as `<sender> message`. The `msg_command` type uses
651/// `commands.message.display.incoming` for `/msg` whispers.
652fn build_chat_type_registry() -> ClientboundConfigurationRegistryData {
653    // Helper to build a chat/narration decoration
654    fn decoration(translation_key: &str, parameters: &[&str]) -> NbtCompound {
655        let mut dec = NbtCompound::new();
656        dec.insert("translation_key", NbtTag::String(translation_key.into()));
657        let params: Vec<NbtTag> = parameters
658            .iter()
659            .map(|p| NbtTag::String((*p).into()))
660            .collect();
661        dec.insert(
662            "parameters",
663            NbtTag::List(basalt_types::nbt::NbtList::from_tags(params).unwrap()),
664        );
665        dec.insert("style", NbtTag::Compound(NbtCompound::new()));
666        dec
667    }
668
669    // "chat" type — used for regular player chat messages
670    let mut chat_type = NbtCompound::new();
671    chat_type.insert(
672        "chat",
673        NbtTag::Compound(decoration("chat.type.text", &["sender", "content"])),
674    );
675    chat_type.insert(
676        "narration",
677        NbtTag::Compound(decoration("chat.type.text.narrate", &["sender", "content"])),
678    );
679
680    // "msg_command" type — used for /msg (whisper) messages
681    let mut msg_command = NbtCompound::new();
682    msg_command.insert(
683        "chat",
684        NbtTag::Compound(decoration(
685            "commands.message.display.incoming",
686            &["sender", "content"],
687        )),
688    );
689    msg_command.insert(
690        "narration",
691        NbtTag::Compound(decoration("chat.type.text.narrate", &["sender", "content"])),
692    );
693
694    ClientboundConfigurationRegistryData {
695        id: "minecraft:chat_type".into(),
696        entries: vec![
697            ClientboundConfigurationRegistryDataEntries {
698                key: "minecraft:chat".into(),
699                value: Some(chat_type),
700            },
701            ClientboundConfigurationRegistryDataEntries {
702                key: "minecraft:msg_command".into(),
703                value: Some(msg_command),
704            },
705        ],
706    }
707}
708
709// Future registries — these have complex NBT formats that require
710// matching the exact vanilla data generator output. Tracked in
711// separate issues for each registry group.
712//
713// - trim_pattern / trim_material
714// - banner_pattern
715// - enchantment
716// - jukebox_song / instrument
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    #[test]
723    fn build_all_registries() {
724        let registries = build_default_registries();
725        assert_eq!(registries.len(), 6);
726
727        let ids: Vec<&str> = registries.iter().map(|r| r.id.as_str()).collect();
728        assert!(ids.contains(&"minecraft:dimension_type"));
729        assert!(ids.contains(&"minecraft:worldgen/biome"));
730        assert!(ids.contains(&"minecraft:damage_type"));
731        assert!(ids.contains(&"minecraft:painting_variant"));
732        assert!(ids.contains(&"minecraft:wolf_variant"));
733        assert!(ids.contains(&"minecraft:chat_type"));
734    }
735
736    #[test]
737    fn chat_type_has_entries() {
738        let reg = build_chat_type_registry();
739        assert_eq!(reg.entries.len(), 2);
740        let keys: Vec<&str> = reg.entries.iter().map(|e| e.key.as_str()).collect();
741        assert!(keys.contains(&"minecraft:chat"));
742        assert!(keys.contains(&"minecraft:msg_command"));
743    }
744
745    #[test]
746    fn dimension_type_has_entries() {
747        let reg = build_dimension_type_registry();
748        assert_eq!(reg.entries.len(), 1);
749        assert_eq!(reg.entries[0].key, "minecraft:overworld");
750        assert!(reg.entries[0].value.is_some());
751    }
752
753    #[test]
754    fn biome_has_effects() {
755        let reg = build_biome_registry();
756        let value = reg.entries[0].value.as_ref().unwrap();
757        assert!(value.get("effects").is_some());
758    }
759
760    #[test]
761    fn damage_type_has_all_entries() {
762        let reg = build_damage_type_registry();
763        assert_eq!(reg.entries.len(), DAMAGE_TYPES.len());
764
765        // Check that critical damage types the client requires are present
766        let keys: Vec<&str> = reg.entries.iter().map(|e| e.key.as_str()).collect();
767        for required in [
768            "minecraft:in_fire",
769            "minecraft:generic",
770            "minecraft:fall",
771            "minecraft:out_of_world",
772            "minecraft:wind_charge",
773            "minecraft:mace_smash",
774        ] {
775            assert!(keys.contains(&required), "missing damage type: {required}");
776        }
777    }
778
779    #[test]
780    fn registries_encode() {
781        let registries = build_default_registries();
782        for reg in &registries {
783            let mut buf = Vec::with_capacity(reg.encoded_size());
784            reg.encode(&mut buf).unwrap();
785            assert!(
786                !buf.is_empty(),
787                "registry {} should encode to non-empty bytes",
788                reg.id
789            );
790        }
791    }
792
793    #[test]
794    fn cached_payloads_match_freshly_built() {
795        let cached = cached_registry_payloads();
796        let built: Vec<Vec<u8>> = build_default_registries()
797            .into_iter()
798            .map(|reg| {
799                let mut buf = Vec::with_capacity(reg.encoded_size());
800                reg.encode(&mut buf).unwrap();
801                buf
802            })
803            .collect();
804        assert_eq!(cached.len(), built.len(), "cached entry count mismatch");
805        for (i, (c, b)) in cached.iter().zip(built.iter()).enumerate() {
806            assert_eq!(c, b, "cached payload {i} differs from freshly encoded");
807        }
808    }
809
810    #[test]
811    fn cached_payloads_returns_same_storage_across_calls() {
812        // Two consecutive calls must hand back the same backing slice —
813        // proves the OnceLock is hit instead of rebuilding on every call.
814        let first = cached_registry_payloads();
815        let second = cached_registry_payloads();
816        assert!(
817            std::ptr::eq(first.as_ptr(), second.as_ptr()),
818            "cached_registry_payloads must return the same storage on repeat calls"
819        );
820    }
821}