1use std::sync::OnceLock;
21
22use crate::packets::configuration::{
23 ClientboundConfigurationRegistryData, ClientboundConfigurationRegistryDataEntries,
24};
25use basalt_types::nbt::{NbtCompound, NbtTag};
26use basalt_types::{Encode, EncodedSize};
27
28pub 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
58pub 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
73fn 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
110fn 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
137struct DamageTypeDef {
142 key: &'static str,
144 message_id: &'static str,
146 scaling: &'static str,
148 exhaustion: f32,
150 effects: Option<&'static str>,
152 death_message_type: Option<&'static str>,
154}
155
156const DAMAGE_TYPES: &[DamageTypeDef] = &[
163 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 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 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 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 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 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
563fn 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
597fn 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
616fn 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
645fn build_chat_type_registry() -> ClientboundConfigurationRegistryData {
653 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 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 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#[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 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 ®istries {
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 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}