Skip to main content

source2_demo/entity/
mod.rs

1//! # Overview
2//!
3//! Entities have:
4//! - A unique index and serial number
5//! - A class that defines their type
6//! - A state containing all their properties
7//!
8//! # Examples
9//!
10//! ## Getting entity properties
11//!
12//! ```no_run
13//! use source2_demo::prelude::*;
14//!
15//! # fn example(entity: &Entity) -> anyhow::Result<()> {
16//! // Using try_into
17//! let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
18//!
19//! // Using property! macro
20//! let mana: i32 = property!(entity, "m_flMana");
21//!
22//! // With type annotation
23//! let team = property!(entity, u32, "m_iTeamNum");
24//!
25//! // Formatted property names
26//! let player_id = 5;
27//! let name: String = property!(entity, "m_vecPlayerData.{:04}.m_iszPlayerName", player_id);
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! ## Filtering entities
33//!
34//! ```no_run
35//! use source2_demo::prelude::*;
36//!
37//! # fn example(ctx: &Context) -> anyhow::Result<()> {
38//! // Find all heroes on Radiant team
39//! let radiant_heroes: Vec<&Entity> = ctx
40//!     .entities()
41//!     .iter()
42//!     .filter(|e| {
43//!         e.class().name().starts_with("CDOTA_Unit_Hero_")
44//!             && try_property!(e, u32, "m_iTeamNum") == Some(2)
45//!     })
46//!     .collect();
47//! # Ok(())
48//! # }
49//! ```
50
51mod baseline;
52mod class;
53mod container;
54
55pub(crate) use baseline::*;
56pub(crate) mod field;
57pub use class::*;
58pub use container::*;
59
60use crate::error::EntityError;
61use crate::field::{FieldPath, FieldState};
62use crate::FieldValue;
63use std::rc::Rc;
64
65/// Events that can occur to entities during replay parsing.
66///
67/// These events are passed to the `Observer::on_entity` callback when
68/// an entity is created, updated, or deleted.
69///
70/// # Examples
71///
72/// ```no_run
73/// use source2_demo::prelude::*;
74///
75/// #[derive(Default)]
76/// struct EntityTracker {
77///     created: usize,
78///     updated: usize,
79///     deleted: usize,
80/// }
81///
82/// #[observer]
83/// #[uses_entities]
84/// impl EntityTracker {
85///     fn on_entity(
86///         &mut self,
87///         ctx: &Context,
88///         event: EntityEvents,
89///         entity: &Entity,
90///     ) -> ObserverResult {
91///         match event {
92///             EntityEvents::Created => self.created += 1,
93///             EntityEvents::Updated => self.updated += 1,
94///             EntityEvents::Deleted => self.deleted += 1,
95///         }
96///         Ok(())
97///     }
98/// }
99/// ```
100#[derive(Debug, Clone, Copy, Eq, PartialEq)]
101pub enum EntityEvents {
102    /// Entity was created and added
103    Created,
104    /// Entity properties were updated
105    Updated,
106    /// Entity was removed
107    Deleted,
108}
109
110/// A read-only entity field entry for inspection UIs and debugging tools.
111pub struct EntityField<'a> {
112    /// Numeric field path used by the demo stream.
113    pub path: Vec<u16>,
114    /// Human-readable dotted field path.
115    pub name: String,
116    /// Source 2 field type reported by the serializer.
117    pub field_type: String,
118    /// Decoded value type used by `source2-demo`.
119    pub decoded_type: Option<&'static str>,
120    /// Current field value, if the field is present in the state.
121    pub value: Option<&'a FieldValue>,
122}
123
124/// A baseline entity entry keyed by class id.
125pub struct BaselineEntity {
126    /// Entity class id used to look up the baseline.
127    pub class_id: i32,
128    /// Human-readable entity class name.
129    pub class_name: String,
130}
131
132impl EntityEvents {
133    #[inline]
134    pub(crate) fn from_cmd(cmd: u32) -> Self {
135        match cmd {
136            0 => EntityEvents::Updated,
137            2 => EntityEvents::Created,
138            3 => EntityEvents::Deleted,
139            _ => unreachable!(),
140        }
141    }
142}
143
144/// Represents a game entity with its properties and state.
145///
146/// Entities are the fundamental objects in Source 2 games, representing
147/// everything from players and heroes to items and buildings. Each entity has:
148/// - An index (position in the entity list)
149/// - A serial number (for handle-based lookups)
150/// - A class (defines what type of entity it is)
151/// - A state (contains all property values)
152///
153/// # Property Access
154///
155/// Entity properties can be accessed in multiple ways:
156///
157/// 1. Using [`get_property`](Entity::get_property) and converting manually
158/// 2. Using the `property!` macro
159/// 3. Using the `try_property!` macro for optional properties
160///
161/// # Examples
162///
163/// ## Basic property access
164///
165/// ```no_run
166/// use source2_demo::prelude::*;
167///
168/// # fn example(entity: &Entity) -> anyhow::Result<()> {
169/// // Get a property and convert it
170/// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
171///
172/// // Using the property! macro (simpler)
173/// let max_health: i32 = property!(entity, "m_iHealth");
174///
175/// // With type annotation
176/// let position = property!(entity, i32, "m_iHealth.m_vecPosition");
177/// # Ok(())
178/// # }
179/// ```
180///
181/// ## Working with arrays (formatted property names)
182///
183/// ```no_run
184/// use source2_demo::prelude::*;
185///
186/// # fn example(entity: &Entity) -> anyhow::Result<()> {
187/// // Access array element using formatting
188/// let player_id = 3;
189/// let name: String = property!(entity, "m_vecPlayerData.{:04}.m_iszPlayerName", player_id);
190/// # Ok(())
191/// # }
192/// ```
193///
194/// ## Optional properties
195///
196/// ```no_run
197/// use source2_demo::prelude::*;
198///
199/// # fn example(entity: &Entity) {
200/// // Returns None if property doesn't exist or can't be converted
201/// if let Some(health) = try_property!(entity, i32, "m_iHealth") {
202///     println!("Health: {}", health);
203/// }
204/// # }
205/// ```
206#[derive(Clone)]
207pub struct Entity {
208    pub(crate) index: u32,
209    pub(crate) serial: u32,
210    pub(crate) class: Rc<Class>,
211    pub(crate) state: FieldState,
212}
213
214impl Default for Entity {
215    fn default() -> Self {
216        Entity {
217            index: u32::MAX,
218            serial: 0,
219            class: Class::default().into(),
220            state: FieldState::default(),
221        }
222    }
223}
224
225impl Entity {
226    pub(crate) fn new(index: u32, serial: u32, class: Rc<Class>, state: FieldState) -> Self {
227        Entity {
228            index,
229            serial,
230            class,
231            state,
232        }
233    }
234
235    /// Returns the entity's index in the entity list.
236    ///
237    /// The index is the position of this entity in the internal entity array.
238    /// Valid entities have indices in the range 0..8192.
239    pub fn index(&self) -> u32 {
240        self.index
241    }
242
243    /// Returns the entity's serial number.
244    ///
245    /// The serial number is used for handle-based entity lookups and is
246    /// incremented each time an entity slot is reused.
247    pub fn serial(&self) -> u32 {
248        self.serial
249    }
250
251    /// Returns the entity's handle.
252    ///
253    /// The handle combines the serial number and index into a single value
254    /// that uniquely identifies this entity. It's calculated as:
255    /// `(serial << 14) | index`
256    pub fn handle(&self) -> u32 {
257        self.serial << 14 | self.index
258    }
259
260    /// Returns all serializer-backed fields for this entity.
261    ///
262    /// This is intended for generic inspection tools that need to enumerate an
263    /// entity without knowing property names ahead of time.
264    pub fn fields(&self) -> Vec<EntityField<'_>> {
265        self.class
266            .serializer
267            .get_paths(&mut FieldPath::default(), &self.state)
268            .into_iter()
269            .map(|fp| {
270                let value = self.state.get_value(&fp);
271                EntityField {
272                    path: (0..=fp.last).map(|idx| fp.path[idx]).collect(),
273                    name: self.class.serializer.get_name(&fp).to_string(),
274                    field_type: self.class.serializer.get_type(&fp).to_string(),
275                    decoded_type: value.map(FieldValue::type_name),
276                    value,
277                }
278            })
279            .collect()
280    }
281
282    /// Returns a reference to the entity's class.
283    ///
284    /// The class defines what type of entity this is (e.g.,
285    /// "CDOTA_Unit_Hero_Axe"). It also contains the serializer that defines
286    /// what properties the entity has.
287    ///
288    /// # Examples
289    ///
290    /// ```no_run
291    /// use source2_demo::prelude::*;
292    ///
293    /// # fn example(entity: &Entity) {
294    /// let class = entity.class();
295    /// println!("Class name: {}", class.name());
296    /// println!("Class ID: {}", class.id());
297    ///
298    /// // Check if entity is a hero
299    /// if class.name().starts_with("CDOTA_Unit_Hero_") {
300    ///     println!("This is a hero!");
301    /// }
302    /// # }
303    /// ```
304    pub fn class(&self) -> &Class {
305        &self.class
306    }
307
308    /// See [`get_property`](Entity::get_property) - this method is deprecated
309    /// in favor of the more clearly named `get_property`.
310    #[deprecated]
311    pub fn get_property_by_name(&self, name: &str) -> Result<&FieldValue, EntityError> {
312        self.get_property_by_path(&self.class.serializer.get_path(name)?)
313    }
314
315    /// Gets the value of an entity property by its name.
316    ///
317    /// This method looks up a property by its string name (e.g., "m_iHealth")
318    /// and returns a reference to its [`FieldValue`]. The value can then be
319    /// converted to the desired Rust type using [`TryInto`].
320    ///
321    /// # Property Names
322    ///
323    /// Property names use dot notation for nested properties:
324    /// - Simple: `"m_iHealth"`, `"m_flMana"`
325    /// - Nested: `"CBodyComponent.m_cellX"`
326    /// - Arrays: `"m_vecPlayerData.0000.m_iszPlayerName"` (use formatting for
327    ///   indices)
328    ///
329    /// # Recommended Alternatives
330    ///
331    /// For most use cases, prefer the [`property!`] or [`try_property!`] macros
332    /// which provide a more ergonomic interface with automatic type conversion:
333    ///
334    /// ```ignore
335    /// // Instead of:
336    /// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
337    ///
338    /// // Use:
339    /// let health: i32 = property!(entity, "m_iHealth");
340    /// ```
341    ///
342    /// # Arguments
343    ///
344    /// * `name` - The property name in dot notation (e.g.,
345    ///   "CBodyComponent.m_cellX")
346    ///
347    /// # Returns
348    ///
349    /// Returns `Ok(&FieldValue)` if the property exists, or an error if:
350    /// - The property name is invalid or doesn't exist
351    /// - The entity class doesn't have this property
352    ///
353    /// # Errors
354    ///
355    /// Returns [`EntityError::PropertyNameNotFound`] if the property doesn't
356    /// exist on this entity or if the name is invalid.
357    ///
358    /// # Examples
359    ///
360    /// ## Basic usage with manual conversion
361    ///
362    /// ```no_run
363    /// use source2_demo::prelude::*;
364    ///
365    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
366    /// // Get property and convert to i32
367    /// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
368    ///
369    /// // Get nested property
370    /// let cell_x: u8 = entity.get_property("CBodyComponent.m_cellX")?.try_into()?;
371    ///
372    /// // Get vector property
373    /// let position: i32 = entity.get_property("m_iHealth")?.try_into()?;
374    /// # Ok(())
375    /// # }
376    /// ```
377    ///
378    /// ## Using in an observer
379    ///
380    /// ```no_run
381    /// use source2_demo::prelude::*;
382    ///
383    /// #[derive(Default)]
384    /// struct HealthTracker;
385    ///
386    /// #[observer]
387    /// #[uses_entities]
388    /// impl HealthTracker {
389    ///     fn on_entity(
390    ///         &mut self,
391    ///         ctx: &Context,
392    ///         event: EntityEvents,
393    ///         entity: &Entity,
394    ///     ) -> ObserverResult {
395    ///         // Manual conversion with get_property
396    ///         let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
397    ///
398    ///         // Recommended: using property! macro instead
399    ///         let max_health: i32 = property!(entity, "m_iMaxHealth");
400    ///
401    ///         println!("Health: {}/{}", health, max_health);
402    ///         Ok(())
403    ///     }
404    /// }
405    /// ```
406    ///
407    /// ## Comparison with macros
408    ///
409    /// ```no_run
410    /// use source2_demo::prelude::*;
411    ///
412    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
413    /// // Method 1: get_property (verbose)
414    /// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
415    ///
416    /// // Method 2: property! macro (recommended)
417    /// let health: i32 = property!(entity, "m_iHealth");
418    ///
419    /// // Method 3: try_property! macro (for optional properties)
420    /// let health: Option<i32> = try_property!(entity, i32, "m_iHealth");
421    /// # Ok(())
422    /// # }
423    /// ```
424    ///
425    /// # See Also
426    ///
427    /// - [`property!`] - Macro for concise property access with automatic
428    ///   conversion
429    /// - [`try_property!`] - Macro for optional property access (returns
430    ///   `Option`)
431    /// - [`FieldValue`] - The type returned by this method
432    ///
433    /// [`property!`]: crate::property
434    /// [`try_property!`]: crate::try_property
435    pub fn get_property(&self, name: &str) -> Result<&FieldValue, EntityError> {
436        self.get_property_by_path(&self.class.serializer.get_path(name)?)
437    }
438
439    pub(crate) fn get_property_by_path(&self, fp: &FieldPath) -> Result<&FieldValue, EntityError> {
440        self.state.get_value(fp).ok_or_else(|| {
441            EntityError::PropertyNameNotFound(
442                self.class.serializer.get_name(fp).to_string(),
443                self.class.name().to_string(),
444                format!("{}", fp),
445            )
446        })
447    }
448
449    /// Returns an iterator over the values inside a vector-like entity
450    /// property.
451    ///
452    /// This is useful for properties that contain multiple field states, such
453    /// as handle arrays like `"m_hItems"`. Each element is returned as an
454    /// `Option<&FieldValue>` because some entries may not have a value.
455    ///
456    /// # Arguments
457    ///
458    /// * `name` - The property name in dot notation.
459    ///
460    /// # Returns
461    ///
462    /// Returns an iterator over the property's values if the property exists.
463    ///
464    /// # Errors
465    ///
466    /// Returns [`EntityError::PropertyNameNotFound`] if the property name is
467    /// invalid or the entity does not contain this field.
468    ///
469    /// # Examples
470    ///
471    /// ```no_run
472    /// use source2_demo::prelude::*;
473    ///
474    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
475    /// for value in entity.get_iter("m_hItems")?.flatten() {
476    ///     let handle: usize = value.try_into()?;
477    ///     println!("Item handle: {}", handle);
478    /// }
479    /// # Ok(())
480    /// # }
481    /// ```
482    pub fn get_iter(
483        &self,
484        name: &str,
485    ) -> Result<impl Iterator<Item = Option<&FieldValue>>, EntityError> {
486        Ok(self
487            .get_state(&self.class.serializer.get_path(name)?)?
488            .children()
489            .iter()
490            .map(|fs| fs.value.as_ref()))
491    }
492
493    pub(crate) fn get_state(&self, fp: &FieldPath) -> Result<&FieldState, EntityError> {
494        self.state.get_state(fp).ok_or_else(|| {
495            EntityError::PropertyNameNotFound(
496                self.class.serializer.get_name(fp).to_string(),
497                self.class.name().to_string(),
498                format!("{}", fp),
499            )
500        })
501    }
502}