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}