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
110impl EntityEvents {
111    #[inline]
112    pub(crate) fn from_cmd(cmd: u32) -> Self {
113        match cmd {
114            0 => EntityEvents::Updated,
115            2 => EntityEvents::Created,
116            3 => EntityEvents::Deleted,
117            _ => unreachable!(),
118        }
119    }
120}
121
122/// Represents a game entity with its properties and state.
123///
124/// Entities are the fundamental objects in Source 2 games, representing
125/// everything from players and heroes to items and buildings. Each entity has:
126/// - An index (position in the entity list)
127/// - A serial number (for handle-based lookups)
128/// - A class (defines what type of entity it is)
129/// - A state (contains all property values)
130///
131/// # Property Access
132///
133/// Entity properties can be accessed in multiple ways:
134///
135/// 1. Using [`get_property`](Entity::get_property) and converting manually
136/// 2. Using the `property!` macro
137/// 3. Using the `try_property!` macro for optional properties
138///
139/// # Examples
140///
141/// ## Basic property access
142///
143/// ```no_run
144/// use source2_demo::prelude::*;
145///
146/// # fn example(entity: &Entity) -> anyhow::Result<()> {
147/// // Get a property and convert it
148/// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
149///
150/// // Using the property! macro (simpler)
151/// let max_health: i32 = property!(entity, "m_iHealth");
152///
153/// // With type annotation
154/// let position = property!(entity, i32, "m_iHealth.m_vecPosition");
155/// # Ok(())
156/// # }
157/// ```
158///
159/// ## Working with arrays (formatted property names)
160///
161/// ```no_run
162/// use source2_demo::prelude::*;
163///
164/// # fn example(entity: &Entity) -> anyhow::Result<()> {
165/// // Access array element using formatting
166/// let player_id = 3;
167/// let name: String = property!(entity, "m_vecPlayerData.{:04}.m_iszPlayerName", player_id);
168/// # Ok(())
169/// # }
170/// ```
171///
172/// ## Optional properties
173///
174/// ```no_run
175/// use source2_demo::prelude::*;
176///
177/// # fn example(entity: &Entity) {
178/// // Returns None if property doesn't exist or can't be converted
179/// if let Some(health) = try_property!(entity, i32, "m_iHealth") {
180///     println!("Health: {}", health);
181/// }
182/// # }
183/// ```
184#[derive(Clone)]
185pub struct Entity {
186    pub(crate) index: u32,
187    pub(crate) serial: u32,
188    pub(crate) class: Rc<Class>,
189    pub(crate) state: FieldState,
190}
191
192impl Default for Entity {
193    fn default() -> Self {
194        Entity {
195            index: u32::MAX,
196            serial: 0,
197            class: Class::default().into(),
198            state: FieldState::default(),
199        }
200    }
201}
202
203impl Entity {
204    pub(crate) fn new(index: u32, serial: u32, class: Rc<Class>, state: FieldState) -> Self {
205        Entity {
206            index,
207            serial,
208            class,
209            state,
210        }
211    }
212
213    /// Returns the entity's index in the entity list.
214    ///
215    /// The index is the position of this entity in the internal entity array.
216    /// Valid entities have indices in the range 0..8192.
217    pub fn index(&self) -> u32 {
218        self.index
219    }
220
221    /// Returns the entity's serial number.
222    ///
223    /// The serial number is used for handle-based entity lookups and is
224    /// incremented each time an entity slot is reused.
225    pub fn serial(&self) -> u32 {
226        self.serial
227    }
228
229    /// Returns the entity's handle.
230    ///
231    /// The handle combines the serial number and index into a single value
232    /// that uniquely identifies this entity. It's calculated as:
233    /// `(serial << 14) | index`
234    pub fn handle(&self) -> u32 {
235        self.serial << 14 | self.index
236    }
237
238    /// Returns a reference to the entity's class.
239    ///
240    /// The class defines what type of entity this is (e.g.,
241    /// "CDOTA_Unit_Hero_Axe"). It also contains the serializer that defines
242    /// what properties the entity has.
243    ///
244    /// # Examples
245    ///
246    /// ```no_run
247    /// use source2_demo::prelude::*;
248    ///
249    /// # fn example(entity: &Entity) {
250    /// let class = entity.class();
251    /// println!("Class name: {}", class.name());
252    /// println!("Class ID: {}", class.id());
253    ///
254    /// // Check if entity is a hero
255    /// if class.name().starts_with("CDOTA_Unit_Hero_") {
256    ///     println!("This is a hero!");
257    /// }
258    /// # }
259    /// ```
260    pub fn class(&self) -> &Class {
261        &self.class
262    }
263
264    /// See [`get_property`](Entity::get_property) - this method is deprecated
265    /// in favor of the more clearly named `get_property`.
266    #[deprecated]
267    pub fn get_property_by_name(&self, name: &str) -> Result<&FieldValue, EntityError> {
268        self.get_property_by_path(&self.class.serializer.get_path(name)?)
269    }
270
271    /// Gets the value of an entity property by its name.
272    ///
273    /// This method looks up a property by its string name (e.g., "m_iHealth")
274    /// and returns a reference to its [`FieldValue`]. The value can then be
275    /// converted to the desired Rust type using [`TryInto`].
276    ///
277    /// # Property Names
278    ///
279    /// Property names use dot notation for nested properties:
280    /// - Simple: `"m_iHealth"`, `"m_flMana"`
281    /// - Nested: `"CBodyComponent.m_cellX"`
282    /// - Arrays: `"m_vecPlayerData.0000.m_iszPlayerName"` (use formatting for
283    ///   indices)
284    ///
285    /// # Recommended Alternatives
286    ///
287    /// For most use cases, prefer the [`property!`] or [`try_property!`] macros
288    /// which provide a more ergonomic interface with automatic type conversion:
289    ///
290    /// ```ignore
291    /// // Instead of:
292    /// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
293    ///
294    /// // Use:
295    /// let health: i32 = property!(entity, "m_iHealth");
296    /// ```
297    ///
298    /// # Arguments
299    ///
300    /// * `name` - The property name in dot notation (e.g.,
301    ///   "CBodyComponent.m_cellX")
302    ///
303    /// # Returns
304    ///
305    /// Returns `Ok(&FieldValue)` if the property exists, or an error if:
306    /// - The property name is invalid or doesn't exist
307    /// - The entity class doesn't have this property
308    ///
309    /// # Errors
310    ///
311    /// Returns [`EntityError::PropertyNameNotFound`] if the property doesn't
312    /// exist on this entity or if the name is invalid.
313    ///
314    /// # Examples
315    ///
316    /// ## Basic usage with manual conversion
317    ///
318    /// ```no_run
319    /// use source2_demo::prelude::*;
320    ///
321    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
322    /// // Get property and convert to i32
323    /// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
324    ///
325    /// // Get nested property
326    /// let cell_x: u8 = entity.get_property("CBodyComponent.m_cellX")?.try_into()?;
327    ///
328    /// // Get vector property
329    /// let position: i32 = entity.get_property("m_iHealth")?.try_into()?;
330    /// # Ok(())
331    /// # }
332    /// ```
333    ///
334    /// ## Using in an observer
335    ///
336    /// ```no_run
337    /// use source2_demo::prelude::*;
338    ///
339    /// #[derive(Default)]
340    /// struct HealthTracker;
341    ///
342    /// #[observer]
343    /// #[uses_entities]
344    /// impl HealthTracker {
345    ///     fn on_entity(
346    ///         &mut self,
347    ///         ctx: &Context,
348    ///         event: EntityEvents,
349    ///         entity: &Entity,
350    ///     ) -> ObserverResult {
351    ///         // Manual conversion with get_property
352    ///         let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
353    ///
354    ///         // Recommended: using property! macro instead
355    ///         let max_health: i32 = property!(entity, "m_iMaxHealth");
356    ///
357    ///         println!("Health: {}/{}", health, max_health);
358    ///         Ok(())
359    ///     }
360    /// }
361    /// ```
362    ///
363    /// ## Comparison with macros
364    ///
365    /// ```no_run
366    /// use source2_demo::prelude::*;
367    ///
368    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
369    /// // Method 1: get_property (verbose)
370    /// let health: i32 = entity.get_property("m_iHealth")?.try_into()?;
371    ///
372    /// // Method 2: property! macro (recommended)
373    /// let health: i32 = property!(entity, "m_iHealth");
374    ///
375    /// // Method 3: try_property! macro (for optional properties)
376    /// let health: Option<i32> = try_property!(entity, i32, "m_iHealth");
377    /// # Ok(())
378    /// # }
379    /// ```
380    ///
381    /// # See Also
382    ///
383    /// - [`property!`] - Macro for concise property access with automatic
384    ///   conversion
385    /// - [`try_property!`] - Macro for optional property access (returns
386    ///   `Option`)
387    /// - [`FieldValue`] - The type returned by this method
388    ///
389    /// [`property!`]: crate::property
390    /// [`try_property!`]: crate::try_property
391    pub fn get_property(&self, name: &str) -> Result<&FieldValue, EntityError> {
392        self.get_property_by_path(&self.class.serializer.get_path(name)?)
393    }
394
395    pub(crate) fn get_property_by_path(&self, fp: &FieldPath) -> Result<&FieldValue, EntityError> {
396        self.state.get_value(fp).ok_or_else(|| {
397            EntityError::PropertyNameNotFound(
398                self.class.serializer.get_name(fp),
399                self.class.name().to_string(),
400                format!("{}", fp),
401            )
402        })
403    }
404
405    /// Returns an iterator over the values inside a vector-like entity
406    /// property.
407    ///
408    /// This is useful for properties that contain multiple field states, such
409    /// as handle arrays like `"m_hItems"`. Each element is returned as an
410    /// `Option<&FieldValue>` because some entries may not have a value.
411    ///
412    /// # Arguments
413    ///
414    /// * `name` - The property name in dot notation.
415    ///
416    /// # Returns
417    ///
418    /// Returns an iterator over the property's values if the property exists.
419    ///
420    /// # Errors
421    ///
422    /// Returns [`EntityError::PropertyNameNotFound`] if the property name is
423    /// invalid or the entity does not contain this field.
424    ///
425    /// # Examples
426    ///
427    /// ```no_run
428    /// use source2_demo::prelude::*;
429    ///
430    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
431    /// for value in entity.get_iter("m_hItems")?.flatten() {
432    ///     let handle: usize = value.try_into()?;
433    ///     println!("Item handle: {}", handle);
434    /// }
435    /// # Ok(())
436    /// # }
437    /// ```
438    pub fn get_iter(
439        &self,
440        name: &str,
441    ) -> Result<impl Iterator<Item = Option<&FieldValue>>, EntityError> {
442        Ok(self
443            .get_state(&self.class.serializer.get_path(name)?)?
444            .vec
445            .iter()
446            .map(|fs| fs.value.as_ref()))
447    }
448
449    pub(crate) fn get_state(&self, fp: &FieldPath) -> Result<&FieldState, EntityError> {
450        self.state.get_state(fp).ok_or_else(|| {
451            EntityError::PropertyNameNotFound(
452                self.class.serializer.get_name(fp),
453                self.class.name().to_string(),
454                format!("{}", fp),
455            )
456        })
457    }
458}