Skip to main content

boon/entity/
class_info.rs

1use boon_proto::proto::CDemoClassInfo;
2
3/// A single entity class entry.
4#[derive(Debug, Clone, serde::Serialize)]
5pub struct ClassEntry {
6    pub class_id: i32,
7    pub network_name: String,
8    pub table_name: String,
9}
10
11/// Parsed class info from DEM_ClassInfo. Maps class IDs to network names.
12#[derive(Debug, Clone)]
13pub struct ClassInfo {
14    pub classes: Vec<ClassEntry>,
15    /// Number of bits needed to encode a class_id.
16    pub bits: usize,
17    /// O(1) lookup table: index is class_id, value is index into `classes`.
18    lookup: Vec<Option<usize>>,
19}
20
21impl ClassInfo {
22    /// Create an empty `ClassInfo` (used for tests / default initialization).
23    pub fn empty() -> Self {
24        ClassInfo {
25            classes: Vec::new(),
26            bits: 1,
27            lookup: Vec::new(),
28        }
29    }
30
31    /// Parse a `CDemoClassInfo` protobuf message into a [`ClassInfo`].
32    pub fn parse(cmd: CDemoClassInfo) -> Self {
33        let classes: Vec<ClassEntry> = cmd
34            .classes
35            .into_iter()
36            .map(|c| ClassEntry {
37                class_id: c.class_id.unwrap_or(0),
38                network_name: c.network_name.unwrap_or_default(),
39                table_name: c.table_name.unwrap_or_default(),
40            })
41            .collect();
42
43        // Compute the minimum number of bits needed to represent the largest
44        // class_id (ceiling of log2). The entity system reads this many bits
45        // from the bitstream when decoding a DELTA_CREATE header.
46        let max_id = classes.iter().map(|c| c.class_id).max().unwrap_or(0) as u32;
47        let bits = if max_id == 0 {
48            1
49        } else {
50            32 - max_id.leading_zeros() as usize
51        };
52
53        // Build flat lookup table indexed by class_id.
54        let mut lookup = vec![None; max_id as usize + 1];
55        for (i, c) in classes.iter().enumerate() {
56            if c.class_id >= 0 {
57                lookup[c.class_id as usize] = Some(i);
58            }
59        }
60
61        ClassInfo {
62            classes,
63            bits,
64            lookup,
65        }
66    }
67
68    /// Look up a class entry by its numeric ID. O(1).
69    pub fn by_id(&self, class_id: i32) -> Option<&ClassEntry> {
70        let idx = *self.lookup.get(class_id as usize)?.as_ref()?;
71        Some(&self.classes[idx])
72    }
73
74    /// Shorthand to get the network name for a class ID.
75    pub fn name_by_id(&self, class_id: i32) -> Option<&str> {
76        self.by_id(class_id).map(|c| c.network_name.as_str())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use boon_proto::proto::c_demo_class_info;
84
85    fn make_class_info(ids: &[(i32, &str)]) -> ClassInfo {
86        let cmd = CDemoClassInfo {
87            classes: ids
88                .iter()
89                .map(|(id, name)| c_demo_class_info::ClassT {
90                    class_id: Some(*id),
91                    network_name: Some(name.to_string()),
92                    table_name: Some(String::new()),
93                })
94                .collect(),
95        };
96        ClassInfo::parse(cmd)
97    }
98
99    #[test]
100    fn empty_classes_bits_is_1() {
101        let ci = make_class_info(&[]);
102        assert_eq!(ci.bits, 1);
103    }
104
105    #[test]
106    fn single_class_id_0_bits_is_1() {
107        let ci = make_class_info(&[(0, "A")]);
108        assert_eq!(ci.bits, 1);
109    }
110
111    #[test]
112    fn max_id_10_bits_is_4() {
113        let ci = make_class_info(&[(0, "A"), (10, "B")]);
114        assert_eq!(ci.bits, 4);
115    }
116
117    #[test]
118    fn max_id_8_bits_is_4() {
119        let ci = make_class_info(&[(8, "A")]);
120        assert_eq!(ci.bits, 4);
121    }
122
123    #[test]
124    fn max_id_255_bits_is_8() {
125        let ci = make_class_info(&[(255, "A")]);
126        assert_eq!(ci.bits, 8);
127    }
128
129    #[test]
130    fn by_id_found() {
131        let ci = make_class_info(&[(5, "Hero"), (10, "Creep")]);
132        let entry = ci.by_id(10).unwrap();
133        assert_eq!(entry.network_name, "Creep");
134    }
135
136    #[test]
137    fn by_id_not_found() {
138        let ci = make_class_info(&[(5, "Hero")]);
139        assert!(ci.by_id(99).is_none());
140    }
141
142    #[test]
143    fn name_by_id_returns_correct_name() {
144        let ci = make_class_info(&[(1, "Player"), (2, "NPC")]);
145        assert_eq!(ci.name_by_id(1), Some("Player"));
146        assert_eq!(ci.name_by_id(99), None);
147    }
148}