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}