Skip to main content

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}