11use indexmap:: IndexMap ;
22use quote:: quote;
33use serde_json:: Value ;
4+ use std:: collections:: HashMap ;
5+
6+ use craftflow_nbt:: DynNBT ;
47
58pub ( crate ) fn build_mapping ( _: proc_macro:: TokenStream ) -> proc_macro:: TokenStream {
69 let json_file = include_bytes ! ( "../../../../../assets/data/registry_packets.json" ) ;
@@ -12,7 +15,20 @@ pub(crate) fn build_mapping(_: proc_macro::TokenStream) -> proc_macro::TokenStre
1215 let mut packets = vec ! [ ] ;
1316 for ( value_name, value) in & value_set {
1417 let mut nbt_data_buf = Vec :: new ( ) ;
15- craftflow_nbt:: to_writer ( & mut nbt_data_buf, & value) . unwrap ( ) ;
18+ // The registry data is sourced from JSON, which cannot express NBT's distinct numeric
19+ // tags: every JSON integer would otherwise serialise as a `Long` and every real as a
20+ // `Double`. The vanilla client coerces numeric tags leniently and tolerates that, but
21+ // strict clients deserialise the registry into typed structs and reject a field whose
22+ // tag is not exactly what the schema expects (e.g. `dimension_type.height` must be an
23+ // `Int`, not a `Long`). `dimension_type` is encoded through a schema-aware converter so
24+ // every field carries its correct tag; all other registries keep the byte-for-byte
25+ // output of the previous generic path until they, too, need a schema.
26+ if reg_entry == "minecraft:dimension_type" {
27+ let nbt = dimension_type_to_nbt ( value) ;
28+ craftflow_nbt:: to_writer ( & mut nbt_data_buf, & nbt) . unwrap ( ) ;
29+ } else {
30+ craftflow_nbt:: to_writer ( & mut nbt_data_buf, & value) . unwrap ( ) ;
31+ }
1632 let kv = ( value_name. clone ( ) , nbt_data_buf) ;
1733 packets. push ( kv) ;
1834 }
@@ -35,3 +51,93 @@ pub(crate) fn build_mapping(_: proc_macro::TokenStream) -> proc_macro::TokenStre
3551 }
3652 . into ( )
3753}
54+
55+ /// The NBT numeric tag a value must use, when it differs from the generic default (integers → Int,
56+ /// reals → Double). Only the tags actually needed by the current schema overrides are listed.
57+ #[ derive( Clone , Copy ) ]
58+ enum NumTag {
59+ Long ,
60+ Float ,
61+ Double ,
62+ }
63+
64+ /// The `dimension_type` fields whose vanilla NBT tag differs from the generic default. Every other
65+ /// field is an `Int` (e.g. `height`, `min_y`, `logical_height`), a `Byte` boolean, a `String`, or a
66+ /// nested compound, all of which the generic conversion already produces correctly.
67+ fn dimension_type_field_tag ( field : & str ) -> Option < NumTag > {
68+ match field {
69+ // Stored as `0`/`0.x` in JSON but a float in the dimension codec.
70+ "ambient_light" => Some ( NumTag :: Float ) ,
71+ // A double in the dimension codec; JSON carries it as the integer `1`.
72+ "coordinate_scale" => Some ( NumTag :: Double ) ,
73+ // A long in the dimension codec (optional; present for the Nether and the End).
74+ "fixed_time" => Some ( NumTag :: Long ) ,
75+ _ => None ,
76+ }
77+ }
78+
79+ /// Converts one `dimension_type` entry's JSON into correctly-typed NBT, applying the per-field tag
80+ /// overrides above to the entry's top-level fields. Nested values (e.g. the
81+ /// `monster_spawn_light_level` int-provider compound) use the generic conversion, which already
82+ /// yields `Int` for their integers.
83+ fn dimension_type_to_nbt ( entry : & Value ) -> DynNBT {
84+ let obj = entry
85+ . as_object ( )
86+ . expect ( "dimension_type entry must be a JSON object" ) ;
87+ let mut map = HashMap :: with_capacity ( obj. len ( ) ) ;
88+ for ( key, value) in obj {
89+ map. insert (
90+ key. clone ( ) ,
91+ json_to_nbt ( value, dimension_type_field_tag ( key) ) ,
92+ ) ;
93+ }
94+ DynNBT :: Compound ( map)
95+ }
96+
97+ /// Generic JSON → NBT conversion with Minecraft-appropriate defaults: integers become `Int` (not
98+ /// `Long`), reals become `Double`, and booleans become `Byte`. `force` overrides the numeric tag
99+ /// for a single scalar where the schema demands a non-default tag; it does not propagate into
100+ /// nested lists or compounds.
101+ fn json_to_nbt ( value : & Value , force : Option < NumTag > ) -> DynNBT {
102+ match value {
103+ Value :: Bool ( b) => DynNBT :: Byte ( i8:: from ( * b) ) ,
104+ Value :: Number ( n) => match force {
105+ Some ( NumTag :: Float ) => DynNBT :: Float ( num_f64 ( n) as f32 ) ,
106+ Some ( NumTag :: Double ) => DynNBT :: Double ( num_f64 ( n) ) ,
107+ Some ( NumTag :: Long ) => DynNBT :: Long ( num_i64 ( n) ) ,
108+ None => {
109+ if let Some ( i) = n. as_i64 ( ) {
110+ // Default integers to Int (the common registry tag), widening to Long only when
111+ // the value genuinely does not fit in an i32.
112+ match i32:: try_from ( i) {
113+ Ok ( v) => DynNBT :: Int ( v) ,
114+ Err ( _) => DynNBT :: Long ( i) ,
115+ }
116+ } else {
117+ DynNBT :: Double ( num_f64 ( n) )
118+ }
119+ }
120+ } ,
121+ Value :: String ( s) => DynNBT :: String ( s. clone ( ) ) ,
122+ Value :: Array ( items) => DynNBT :: List ( items. iter ( ) . map ( |v| json_to_nbt ( v, None ) ) . collect ( ) ) ,
123+ Value :: Object ( obj) => {
124+ let mut map = HashMap :: with_capacity ( obj. len ( ) ) ;
125+ for ( key, value) in obj {
126+ map. insert ( key. clone ( ) , json_to_nbt ( value, None ) ) ;
127+ }
128+ DynNBT :: Compound ( map)
129+ }
130+ // Registries contain no JSON nulls; encode defensively as a zero byte rather than panicking.
131+ Value :: Null => DynNBT :: Byte ( 0 ) ,
132+ }
133+ }
134+
135+ fn num_f64 ( n : & serde_json:: Number ) -> f64 {
136+ n. as_f64 ( )
137+ . expect ( "registry numeric value is representable as f64" )
138+ }
139+
140+ fn num_i64 ( n : & serde_json:: Number ) -> i64 {
141+ n. as_i64 ( )
142+ . expect ( "registry numeric value forced to an integer tag must be an integer" )
143+ }
0 commit comments