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_by_name("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.entities()
40//!     .iter()
41//!     .filter(|e| {
42//!         e.class().name().starts_with("CDOTA_Unit_Hero_")
43//!             && try_property!(e, u32, "m_iTeamNum") == Some(2)
44//!     })
45//!     .collect();
46//! # Ok(())
47//! # }
48//! ```
49
50mod baseline;
51mod class;
52mod container;
53
54pub(crate) use baseline::*;
55pub(crate) mod field;
56pub use class::*;
57pub use container::*;
58
59use crate::error::EntityError;
60use crate::field::{FieldPath, FieldState};
61use crate::FieldValue;
62use std::rc::Rc;
63
64/// Events that can occur to entities during replay parsing.
65///
66/// These events are passed to the `Observer::on_entity` callback when
67/// an entity is created, updated, or deleted.
68///
69/// # Examples
70///
71/// ```no_run
72/// use source2_demo::prelude::*;
73///
74/// #[derive(Default)]
75/// struct EntityTracker {
76///     created: usize,
77///     updated: usize,
78///     deleted: usize,
79/// }
80///
81/// #[observer]
82/// #[uses_entities]
83/// impl Observer for EntityTracker {
84///     fn on_entity(&mut self, ctx: &Context, event: EntityEvents, entity: &Entity) -> ObserverResult {
85///         match event {
86///             EntityEvents::Created => self.created += 1,
87///             EntityEvents::Updated => self.updated += 1,
88///             EntityEvents::Deleted => self.deleted += 1,
89///         }
90///         Ok(())
91///     }
92/// }
93/// ```
94#[derive(Debug, Clone, Copy, Eq, PartialEq)]
95pub enum EntityEvents {
96    /// Entity was created and added
97    Created,
98    /// Entity properties were updated
99    Updated,
100    /// Entity was removed
101    Deleted,
102}
103
104impl EntityEvents {
105    #[inline]
106    pub(crate) fn from_cmd(cmd: u32) -> Self {
107        match cmd {
108            0 => EntityEvents::Updated,
109            2 => EntityEvents::Created,
110            3 => EntityEvents::Deleted,
111            _ => unreachable!(),
112        }
113    }
114}
115
116/// Represents a game entity with its properties and state.
117///
118/// Entities are the fundamental objects in Source 2 games, representing everything
119/// from players and heroes to items and buildings. Each entity has:
120/// - An index (position in the entity list)
121/// - A serial number (for handle-based lookups)
122/// - A class (defines what type of entity it is)
123/// - A state (contains all property values)
124///
125/// # Property Access
126///
127/// Entity properties can be accessed in multiple ways:
128///
129/// 1. Using [`get_property_by_name`](Entity::get_property_by_name) and converting manually
130/// 2. Using the `property!` macro
131/// 3. Using the `try_property!` macro for optional properties
132///
133/// # Examples
134///
135/// ## Basic property access
136///
137/// ```no_run
138/// use source2_demo::prelude::*;
139///
140/// # fn example(entity: &Entity) -> anyhow::Result<()> {
141/// // Get a property and convert it
142/// let health: i32 = entity.get_property_by_name("m_iHealth")?.try_into()?;
143///
144/// // Using the property! macro (simpler)
145/// let max_health: i32 = property!(entity, "m_iHealth");
146///
147/// // With type annotation
148/// let position = property!(entity, i32, "m_iHealth.m_vecPosition");
149/// # Ok(())
150/// # }
151/// ```
152///
153/// ## Working with arrays (formatted property names)
154///
155/// ```no_run
156/// use source2_demo::prelude::*;
157///
158/// # fn example(entity: &Entity) -> anyhow::Result<()> {
159/// // Access array element using formatting
160/// let player_id = 3;
161/// let name: String = property!(
162///     entity,
163///     "m_vecPlayerData.{:04}.m_iszPlayerName",
164///     player_id
165/// );
166/// # Ok(())
167/// # }
168/// ```
169///
170/// ## Optional properties
171///
172/// ```no_run
173/// use source2_demo::prelude::*;
174///
175/// # fn example(entity: &Entity) {
176/// // Returns None if property doesn't exist or can't be converted
177/// if let Some(health) = try_property!(entity, i32, "m_iHealth") {
178///     println!("Health: {}", health);
179/// }
180/// # }
181/// ```
182#[derive(Clone)]
183pub struct Entity {
184    pub(crate) index: u32,
185    pub(crate) serial: u32,
186    pub(crate) class: Rc<Class>,
187    pub(crate) state: FieldState,
188}
189
190impl Default for Entity {
191    fn default() -> Self {
192        Entity {
193            index: u32::MAX,
194            serial: 0,
195            class: Class::default().into(),
196            state: FieldState::default(),
197        }
198    }
199}
200
201impl Entity {
202    /// Returns the entity's index in the entity list.
203    ///
204    /// The index is the position of this entity in the internal entity array.
205    /// Valid entities have indices in the range 0..8192.
206    pub fn index(&self) -> u32 {
207        self.index
208    }
209
210    /// Returns the entity's serial number.
211    ///
212    /// The serial number is used for handle-based entity lookups and is
213    /// incremented each time an entity slot is reused.
214    pub fn serial(&self) -> u32 {
215        self.serial
216    }
217
218    /// Returns the entity's handle.
219    ///
220    /// The handle combines the serial number and index into a single value
221    /// that uniquely identifies this entity. It's calculated as:
222    /// `(serial << 14) | index`
223    pub fn handle(&self) -> u32 {
224        self.serial << 14 | self.index
225    }
226
227    /// Returns a reference to the entity's class.
228    ///
229    /// The class defines what type of entity this is (e.g., "CDOTA_Unit_Hero_Axe").
230    /// It also contains the serializer that defines what properties the entity has.
231    ///
232    /// # Examples
233    ///
234    /// ```no_run
235    /// use source2_demo::prelude::*;
236    ///
237    /// # fn example(entity: &Entity) {
238    /// let class = entity.class();
239    /// println!("Class name: {}", class.name());
240    /// println!("Class ID: {}", class.id());
241    ///
242    /// // Check if entity is a hero
243    /// if class.name().starts_with("CDOTA_Unit_Hero_") {
244    ///     println!("This is a hero!");
245    /// }
246    /// # }
247    /// ```
248    pub fn class(&self) -> &Class {
249        &self.class
250    }
251
252    /// Gets the value of an entity property by its name.
253    ///
254    /// This method looks up a property by its string name (e.g., "m_iHealth")
255    /// and returns a reference to its [`FieldValue`]. The value can then be
256    /// converted to the desired Rust type using [`TryInto`].
257    ///
258    /// # Property Names
259    ///
260    /// Property names use dot notation for nested properties:
261    /// - Simple: `"m_iHealth"`, `"m_flMana"`
262    /// - Nested: `"CBodyComponent.m_cellX"`
263    /// - Arrays: `"m_vecPlayerData.0000.m_iszPlayerName"` (use formatting for indices)
264    ///
265    /// # Recommended Alternatives
266    ///
267    /// For most use cases, prefer the [`property!`] or [`try_property!`] macros
268    /// which provide a more ergonomic interface with automatic type conversion:
269    ///
270    /// ```ignore
271    /// // Instead of:
272    /// let health: i32 = entity.get_property_by_name("m_iHealth")?.try_into()?;
273    ///
274    /// // Use:
275    /// let health: i32 = property!(entity, "m_iHealth");
276    /// ```
277    ///
278    /// # Arguments
279    ///
280    /// * `name` - The property name in dot notation (e.g., "CBodyComponent.m_cellX")
281    ///
282    /// # Returns
283    ///
284    /// Returns `Ok(&FieldValue)` if the property exists, or an error if:
285    /// - The property name is invalid or doesn't exist
286    /// - The entity class doesn't have this property
287    ///
288    /// # Errors
289    ///
290    /// Returns [`EntityError::PropertyNameNotFound`] if the property doesn't exist
291    /// on this entity or if the name is invalid.
292    ///
293    /// # Examples
294    ///
295    /// ## Basic usage with manual conversion
296    ///
297    /// ```no_run
298    /// use source2_demo::prelude::*;
299    ///
300    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
301    /// // Get property and convert to i32
302    /// let health: i32 = entity.get_property_by_name("m_iHealth")?.try_into()?;
303    ///
304    /// // Get nested property
305    /// let cell_x: u8 = entity
306    ///     .get_property_by_name("CBodyComponent.m_cellX")?
307    ///     .try_into()?;
308    ///
309    /// // Get vector property
310    /// let position: i32 = entity
311    ///     .get_property_by_name("m_iHealth")?
312    ///     .try_into()?;
313    /// # Ok(())
314    /// # }
315    /// ```
316    ///
317    /// ## Using in an observer
318    ///
319    /// ```no_run
320    /// use source2_demo::prelude::*;
321    ///
322    /// #[derive(Default)]
323    /// struct HealthTracker;
324    ///
325    /// #[observer]
326    /// #[uses_entities]
327    /// impl HealthTracker {
328    ///     fn on_entity(
329    ///         &mut self,
330    ///         ctx: &Context,
331    ///         event: EntityEvents,
332    ///         entity: &Entity,
333    ///     ) -> ObserverResult {
334    ///         // Manual conversion with get_property_by_name
335    ///         let health: i32 = entity
336    ///             .get_property_by_name("m_iHealth")?
337    ///             .try_into()?;
338    ///
339    ///         // Recommended: using property! macro instead
340    ///         let max_health: i32 = property!(entity, "m_iMaxHealth");
341    ///
342    ///         println!("Health: {}/{}", health, max_health);
343    ///         Ok(())
344    ///     }
345    /// }
346    /// ```
347    ///
348    /// ## Comparison with macros
349    ///
350    /// ```no_run
351    /// use source2_demo::prelude::*;
352    ///
353    /// # fn example(entity: &Entity) -> anyhow::Result<()> {
354    /// // Method 1: get_property_by_name (verbose)
355    /// let health: i32 = entity.get_property_by_name("m_iHealth")?.try_into()?;
356    ///
357    /// // Method 2: property! macro (recommended)
358    /// let health: i32 = property!(entity, "m_iHealth");
359    ///
360    /// // Method 3: try_property! macro (for optional properties)
361    /// let health: Option<i32> = try_property!(entity, i32, "m_iHealth");
362    /// # Ok(())
363    /// # }
364    /// ```
365    ///
366    /// # See Also
367    ///
368    /// - [`property!`] - Macro for concise property access with automatic conversion
369    /// - [`try_property!`] - Macro for optional property access (returns `Option`)
370    /// - [`FieldValue`] - The type returned by this method
371    ///
372    /// [`property!`]: crate::property
373    /// [`try_property!`]: crate::try_property
374    pub fn get_property_by_name(&self, name: &str) -> Result<&FieldValue, EntityError> {
375        self.get_property_by_field_path(&self.class.serializer.get_field_path_for_name(name)?)
376    }
377
378    pub(crate) fn get_property_by_field_path(
379        &self,
380        fp: &FieldPath,
381    ) -> Result<&FieldValue, EntityError> {
382        self.state.get_value(fp).ok_or_else(|| {
383            EntityError::PropertyNameNotFound(
384                self.class.serializer.get_name_for_field_path(fp),
385                self.class.name().to_string(),
386                format!("{}", fp),
387            )
388        })
389    }
390}