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}