source2_demo/parser/mod.rs
1mod context;
2mod demo;
3mod observer;
4
5pub use context::*;
6pub use demo::runner::*;
7pub use demo::writer::*;
8pub use observer::*;
9
10use crate::error::*;
11use crate::proto::*;
12use crate::reader::*;
13use std::cell::RefCell;
14use std::rc::Rc;
15
16use crate::parser::demo::DemoCommands;
17use crate::try_observers;
18#[cfg(feature = "dota")]
19use std::collections::VecDeque;
20
21/// Main parser for Source 2 demo files.
22///
23/// The parser maintains the replay state and processes demo commands
24/// sequentially. It supports multiple observers that can react to different
25/// types of events.
26///
27/// # Examples
28///
29/// ## Basic usage with chat messages
30///
31/// ```ignore
32/// use source2_demo::prelude::*;
33///
34/// #[derive(Default)]
35/// struct ChatLogger;
36///
37/// #[observer]
38/// impl ChatLogger {
39/// #[on_message]
40/// fn on_chat(&mut self, ctx: &Context, msg: CDotaUserMsgChatMessage) -> ObserverResult {
41/// println!("{}", msg.message_text());
42/// Ok(())
43/// }
44/// }
45///
46/// fn main() -> anyhow::Result<()> {
47/// let replay = std::fs::File::open("replay.dem")?;
48///
49/// let mut parser = Parser::from_reader(&replay)?;
50/// parser.register_observer::<ChatLogger>();
51/// parser.run_to_end()?;
52///
53/// Ok(())
54/// }
55/// ```
56///
57/// ## Processing entities
58///
59/// ```no_run
60/// use source2_demo::prelude::*;
61///
62/// #[derive(Default)]
63/// struct HeroTracker;
64///
65/// impl Observer for HeroTracker {
66/// fn interests(&self) -> Interests {
67/// Interests::ENTITY_STATE | Interests::ENTITY_EVENTS
68/// }
69///
70/// fn on_entity(
71/// &mut self,
72/// ctx: &Context,
73/// event: EntityEvents,
74/// entity: &Entity,
75/// ) -> ObserverResult {
76/// if entity.class().name().starts_with("CDOTA_Unit_Hero_") {
77/// let health: i32 = property!(entity, "m_iHealth");
78/// println!("Hero {} health: {}", entity.class().name(), health);
79/// }
80/// Ok(())
81/// }
82/// }
83/// # fn main() {}
84/// ```
85pub struct Parser<'a, R = SliceReader<'a>>
86where
87 R: BitsReader + MessageReader,
88{
89 pub(crate) reader: R,
90 pub(crate) field_reader: FieldReader,
91
92 pub(crate) observers: Vec<Box<dyn Observer + 'a>>,
93 pub(crate) observer_masks: Vec<Interests>,
94 pub(crate) global_mask: Interests,
95
96 #[cfg(feature = "dota")]
97 pub(crate) combat_log: VecDeque<CMsgDotaCombatLogEntry>,
98
99 pub(crate) prologue_completed: bool,
100 pub(crate) skip_deltas: bool,
101
102 pub(crate) replay_info: CDemoFileInfo,
103 pub(crate) last_tick: u32,
104 pub(crate) context: Context,
105
106 _phantom: std::marker::PhantomData<&'a ()>,
107}
108
109impl<'a> Parser<'a, SliceReader<'a>> {
110 /// Creates a new parser instance from replay bytes.
111 ///
112 /// This method validates the replay file format and reads the file header.
113 /// The replay data should remain valid for the lifetime of the parser.
114 ///
115 /// # Arguments
116 ///
117 /// * `replay` - Byte slice containing the demo file data (typically
118 /// memory-mapped)
119 ///
120 /// # Errors
121 ///
122 /// Returns [`ParserError::WrongMagic`] if the file is not a valid Source 2
123 /// demo file. Returns [`ParserError::ReplayEncodingError`] if the file
124 /// header is corrupted.
125 ///
126 /// # Examples
127 ///
128 /// ```ignore
129 /// use source2_demo::prelude::*;
130 /// use std::fs::File;
131 ///
132 /// # fn main() -> anyhow::Result<()> {
133 /// // Using memory-mapped file (recommended for large files)
134 /// let file = File::open("replay.dem")?;
135 /// let replay = unsafe { memmap2::Mmap::map(&file)? };
136 /// let parser = Parser::new(&replay)?;
137 ///
138 /// // Or read into memory (for small files)
139 /// let replay = std::fs::read("replay.dem")?;
140 /// let parser = Parser::new(&replay)?;
141 /// # Ok(())
142 /// # }
143 /// ```
144 pub fn new(replay: &'a [u8]) -> Result<Self, ParserError> {
145 let mut reader = SliceReader::new(replay);
146
147 if replay.len() < 16 || reader.read_bytes(8) != b"PBDEMS2\0" {
148 return Err(ParserError::WrongMagic);
149 };
150
151 reader.read_bytes(8);
152
153 let replay_info = reader.read_replay_info()?;
154 let last_tick = replay_info.playback_ticks() as u32;
155
156 reader.seek(16);
157
158 Ok(Parser {
159 reader,
160 field_reader: FieldReader::default(),
161
162 observers: Vec::default(),
163 observer_masks: Vec::default(),
164 global_mask: Interests::empty(),
165
166 #[cfg(feature = "dota")]
167 combat_log: VecDeque::default(),
168
169 prologue_completed: false,
170 skip_deltas: false,
171
172 context: Context::new(replay_info.clone()),
173
174 replay_info,
175 last_tick,
176 _phantom: std::marker::PhantomData,
177 })
178 }
179
180 /// Creates a new parser from replay bytes (same as `new`).
181 ///
182 /// This is an alias for [`Parser::new`] that makes it explicit if you're
183 /// using a slice.
184 ///
185 /// # Arguments
186 ///
187 /// * `replay` - Byte slice containing the demo file data
188 ///
189 /// # Errors
190 ///
191 /// Returns [`ParserError::WrongMagic`] if the file is not a valid Source 2
192 /// demo file.
193 #[inline]
194 pub fn from_slice(replay: &'a [u8]) -> Result<Self, ParserError> {
195 Self::new(replay)
196 }
197}
198
199impl<S> Parser<'static, SeekableReader<S>>
200where
201 S: std::io::Read + std::io::Seek,
202{
203 /// Creates a new parser from a reader.
204 ///
205 /// Uses SeekableReader for reading data from the reader, but internally
206 /// uses SliceReader for parsing message buffers for maximum
207 /// performance.
208 ///
209 /// # Arguments
210 ///
211 /// * `reader` - Any type implementing Read + Seek (e.g., File, Cursor,
212 /// BufReader)
213 ///
214 /// # Errors
215 ///
216 /// Returns an error if reading from the reader fails or data is invalid.
217 ///
218 /// # Examples
219 ///
220 /// ```ignore
221 /// use source2_demo::prelude::*;
222 /// use std::fs::File;
223 ///
224 /// # fn main() -> anyhow::Result<()> {
225 /// let file = File::open("replay.dem")?;
226 /// let mut parser = Parser::from_reader(file)?;
227 /// parser.run_to_end()?;
228 /// # Ok(())
229 /// # }
230 /// ```
231 pub fn from_reader(reader: S) -> Result<Self, ParserError> {
232 let mut reader =
233 SeekableReader::new(reader).map_err(|e| ParserError::IoError(e.to_string()))?;
234
235 let magic = reader.read_bytes(8);
236 if magic != b"PBDEMS2\0" {
237 return Err(ParserError::WrongMagic);
238 }
239
240 reader.read_bytes(8);
241
242 let replay_info = Self::read_file_info_from_reader(&mut reader)?;
243 let last_tick = replay_info.playback_ticks() as u32;
244
245 reader.seek(16);
246
247 Ok(Parser {
248 reader,
249 field_reader: FieldReader::default(),
250 observers: Vec::default(),
251 observer_masks: Vec::default(),
252 global_mask: Interests::empty(),
253
254 #[cfg(feature = "dota")]
255 combat_log: VecDeque::default(),
256
257 prologue_completed: false,
258 skip_deltas: false,
259
260 context: Context::new(replay_info.clone()),
261
262 replay_info,
263 last_tick,
264 _phantom: std::marker::PhantomData,
265 })
266 }
267
268 fn read_file_info_from_reader(
269 reader: &mut SeekableReader<S>,
270 ) -> Result<CDemoFileInfo, ParserError> {
271 reader.seek(8);
272 let offset_bytes = reader.read_bytes(4);
273 let offset = u32::from_le_bytes([
274 offset_bytes[0],
275 offset_bytes[1],
276 offset_bytes[2],
277 offset_bytes[3],
278 ]) as usize;
279
280 reader.seek(offset);
281
282 if let Some(msg) = reader.read_next_message()? {
283 Ok(CDemoFileInfo::decode(msg.buf.as_slice())?)
284 } else {
285 Err(ParserError::ReplayEncodingError)
286 }
287 }
288}
289
290impl<'a, R> Parser<'a, R>
291where
292 R: BitsReader + MessageReader,
293{
294 /// Returns a reference to the current parser context.
295 ///
296 /// The context contains the current state of the replay, including
297 /// - Entities and their properties
298 /// - String tables
299 /// - Game events
300 /// - Current tick and game build
301 ///
302 /// # Examples
303 ///
304 /// ```ignore
305 /// use source2_demo::prelude::*;
306 ///
307 /// # fn main() -> anyhow::Result<()> {
308 /// # let replay = std::fs::File::open("replay.dem")?;
309 /// let parser = Parser::from_reader(&replay)?;
310 /// let ctx = parser.context();
311 /// println!("Current tick: {}", ctx.tick());
312 /// println!("Game build: {}", ctx.game_build());
313 /// # Ok(())
314 /// # }
315 /// ```
316 pub fn context(&self) -> &Context {
317 &self.context
318 }
319
320 /// Returns replay file information.
321 /// Contains metadata about the replay including:
322 /// - Playback duration
323 /// - Server information
324 /// - Game-specific details
325 ///
326 /// # Examples
327 ///
328 /// ```ignore
329 /// use source2_demo::prelude::*;
330 ///
331 /// # fn main() -> anyhow::Result<()> {
332 /// # let replay = std::fs::File::open("replay.dem")?;
333 /// let parser = Parser::from_reader(&replay)?;
334 /// let info = parser.replay_info();
335 /// println!("Playback ticks: {}", info.playback_ticks());
336 /// # Ok(())
337 /// # }
338 /// ```
339 pub fn replay_info(&self) -> &CDemoFileInfo {
340 &self.replay_info
341 }
342
343 /// Registers an observer and returns a reference-counted handle to it.
344 ///
345 /// Observers must implement the [`Observer`] trait and [`Default`].
346 /// Use the `#[observer]` attribute macro to automatically implement the
347 /// trait.
348 ///
349 /// The returned `Rc<RefCell<T>>` allows you to access the observer's state
350 /// after parsing completes.
351 ///
352 /// # Type Parameters
353 ///
354 /// * `T` - Observer type that implements [`Observer`] and [`Default`]
355 ///
356 /// # Examples
357 ///
358 /// ```ignore
359 /// use source2_demo::prelude::*;
360 /// use std::cell::RefCell;
361 /// use std::rc::Rc;
362 ///
363 /// #[derive(Default)]
364 /// struct Stats {
365 /// message_count: usize,
366 /// }
367 ///
368 /// #[observer]
369 /// impl Stats {
370 /// #[on_message]
371 /// fn on_chat(&mut self, ctx: &Context, msg: CDotaUserMsgChatMessage) -> ObserverResult {
372 /// self.message_count += 1;
373 /// Ok(())
374 /// }
375 /// }
376 ///
377 /// # fn main() -> anyhow::Result<()> {
378 /// # let replay = std::fs::File::open("replay.dem")?;
379 /// let mut parser = Parser::from_reader(&replay)?;
380 /// let stats = parser.register_observer::<Stats>();
381 /// parser.run_to_end()?;
382 ///
383 /// println!("Total messages: {}", stats.borrow().message_count);
384 /// # Ok(())
385 /// # }
386 /// ```
387 pub fn register_observer<T>(&mut self) -> Rc<RefCell<T>>
388 where
389 T: Observer + Default + 'a,
390 {
391 self.add_observer(T::default())
392 }
393
394 /// Adds an already constructed observer and returns a handle to its state.
395 ///
396 /// Use this when the observer needs custom constructor state. Observers run
397 /// in registration order.
398 pub fn add_observer<T>(&mut self, observer: T) -> Rc<RefCell<T>>
399 where
400 T: Observer + 'a,
401 {
402 let rc = Rc::new(RefCell::new(observer));
403 let mask = rc.borrow().interests();
404 self.global_mask |= mask;
405 self.observer_masks.push(mask);
406 self.observers.push(Box::new(rc.clone()));
407 rc
408 }
409
410 #[inline]
411 fn anyone_interested(&self, flag: Interests) -> bool {
412 self.global_mask.intersects(flag)
413 }
414
415 pub(crate) fn prologue(&mut self) -> Result<(), ParserError> {
416 if self.prologue_completed && self.context.tick != u32::MAX {
417 return Ok(());
418 }
419
420 while let Some(message) = self.reader.read_next_message()? {
421 if self.prologue_completed
422 && (message.msg_type == EDemoCommands::DemSendTables
423 || message.msg_type == EDemoCommands::DemClassInfo)
424 {
425 continue;
426 }
427
428 self.on_demo_command(message.msg_type, message.buf.as_slice())?;
429
430 if message.msg_type == EDemoCommands::DemSyncTick {
431 self.prologue_completed = true;
432 break;
433 }
434 }
435
436 Ok(())
437 }
438
439 pub(crate) fn on_demo_command(
440 &mut self,
441 msg_type: EDemoCommands,
442 msg: &[u8],
443 ) -> Result<(), ParserError> {
444 match msg_type {
445 EDemoCommands::DemSendTables => {
446 self.dem_send_tables(CDemoSendTables::decode(msg)?)?;
447 }
448 EDemoCommands::DemClassInfo => {
449 self.dem_class_info(CDemoClassInfo::decode(msg)?)?;
450 }
451 EDemoCommands::DemPacket | EDemoCommands::DemSignonPacket => {
452 self.dem_packet(CDemoPacket::decode(msg)?)?;
453 }
454 EDemoCommands::DemFullPacket => self.dem_full_packet(CDemoFullPacket::decode(msg)?)?,
455 EDemoCommands::DemStringTables => {
456 self.dem_string_tables(CDemoStringTables::decode(msg)?)?
457 }
458 EDemoCommands::DemStop => {
459 self.dem_stop()?;
460 }
461 _ => {}
462 };
463
464 try_observers!(
465 self,
466 DEMO_MESSAGE,
467 on_demo_command(&self.context, msg_type, msg)
468 )?;
469 Ok(())
470 }
471}
472
473impl<S> Parser<'static, SeekableReader<S>>
474where
475 S: std::io::Read + std::io::Seek,
476{
477 /// Extracts match details from a Deadlock replay.
478 ///
479 /// This method scans through the replay to find and extract post-match
480 /// details specific to Deadlock games. It searches for the
481 /// `KEUserMsgPostMatchDetails` message and returns the decoded match
482 /// metadata.
483 ///
484 /// # Errors
485 ///
486 /// Returns `ParserError::MatchDetailsNotFound` if the match details message
487 /// cannot be found in the replay.
488 ///
489 /// # Examples
490 ///
491 /// ```no_run
492 /// use source2_demo::prelude::*;
493 ///
494 /// # fn main() -> anyhow::Result<()> {
495 /// let replay = std::fs::File::open("deadlock_replay.dem")?;
496 /// let mut parser = Parser::from_reader(&replay)?;
497 /// let match_details = parser.deadlock_match_details()?;
498 /// println!("Match info available: {}", match_details.match_info.is_some());
499 ///
500 /// Ok(())
501 /// }
502 /// ```
503 #[cfg(feature = "deadlock")]
504 pub fn deadlock_match_details(&mut self) -> Result<CMsgMatchMetaDataContents, ParserError> {
505 self.reader.read_deadlock_match_details()
506 }
507}
508
509impl<'a> Parser<'a, SliceReader<'a>> {
510 /// Extracts match details from a Deadlock replay.
511 ///
512 /// This method scans through the replay to find and extract post-match
513 /// details specific to Deadlock games. It searches for the
514 /// `KEUserMsgPostMatchDetails` message and returns the decoded match
515 /// metadata.
516 ///
517 /// # Errors
518 ///
519 /// Returns `ParserError::MatchDetailsNotFound` if the match details message
520 /// cannot be found in the replay.
521 ///
522 /// # Examples
523 ///
524 /// ```no_run
525 /// use source2_demo::prelude::*;
526 ///
527 /// # fn main() -> anyhow::Result<()> {
528 /// let replay = std::fs::read("deadlock_replay.dem")?;
529 /// let mut parser = Parser::new(&replay)?;
530 /// let match_details = parser.deadlock_match_details()?;
531 /// println!("Match info available: {}", match_details.match_info.is_some());
532 ///
533 /// Ok(())
534 /// }
535 /// ```
536 #[cfg(feature = "deadlock")]
537 pub fn deadlock_match_details(&mut self) -> Result<CMsgMatchMetaDataContents, ParserError> {
538 self.reader.read_deadlock_match_details()
539 }
540}