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, SerializerField};
13use super::string_tables::StringTableContainer;
14
15use boon_proto::proto::CsvcMsgPacketEntities;
16
17// Serial-number layout for the entity *create* delta (legacy Source CBaseHandle):
18// a create carries a 15-bit entity entry (`MAX_EDICT_BITS + 1`) followed by a
19// 17-bit serial number. This governs only the serial read in `handle_create`; it
20// is NOT the layout of a networked CHandle *field value*, whose entity index is
21// the low 14 bits — see [`ENTITY_HANDLE_INDEX_MASK`] / [`EntityContainer::get_by_handle`].
22const MAX_EDICT_BITS: u32 = 14;
23const NUM_ENT_ENTRY_BITS: u32 = MAX_EDICT_BITS + 1;
24const NUM_SERIAL_NUM_BITS: u32 = 32 - NUM_ENT_ENTRY_BITS;
25
26/// Largest valid entity-array index (0..=16383, a 14-bit edict index). Corrupt
27/// demos can drive the running delta past this; clamping to it avoids both an
28/// arithmetic overflow and a runaway slot-array allocation.
29const MAX_ENTITY_INDEX: i64 = (1i64 << MAX_EDICT_BITS) - 1;
30
31/// Mask that extracts the entity-array index from a networked `CHandle` value.
32///
33/// Source 2 packs a 14-bit edict index (entity indices run 0–16383) in the low
34/// bits of a handle with a serial number above it, so the index is recovered
35/// with this 14-bit mask. A wider mask (e.g. `0x7FFF`) leaks a serial bit into
36/// the index and resolves the wrong entity for any handle with an odd serial.
37pub const ENTITY_HANDLE_INDEX_MASK: u32 = 0x3FFF;
38
39/// Sentinel value for a `CHandle` field that points at no entity.
40///
41/// Protobuf handle fields (e.g. `modifier.parent`) are optional; callers
42/// substitute this when the field is absent and skip handles equal to it.
43pub const INVALID_ENTITY_HANDLE: u32 = 0x00FF_FFFF;
44
45/// Resolve a protobuf-style optional `CHandle` to an entity-array index.
46///
47/// Returns `None` when the handle is absent (the protobuf field was not set)
48/// or holds the [`INVALID_ENTITY_HANDLE`] sentinel; otherwise applies
49/// [`ENTITY_HANDLE_INDEX_MASK`] and returns the entity-array index. Use this
50/// for `Option<u32>` fields on `CitadelUserMessage`s such as `modifier.parent`
51/// or `msg.player`; it pairs naturally with `let-else`:
52///
53/// ```ignore
54/// let Some(parent_idx) = boon::protobuf_handle_index(modifier.parent) else { continue };
55/// ```
56pub fn protobuf_handle_index(handle: Option<u32>) -> Option<i32> {
57    handle
58        .filter(|&h| h != INVALID_ENTITY_HANDLE)
59        .map(|h| (h & ENTITY_HANDLE_INDEX_MASK) as i32)
60}
61
62/// Delta header values (2-bit codes) indicating entity state changes.
63const DELTA_UPDATE: u8 = 0b00;
64const DELTA_CREATE: u8 = 0b10;
65const DELTA_LEAVE: u8 = 0b01;
66const DELTA_DELETE: u8 = 0b11;
67
68/// Walk a serializer hierarchy along a field path to the addressed field.
69///
70/// Returns an [`Error::Parse`] rather than panicking when any component indexes
71/// past its serializer's fields — the tell-tale symptom of a bitstream desync (a
72/// corrupted field path addressing a nonexistent field). Surfacing it as an
73/// error keeps one bad decode from taking down the whole parse.
74fn resolve_field_path<'a>(
75    serializer: &'a Serializer,
76    fp: &FieldPath,
77) -> Result<&'a SerializerField> {
78    let oob = || Error::Parse {
79        context: format!(
80            "field path out of range for serializer '{}' ({} fields)",
81            serializer.name,
82            serializer.fields.len()
83        ),
84    };
85
86    let mut field = serializer.fields.get(fp.get(0)).ok_or_else(oob)?;
87    for i in 1..=fp.last {
88        let idx = fp.get(i);
89        if field.is_dynamic_array() {
90            match field.field_serializer.as_ref() {
91                Some(fs) => field = fs.fields.first().ok_or_else(oob)?,
92                None => break,
93            }
94        } else if let Some(fs) = field.field_serializer.as_ref() {
95            field = fs.fields.get(idx).ok_or_else(oob)?;
96        } else {
97            break;
98        }
99    }
100    Ok(field)
101}
102
103/// A single entity with its class, fields, and current state.
104#[derive(Debug, Clone)]
105pub struct Entity {
106    /// Slot index in the entity array (0–16383).
107    pub index: i32,
108    /// Serial number for this slot (increments on reuse).
109    pub serial: u32,
110    /// Numeric class ID (indexes into [`ClassInfo`]).
111    pub class_id: i32,
112    /// Network class name (e.g. `"CCitadelPlayerController"`).
113    pub class_name: String,
114    /// Current field values, keyed by packed field path keys.
115    pub fields: FxHashMap<u64, FieldValue>,
116}
117
118impl Entity {
119    fn new(index: i32, class_id: i32, class_name: String) -> Self {
120        Self {
121            index,
122            serial: 0,
123            class_id,
124            class_name,
125            fields: FxHashMap::default(),
126        }
127    }
128
129    /// Apply field path deltas from a bit reader using the given serializer.
130    #[allow(clippy::needless_range_loop)]
131    fn apply_update(
132        &mut self,
133        br: &mut BitReader,
134        serializer: &Serializer,
135        ctx: &mut FieldDecodeContext,
136        fp_buf: &mut Vec<FieldPath>,
137    ) -> Result<()> {
138        field_path::read_field_paths(br, fp_buf)?;
139
140        for fp_idx in 0..fp_buf.len() {
141            let field = resolve_field_path(serializer, &fp_buf[fp_idx])?;
142
143            let key = fp_buf[fp_idx].pack();
144            let value = field
145                .metadata
146                .decoder
147                .decode(ctx, br)
148                .map_err(|e| Error::Parse {
149                    context: format!(
150                        "field #{} key={:#x} (type: {}, decoder: {:?}, pos: {}, remaining: {}): {}",
151                        fp_idx,
152                        key,
153                        field.var_type,
154                        field.metadata.decoder,
155                        br.position(),
156                        br.bits_remaining(),
157                        e
158                    ),
159                })?;
160            self.fields.insert(key, value);
161        }
162
163        Ok(())
164    }
165
166    /// Skip field updates - reads the data to advance the bit reader but doesn't store anything.
167    /// This avoids allocations and FxHashMap insertions for entities we don't care about.
168    #[allow(clippy::needless_range_loop)]
169    fn skip_update(
170        br: &mut BitReader,
171        serializer: &Serializer,
172        ctx: &mut FieldDecodeContext,
173        fp_buf: &mut Vec<FieldPath>,
174    ) -> Result<()> {
175        field_path::read_field_paths(br, fp_buf)?;
176
177        for fp_idx in 0..fp_buf.len() {
178            let field = resolve_field_path(serializer, &fp_buf[fp_idx])?;
179
180            // Skip the value - just advances the bit reader without decoding
181            field.metadata.decoder.skip(ctx, br)?;
182        }
183
184        Ok(())
185    }
186
187    /// Look up a field by its dotted name string using the serializer to resolve the key.
188    pub fn get_by_name(&self, path: &str, serializer: &Serializer) -> Option<&FieldValue> {
189        let key = serializer.resolve_field_key(path)?;
190        self.fields.get(&key)
191    }
192
193    // ── Typed field accessors ──
194    //
195    // Each takes a field key pre-resolved with [`Serializer::resolve_field_key`]
196    // (so it can be resolved once and reused across ticks) and reads the field
197    // leniently: a missing key, an absent field, or a value of a different type
198    // all yield the type's default rather than an error. Integer variants accept
199    // any of the four integer encodings, since the network type is not always
200    // known ahead of time.
201
202    /// Read a field as `i64`, returning `0` when absent or non-integer.
203    pub fn get_i64(&self, key: Option<u64>) -> i64 {
204        key.and_then(|k| self.fields.get(&k))
205            .and_then(|v| match v {
206                FieldValue::U32(n) => Some(*n as i64),
207                FieldValue::U64(n) => Some(*n as i64),
208                FieldValue::I32(n) => Some(*n as i64),
209                FieldValue::I64(n) => Some(*n),
210                _ => None,
211            })
212            .unwrap_or(0)
213    }
214
215    /// Read a field as `u32`, returning `0` when absent or non-integer.
216    pub fn get_u32(&self, key: Option<u64>) -> u32 {
217        key.and_then(|k| self.fields.get(&k))
218            .and_then(|v| match v {
219                FieldValue::U32(n) => Some(*n),
220                FieldValue::U64(n) => Some(*n as u32),
221                FieldValue::I32(n) => Some(*n as u32),
222                FieldValue::I64(n) => Some(*n as u32),
223                _ => None,
224            })
225            .unwrap_or(0)
226    }
227
228    /// Read a field as `f32`, returning `0.0` when absent or non-float.
229    pub fn get_f32(&self, key: Option<u64>) -> f32 {
230        key.and_then(|k| self.fields.get(&k))
231            .and_then(|v| match v {
232                FieldValue::F32(f) => Some(*f),
233                _ => None,
234            })
235            .unwrap_or(0.0)
236    }
237
238    /// Read a field as `bool`, returning `false` when absent or non-bool.
239    pub fn get_bool(&self, key: Option<u64>) -> bool {
240        key.and_then(|k| self.fields.get(&k))
241            .and_then(|v| match v {
242                FieldValue::Bool(b) => Some(*b),
243                _ => None,
244            })
245            .unwrap_or(false)
246    }
247
248    /// Read a field as a `QAngle`, returning `[0.0; 3]` when absent or non-angle.
249    pub fn get_qangle(&self, key: Option<u64>) -> [f32; 3] {
250        key.and_then(|k| self.fields.get(&k))
251            .and_then(|v| match v {
252                FieldValue::QAngle(a) => Some(*a),
253                _ => None,
254            })
255            .unwrap_or([0.0; 3])
256    }
257
258    /// Combine cell + in-cell offset fields into a world-coordinate `[x, y, z]`.
259    ///
260    /// Source 2 splits each axis of an entity's position across two networked
261    /// fields — an integer cell index (e.g. `m_cellX`) and a quantized offset
262    /// inside that cell (e.g. `m_vecOrigin.m_vecX`). Pass the resolved keys
263    /// for both halves and this returns the full world position in Hammer
264    /// units via [`cell_to_world`](crate::position::cell_to_world). Reading
265    /// the offset alone gives a sawtooth that resets every cell boundary, not
266    /// a usable coordinate.
267    ///
268    /// Cell keys with no resolved field decode as cell `0`, which means the
269    /// result is shifted into a single cell-grid quadrant rather than
270    /// returning a sentinel — verify the keys before relying on it.
271    pub fn world_position(
272        &self,
273        cell_keys: [Option<u64>; 3],
274        offset_keys: [Option<u64>; 3],
275    ) -> [f32; 3] {
276        let cell = [
277            self.get_i64(cell_keys[0]) as i32,
278            self.get_i64(cell_keys[1]) as i32,
279            self.get_i64(cell_keys[2]) as i32,
280        ];
281        let offset = [
282            self.get_f32(offset_keys[0]),
283            self.get_f32(offset_keys[1]),
284            self.get_f32(offset_keys[2]),
285        ];
286        [
287            crate::position::cell_to_world(cell[0], offset[0]),
288            crate::position::cell_to_world(cell[1], offset[1]),
289            crate::position::cell_to_world(cell[2], offset[2]),
290        ]
291    }
292
293    /// Read a raw `CHandle` field as `u32`, if present.
294    ///
295    /// Pass the result to [`EntityContainer::get_by_handle`] to follow the handle
296    /// to its entity; that helper owns the index mask so callers never decode it
297    /// by hand.
298    pub fn get_handle(&self, key: Option<u64>) -> Option<u32> {
299        key.and_then(|k| self.fields.get(&k)).and_then(|v| match v {
300            FieldValue::U32(n) => Some(*n),
301            FieldValue::U64(n) => Some(*n as u32),
302            FieldValue::I32(n) => Some(*n as u32),
303            FieldValue::I64(n) => Some(*n as u32),
304            _ => None,
305        })
306    }
307}
308
309/// Container managing all active entities.
310#[derive(Default)]
311pub struct EntityContainer {
312    /// Active entities, indexed **directly by entity index** — a dense slot
313    /// array rather than a hash map, so every `get` / `entity_mut` /
314    /// `get_by_handle` (which fire on essentially every entity update) is a
315    /// bounds-checked index instead of a hash lookup. Indices are `0..=16383`;
316    /// empty slots hold `None`.
317    pub entities: Vec<Option<Entity>>,
318    /// `class_id` per index for entities filtered out at create time, so a later
319    /// filtered update can pick the right serializer to advance past them. `-1`
320    /// means "no skipped entity here". Same dense-slot layout.
321    skipped_entity_classes: Vec<i32>,
322    /// Indices of tracked entities created or updated since the last
323    /// [`clear_updated`](Self::clear_updated). Accumulates across every packet in
324    /// a tick so change-only consumers can process just what changed instead of
325    /// rescanning every active entity. The parser clears it after each per-tick
326    /// callback.
327    updated: Vec<i32>,
328}
329
330impl EntityContainer {
331    pub fn new() -> Self {
332        Self::default()
333    }
334
335    /// A slotted entity, mutably (`None` if the index is out of range or empty).
336    #[inline]
337    fn entity_mut(&mut self, index: i32) -> Option<&mut Entity> {
338        usize::try_from(index)
339            .ok()
340            .and_then(|i| self.entities.get_mut(i))
341            .and_then(Option::as_mut)
342    }
343
344    /// Store an entity at its index, growing the slot array as needed.
345    #[inline]
346    fn put_entity(&mut self, index: i32, entity: Entity) {
347        let Ok(i) = usize::try_from(index) else {
348            return;
349        };
350        if i >= self.entities.len() {
351            self.entities.resize_with(i + 1, || None);
352        }
353        self.entities[i] = Some(entity);
354    }
355
356    /// Empty an entity's slot.
357    #[inline]
358    fn take_entity(&mut self, index: i32) {
359        if let Ok(i) = usize::try_from(index)
360            && let Some(slot) = self.entities.get_mut(i)
361        {
362            *slot = None;
363        }
364    }
365
366    /// The class_id of a skipped entity at `index`, if one was recorded.
367    #[inline]
368    fn skipped_class(&self, index: i32) -> Option<i32> {
369        usize::try_from(index)
370            .ok()
371            .and_then(|i| self.skipped_entity_classes.get(i))
372            .copied()
373            .filter(|&c| c >= 0)
374    }
375
376    /// Record (or clear, with `None`) the class_id of a skipped entity at `index`.
377    #[inline]
378    fn set_skipped(&mut self, index: i32, class_id: Option<i32>) {
379        let Ok(i) = usize::try_from(index) else {
380            return;
381        };
382        let value = class_id.unwrap_or(-1);
383        if i >= self.skipped_entity_classes.len() {
384            if value < 0 {
385                return; // clearing an absent slot is a no-op
386            }
387            self.skipped_entity_classes.resize(i + 1, -1);
388        }
389        self.skipped_entity_classes[i] = value;
390    }
391
392    /// Handle a CSVCMsg_PacketEntities message.
393    pub fn handle_packet_entities(
394        &mut self,
395        msg: CsvcMsgPacketEntities,
396        class_info: &ClassInfo,
397        serializers: &SerializerContainer,
398        string_tables: &StringTableContainer,
399        field_decode_ctx: &mut FieldDecodeContext,
400        fp_buf: &mut Vec<FieldPath>,
401    ) -> Result<()> {
402        let has_pvs_vis_bits = msg.has_pvs_vis_bits_deprecated.unwrap_or(0);
403        let entity_data = msg.entity_data.unwrap_or_default();
404        let mut br = BitReader::new(&entity_data);
405
406        let mut entity_index: i32 = -1;
407
408        for _ in 0..msg.updated_entries.unwrap_or(0) {
409            let next = entity_index as i64 + br.read_ubitvar()? as i64 + 1;
410            if !(0..=MAX_ENTITY_INDEX).contains(&next) {
411                return Err(Error::Parse {
412                    context: format!("entity index {next} out of range"),
413                });
414            }
415            entity_index = next as i32;
416
417            // Read delta header (2 bits)
418            let dh = br.read_bits(2)? as u8;
419
420            match dh {
421                DELTA_CREATE => {
422                    self.handle_create(
423                        entity_index,
424                        &mut br,
425                        class_info,
426                        serializers,
427                        string_tables,
428                        field_decode_ctx,
429                        fp_buf,
430                    )
431                    .map_err(|e| Error::Parse {
432                        context: format!("entity create #{}: {}", entity_index, e),
433                    })?;
434                    self.updated.push(entity_index);
435                }
436                DELTA_UPDATE => {
437                    // Legacy PVS-visibility bits (absent in modern demos): when
438                    // present, a set low bit means the entity was not actually
439                    // updated, so skip it.
440                    if has_pvs_vis_bits > 0 && (br.read_bits(2)? & 0x01) != 0 {
441                        continue;
442                    }
443                    self.handle_update(
444                        entity_index,
445                        &mut br,
446                        class_info,
447                        serializers,
448                        field_decode_ctx,
449                        fp_buf,
450                    )
451                    .map_err(|e| Error::Parse {
452                        context: format!(
453                            "entity update #{} (class: {:?}): {}",
454                            entity_index,
455                            self.get(entity_index).map(|e| &e.class_name),
456                            e
457                        ),
458                    })?;
459                    self.updated.push(entity_index);
460                }
461                DELTA_DELETE | DELTA_LEAVE => {
462                    self.take_entity(entity_index);
463                }
464                _ => {}
465            }
466        }
467
468        Ok(())
469    }
470
471    /// Handle a CSVCMsg_PacketEntities message, only tracking specified entity classes.
472    /// Entities not in the filter are parsed (to advance the bit reader) but not stored.
473    #[allow(clippy::too_many_arguments)]
474    pub fn handle_packet_entities_filtered(
475        &mut self,
476        msg: CsvcMsgPacketEntities,
477        class_info: &ClassInfo,
478        serializers: &SerializerContainer,
479        string_tables: &StringTableContainer,
480        field_decode_ctx: &mut FieldDecodeContext,
481        class_filter: &HashSet<&str>,
482        fp_buf: &mut Vec<FieldPath>,
483    ) -> Result<()> {
484        let has_pvs_vis_bits = msg.has_pvs_vis_bits_deprecated.unwrap_or(0);
485        let entity_data = msg.entity_data.unwrap_or_default();
486        let mut br = BitReader::new(&entity_data);
487
488        let mut entity_index: i32 = -1;
489
490        for _ in 0..msg.updated_entries.unwrap_or(0) {
491            let next = entity_index as i64 + br.read_ubitvar()? as i64 + 1;
492            if !(0..=MAX_ENTITY_INDEX).contains(&next) {
493                return Err(Error::Parse {
494                    context: format!("entity index {next} out of range"),
495                });
496            }
497            entity_index = next as i32;
498
499            // Read delta header (2 bits)
500            let dh = br.read_bits(2)? as u8;
501
502            match dh {
503                DELTA_CREATE => {
504                    if self.handle_create_filtered(
505                        entity_index,
506                        &mut br,
507                        class_info,
508                        serializers,
509                        string_tables,
510                        field_decode_ctx,
511                        class_filter,
512                        fp_buf,
513                    )? {
514                        self.updated.push(entity_index);
515                    }
516                }
517                DELTA_UPDATE => {
518                    // Legacy PVS-visibility bits (see handle_packet_entities).
519                    if has_pvs_vis_bits > 0 && (br.read_bits(2)? & 0x01) != 0 {
520                        continue;
521                    }
522                    if self.handle_update_filtered(
523                        entity_index,
524                        &mut br,
525                        class_info,
526                        serializers,
527                        field_decode_ctx,
528                        class_filter,
529                        fp_buf,
530                    )? {
531                        self.updated.push(entity_index);
532                    }
533                }
534                DELTA_DELETE | DELTA_LEAVE => {
535                    self.take_entity(entity_index);
536                    self.set_skipped(entity_index, None);
537                }
538                _ => {}
539            }
540        }
541
542        Ok(())
543    }
544
545    #[allow(clippy::too_many_arguments)]
546    fn handle_create(
547        &mut self,
548        index: i32,
549        br: &mut BitReader,
550        class_info: &ClassInfo,
551        serializers: &SerializerContainer,
552        string_tables: &StringTableContainer,
553        field_decode_ctx: &mut FieldDecodeContext,
554        fp_buf: &mut Vec<FieldPath>,
555    ) -> Result<()> {
556        let class_id = br.read_bits(class_info.bits)? as i32;
557        let _serial = br.read_bits(NUM_SERIAL_NUM_BITS as usize)?;
558        let _unknown = br.read_uvarint32()?;
559
560        let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
561            context: format!("unknown class_id {}", class_id),
562        })?;
563
564        let serializer =
565            serializers
566                .get(&class_entry.network_name)
567                .ok_or_else(|| Error::Parse {
568                    context: format!("no serializer for {}", class_entry.network_name),
569                })?;
570
571        let mut entity = Entity::new(index, class_id, class_entry.network_name.clone());
572        // Pre-size the field map to the class's field count: a create applies the
573        // baseline plus the create delta, setting many fields at once, so starting
574        // from an empty map otherwise rehashes repeatedly as it grows.
575        entity.fields.reserve(serializer.fields.len());
576
577        // Apply baseline from instancebaseline string table
578        if let Some(baseline_data) = string_tables.instance_baselines.get(&class_id) {
579            let mut baseline_br = BitReader::new(baseline_data);
580            entity
581                .apply_update(&mut baseline_br, serializer, field_decode_ctx, fp_buf)
582                .map_err(|err| Error::Parse {
583                    context: format!(
584                        "baseline for {} (class_id {}): {}",
585                        class_entry.network_name, class_id, err
586                    ),
587                })?;
588        }
589
590        // Apply create delta
591        entity
592            .apply_update(br, serializer, field_decode_ctx, fp_buf)
593            .map_err(|err| Error::Parse {
594                context: format!(
595                    "create delta for {} (class_id {}): {}",
596                    class_entry.network_name, class_id, err
597                ),
598            })?;
599        self.put_entity(index, entity);
600
601        Ok(())
602    }
603
604    fn handle_update(
605        &mut self,
606        index: i32,
607        br: &mut BitReader,
608        _class_info: &ClassInfo,
609        serializers: &SerializerContainer,
610        field_decode_ctx: &mut FieldDecodeContext,
611        fp_buf: &mut Vec<FieldPath>,
612    ) -> Result<()> {
613        let entity = match self.entity_mut(index) {
614            Some(e) => e,
615            None => {
616                return Err(Error::Parse {
617                    context: format!("tried to update non-existent entity #{}", index),
618                });
619            }
620        };
621
622        let serializer = serializers
623            .get(&entity.class_name)
624            .ok_or_else(|| Error::Parse {
625                context: format!("no serializer for {}", entity.class_name),
626            })?;
627
628        entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
629        Ok(())
630    }
631
632    /// Returns `true` if the entity's class passed the filter and was stored.
633    #[allow(clippy::too_many_arguments)]
634    fn handle_create_filtered(
635        &mut self,
636        index: i32,
637        br: &mut BitReader,
638        class_info: &ClassInfo,
639        serializers: &SerializerContainer,
640        string_tables: &StringTableContainer,
641        field_decode_ctx: &mut FieldDecodeContext,
642        class_filter: &HashSet<&str>,
643        fp_buf: &mut Vec<FieldPath>,
644    ) -> Result<bool> {
645        let class_id = br.read_bits(class_info.bits)? as i32;
646        let _serial = br.read_bits(NUM_SERIAL_NUM_BITS as usize)?;
647        let _unknown = br.read_uvarint32()?;
648
649        let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
650            context: format!("unknown class_id {}", class_id),
651        })?;
652
653        let serializer =
654            serializers
655                .get(&class_entry.network_name)
656                .ok_or_else(|| Error::Parse {
657                    context: format!("no serializer for {}", class_entry.network_name),
658                })?;
659
660        // Check if this class is in our filter
661        if !class_filter.contains(class_entry.network_name.as_str()) {
662            // Skip this entity - just advance the bit reader
663            // But track its class_id so we can skip updates later
664            self.set_skipped(index, Some(class_id));
665            Entity::skip_update(br, serializer, field_decode_ctx, fp_buf)?;
666            return Ok(false);
667        }
668
669        // Full processing for filtered entities
670        let mut entity = Entity::new(index, class_id, class_entry.network_name.clone());
671        // Pre-size the field map to the class's field count: a create applies the
672        // baseline plus the create delta, setting many fields at once, so starting
673        // from an empty map otherwise rehashes repeatedly as it grows.
674        entity.fields.reserve(serializer.fields.len());
675
676        if let Some(baseline_data) = string_tables.instance_baselines.get(&class_id) {
677            let mut baseline_br = BitReader::new(baseline_data);
678            entity.apply_update(&mut baseline_br, serializer, field_decode_ctx, fp_buf)?;
679        }
680
681        entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
682        self.put_entity(index, entity);
683
684        Ok(true)
685    }
686
687    /// Returns `true` if this entity is tracked and was updated (vs. skipped).
688    #[allow(clippy::too_many_arguments)]
689    fn handle_update_filtered(
690        &mut self,
691        index: i32,
692        br: &mut BitReader,
693        class_info: &ClassInfo,
694        serializers: &SerializerContainer,
695        field_decode_ctx: &mut FieldDecodeContext,
696        _class_filter: &HashSet<&str>,
697        fp_buf: &mut Vec<FieldPath>,
698    ) -> Result<bool> {
699        // Check if we're tracking this entity
700        if let Some(entity) = self.entity_mut(index) {
701            let serializer = serializers
702                .get(&entity.class_name)
703                .ok_or_else(|| Error::Parse {
704                    context: format!("no serializer for {}", entity.class_name),
705                })?;
706
707            entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
708            return Ok(true);
709        }
710
711        // Entity is not tracked - check if we know its class from skipped creates
712        if let Some(class_id) = self.skipped_class(index) {
713            let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
714                context: format!("unknown class_id {}", class_id),
715            })?;
716
717            let serializer =
718                serializers
719                    .get(&class_entry.network_name)
720                    .ok_or_else(|| Error::Parse {
721                        context: format!("no serializer for {}", class_entry.network_name),
722                    })?;
723
724            // Skip this update
725            Entity::skip_update(br, serializer, field_decode_ctx, fp_buf)?;
726        }
727
728        // If we don't know about this entity at all, it was created before filtering started
729        // This shouldn't happen if we start filtering from the beginning
730        Ok(false)
731    }
732
733    /// Look up an entity by its slot index.
734    pub fn get(&self, index: i32) -> Option<&Entity> {
735        usize::try_from(index)
736            .ok()
737            .and_then(|i| self.entities.get(i))
738            .and_then(Option::as_ref)
739    }
740
741    /// Resolve a networked `CHandle` to the entity it refers to, if still active.
742    ///
743    /// Applies [`ENTITY_HANDLE_INDEX_MASK`] to recover the entity index, then
744    /// looks it up. This is the canonical way to follow a handle field such as
745    /// `m_hPawn`; decoding the mask by hand risks resolving the wrong entity.
746    pub fn get_by_handle(&self, handle: u32) -> Option<&Entity> {
747        self.get((handle & ENTITY_HANDLE_INDEX_MASK) as i32)
748    }
749
750    /// Iterate over all active entities as `(index, entity)` pairs, in ascending
751    /// index order.
752    pub fn iter(&self) -> impl Iterator<Item = (i32, &Entity)> {
753        self.entities
754            .iter()
755            .enumerate()
756            .filter_map(|(i, slot)| slot.as_ref().map(|e| (i as i32, e)))
757    }
758
759    /// Indices of tracked entities created or updated during the current tick.
760    ///
761    /// Lets change-only consumers (cooldowns, modifiers, …) process just the
762    /// entities a tick touched instead of rescanning every active entity. Valid
763    /// inside a `run_to_end*` per-tick callback; the parser clears it after each
764    /// callback returns.
765    ///
766    /// Caveats: an index may appear more than once if multiple packets in the
767    /// same tick updated the entity, and may refer to an entity that is no
768    /// longer active if it was deleted later in the same tick. Resolve each with
769    /// [`get`](Self::get) and skip `None`.
770    pub fn updated_indices(&self) -> &[i32] {
771        &self.updated
772    }
773
774    /// Clear the per-tick updated set. Called by the parser after each callback.
775    pub fn clear_updated(&mut self) {
776        self.updated.clear();
777    }
778
779    /// Number of currently active entities.
780    pub fn len(&self) -> usize {
781        self.entities.iter().filter(|slot| slot.is_some()).count()
782    }
783
784    pub fn is_empty(&self) -> bool {
785        self.entities.iter().all(Option::is_none)
786    }
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792
793    #[test]
794    fn container_new_is_empty() {
795        let c = EntityContainer::new();
796        assert!(c.is_empty());
797        assert_eq!(c.len(), 0);
798        assert!(c.get(0).is_none());
799    }
800
801    #[test]
802    fn entity_fields_insert_and_get() {
803        let mut e = Entity::new(1, 10, "TestClass".to_string());
804        e.fields.insert(42, FieldValue::I32(100));
805        assert!(matches!(e.fields.get(&42), Some(FieldValue::I32(100))));
806    }
807
808    #[test]
809    fn container_insert_and_iter() {
810        let mut c = EntityContainer::new();
811        let e = Entity::new(5, 10, "Hero".to_string());
812        c.put_entity(5, e);
813        assert_eq!(c.len(), 1);
814        assert!(!c.is_empty());
815        assert!(c.get(5).is_some());
816        assert_eq!(c.get(5).unwrap().class_name, "Hero");
817    }
818
819    #[test]
820    fn container_iter_yields_entries() {
821        let mut c = EntityContainer::new();
822        c.put_entity(1, Entity::new(1, 1, "A".to_string()));
823        c.put_entity(2, Entity::new(2, 2, "B".to_string()));
824        let keys: Vec<i32> = c.iter().map(|(k, _)| k).collect();
825        assert_eq!(keys.len(), 2);
826    }
827
828    #[test]
829    fn entity_basic_fields() {
830        let e = Entity::new(7, 42, "NPC".to_string());
831        assert_eq!(e.index, 7);
832        assert_eq!(e.class_id, 42);
833        assert_eq!(e.class_name, "NPC");
834        assert_eq!(e.serial, 0);
835        assert!(e.fields.is_empty());
836    }
837}