source2_demo/entity/container.rs
1use crate::error::EntityError;
2use crate::Entity;
3
4/// Container for all entities in the replay.
5///
6/// Entities represent all game objects (heroes, NPCs, items, wards, etc.).
7/// This container provides multiple ways to access them:
8/// - By index (direct position in entity array)
9/// - By handle (combined serial + index)
10/// - By class ID (entity type)
11/// - By class name (e.g., "CDOTA_Unit_Hero_Axe")
12///
13/// # Entity Access Patterns
14///
15/// Different scenarios require different lookup methods:
16/// - **Known class name**: Use [`get_by_class_name`](Entities::get_by_class_name)
17/// - **Iterating all**: Use [`iter`](Entities::iter)
18/// - **Known index**: Use [`get_by_index`](Entities::get_by_index)
19/// - **Finding by type**: Use iterator with filter
20///
21/// # Examples
22///
23/// ## Get the player resource
24///
25/// ```no_run
26/// use source2_demo::prelude::*;
27///
28/// # fn example(ctx: &Context) -> anyhow::Result<()> {
29/// let player_resource = ctx.entities().get_by_class_name("CDOTA_PlayerResource")?;
30/// # Ok(())
31/// # }
32/// ```
33///
34/// ## Find all heroes on a specific team
35///
36/// ```no_run
37/// use source2_demo::prelude::*;
38///
39/// # fn example(ctx: &Context) -> anyhow::Result<()> {
40/// let radiant_heroes: Vec<&Entity> = ctx.entities()
41/// .iter()
42/// .filter(|e| {
43/// e.class().name().starts_with("CDOTA_Unit_Hero_")
44/// && try_property!(e, u32, "m_iTeamNum") == Some(2) // 2 = Radiant
45/// })
46/// .collect();
47/// # Ok(())
48/// # }
49/// ```
50///
51/// ## Get entity by index
52///
53/// ```no_run
54/// use source2_demo::prelude::*;
55///
56/// # fn example(ctx: &Context) -> anyhow::Result<()> {
57/// let entity = ctx.entities().get_by_index(256)?;
58/// println!("Entity type: {}", entity.class().name());
59/// # Ok(())
60/// # }
61/// ```
62pub struct Entities {
63 pub(crate) entities_vec: Vec<Entity>,
64}
65
66impl Default for Entities {
67 fn default() -> Self {
68 Entities {
69 entities_vec: vec![Entity::default(); 8192],
70 }
71 }
72}
73
74impl Entities {
75 /// Returns an iterator over all active entities.
76 ///
77 /// Iterates only entities that are currently alive/active in the replay.
78 /// Deleted entities are automatically skipped.
79 ///
80 /// # Examples
81 ///
82 /// ```
83 /// use source2_demo::prelude::*;
84 /// use source2_demo::proto::*;
85 ///
86 /// #[derive(Default)]
87 /// struct MyObs;
88 ///
89 /// impl Observer for MyObs {
90 /// fn on_tick_start(&mut self, ctx: &Context) -> ObserverResult {
91 /// let dire_heroes = ctx
92 /// .entities()
93 /// .iter()
94 /// .filter(|&e| {
95 /// e.class().name().starts_with("CDOTA_Hero_Unit")
96 /// && try_property!(e, u32, "m_iTeamNum") == Some(3)
97 /// && try_property!(e, u32, "m_hReplicatingOtherHeroModel") == Some(u32::MAX)
98 /// })
99 /// .collect::<Vec<_>>();
100 /// Ok(())
101 /// }
102 /// }
103 /// ```
104 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
105 self.entities_vec.iter().filter(|e| e.index != u32::MAX)
106 }
107
108 /// Gets an entity by its index in the entity array.
109 ///
110 /// Entity indices are in the range 0-8191. This is the fastest way to
111 /// access an entity if you already know its index.
112 ///
113 /// # Arguments
114 ///
115 /// * `index` - The entity index (0-8191)
116 ///
117 /// # Errors
118 ///
119 /// Returns [`EntityError::IndexNotFound`] if no entity exists at the given index
120 /// or if the entity at that index has been deleted.
121 ///
122 /// # Examples
123 ///
124 /// ```no_run
125 /// use source2_demo::prelude::*;
126 ///
127 /// # fn example(ctx: &Context) -> anyhow::Result<()> {
128 /// let entity = ctx.entities().get_by_index(0)?;
129 /// println!("First entity type: {}", entity.class().name());
130 /// # Ok(())
131 /// # }
132 /// ```
133 pub fn get_by_index(&self, index: usize) -> Result<&Entity, EntityError> {
134 if let Some(e) = self.entities_vec.get(index) {
135 if e.index != u32::MAX {
136 return Ok(e);
137 }
138 }
139 Err(EntityError::IndexNotFound(index))
140 }
141
142 /// Gets an entity by its handle.
143 ///
144 /// A handle combines the serial number and index into a single identifier.
145 /// This is useful when you have a handle reference from entity properties.
146 ///
147 /// # Arguments
148 ///
149 /// * `handle` - The entity handle (serial << 14 | index)
150 ///
151 /// # Errors
152 ///
153 /// Returns [`EntityError::HandleNotFound`] if no valid entity exists
154 /// for the given handle.
155 ///
156 /// # Examples
157 ///
158 /// ```no_run
159 /// use source2_demo::prelude::*;
160 ///
161 /// # fn example(ctx: &Context) -> anyhow::Result<()> {
162 /// let handle = 123; // Example handle from entity property
163 /// let entity = ctx.entities().get_by_handle(handle as usize)?;
164 /// # Ok(())
165 /// # }
166 /// ```
167 pub fn get_by_handle(&self, handle: usize) -> Result<&Entity, EntityError> {
168 self.get_by_index(handle & 0x3fff)
169 .map_err(|_| EntityError::HandleNotFound(handle))
170 }
171
172 /// Gets the first entity with the specified class ID.
173 ///
174 /// Typically only useful if you know there's only one entity of that class,
175 /// or if you only need the first one. For finding multiple entities of a type,
176 /// use [`iter`](Entities::iter) with a filter.
177 ///
178 /// # Arguments
179 ///
180 /// * `id` - The class ID to search for
181 ///
182 /// # Errors
183 ///
184 /// Returns [`EntityError::ClassIdNotFound`] if no entity with the given class ID exists.
185 ///
186 /// # Examples
187 ///
188 /// ```no_run
189 /// use source2_demo::prelude::*;
190 ///
191 /// # fn example(ctx: &Context) -> anyhow::Result<()> {
192 /// // Find entity by class ID
193 /// let entity = ctx.entities().get_by_class_id(42)?;
194 /// # Ok(())
195 /// # }
196 /// ```
197 pub fn get_by_class_id(&self, id: i32) -> Result<&Entity, EntityError> {
198 self.iter()
199 .find(|&entity| entity.class().id() == id)
200 .ok_or(EntityError::ClassIdNotFound(id))
201 }
202
203 /// Gets the first entity with the specified class name.
204 ///
205 /// This is useful for finding unique entities like "CDOTA_PlayerResource"
206 /// or specific entity types. For finding multiple entities of a class type,
207 /// use [`iter`](Entities::iter) with a filter.
208 ///
209 /// # Arguments
210 ///
211 /// * `name` - The class name to search for (e.g., "CDOTA_PlayerResource")
212 ///
213 /// # Errors
214 ///
215 /// Returns [`EntityError::ClassNameNotFound`] if no entity with the given
216 /// class name exists.
217 ///
218 /// # Examples
219 ///
220 /// ```no_run
221 /// use source2_demo::prelude::*;
222 ///
223 /// # fn example(ctx: &Context) -> anyhow::Result<()> {
224 /// // Find the player resource entity
225 /// let player_resource = ctx.entities().get_by_class_name("CDOTA_PlayerResource")?;
226 ///
227 /// // Now you can get player info from this entity
228 /// let player_name: String = property!(player_resource, "m_vecPlayerData.{:04}.m_iszPlayerName", 0);
229 /// # Ok(())
230 /// # }
231 /// ```
232 pub fn get_by_class_name(&self, name: &str) -> Result<&Entity, EntityError> {
233 self.iter()
234 .find(|&entity| entity.class().name() == name)
235 .ok_or(EntityError::ClassNameNotFound(name.to_string()))
236 }
237}