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