Skip to main content

boon/entity/
field_decoder.rs

1use crate::error::Result;
2use crate::io::BitReader;
3
4use super::field_value::FieldValue;
5use super::quantized_float::QuantizedFloat;
6
7/// Mutable state shared across field decoders during a single parse pass.
8pub struct FieldDecodeContext {
9    /// Current tick interval; used by the simulation-time decoder.
10    pub tick_interval: f32,
11    /// Reusable buffer for string decoding (avoids per-field allocations).
12    pub string_buf: Vec<u8>,
13}
14
15impl FieldDecodeContext {
16    pub fn new(tick_interval: f32) -> Self {
17        Self {
18            tick_interval,
19            string_buf: Vec::with_capacity(512),
20        }
21    }
22}
23
24/// Describes how to read a single field value from a [`BitReader`](crate::io::BitReader).
25///
26/// Each variant corresponds to a Source 2 wire encoding. The correct
27/// variant is chosen at parse time by [`get_field_metadata`].
28#[derive(Debug, Clone)]
29pub enum Decoder {
30    Bool,
31    I64,
32    U64,
33    U64Fixed64,
34    F32NoScale,
35    F32SimulationTime,
36    F32Coord,
37    F32Normal,
38    F32Quantized(QuantizedFloat),
39    String,
40    Vector2(Box<Decoder>),
41    Vector3(Box<Decoder>),
42    Vector3Normal,
43    Vector4(Box<Decoder>),
44    QAnglePitchYaw {
45        bit_count: usize,
46    },
47    QAnglePrecise,
48    QAngleBitCount {
49        bit_count: usize,
50    },
51    QAngleCoord,
52    /// Used as a placeholder/invalid decoder.
53    Default,
54}
55
56impl Decoder {
57    /// Read a single field value from the bitstream.
58    pub fn decode(&self, ctx: &mut FieldDecodeContext, br: &mut BitReader) -> Result<FieldValue> {
59        match self {
60            Decoder::Bool => Ok(FieldValue::Bool(br.read_bool()?)),
61
62            Decoder::I64 => Ok(FieldValue::I64(br.read_varint64()?)),
63
64            Decoder::U64 => Ok(FieldValue::U64(br.read_uvarint64()?)),
65
66            Decoder::U64Fixed64 => {
67                let mut buf = [0u8; 8];
68                br.read_bytes(&mut buf)?;
69                Ok(FieldValue::U64(u64::from_le_bytes(buf)))
70            }
71
72            Decoder::F32NoScale => Ok(FieldValue::F32(br.read_f32()?)),
73
74            Decoder::F32SimulationTime => {
75                let ticks = br.read_uvarint32()?;
76                Ok(FieldValue::F32(ticks as f32 * ctx.tick_interval))
77            }
78
79            Decoder::F32Coord => Ok(FieldValue::F32(br.read_bitcoord()?)),
80
81            Decoder::F32Normal => Ok(FieldValue::F32(br.read_bitnormal()?)),
82
83            Decoder::F32Quantized(qf) => Ok(FieldValue::F32(qf.decode(br)?)),
84
85            Decoder::String => {
86                ctx.string_buf.clear();
87                br.read_string_raw(&mut ctx.string_buf)?;
88                Ok(FieldValue::String(ctx.string_buf.clone()))
89            }
90
91            Decoder::Vector2(inner) => {
92                let x = inner.decode_f32(ctx, br)?;
93                let y = inner.decode_f32(ctx, br)?;
94                Ok(FieldValue::Vector2([x, y]))
95            }
96
97            Decoder::Vector3(inner) => {
98                let x = inner.decode_f32(ctx, br)?;
99                let y = inner.decode_f32(ctx, br)?;
100                let z = inner.decode_f32(ctx, br)?;
101                Ok(FieldValue::Vector3([x, y, z]))
102            }
103
104            Decoder::Vector3Normal => Ok(FieldValue::Vector3(br.read_bitvec3normal()?)),
105
106            Decoder::Vector4(inner) => {
107                let x = inner.decode_f32(ctx, br)?;
108                let y = inner.decode_f32(ctx, br)?;
109                let z = inner.decode_f32(ctx, br)?;
110                let w = inner.decode_f32(ctx, br)?;
111                Ok(FieldValue::Vector4([x, y, z, w]))
112            }
113
114            Decoder::QAnglePitchYaw { bit_count } => {
115                let pitch = br.read_bitangle(*bit_count)?;
116                let yaw = br.read_bitangle(*bit_count)?;
117                Ok(FieldValue::QAngle([pitch, yaw, 0.0]))
118            }
119
120            Decoder::QAnglePrecise => {
121                let mut v = [0.0f32; 3];
122                let rx = br.read_bool()?;
123                let ry = br.read_bool()?;
124                let rz = br.read_bool()?;
125                if rx {
126                    v[0] = br.read_bitangle(20)?;
127                }
128                if ry {
129                    v[1] = br.read_bitangle(20)?;
130                }
131                if rz {
132                    v[2] = br.read_bitangle(20)?;
133                }
134                Ok(FieldValue::QAngle(v))
135            }
136
137            Decoder::QAngleBitCount { bit_count } => {
138                let x = br.read_bitangle(*bit_count)?;
139                let y = br.read_bitangle(*bit_count)?;
140                let z = br.read_bitangle(*bit_count)?;
141                Ok(FieldValue::QAngle([x, y, z]))
142            }
143
144            Decoder::QAngleCoord => Ok(FieldValue::QAngle(br.read_bitvec3coord()?)),
145
146            Decoder::Default => Ok(FieldValue::U64(br.read_uvarint64()?)),
147        }
148    }
149
150    /// Helper to decode a field value as f32 (used by vector decoders).
151    fn decode_f32(&self, ctx: &mut FieldDecodeContext, br: &mut BitReader) -> Result<f32> {
152        match self.decode(ctx, br)? {
153            FieldValue::F32(v) => Ok(v),
154            _ => Ok(0.0),
155        }
156    }
157
158    /// Skip a field value without fully decoding it - just advances the bit reader.
159    /// This is faster than decode() when we don't need the value.
160    #[allow(clippy::only_used_in_recursion)]
161    pub fn skip(&self, ctx: &mut FieldDecodeContext, br: &mut BitReader) -> Result<()> {
162        match self {
163            Decoder::Bool => {
164                br.skip_bits(1)?;
165            }
166
167            Decoder::I64 => {
168                br.skip_varint()?;
169            }
170
171            Decoder::U64 => {
172                br.skip_varint()?;
173            }
174
175            Decoder::U64Fixed64 => {
176                br.skip_bits(64)?;
177            }
178
179            Decoder::F32NoScale => {
180                br.skip_bits(32)?;
181            }
182
183            Decoder::F32SimulationTime => {
184                br.skip_varint()?;
185            }
186
187            Decoder::F32Coord => {
188                br.skip_bitcoord()?;
189            }
190
191            Decoder::F32Normal => {
192                br.skip_bitnormal()?;
193            }
194
195            Decoder::F32Quantized(qf) => {
196                qf.skip(br)?;
197            }
198
199            Decoder::String => {
200                br.skip_string()?;
201            }
202
203            Decoder::Vector2(inner) => {
204                inner.skip(ctx, br)?;
205                inner.skip(ctx, br)?;
206            }
207
208            Decoder::Vector3(inner) => {
209                inner.skip(ctx, br)?;
210                inner.skip(ctx, br)?;
211                inner.skip(ctx, br)?;
212            }
213
214            Decoder::Vector3Normal => {
215                br.skip_bitvec3normal()?;
216            }
217
218            Decoder::Vector4(inner) => {
219                inner.skip(ctx, br)?;
220                inner.skip(ctx, br)?;
221                inner.skip(ctx, br)?;
222                inner.skip(ctx, br)?;
223            }
224
225            Decoder::QAnglePitchYaw { bit_count } => {
226                br.skip_bits(*bit_count * 2)?;
227            }
228
229            Decoder::QAnglePrecise => {
230                let rx = br.read_bool()?;
231                let ry = br.read_bool()?;
232                let rz = br.read_bool()?;
233                if rx {
234                    br.skip_bits(20)?;
235                }
236                if ry {
237                    br.skip_bits(20)?;
238                }
239                if rz {
240                    br.skip_bits(20)?;
241                }
242            }
243
244            Decoder::QAngleBitCount { bit_count } => {
245                br.skip_bits(*bit_count * 3)?;
246            }
247
248            Decoder::QAngleCoord => {
249                br.skip_bitvec3coord()?;
250            }
251
252            Decoder::Default => {
253                br.skip_varint()?;
254            }
255        }
256        Ok(())
257    }
258}
259
260/// Special descriptor for fields that need non-standard handling
261/// (arrays, pointers, or nested serializers).
262#[derive(Debug, Clone)]
263pub enum FieldSpecialDescriptor {
264    /// Fixed-length array (e.g. `int32[4]`).
265    FixedArray { length: usize },
266    /// Variable-length array of a primitive type (e.g. `CNetworkUtlVectorBase<int32>`).
267    DynamicArray { inner_decoder: Decoder },
268    /// Variable-length array whose elements have a nested serializer.
269    DynamicSerializerArray,
270    /// Pointer / entity handle (encoded as a single boolean "present" flag).
271    Pointer,
272}
273
274/// Metadata about how to decode a field.
275#[derive(Debug, Clone)]
276pub struct FieldMetadata {
277    pub decoder: Decoder,
278    pub special: Option<FieldSpecialDescriptor>,
279}
280
281impl Default for FieldMetadata {
282    fn default() -> Self {
283        Self {
284            decoder: Decoder::Default,
285            special: None,
286        }
287    }
288}
289
290impl FieldMetadata {
291    pub fn is_dynamic_array(&self) -> bool {
292        matches!(
293            self.special,
294            Some(FieldSpecialDescriptor::DynamicArray { .. })
295                | Some(FieldSpecialDescriptor::DynamicSerializerArray)
296        )
297    }
298
299    pub fn is_fixed_array(&self) -> bool {
300        matches!(
301            self.special,
302            Some(FieldSpecialDescriptor::FixedArray { .. })
303        )
304    }
305
306    pub fn fixed_array_length(&self) -> Option<usize> {
307        match &self.special {
308            Some(FieldSpecialDescriptor::FixedArray { length }) => Some(*length),
309            _ => None,
310        }
311    }
312
313    pub fn is_dynamic_serializer_array(&self) -> bool {
314        matches!(
315            self.special,
316            Some(FieldSpecialDescriptor::DynamicSerializerArray)
317        )
318    }
319
320    pub fn is_pointer(&self) -> bool {
321        matches!(self.special, Some(FieldSpecialDescriptor::Pointer))
322    }
323
324    pub fn dynamic_array_inner_metadata(&self) -> FieldMetadata {
325        match &self.special {
326            Some(FieldSpecialDescriptor::DynamicArray { inner_decoder }) => FieldMetadata {
327                decoder: inner_decoder.clone(),
328                special: None,
329            },
330            _ => FieldMetadata::default(),
331        }
332    }
333}
334
335/// Build a float decoder based on field properties.
336fn build_f32_decoder(
337    var_name: &str,
338    bit_count: Option<i32>,
339    low_value: Option<f32>,
340    high_value: Option<f32>,
341    encode_flags: Option<i32>,
342    var_encoder: Option<&str>,
343) -> Decoder {
344    // Simulation time special case
345    if var_name == "m_flSimulationTime" || var_name == "m_flAnimTime" {
346        return Decoder::F32SimulationTime;
347    }
348
349    // Check var_encoder
350    if let Some(encoder) = var_encoder {
351        match encoder {
352            "coord" => return Decoder::F32Coord,
353            "normal" => return Decoder::F32Normal,
354            _ => {}
355        }
356    }
357
358    let bc = bit_count.unwrap_or(0);
359    if bc == 0 || bc == 32 {
360        return Decoder::F32NoScale;
361    }
362
363    // Quantized float
364    match QuantizedFloat::new(
365        bc,
366        encode_flags.unwrap_or(0),
367        low_value.unwrap_or(0.0),
368        high_value.unwrap_or(0.0),
369    ) {
370        Ok(qf) => Decoder::F32Quantized(qf),
371        Err(_) => Decoder::F32NoScale,
372    }
373}
374
375/// Determine the [`FieldMetadata`] (decoder + special descriptor) for a serializer field.
376///
377/// This is the main dispatch function that maps Source 2 network field descriptions
378/// to the correct binary decoder. It inspects the type string, field name, and
379/// encoder hints to choose the appropriate [`Decoder`] variant and, when the field
380/// represents an array or pointer, attaches a [`FieldSpecialDescriptor`].
381///
382/// # Parameters
383///
384/// * `var_type` — the Source 2 type name (e.g. `"int32"`, `"Vector"`, `"CBaseEntity*"`,
385///   `"CNetworkUtlVectorBase< float32 >"`). Pointer suffix (`*`), array brackets
386///   (`[N]`), and generic angle brackets (`< T >`) are all handled.
387/// * `var_name` — the field name (e.g. `"m_flSimulationTime"`). Certain names trigger
388///   special-case decoders.
389/// * `bit_count` — optional bit width from the serializer; used for quantized floats
390///   and `QAngle` variants.
391/// * `low_value` / `high_value` — optional range bounds for quantized float encoding.
392/// * `encode_flags` — optional flags passed to [`QuantizedFloat`] when constructing a
393///   quantized decoder.
394/// * `var_encoder` — optional encoder hint string (e.g. `"coord"`, `"normal"`,
395///   `"qangle_pitch_yaw"`, `"fixed64"`).
396/// * `has_field_serializer` — `true` when the field carries a nested serializer,
397///   which upgrades dynamic arrays to [`FieldSpecialDescriptor::DynamicSerializerArray`].
398#[allow(clippy::too_many_arguments)]
399pub fn get_field_metadata(
400    var_type: &str,
401    var_name: &str,
402    bit_count: Option<i32>,
403    low_value: Option<f32>,
404    high_value: Option<f32>,
405    encode_flags: Option<i32>,
406    var_encoder: Option<&str>,
407    has_field_serializer: bool,
408) -> FieldMetadata {
409    // Parse the type to determine category
410    let trimmed = var_type.trim();
411
412    // Pointer types
413    if trimmed.ends_with('*') {
414        return FieldMetadata {
415            decoder: Decoder::Bool,
416            special: Some(FieldSpecialDescriptor::Pointer),
417        };
418    }
419
420    // Array types: type[length]
421    if let Some(bracket_pos) = trimmed.find('[')
422        && trimmed.ends_with(']')
423    {
424        let base = trimmed[..bracket_pos].trim();
425        let len_str = trimmed[bracket_pos + 1..trimmed.len() - 1].trim();
426
427        // char[N] is a string
428        if base == "char" {
429            return FieldMetadata {
430                decoder: Decoder::String,
431                special: None,
432            };
433        }
434
435        let length = len_str.parse::<usize>().unwrap_or(match len_str {
436            "MAX_ABILITY_DRAFT_ABILITIES" => 48,
437            "DOTA_ABILITY_DRAFT_HEROES_PER_GAME" => 10,
438            _ => 64,
439        });
440
441        let inner = get_field_metadata(
442            base,
443            var_name,
444            bit_count,
445            low_value,
446            high_value,
447            encode_flags,
448            var_encoder,
449            has_field_serializer,
450        );
451
452        return FieldMetadata {
453            decoder: inner.decoder,
454            special: Some(FieldSpecialDescriptor::FixedArray { length }),
455        };
456    }
457
458    // Generic/template types: CNetworkUtlVectorBase< T >
459    if let Some(angle_pos) = trimmed.find('<')
460        && let Some(close_pos) = trimmed.rfind('>')
461    {
462        let base = trimmed[..angle_pos].trim();
463        let inner_type = trimmed[angle_pos + 1..close_pos].trim();
464
465        let is_vector_base = matches!(
466            base,
467            "CNetworkUtlVectorBase" | "CUtlVectorEmbeddedNetworkVar" | "CUtlVector"
468        );
469
470        if is_vector_base {
471            if has_field_serializer {
472                return FieldMetadata {
473                    decoder: Decoder::U64,
474                    special: Some(FieldSpecialDescriptor::DynamicSerializerArray),
475                };
476            }
477
478            let inner = get_field_metadata(
479                inner_type,
480                var_name,
481                bit_count,
482                low_value,
483                high_value,
484                encode_flags,
485                var_encoder,
486                has_field_serializer,
487            );
488
489            return FieldMetadata {
490                decoder: Decoder::U64,
491                special: Some(FieldSpecialDescriptor::DynamicArray {
492                    inner_decoder: inner.decoder,
493                }),
494            };
495        }
496
497        // For non-vector templates, decode as the base type
498        return get_field_metadata(
499            base,
500            var_name,
501            bit_count,
502            low_value,
503            high_value,
504            encode_flags,
505            var_encoder,
506            has_field_serializer,
507        );
508    }
509
510    // Identify the base type
511    match trimmed {
512        // Primitives
513        "int8" | "int16" | "int32" | "int64" => FieldMetadata {
514            decoder: Decoder::I64,
515            special: None,
516        },
517
518        "bool" => FieldMetadata {
519            decoder: Decoder::Bool,
520            special: None,
521        },
522
523        "float32" | "CNetworkedQuantizedFloat" | "GameTime_t" => {
524            let decoder = build_f32_decoder(
525                var_name,
526                bit_count,
527                low_value,
528                high_value,
529                encode_flags,
530                var_encoder,
531            );
532            FieldMetadata {
533                decoder,
534                special: None,
535            }
536        }
537
538        // Pointer types: entity body/component handles transmitted as a
539        // single boolean "present" flag on the wire.
540        "CBodyComponentDCGBaseAnimating"
541        | "CBodyComponentBaseAnimating"
542        | "CBodyComponentBaseAnimatingOverlay"
543        | "CBodyComponentBaseModelEntity"
544        | "CBodyComponent"
545        | "CBodyComponentSkeletonInstance"
546        | "CBodyComponentPoint"
547        | "CLightComponent"
548        | "CRenderComponent"
549        | "C_BodyComponentBaseAnimating"
550        | "C_BodyComponentBaseAnimatingOverlay"
551        | "CPhysicsComponent" => FieldMetadata {
552            decoder: Decoder::Bool,
553            special: Some(FieldSpecialDescriptor::Pointer),
554        },
555
556        // String types
557        "CUtlSymbolLarge" | "CUtlString" => FieldMetadata {
558            decoder: Decoder::String,
559            special: None,
560        },
561
562        // Angle type
563        "QAngle" => {
564            let bc = bit_count.unwrap_or(0) as usize;
565            let decoder = if let Some(encoder) = var_encoder {
566                match encoder {
567                    "qangle_pitch_yaw" => Decoder::QAnglePitchYaw { bit_count: bc },
568                    "qangle_precise" => Decoder::QAnglePrecise,
569                    _ => {
570                        if bc == 0 {
571                            Decoder::QAngleCoord
572                        } else {
573                            Decoder::QAngleBitCount { bit_count: bc }
574                        }
575                    }
576                }
577            } else if bc == 0 {
578                Decoder::QAngleCoord
579            } else {
580                Decoder::QAngleBitCount { bit_count: bc }
581            };
582            FieldMetadata {
583                decoder,
584                special: None,
585            }
586        }
587
588        // Vector types
589        "Vector" | "VectorWS" => {
590            if var_encoder == Some("normal") {
591                FieldMetadata {
592                    decoder: Decoder::Vector3Normal,
593                    special: None,
594                }
595            } else {
596                let inner = build_f32_decoder(
597                    var_name,
598                    bit_count,
599                    low_value,
600                    high_value,
601                    encode_flags,
602                    var_encoder,
603                );
604                FieldMetadata {
605                    decoder: Decoder::Vector3(Box::new(inner)),
606                    special: None,
607                }
608            }
609        }
610
611        "Vector2D" => {
612            let inner = build_f32_decoder(
613                var_name,
614                bit_count,
615                low_value,
616                high_value,
617                encode_flags,
618                var_encoder,
619            );
620            FieldMetadata {
621                decoder: Decoder::Vector2(Box::new(inner)),
622                special: None,
623            }
624        }
625
626        "Vector4D" => {
627            let inner = build_f32_decoder(
628                var_name,
629                bit_count,
630                low_value,
631                high_value,
632                encode_flags,
633                var_encoder,
634            );
635            FieldMetadata {
636                decoder: Decoder::Vector4(Box::new(inner)),
637                special: None,
638            }
639        }
640
641        // Dynamic serializer arrays (special cases)
642        "m_SpeechBubbles" | "DOTA_CombatLogQueryProgress" => FieldMetadata {
643            decoder: Decoder::U64,
644            special: Some(FieldSpecialDescriptor::DynamicSerializerArray),
645        },
646
647        // Default: unsigned integer
648        _ => {
649            let decoder = if var_encoder == Some("fixed64") {
650                Decoder::U64Fixed64
651            } else {
652                Decoder::U64
653            };
654            FieldMetadata {
655                decoder,
656                special: None,
657            }
658        }
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::io::BitReader;
666
667    fn meta(var_type: &str, var_name: &str) -> FieldMetadata {
668        get_field_metadata(var_type, var_name, None, None, None, None, None, false)
669    }
670
671    #[allow(clippy::too_many_arguments)]
672    fn meta_full(
673        var_type: &str,
674        var_name: &str,
675        bit_count: Option<i32>,
676        low: Option<f32>,
677        high: Option<f32>,
678        encode_flags: Option<i32>,
679        var_encoder: Option<&str>,
680        has_fs: bool,
681    ) -> FieldMetadata {
682        get_field_metadata(
683            var_type,
684            var_name,
685            bit_count,
686            low,
687            high,
688            encode_flags,
689            var_encoder,
690            has_fs,
691        )
692    }
693
694    // ── get_field_metadata dispatch ──
695
696    #[test]
697    fn pointer_type() {
698        let m = meta("CBaseEntity*", "m_hOwner");
699        assert!(matches!(m.decoder, Decoder::Bool));
700        assert!(m.is_pointer());
701    }
702
703    #[test]
704    fn bool_type() {
705        let m = meta("bool", "m_bActive");
706        assert!(matches!(m.decoder, Decoder::Bool));
707        assert!(m.special.is_none());
708    }
709
710    #[test]
711    fn int32_type() {
712        let m = meta("int32", "m_iHealth");
713        assert!(matches!(m.decoder, Decoder::I64));
714    }
715
716    #[test]
717    fn float32_no_scale() {
718        let m = meta("float32", "m_flValue");
719        assert!(matches!(m.decoder, Decoder::F32NoScale));
720    }
721
722    #[test]
723    fn simulation_time() {
724        let m = meta("float32", "m_flSimulationTime");
725        assert!(matches!(m.decoder, Decoder::F32SimulationTime));
726    }
727
728    #[test]
729    fn coord_encoder() {
730        let m = meta_full(
731            "float32",
732            "m_x",
733            None,
734            None,
735            None,
736            None,
737            Some("coord"),
738            false,
739        );
740        assert!(matches!(m.decoder, Decoder::F32Coord));
741    }
742
743    #[test]
744    fn quantized_float() {
745        let m = meta_full(
746            "float32",
747            "m_val",
748            Some(8),
749            Some(0.0),
750            Some(255.0),
751            None,
752            None,
753            false,
754        );
755        assert!(matches!(m.decoder, Decoder::F32Quantized(_)));
756    }
757
758    #[test]
759    fn string_utl_symbol() {
760        let m = meta("CUtlSymbolLarge", "m_iszName");
761        assert!(matches!(m.decoder, Decoder::String));
762    }
763
764    #[test]
765    fn char_array_is_string() {
766        let m = meta("char[256]", "m_szName");
767        assert!(matches!(m.decoder, Decoder::String));
768        assert!(!m.is_fixed_array());
769    }
770
771    #[test]
772    fn int32_array_is_fixed_array() {
773        let m = meta("int32[4]", "m_values");
774        assert!(m.is_fixed_array());
775        assert_eq!(m.fixed_array_length(), Some(4));
776    }
777
778    #[test]
779    fn dynamic_array_without_serializer() {
780        let m = meta("CNetworkUtlVectorBase< int32 >", "m_items");
781        assert!(m.is_dynamic_array());
782        assert!(!m.is_dynamic_serializer_array());
783    }
784
785    #[test]
786    fn dynamic_serializer_array() {
787        let m = meta_full(
788            "CNetworkUtlVectorBase< SomeType >",
789            "m_items",
790            None,
791            None,
792            None,
793            None,
794            None,
795            true,
796        );
797        assert!(m.is_dynamic_serializer_array());
798    }
799
800    #[test]
801    fn qangle_no_encoder_no_bits() {
802        let m = meta("QAngle", "m_angle");
803        assert!(matches!(m.decoder, Decoder::QAngleCoord));
804    }
805
806    #[test]
807    fn qangle_with_bitcount() {
808        let m = meta_full("QAngle", "m_angle", Some(16), None, None, None, None, false);
809        assert!(matches!(
810            m.decoder,
811            Decoder::QAngleBitCount { bit_count: 16 }
812        ));
813    }
814
815    #[test]
816    fn qangle_pitch_yaw() {
817        let m = meta_full(
818            "QAngle",
819            "m_angle",
820            Some(10),
821            None,
822            None,
823            None,
824            Some("qangle_pitch_yaw"),
825            false,
826        );
827        assert!(matches!(
828            m.decoder,
829            Decoder::QAnglePitchYaw { bit_count: 10 }
830        ));
831    }
832
833    // ── Decoder::decode with BitReader ──
834
835    #[test]
836    fn decode_bool_from_1bit() {
837        let data = [0x01];
838        let mut br = BitReader::new(&data);
839        let mut ctx = FieldDecodeContext::new(1.0 / 64.0);
840        let val = Decoder::Bool.decode(&mut ctx, &mut br).unwrap();
841        assert!(matches!(val, FieldValue::Bool(true)));
842    }
843
844    #[test]
845    fn decode_f32_no_scale() {
846        let bytes = 1.5f32.to_le_bytes();
847        let mut br = BitReader::new(&bytes);
848        let mut ctx = FieldDecodeContext::new(1.0 / 64.0);
849        let val = Decoder::F32NoScale.decode(&mut ctx, &mut br).unwrap();
850        if let FieldValue::F32(f) = val {
851            assert!((f - 1.5).abs() < f32::EPSILON);
852        } else {
853            panic!("expected F32");
854        }
855    }
856
857    #[test]
858    fn decode_string_null_terminated() {
859        let data = b"hello\0";
860        let mut br = BitReader::new(data);
861        let mut ctx = FieldDecodeContext::new(1.0 / 64.0);
862        let val = Decoder::String.decode(&mut ctx, &mut br).unwrap();
863        if let FieldValue::String(s) = val {
864            assert_eq!(&s, b"hello");
865        } else {
866            panic!("expected String");
867        }
868    }
869
870    // ── FieldMetadata helpers ──
871
872    #[test]
873    fn field_metadata_helpers() {
874        let dyn_arr = FieldMetadata {
875            decoder: Decoder::U64,
876            special: Some(FieldSpecialDescriptor::DynamicArray {
877                inner_decoder: Decoder::I64,
878            }),
879        };
880        assert!(dyn_arr.is_dynamic_array());
881        assert!(!dyn_arr.is_fixed_array());
882        assert!(!dyn_arr.is_pointer());
883
884        let fixed = FieldMetadata {
885            decoder: Decoder::I64,
886            special: Some(FieldSpecialDescriptor::FixedArray { length: 8 }),
887        };
888        assert!(fixed.is_fixed_array());
889        assert_eq!(fixed.fixed_array_length(), Some(8));
890
891        let ptr = FieldMetadata {
892            decoder: Decoder::Bool,
893            special: Some(FieldSpecialDescriptor::Pointer),
894        };
895        assert!(ptr.is_pointer());
896    }
897}