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