Skip to main content

source2_demo/parser/
context.rs

1//! Parser context containing the current replay state.
2//!
3//! The [`Context`] struct maintains all the state information about the replay
4//! as it's being parsed, including entities, string tables, game events, and
5//! the current tick.
6
7use crate::entity::field::*;
8use crate::entity::*;
9use crate::event::*;
10use crate::string_table::*;
11use crate::{FieldValue, HashMap};
12use source2_demo_protobufs::CDemoFileInfo;
13use std::rc::Rc;
14
15/// Current replay state accessible to observers.
16///
17/// The context is passed to all observer callbacks and provides access to:
18/// - Entity state and properties
19/// - String tables
20/// - Game event definitions
21/// - Current tick and timing information
22/// - Replay metadata
23///
24/// # Examples
25///
26/// ## Accessing entities
27///
28/// ```no_run
29/// use source2_demo::prelude::*;
30///
31/// #[derive(Default)]
32/// struct HeroStats;
33///
34/// #[observer]
35/// #[uses_entities]
36/// impl HeroStats {
37///     #[on_tick_start]
38///     fn on_tick_start(&mut self, ctx: &Context) -> ObserverResult {
39///         for entity in ctx.entities().iter() {
40///             if entity.class().name().starts_with("CDOTA_Unit_Hero_") {
41///                 let health: i32 = property!(entity, "m_iHealth");
42///                 println!("{}: {}", entity.class().name(), health);
43///             }
44///         }
45///         Ok(())
46///     }
47/// }
48/// ```
49///
50/// ## Accessing string tables
51///
52/// ```no_run
53/// use source2_demo::prelude::*;
54///
55/// #[derive(Default)]
56/// struct TableReader;
57///
58/// #[observer]
59/// #[uses_string_tables]
60/// impl TableReader {
61///     #[on_tick_start]
62///     fn on_tick_start(&mut self, ctx: &Context) -> ObserverResult {
63///         if let Ok(table) = ctx.string_tables().get_by_name("ActiveModifiers") {
64///             println!("Active modifiers: {}", table.iter().count());
65///         }
66///         Ok(())
67///     }
68/// }
69/// ```
70pub struct Context {
71    pub(crate) classes: Classes,
72    pub(crate) entities: Entities,
73    pub(crate) string_tables: StringTables,
74    pub(crate) game_events: GameEventList,
75
76    pub(crate) tick: u32,
77    pub(crate) previous_tick: u32,
78    pub(crate) net_tick: u32,
79
80    pub(crate) game_build: u32,
81    pub(crate) replay_info: CDemoFileInfo,
82
83    pub(crate) baselines: BaselineContainer,
84    pub(crate) serializers: HashMap<Box<str>, Rc<Serializer>>,
85    pub(crate) last_full_packet_tick: u32,
86}
87
88impl Default for Context {
89    fn default() -> Self {
90        Context {
91            classes: Classes::default(),
92            entities: Entities::default(),
93            string_tables: StringTables::default(),
94            game_events: Default::default(),
95            tick: u32::MAX,
96            previous_tick: u32::MAX,
97            net_tick: u32::MAX,
98            game_build: 0,
99            replay_info: CDemoFileInfo::default(),
100            baselines: BaselineContainer::default(),
101            serializers: HashMap::default(),
102            last_full_packet_tick: u32::MAX,
103        }
104    }
105}
106
107impl Context {
108    pub(crate) fn new(replay_info: CDemoFileInfo) -> Self {
109        Context {
110            classes: Classes::default(),
111            entities: Entities::default(),
112            string_tables: StringTables::default(),
113            game_events: GameEventList::default(),
114            tick: u32::MAX,
115            previous_tick: u32::MAX,
116            net_tick: u32::MAX,
117            game_build: 0,
118            replay_info,
119            baselines: BaselineContainer::default(),
120            serializers: HashMap::default(),
121            last_full_packet_tick: u32::MAX,
122        }
123    }
124}
125
126impl Context {
127    /// Returns a reference to the entity container.
128    ///
129    /// Use this to access all entities in the game and query their properties.
130    ///
131    /// # Examples
132    ///
133    /// ```no_run
134    /// use source2_demo::prelude::*;
135    ///
136    /// # fn example(ctx: &Context) -> anyhow::Result<()> {
137    /// // Get entity by index
138    /// let entity = ctx.entities().get_by_index(0)?;
139    ///
140    /// // Find entity by class name
141    /// let player_resource = ctx.entities().get_by_class_name("CDOTA_PlayerResource")?;
142    ///
143    /// // Iterate all entities
144    /// for entity in ctx.entities().iter() {
145    ///     println!("{}", entity.class().name());
146    /// }
147    /// # Ok(())
148    /// # }
149    /// ```
150    pub fn classes(&self) -> &Classes {
151        &self.classes
152    }
153
154    /// Returns a reference to the entity container.
155    ///
156    /// Provides access to all game entities and their properties.
157    /// Requires `Interests::ENTITY_STATE` to be populated.
158    pub fn entities(&self) -> &Entities {
159        &self.entities
160    }
161
162    /// Returns a reference to the string tables.
163    ///
164    /// String tables contain game data like hero names, item names, etc.
165    /// Requires `Interests::STRING_TABLE_STATE` to be populated.
166    pub fn string_tables(&self) -> &StringTables {
167        &self.string_tables
168    }
169
170    /// Returns a reference to the game event list.
171    ///
172    /// Contains definitions for all game events in the replay.
173    pub fn game_events(&self) -> &GameEventList {
174        &self.game_events
175    }
176
177    /// Returns the current tick number.
178    ///
179    /// The tick represents the current game simulation step.
180    /// Typically runs at 30 ticks per second.
181    pub fn tick(&self) -> u32 {
182        self.tick
183    }
184
185    /// Returns the previous tick number.
186    ///
187    /// This is the last tick observed before the current tick advanced.
188    pub fn previous_tick(&self) -> u32 {
189        self.previous_tick
190    }
191
192    /// Returns the current network tick.
193    ///
194    /// The network tick from the last processed packet.
195    pub fn net_tick(&self) -> u32 {
196        self.net_tick
197    }
198
199    /// Returns the game build number.
200    ///
201    /// Identifies the specific version of the game that created this replay.
202    pub fn game_build(&self) -> u32 {
203        self.game_build
204    }
205
206    /// Returns replay file metadata.
207    ///
208    /// Contains information about the replay including duration, map, and game
209    /// info.
210    pub fn replay_info(&self) -> &CDemoFileInfo {
211        &self.replay_info
212    }
213
214    /// Returns the baseline entity classes currently known to the parser.
215    ///
216    /// Baselines are keyed by entity class id rather than live entity index.
217    /// This method is primarily useful for replay inspection and debugging
218    /// tools.
219    pub fn baseline_entities(&self) -> Vec<BaselineEntity> {
220        let mut entities = self
221            .baselines
222            .states
223            .keys()
224            .filter_map(|&class_id| {
225                self.classes
226                    .get_by_id(class_id as usize)
227                    .ok()
228                    .map(|class| BaselineEntity {
229                        class_id,
230                        class_name: class.name().to_string(),
231                    })
232            })
233            .collect::<Vec<_>>();
234
235        entities.sort_by_key(|entity| entity.class_id);
236        entities
237    }
238
239    /// Returns all fields for a baseline entity class id.
240    ///
241    /// Returns `None` if the baseline or matching class serializer is not
242    /// present in the current parser context.
243    pub fn baseline_fields(&self, class_id: i32) -> Option<Vec<EntityField<'_>>> {
244        let state = self.baselines.states.get(&class_id)?;
245        let class = self.classes.get_by_id(class_id as usize).ok()?;
246        class
247            .serializer
248            .get_paths(&mut FieldPath::default(), state)
249            .into_iter()
250            .map(|fp| {
251                let value = state.get_value(&fp);
252                EntityField {
253                    path: (0..=fp.last).map(|idx| fp.path[idx]).collect(),
254                    name: class.serializer.get_name(&fp).to_string(),
255                    field_type: class.serializer.get_type(&fp).to_string(),
256                    decoded_type: value.map(FieldValue::type_name),
257                    value,
258                }
259            })
260            .collect::<Vec<_>>()
261            .into()
262    }
263}