Skip to main content

boon/entity/
entities.rs

1use std::collections::HashSet;
2
3use rustc_hash::FxHashMap;
4
5use crate::error::{Error, Result};
6use crate::io::BitReader;
7
8use super::class_info::ClassInfo;
9use super::field_decoder::FieldDecodeContext;
10use super::field_path::{self, FieldPath};
11use super::field_value::FieldValue;
12use super::serializers::{Serializer, SerializerContainer};
13use super::string_tables::StringTableContainer;
14
15use boon_proto::proto::CsvcMsgPacketEntities;
16
17// Entity handle bit layout (32 bits total):
18// - Lower 15 bits: entity index (up to 2^14 edicts + 1 bit)
19// - Upper 17 bits: serial number (disambiguates reused indices)
20const MAX_EDICT_BITS: u32 = 14;
21const NUM_ENT_ENTRY_BITS: u32 = MAX_EDICT_BITS + 1;
22const NUM_SERIAL_NUM_BITS: u32 = 32 - NUM_ENT_ENTRY_BITS;
23
24/// Delta header values (2-bit codes) indicating entity state changes.
25const DELTA_UPDATE: u8 = 0b00;
26const DELTA_CREATE: u8 = 0b10;
27const DELTA_LEAVE: u8 = 0b01;
28const DELTA_DELETE: u8 = 0b11;
29
30/// A single entity with its class, fields, and current state.
31#[derive(Debug, Clone)]
32pub struct Entity {
33    /// Slot index in the entity array (0–16383).
34    pub index: i32,
35    /// Serial number for this slot (increments on reuse).
36    pub serial: u32,
37    /// Numeric class ID (indexes into [`ClassInfo`]).
38    pub class_id: i32,
39    /// Network class name (e.g. `"CCitadelPlayerController"`).
40    pub class_name: String,
41    /// Current field values, keyed by packed field path keys.
42    pub fields: FxHashMap<u64, FieldValue>,
43}
44
45impl Entity {
46    fn new(index: i32, class_id: i32, class_name: String) -> Self {
47        Self {
48            index,
49            serial: 0,
50            class_id,
51            class_name,
52            fields: FxHashMap::default(),
53        }
54    }
55
56    /// Apply field path deltas from a bit reader using the given serializer.
57    #[allow(clippy::needless_range_loop)]
58    fn apply_update(
59        &mut self,
60        br: &mut BitReader,
61        serializer: &Serializer,
62        ctx: &mut FieldDecodeContext,
63        fp_buf: &mut Vec<FieldPath>,
64    ) -> Result<()> {
65        field_path::read_field_paths(br, fp_buf)?;
66
67        for fp_idx in 0..fp_buf.len() {
68            // Walk the serializer hierarchy to find the decoder (same as skip_update)
69            let fp_last = fp_buf[fp_idx].last;
70            let mut field = &serializer.fields[fp_buf[fp_idx].get(0)];
71
72            for i in 1..=fp_last {
73                let idx = fp_buf[fp_idx].get(i);
74                if field.is_dynamic_array() {
75                    if let Some(ref fs) = field.field_serializer {
76                        field = &fs.fields[0];
77                    }
78                } else if let Some(ref fs) = field.field_serializer {
79                    field = &fs.fields[idx];
80                } else {
81                    break;
82                }
83            }
84
85            let key = fp_buf[fp_idx].pack();
86            let value = field
87                .metadata
88                .decoder
89                .decode(ctx, br)
90                .map_err(|e| Error::Parse {
91                    context: format!(
92                        "field #{} key={:#x} (type: {}, decoder: {:?}, pos: {}, remaining: {}): {}",
93                        fp_idx,
94                        key,
95                        field.var_type,
96                        field.metadata.decoder,
97                        br.position(),
98                        br.bits_remaining(),
99                        e
100                    ),
101                })?;
102            self.fields.insert(key, value);
103        }
104
105        Ok(())
106    }
107
108    /// Skip field updates - reads the data to advance the bit reader but doesn't store anything.
109    /// This avoids allocations and FxHashMap insertions for entities we don't care about.
110    #[allow(clippy::needless_range_loop)]
111    fn skip_update(
112        br: &mut BitReader,
113        serializer: &Serializer,
114        ctx: &mut FieldDecodeContext,
115        fp_buf: &mut Vec<FieldPath>,
116    ) -> Result<()> {
117        field_path::read_field_paths(br, fp_buf)?;
118
119        for fp_idx in 0..fp_buf.len() {
120            // Walk the serializer hierarchy to find the decoder
121            let fp_last = fp_buf[fp_idx].last;
122            let mut field = &serializer.fields[fp_buf[fp_idx].get(0)];
123
124            for i in 1..=fp_last {
125                let idx = fp_buf[fp_idx].get(i);
126                if field.is_dynamic_array() {
127                    if let Some(ref fs) = field.field_serializer {
128                        field = &fs.fields[0];
129                    }
130                } else if let Some(ref fs) = field.field_serializer {
131                    field = &fs.fields[idx];
132                } else {
133                    break;
134                }
135            }
136
137            // Skip the value - just advances the bit reader without decoding
138            field.metadata.decoder.skip(ctx, br)?;
139        }
140
141        Ok(())
142    }
143
144    /// Look up a field by its dotted name string using the serializer to resolve the key.
145    pub fn get_by_name(&self, path: &str, serializer: &Serializer) -> Option<&FieldValue> {
146        let key = serializer.resolve_field_key(path)?;
147        self.fields.get(&key)
148    }
149}
150
151/// Container managing all active entities.
152#[derive(Default)]
153pub struct EntityContainer {
154    pub entities: FxHashMap<i32, Entity>,
155    /// Tracks class_id for entities we're not fully tracking (for filtered parsing).
156    /// This lets us skip updates properly by knowing which serializer to use.
157    skipped_entity_classes: FxHashMap<i32, i32>,
158}
159
160impl EntityContainer {
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Handle a CSVCMsg_PacketEntities message.
166    pub fn handle_packet_entities(
167        &mut self,
168        msg: CsvcMsgPacketEntities,
169        class_info: &ClassInfo,
170        serializers: &SerializerContainer,
171        string_tables: &StringTableContainer,
172        field_decode_ctx: &mut FieldDecodeContext,
173        fp_buf: &mut Vec<FieldPath>,
174    ) -> Result<()> {
175        let entity_data = msg.entity_data.unwrap_or_default();
176        let mut br = BitReader::new(&entity_data);
177
178        let mut entity_index: i32 = -1;
179
180        for _ in 0..msg.updated_entries.unwrap_or(0) {
181            entity_index += br.read_ubitvar()? as i32 + 1;
182
183            // Read delta header (2 bits)
184            let dh = br.read_bits(2)? as u8;
185
186            match dh {
187                DELTA_CREATE => {
188                    self.handle_create(
189                        entity_index,
190                        &mut br,
191                        class_info,
192                        serializers,
193                        string_tables,
194                        field_decode_ctx,
195                        fp_buf,
196                    )
197                    .map_err(|e| Error::Parse {
198                        context: format!("entity create #{}: {}", entity_index, e),
199                    })?;
200                }
201                DELTA_UPDATE => {
202                    self.handle_update(
203                        entity_index,
204                        &mut br,
205                        class_info,
206                        serializers,
207                        field_decode_ctx,
208                        fp_buf,
209                    )
210                    .map_err(|e| Error::Parse {
211                        context: format!(
212                            "entity update #{} (class: {:?}): {}",
213                            entity_index,
214                            self.entities.get(&entity_index).map(|e| &e.class_name),
215                            e
216                        ),
217                    })?;
218                }
219                DELTA_DELETE | DELTA_LEAVE => {
220                    self.entities.remove(&entity_index);
221                }
222                _ => {}
223            }
224        }
225
226        Ok(())
227    }
228
229    /// Handle a CSVCMsg_PacketEntities message, only tracking specified entity classes.
230    /// Entities not in the filter are parsed (to advance the bit reader) but not stored.
231    #[allow(clippy::too_many_arguments)]
232    pub fn handle_packet_entities_filtered(
233        &mut self,
234        msg: CsvcMsgPacketEntities,
235        class_info: &ClassInfo,
236        serializers: &SerializerContainer,
237        string_tables: &StringTableContainer,
238        field_decode_ctx: &mut FieldDecodeContext,
239        class_filter: &HashSet<&str>,
240        fp_buf: &mut Vec<FieldPath>,
241    ) -> Result<()> {
242        let entity_data = msg.entity_data.unwrap_or_default();
243        let mut br = BitReader::new(&entity_data);
244
245        let mut entity_index: i32 = -1;
246
247        for _ in 0..msg.updated_entries.unwrap_or(0) {
248            entity_index += br.read_ubitvar()? as i32 + 1;
249
250            // Read delta header (2 bits)
251            let dh = br.read_bits(2)? as u8;
252
253            match dh {
254                DELTA_CREATE => {
255                    self.handle_create_filtered(
256                        entity_index,
257                        &mut br,
258                        class_info,
259                        serializers,
260                        string_tables,
261                        field_decode_ctx,
262                        class_filter,
263                        fp_buf,
264                    )?;
265                }
266                DELTA_UPDATE => {
267                    self.handle_update_filtered(
268                        entity_index,
269                        &mut br,
270                        class_info,
271                        serializers,
272                        field_decode_ctx,
273                        class_filter,
274                        fp_buf,
275                    )?;
276                }
277                DELTA_DELETE | DELTA_LEAVE => {
278                    self.entities.remove(&entity_index);
279                    self.skipped_entity_classes.remove(&entity_index);
280                }
281                _ => {}
282            }
283        }
284
285        Ok(())
286    }
287
288    #[allow(clippy::too_many_arguments)]
289    fn handle_create(
290        &mut self,
291        index: i32,
292        br: &mut BitReader,
293        class_info: &ClassInfo,
294        serializers: &SerializerContainer,
295        string_tables: &StringTableContainer,
296        field_decode_ctx: &mut FieldDecodeContext,
297        fp_buf: &mut Vec<FieldPath>,
298    ) -> Result<()> {
299        let class_id = br.read_bits(class_info.bits)? as i32;
300        let _serial = br.read_bits(NUM_SERIAL_NUM_BITS as usize)?;
301        let _unknown = br.read_uvarint32()?;
302
303        let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
304            context: format!("unknown class_id {}", class_id),
305        })?;
306
307        let serializer =
308            serializers
309                .get(&class_entry.network_name)
310                .ok_or_else(|| Error::Parse {
311                    context: format!("no serializer for {}", class_entry.network_name),
312                })?;
313
314        let mut entity = Entity::new(index, class_id, class_entry.network_name.clone());
315
316        // Apply baseline from instancebaseline string table
317        if let Some(baseline_data) = string_tables.instance_baselines.get(&class_id) {
318            let mut baseline_br = BitReader::new(baseline_data);
319            entity
320                .apply_update(&mut baseline_br, serializer, field_decode_ctx, fp_buf)
321                .map_err(|err| Error::Parse {
322                    context: format!(
323                        "baseline for {} (class_id {}): {}",
324                        class_entry.network_name, class_id, err
325                    ),
326                })?;
327        }
328
329        // Apply create delta
330        entity
331            .apply_update(br, serializer, field_decode_ctx, fp_buf)
332            .map_err(|err| Error::Parse {
333                context: format!(
334                    "create delta for {} (class_id {}): {}",
335                    class_entry.network_name, class_id, err
336                ),
337            })?;
338        self.entities.insert(index, entity);
339
340        Ok(())
341    }
342
343    fn handle_update(
344        &mut self,
345        index: i32,
346        br: &mut BitReader,
347        _class_info: &ClassInfo,
348        serializers: &SerializerContainer,
349        field_decode_ctx: &mut FieldDecodeContext,
350        fp_buf: &mut Vec<FieldPath>,
351    ) -> Result<()> {
352        let entity = match self.entities.get_mut(&index) {
353            Some(e) => e,
354            None => {
355                return Err(Error::Parse {
356                    context: format!("tried to update non-existent entity #{}", index),
357                });
358            }
359        };
360
361        let serializer = serializers
362            .get(&entity.class_name)
363            .ok_or_else(|| Error::Parse {
364                context: format!("no serializer for {}", entity.class_name),
365            })?;
366
367        entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
368        Ok(())
369    }
370
371    #[allow(clippy::too_many_arguments)]
372    fn handle_create_filtered(
373        &mut self,
374        index: i32,
375        br: &mut BitReader,
376        class_info: &ClassInfo,
377        serializers: &SerializerContainer,
378        string_tables: &StringTableContainer,
379        field_decode_ctx: &mut FieldDecodeContext,
380        class_filter: &HashSet<&str>,
381        fp_buf: &mut Vec<FieldPath>,
382    ) -> Result<()> {
383        let class_id = br.read_bits(class_info.bits)? as i32;
384        let _serial = br.read_bits(NUM_SERIAL_NUM_BITS as usize)?;
385        let _unknown = br.read_uvarint32()?;
386
387        let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
388            context: format!("unknown class_id {}", class_id),
389        })?;
390
391        let serializer =
392            serializers
393                .get(&class_entry.network_name)
394                .ok_or_else(|| Error::Parse {
395                    context: format!("no serializer for {}", class_entry.network_name),
396                })?;
397
398        // Check if this class is in our filter
399        if !class_filter.contains(class_entry.network_name.as_str()) {
400            // Skip this entity - just advance the bit reader
401            // But track its class_id so we can skip updates later
402            self.skipped_entity_classes.insert(index, class_id);
403            Entity::skip_update(br, serializer, field_decode_ctx, fp_buf)?;
404            return Ok(());
405        }
406
407        // Full processing for filtered entities
408        let mut entity = Entity::new(index, class_id, class_entry.network_name.clone());
409
410        if let Some(baseline_data) = string_tables.instance_baselines.get(&class_id) {
411            let mut baseline_br = BitReader::new(baseline_data);
412            entity.apply_update(&mut baseline_br, serializer, field_decode_ctx, fp_buf)?;
413        }
414
415        entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
416        self.entities.insert(index, entity);
417
418        Ok(())
419    }
420
421    #[allow(clippy::too_many_arguments)]
422    fn handle_update_filtered(
423        &mut self,
424        index: i32,
425        br: &mut BitReader,
426        class_info: &ClassInfo,
427        serializers: &SerializerContainer,
428        field_decode_ctx: &mut FieldDecodeContext,
429        _class_filter: &HashSet<&str>,
430        fp_buf: &mut Vec<FieldPath>,
431    ) -> Result<()> {
432        // Check if we're tracking this entity
433        if let Some(entity) = self.entities.get_mut(&index) {
434            let serializer = serializers
435                .get(&entity.class_name)
436                .ok_or_else(|| Error::Parse {
437                    context: format!("no serializer for {}", entity.class_name),
438                })?;
439
440            entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
441            return Ok(());
442        }
443
444        // Entity is not tracked - check if we know its class from skipped creates
445        if let Some(&class_id) = self.skipped_entity_classes.get(&index) {
446            let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
447                context: format!("unknown class_id {}", class_id),
448            })?;
449
450            let serializer =
451                serializers
452                    .get(&class_entry.network_name)
453                    .ok_or_else(|| Error::Parse {
454                        context: format!("no serializer for {}", class_entry.network_name),
455                    })?;
456
457            // Skip this update
458            Entity::skip_update(br, serializer, field_decode_ctx, fp_buf)?;
459        }
460
461        // If we don't know about this entity at all, it was created before filtering started
462        // This shouldn't happen if we start filtering from the beginning
463        Ok(())
464    }
465
466    /// Look up an entity by its slot index.
467    pub fn get(&self, index: i32) -> Option<&Entity> {
468        self.entities.get(&index)
469    }
470
471    /// Iterate over all active entities as `(index, entity)` pairs.
472    pub fn iter(&self) -> impl Iterator<Item = (&i32, &Entity)> {
473        self.entities.iter()
474    }
475
476    /// Number of currently active entities.
477    pub fn len(&self) -> usize {
478        self.entities.len()
479    }
480
481    pub fn is_empty(&self) -> bool {
482        self.entities.is_empty()
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn container_new_is_empty() {
492        let c = EntityContainer::new();
493        assert!(c.is_empty());
494        assert_eq!(c.len(), 0);
495        assert!(c.get(0).is_none());
496    }
497
498    #[test]
499    fn entity_fields_insert_and_get() {
500        let mut e = Entity::new(1, 10, "TestClass".to_string());
501        e.fields.insert(42, FieldValue::I32(100));
502        assert!(matches!(e.fields.get(&42), Some(FieldValue::I32(100))));
503    }
504
505    #[test]
506    fn container_insert_and_iter() {
507        let mut c = EntityContainer::new();
508        let e = Entity::new(5, 10, "Hero".to_string());
509        c.entities.insert(5, e);
510        assert_eq!(c.len(), 1);
511        assert!(!c.is_empty());
512        assert!(c.get(5).is_some());
513        assert_eq!(c.get(5).unwrap().class_name, "Hero");
514    }
515
516    #[test]
517    fn container_iter_yields_entries() {
518        let mut c = EntityContainer::new();
519        c.entities.insert(1, Entity::new(1, 1, "A".to_string()));
520        c.entities.insert(2, Entity::new(2, 2, "B".to_string()));
521        let keys: Vec<i32> = c.iter().map(|(&k, _)| k).collect();
522        assert_eq!(keys.len(), 2);
523    }
524
525    #[test]
526    fn entity_basic_fields() {
527        let e = Entity::new(7, 42, "NPC".to_string());
528        assert_eq!(e.index, 7);
529        assert_eq!(e.class_id, 42);
530        assert_eq!(e.class_name, "NPC");
531        assert_eq!(e.serial, 0);
532        assert!(e.fields.is_empty());
533    }
534}