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}