Skip to main content

boon/demo/
parser.rs

1use std::path::Path;
2
3use memmap2::Mmap;
4use prost::Message;
5
6use crate::entity::{
7    ClassInfo, EntityContainer, FieldDecodeContext, SerializerContainer, StringTableContainer,
8};
9use crate::error::{Error, Result};
10use crate::io::{BitReader, ByteReader};
11
12use std::collections::HashMap;
13
14use super::command::{self, CmdHeader, dem, ge, svc};
15
16use boon_proto::proto::{
17    CDemoClassInfo, CDemoFileHeader, CDemoFileInfo, CDemoFullPacket, CDemoPacket, CDemoSendTables,
18    CMsgSource1LegacyGameEvent, CMsgSource1LegacyGameEventList, CitadelUserMessageIds,
19    CsvcMsgCreateStringTable, CsvcMsgPacketEntities, CsvcMsgServerInfo, CsvcMsgUpdateStringTable,
20    CsvcMsgUserMessage, EBaseUserMessages, ECitadelGameEvents,
21};
22
23/// Magic bytes at the start of every Source 2 demo file.
24const MAGIC: &[u8; 8] = b"PBDEMS2\0";
25/// File header: 8 bytes magic + 4 bytes fileinfo_offset + 4 bytes spawngroups_offset.
26const HEADER_SIZE: usize = 16;
27
28/// Default tick rate (1/30 s). Used to compute `full_packet_interval` when
29/// `CSVCMsg_ServerInfo.tick_interval` is not yet available.
30const DEFAULT_TICK_INTERVAL: f32 = 1.0 / 30.0;
31/// Default number of ticks between full-packet snapshots (at 30 Hz).
32const DEFAULT_FULL_PACKET_INTERVAL: i32 = 1800;
33
34/// Scratch buffer size for decompressed command bodies and packet payloads.
35const BUF_SIZE: usize = 2 * 1024 * 1024;
36
37/// Information about a demo message in the command stream.
38#[derive(Debug, Clone, serde::Serialize)]
39pub struct MessageInfo {
40    /// Zero-based ordinal position in the command stream.
41    pub index: usize,
42    /// Command type (one of the `dem::*` constants).
43    pub cmd: i32,
44    /// Human-readable command name.
45    pub cmd_name: String,
46    /// Game tick this command applies to.
47    pub tick: i32,
48    /// Whether the body is Snappy-compressed.
49    pub compressed: bool,
50    /// Body size in bytes (before decompression).
51    pub body_size: u32,
52    /// Absolute byte offset from the start of the file.
53    pub offset: usize,
54}
55
56/// Full parser context after initialization.
57///
58/// Holds all decoded game state: serializers, class definitions, string
59/// tables, and live entities. Returned by [`Parser::parse_init`],
60/// [`Parser::parse_to_tick`], and updated incrementally during
61/// [`Parser::run_to_end`].
62pub struct Context {
63    /// Field definitions for every entity class.
64    pub serializers: SerializerContainer,
65    /// Maps numeric class IDs to network names.
66    pub class_info: ClassInfo,
67    /// Key-value tables (models, sounds, instance baselines, etc.).
68    pub string_tables: StringTableContainer,
69    /// Currently active entities keyed by entity index.
70    pub entities: EntityContainer,
71    /// Seconds per tick (from `CSVCMsg_ServerInfo`).
72    pub tick_interval: f32,
73    /// Ticks between full-packet snapshots (derived from tick_interval).
74    pub full_packet_interval: i32,
75    /// Most recent tick processed.
76    pub tick: i32,
77}
78
79/// A game event extracted from the demo.
80#[derive(Debug, Clone, serde::Serialize)]
81pub struct GameEvent {
82    /// Game tick at which this event occurred.
83    pub tick: i32,
84    /// Human-readable event name (e.g. `"player_death"`, `"k_ECitadelUserMsg_Damage"`).
85    pub name: String,
86    /// Numeric message type from the packet stream.
87    pub msg_type: u32,
88    /// Key-value pairs for Source 1 legacy game events; empty for user messages.
89    pub keys: Vec<(String, String)>,
90    /// Raw protobuf bytes of the event. Use [`crate::decode_event_payload`] to decode.
91    #[serde(skip)]
92    pub payload: Vec<u8>,
93}
94
95struct EventDescriptor {
96    name: String,
97    field_names: Vec<String>,
98}
99
100fn format_event_key(key: &boon_proto::proto::c_msg_source1_legacy_game_event::KeyT) -> String {
101    if let Some(ref s) = key.val_string {
102        return s.clone();
103    }
104    if let Some(f) = key.val_float {
105        return f.to_string();
106    }
107    if let Some(l) = key.val_long {
108        return l.to_string();
109    }
110    if let Some(s) = key.val_short {
111        return s.to_string();
112    }
113    if let Some(b) = key.val_byte {
114        return b.to_string();
115    }
116    if let Some(b) = key.val_bool {
117        return b.to_string();
118    }
119    if let Some(u) = key.val_uint64 {
120        return u.to_string();
121    }
122    String::new()
123}
124
125/// Internal storage for demo data — either memory-mapped or an owned byte buffer.
126enum Storage {
127    Mmap(Mmap),
128    Bytes(Vec<u8>),
129}
130
131impl AsRef<[u8]> for Storage {
132    fn as_ref(&self) -> &[u8] {
133        match self {
134            Storage::Mmap(m) => m,
135            Storage::Bytes(b) => b,
136        }
137    }
138}
139
140/// The main parser. Owns the demo file data (memory-mapped or in-memory).
141pub struct Parser {
142    storage: Storage,
143}
144
145impl Parser {
146    /// Open a demo file and memory-map it for zero-copy parsing.
147    pub fn from_file(path: &Path) -> Result<Self> {
148        let file = std::fs::File::open(path)?;
149        // SAFETY: The file is opened read-only and the mapping lives as
150        // long as the Parser.  Undefined behavior can occur if an external
151        // process truncates or modifies the file while mapped; callers must
152        // ensure the file is not concurrently mutated.
153        let mmap = unsafe { Mmap::map(&file)? };
154        Ok(Self {
155            storage: Storage::Mmap(mmap),
156        })
157    }
158
159    /// Create a parser from an in-memory byte buffer.
160    ///
161    /// This is useful for testing, WASM targets (where mmap is unavailable),
162    /// or when the demo data has already been loaded into memory.
163    pub fn from_bytes(bytes: Vec<u8>) -> Self {
164        Self {
165            storage: Storage::Bytes(bytes),
166        }
167    }
168
169    /// Returns the raw demo data.
170    fn data(&self) -> &[u8] {
171        self.storage.as_ref()
172    }
173
174    /// Verify magic bytes.
175    /// Verify that the file has valid demo magic bytes.
176    pub fn verify(&self) -> Result<()> {
177        if self.data().len() < HEADER_SIZE {
178            return Err(Error::Parse {
179                context: "file too small for demo header".into(),
180            });
181        }
182
183        let mut magic = [0u8; 8];
184        magic.copy_from_slice(&self.data()[0..8]);
185        if &magic != MAGIC {
186            return Err(Error::InvalidMagic { got: magic });
187        }
188
189        Ok(())
190    }
191
192    fn read_cmd_header(reader: &mut ByteReader) -> Result<CmdHeader> {
193        let raw_cmd = reader.read_uvarint32()?;
194        let compress_flag = dem::IS_COMPRESSED;
195        let compressed = (raw_cmd & compress_flag) != 0;
196        let cmd = (raw_cmd & !compress_flag) as i32;
197        let tick_raw = reader.read_uvarint32()?;
198        let tick = tick_raw as i32;
199        let body_size = reader.read_uvarint32()?;
200        Ok(CmdHeader {
201            cmd,
202            tick,
203            compressed,
204            body_size,
205        })
206    }
207
208    /// Read and decompress a command body into the provided buffer.
209    /// The buffer is resized as needed and can be reused across calls.
210    fn read_cmd_body(reader: &mut ByteReader, header: &CmdHeader, buf: &mut Vec<u8>) -> Result<()> {
211        let raw = reader.read_bytes(header.body_size as usize)?;
212        if header.compressed {
213            let decompressed_len =
214                snap::raw::decompress_len(raw).map_err(|e| Error::Decompress(e.to_string()))?;
215            buf.clear();
216            buf.resize(decompressed_len, 0);
217            snap::raw::Decoder::new()
218                .decompress(raw, buf)
219                .map_err(|e| Error::Decompress(e.to_string()))?;
220        } else {
221            buf.clear();
222            buf.extend_from_slice(raw);
223        }
224        Ok(())
225    }
226
227    /// Iterate all commands and return metadata about each.
228    /// Continues past DEM_Stop to capture DEM_FileInfo.
229    pub fn messages(&self) -> Result<Vec<MessageInfo>> {
230        self.verify()?;
231        let data = &self.data()[HEADER_SIZE..];
232        let mut reader = ByteReader::new(data);
233        let mut messages = Vec::new();
234        let mut index = 0;
235
236        while reader.remaining() > 0 {
237            let offset = reader.position() + HEADER_SIZE;
238            let header = match Self::read_cmd_header(&mut reader) {
239                Ok(h) => h,
240                Err(_) => break,
241            };
242
243            messages.push(MessageInfo {
244                index,
245                cmd: header.cmd,
246                cmd_name: command::command_name(header.cmd).to_string(),
247                tick: header.tick,
248                compressed: header.compressed,
249                body_size: header.body_size,
250                offset,
251            });
252
253            // DEM_Stop has no body, and DEM_FileInfo follows it
254            if header.cmd == dem::STOP {
255                index += 1;
256                continue;
257            }
258
259            // DEM_FileInfo comes after DEM_Stop; once we've read it, we're done
260            if header.cmd == dem::FILE_INFO {
261                reader.skip(header.body_size as usize).ok();
262                break;
263            }
264
265            if reader.skip(header.body_size as usize).is_err() {
266                break;
267            }
268
269            index += 1;
270        }
271
272        Ok(messages)
273    }
274
275    /// Find and decode the CDemoFileHeader message.
276    pub fn file_header(&self) -> Result<CDemoFileHeader> {
277        self.verify()?;
278        let data = &self.data()[HEADER_SIZE..];
279        let mut reader = ByteReader::new(data);
280        let mut body_buf = Vec::with_capacity(BUF_SIZE);
281
282        while reader.remaining() > 0 {
283            let header = Self::read_cmd_header(&mut reader)?;
284
285            if header.cmd == dem::FILE_HEADER {
286                Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
287                return CDemoFileHeader::decode(&body_buf[..]).map_err(Error::from);
288            }
289
290            if header.cmd == dem::STOP {
291                break;
292            }
293
294            reader.skip(header.body_size as usize)?;
295        }
296
297        Err(Error::Parse {
298            context: "DEM_FileHeader not found".into(),
299        })
300    }
301
302    /// Decode CDemoFileInfo using the offset stored in the file header.
303    pub fn file_info(&self) -> Result<CDemoFileInfo> {
304        self.verify()?;
305
306        // Bytes 8..12 of the file header contain the absolute offset to DEM_FileInfo.
307        let fileinfo_offset = u32::from_le_bytes([
308            self.data()[8],
309            self.data()[9],
310            self.data()[10],
311            self.data()[11],
312        ]) as usize;
313
314        let data = &self.data()[HEADER_SIZE..];
315        let mut reader = ByteReader::new(data);
316        // The offset is relative to the start of the file; adjust for the header we sliced off.
317        reader.seek(fileinfo_offset.saturating_sub(HEADER_SIZE))?;
318
319        let header = Self::read_cmd_header(&mut reader)?;
320        if header.cmd != dem::FILE_INFO {
321            return Err(Error::Parse {
322                context: format!(
323                    "expected DEM_FileInfo at offset {}, found command {}",
324                    fileinfo_offset, header.cmd
325                ),
326            });
327        }
328
329        let mut body_buf = Vec::new();
330        Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
331        CDemoFileInfo::decode(&body_buf[..]).map_err(Error::from)
332    }
333
334    /// Parse game events from the demo.
335    ///
336    /// Extracts Source 1 legacy game events and Citadel user messages from
337    /// `DEM_Packet`, `DEM_SignonPacket`, and `DEM_FullPacket` commands.
338    /// If `max_tick` is set, stops parsing once the tick exceeds the limit.
339    pub fn events(&self, max_tick: Option<i32>) -> Result<Vec<GameEvent>> {
340        self.verify()?;
341        let data = &self.data()[HEADER_SIZE..];
342        let mut reader = ByteReader::new(data);
343        let mut body_buf = Vec::with_capacity(BUF_SIZE);
344        let mut packet_buf = vec![0u8; BUF_SIZE];
345        let mut events = Vec::new();
346        let mut descriptors: HashMap<i32, EventDescriptor> = HashMap::new();
347
348        while reader.remaining() > 0 {
349            let header = match Self::read_cmd_header(&mut reader) {
350                Ok(h) => h,
351                Err(_) => break,
352            };
353
354            if header.cmd == dem::STOP {
355                break;
356            }
357
358            if let Some(max) = max_tick
359                && header.tick > max
360            {
361                break;
362            }
363
364            match header.cmd {
365                dem::PACKET | dem::SIGNON_PACKET => {
366                    Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
367                    let cmd = CDemoPacket::decode(&body_buf[..])?;
368                    let pkt_data = cmd.data.unwrap_or_default();
369                    Self::process_packet_events(
370                        &pkt_data,
371                        header.tick,
372                        &mut descriptors,
373                        &mut events,
374                        &mut packet_buf,
375                    )?;
376                }
377                dem::FULL_PACKET => {
378                    Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
379                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
380                    if let Some(packet) = cmd.packet {
381                        let pkt_data = packet.data.unwrap_or_default();
382                        Self::process_packet_events(
383                            &pkt_data,
384                            header.tick,
385                            &mut descriptors,
386                            &mut events,
387                            &mut packet_buf,
388                        )?;
389                    }
390                }
391                _ => {
392                    reader.skip(header.body_size as usize)?;
393                }
394            }
395        }
396
397        Ok(events)
398    }
399
400    /// Process a packet's inner messages for game events.
401    fn process_packet_events(
402        pkt_data: &[u8],
403        tick: i32,
404        descriptors: &mut HashMap<i32, EventDescriptor>,
405        events: &mut Vec<GameEvent>,
406        packet_buf: &mut Vec<u8>,
407    ) -> Result<()> {
408        let mut br = BitReader::new(pkt_data);
409
410        while br.bits_remaining() > 8 {
411            let msg_type = br.read_ubitvar()?;
412            let size = br.read_uvarint32()? as usize;
413
414            if size > packet_buf.len() {
415                packet_buf.resize(size, 0);
416            }
417            br.read_bytes(&mut packet_buf[..size])?;
418            let msg_data = &packet_buf[..size];
419
420            match msg_type {
421                ge::SOURCE1_LEGACY_GAME_EVENT_LIST => {
422                    let msg = CMsgSource1LegacyGameEventList::decode(msg_data)?;
423                    for desc in msg.descriptors {
424                        let eventid = desc.eventid.unwrap_or_default();
425                        let name = desc.name.unwrap_or_default();
426                        let field_names = desc
427                            .keys
428                            .iter()
429                            .map(|k| k.name.clone().unwrap_or_default())
430                            .collect();
431                        descriptors.insert(eventid, EventDescriptor { name, field_names });
432                    }
433                }
434                ge::SOURCE1_LEGACY_GAME_EVENT => {
435                    let msg = CMsgSource1LegacyGameEvent::decode(msg_data)?;
436                    let eventid = msg.eventid.unwrap_or_default();
437                    let (name, keys) = if let Some(desc) = descriptors.get(&eventid) {
438                        let keys: Vec<(String, String)> = desc
439                            .field_names
440                            .iter()
441                            .zip(msg.keys.iter())
442                            .map(|(fname, key)| (fname.clone(), format_event_key(key)))
443                            .collect();
444                        (desc.name.clone(), keys)
445                    } else {
446                        let name = msg
447                            .event_name
448                            .unwrap_or_else(|| format!("event_{}", eventid));
449                        (name, Vec::new())
450                    };
451                    events.push(GameEvent {
452                        tick,
453                        name,
454                        msg_type,
455                        keys,
456                        payload: msg_data.to_vec(),
457                    });
458                }
459                svc::USER_MESSAGE => {
460                    let msg = CsvcMsgUserMessage::decode(msg_data)?;
461                    let inner_type = msg.msg_type.unwrap_or_default();
462                    let name = command::user_message_name(inner_type);
463                    let inner_payload = msg.msg_data.unwrap_or_default();
464                    events.push(GameEvent {
465                        tick,
466                        name,
467                        msg_type: inner_type as u32,
468                        keys: Vec::new(),
469                        payload: inner_payload,
470                    });
471                }
472                _ => {
473                    // Citadel user messages (300-366) are sent directly in
474                    // the packet stream, not wrapped in CSVCMsg_UserMessage.
475                    let t = msg_type as i32;
476                    let name = if let Ok(e) = CitadelUserMessageIds::try_from(t) {
477                        Some(e.as_str_name().to_string())
478                    } else if let Ok(e) = ECitadelGameEvents::try_from(t) {
479                        Some(e.as_str_name().to_string())
480                    } else if let Ok(e) = EBaseUserMessages::try_from(t) {
481                        Some(e.as_str_name().to_string())
482                    } else {
483                        None
484                    };
485                    if let Some(name) = name {
486                        events.push(GameEvent {
487                            tick,
488                            name,
489                            msg_type,
490                            keys: Vec::new(),
491                            payload: msg_data.to_vec(),
492                        });
493                    }
494                }
495            }
496        }
497
498        Ok(())
499    }
500
501    /// Parse send tables from DEM_SendTables command.
502    pub fn parse_send_tables(&self) -> Result<SerializerContainer> {
503        self.verify()?;
504        let data = &self.data()[HEADER_SIZE..];
505        let mut reader = ByteReader::new(data);
506        let mut body_buf = Vec::with_capacity(BUF_SIZE);
507
508        while reader.remaining() > 0 {
509            let header = Self::read_cmd_header(&mut reader)?;
510
511            if header.cmd == dem::SEND_TABLES {
512                Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
513                let cmd = CDemoSendTables::decode(&body_buf[..])?;
514                return SerializerContainer::parse(cmd);
515            }
516
517            if header.cmd == dem::STOP || header.cmd == dem::SYNC_TICK {
518                break;
519            }
520
521            reader.skip(header.body_size as usize)?;
522        }
523
524        Err(Error::Parse {
525            context: "DEM_SendTables not found".into(),
526        })
527    }
528
529    /// Parse class info from DEM_ClassInfo command.
530    pub fn parse_class_info(&self) -> Result<ClassInfo> {
531        self.verify()?;
532        let data = &self.data()[HEADER_SIZE..];
533        let mut reader = ByteReader::new(data);
534        let mut body_buf = Vec::with_capacity(BUF_SIZE);
535
536        while reader.remaining() > 0 {
537            let header = Self::read_cmd_header(&mut reader)?;
538
539            if header.cmd == dem::CLASS_INFO {
540                Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
541                let cmd = CDemoClassInfo::decode(&body_buf[..])?;
542                return Ok(ClassInfo::parse(cmd));
543            }
544
545            if header.cmd == dem::STOP || header.cmd == dem::SYNC_TICK {
546                break;
547            }
548
549            reader.skip(header.body_size as usize)?;
550        }
551
552        Err(Error::Parse {
553            context: "DEM_ClassInfo not found".into(),
554        })
555    }
556
557    /// Parse all initialization data up to DEM_SyncTick and return a Context.
558    pub fn parse_init(&self) -> Result<Context> {
559        self.verify()?;
560        let data = &self.data()[HEADER_SIZE..];
561        let mut reader = ByteReader::new(data);
562
563        let mut packet_buf = vec![0u8; BUF_SIZE];
564        let mut body_buf = Vec::with_capacity(BUF_SIZE);
565
566        let mut serializers: Option<SerializerContainer> = None;
567        let mut class_info: Option<ClassInfo> = None;
568        let mut string_tables = StringTableContainer::new();
569        let mut tick_interval: f32 = 0.0;
570        let mut full_packet_interval: i32 = DEFAULT_FULL_PACKET_INTERVAL;
571
572        while reader.remaining() > 0 {
573            let header = Self::read_cmd_header(&mut reader)?;
574
575            if header.cmd == dem::SYNC_TICK {
576                reader.skip(header.body_size as usize)?;
577                break;
578            }
579
580            if header.cmd == dem::STOP {
581                break;
582            }
583
584            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
585
586            match header.cmd {
587                dem::SEND_TABLES => {
588                    let cmd = CDemoSendTables::decode(&body_buf[..])?;
589                    serializers = Some(SerializerContainer::parse(cmd)?);
590                }
591                dem::CLASS_INFO => {
592                    let cmd = CDemoClassInfo::decode(&body_buf[..])?;
593                    class_info = Some(ClassInfo::parse(cmd));
594                }
595                dem::PACKET | dem::SIGNON_PACKET => {
596                    let cmd = CDemoPacket::decode(&body_buf[..])?;
597                    let pkt_data = cmd.data.unwrap_or_default();
598                    Self::process_packet_for_init(
599                        &pkt_data,
600                        &mut string_tables,
601                        &mut tick_interval,
602                        &mut full_packet_interval,
603                        &mut packet_buf,
604                    )?;
605                }
606                _ => {}
607            }
608        }
609
610        let serializers = serializers.ok_or_else(|| Error::Parse {
611            context: "DEM_SendTables not found during init".into(),
612        })?;
613        let class_info = class_info.ok_or_else(|| Error::Parse {
614            context: "DEM_ClassInfo not found during init".into(),
615        })?;
616
617        // Update instance baselines
618        string_tables.update_instance_baselines(&class_info);
619
620        Ok(Context {
621            serializers,
622            class_info,
623            string_tables,
624            entities: EntityContainer::new(),
625            tick_interval,
626            full_packet_interval,
627            tick: -1,
628        })
629    }
630
631    /// Process a packet's inner messages during initialization (string tables, server info).
632    fn process_packet_for_init(
633        pkt_data: &[u8],
634        string_tables: &mut StringTableContainer,
635        tick_interval: &mut f32,
636        full_packet_interval: &mut i32,
637        packet_buf: &mut Vec<u8>,
638    ) -> Result<()> {
639        let mut br = BitReader::new(pkt_data);
640
641        while br.bits_remaining() > 8 {
642            let msg_type = br.read_ubitvar()?;
643            let size = br.read_uvarint32()? as usize;
644
645            // Read the message body
646            if size > packet_buf.len() {
647                packet_buf.resize(size, 0);
648            }
649            br.read_bytes(&mut packet_buf[..size])?;
650            let msg_data = &packet_buf[..size];
651
652            match msg_type {
653                svc::CREATE_STRING_TABLE => {
654                    let msg = CsvcMsgCreateStringTable::decode(msg_data)?;
655                    string_tables.handle_create(msg)?;
656                }
657                svc::UPDATE_STRING_TABLE => {
658                    let msg = CsvcMsgUpdateStringTable::decode(msg_data)?;
659                    string_tables.handle_update(msg)?;
660                }
661                svc::SERVER_INFO => {
662                    let msg = CsvcMsgServerInfo::decode(msg_data)?;
663                    if let Some(ti) = msg.tick_interval {
664                        *tick_interval = ti;
665                        let ratio = DEFAULT_TICK_INTERVAL / ti;
666                        *full_packet_interval = DEFAULT_FULL_PACKET_INTERVAL * ratio as i32;
667                    }
668                }
669                _ => {}
670            }
671        }
672
673        Ok(())
674    }
675
676    /// Parse the demo to a specific tick, returning the full game state.
677    ///
678    /// Uses an optimisation where it skips forward to the last
679    /// `DEM_FullPacket` snapshot before `target_tick`, applies that snapshot,
680    /// then replays individual packets until `target_tick` is reached.
681    pub fn parse_to_tick(&self, target_tick: i32) -> Result<Context> {
682        let mut ctx = self.parse_init()?;
683        let data = &self.data()[HEADER_SIZE..];
684        let mut reader = ByteReader::new(data);
685
686        let mut packet_buf = vec![0u8; BUF_SIZE];
687        let mut body_buf = Vec::with_capacity(BUF_SIZE);
688        let mut fp_buf = Vec::with_capacity(256);
689        let mut field_decode_ctx = FieldDecodeContext::new(ctx.tick_interval);
690
691        // Skip past init (up to and including SyncTick)
692        let mut past_sync = false;
693        while reader.remaining() > 0 {
694            let header = Self::read_cmd_header(&mut reader)?;
695            if header.cmd == dem::SYNC_TICK {
696                reader.skip(header.body_size as usize)?;
697                past_sync = true;
698                break;
699            }
700            if header.cmd == dem::STOP {
701                return Ok(ctx);
702            }
703            reader.skip(header.body_size as usize)?;
704        }
705
706        if !past_sync {
707            return Ok(ctx);
708        }
709
710        // Track whether we've handled the last full packet before target
711        let mut did_handle_last_full_packet = false;
712
713        while reader.remaining() > 0 {
714            let header = Self::read_cmd_header(&mut reader)?;
715
716            if header.tick > target_tick && header.cmd != dem::STOP {
717                break;
718            }
719
720            ctx.tick = header.tick;
721
722            if header.cmd == dem::STOP {
723                break;
724            }
725
726            let is_full_packet = header.cmd == dem::FULL_PACKET;
727            let distance = target_tick - header.tick;
728            let has_full_packet_ahead = distance > ctx.full_packet_interval + 100;
729
730            if is_full_packet {
731                Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
732                let cmd = CDemoFullPacket::decode(&body_buf[..])?;
733
734                // Handle string tables from full packet
735                if let Some(st) = cmd.string_table {
736                    ctx.string_tables.do_full_update(st);
737                    ctx.string_tables.update_instance_baselines(&ctx.class_info);
738                }
739
740                // Handle packet from full packet (skip if more full packets ahead)
741                if !has_full_packet_ahead {
742                    if let Some(packet) = cmd.packet {
743                        let pkt_data = packet.data.unwrap_or_default();
744                        Self::process_packet_entities(
745                            &pkt_data,
746                            &mut ctx,
747                            &mut field_decode_ctx,
748                            &mut packet_buf,
749                            &mut fp_buf,
750                        )?;
751                    }
752                    did_handle_last_full_packet = true;
753                }
754
755                continue;
756            }
757
758            if !did_handle_last_full_packet {
759                reader.skip(header.body_size as usize)?;
760                continue;
761            }
762
763            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
764
765            match header.cmd {
766                dem::PACKET | dem::SIGNON_PACKET => {
767                    let cmd = CDemoPacket::decode(&body_buf[..])?;
768                    let pkt_data = cmd.data.unwrap_or_default();
769                    Self::process_packet_entities(
770                        &pkt_data,
771                        &mut ctx,
772                        &mut field_decode_ctx,
773                        &mut packet_buf,
774                        &mut fp_buf,
775                    )?;
776                }
777                _ => {}
778            }
779        }
780
781        Ok(ctx)
782    }
783
784    /// Parse the entire demo, calling a callback at each tick with the current context.
785    /// This is more efficient than calling parse_to_tick repeatedly.
786    pub fn run_to_end<F>(&self, mut on_tick: F) -> Result<()>
787    where
788        F: FnMut(&Context),
789    {
790        let mut ctx = self.parse_init()?;
791        let data = &self.data()[HEADER_SIZE..];
792        let mut reader = ByteReader::new(data);
793
794        let mut packet_buf = vec![0u8; BUF_SIZE];
795        let mut body_buf = Vec::with_capacity(BUF_SIZE);
796        let mut fp_buf = Vec::with_capacity(256);
797        let mut field_decode_ctx = FieldDecodeContext::new(ctx.tick_interval);
798
799        // Skip past init (up to and including SyncTick)
800        while reader.remaining() > 0 {
801            let header = Self::read_cmd_header(&mut reader)?;
802            if header.cmd == dem::SYNC_TICK {
803                reader.skip(header.body_size as usize)?;
804                break;
805            }
806            if header.cmd == dem::STOP {
807                return Ok(());
808            }
809            reader.skip(header.body_size as usize)?;
810        }
811
812        let mut last_tick: i32 = -1;
813
814        while reader.remaining() > 0 {
815            let header = Self::read_cmd_header(&mut reader)?;
816
817            // Call callback when tick changes
818            if header.tick != last_tick && last_tick >= 0 {
819                on_tick(&ctx);
820            }
821            last_tick = header.tick;
822            ctx.tick = header.tick;
823
824            if header.cmd == dem::STOP {
825                // Final callback
826                if last_tick >= 0 {
827                    on_tick(&ctx);
828                }
829                break;
830            }
831
832            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
833
834            match header.cmd {
835                dem::FULL_PACKET => {
836                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
837
838                    if let Some(st) = cmd.string_table {
839                        ctx.string_tables.do_full_update(st);
840                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
841                    }
842
843                    if let Some(packet) = cmd.packet {
844                        let pkt_data = packet.data.unwrap_or_default();
845                        Self::process_packet_entities(
846                            &pkt_data,
847                            &mut ctx,
848                            &mut field_decode_ctx,
849                            &mut packet_buf,
850                            &mut fp_buf,
851                        )?;
852                    }
853                }
854                dem::PACKET | dem::SIGNON_PACKET => {
855                    let cmd = CDemoPacket::decode(&body_buf[..])?;
856                    let pkt_data = cmd.data.unwrap_or_default();
857                    Self::process_packet_entities(
858                        &pkt_data,
859                        &mut ctx,
860                        &mut field_decode_ctx,
861                        &mut packet_buf,
862                        &mut fp_buf,
863                    )?;
864                }
865                _ => {}
866            }
867        }
868
869        Ok(())
870    }
871
872    /// Parse the entire demo with entity class filtering.
873    /// Only entities with classes in the filter are fully tracked.
874    /// This is much faster when you only need specific entity types.
875    pub fn run_to_end_filtered<F>(
876        &self,
877        class_filter: &std::collections::HashSet<&str>,
878        mut on_tick: F,
879    ) -> Result<()>
880    where
881        F: FnMut(&Context),
882    {
883        let mut ctx = self.parse_init()?;
884        let data = &self.data()[HEADER_SIZE..];
885        let mut reader = ByteReader::new(data);
886
887        let mut packet_buf = vec![0u8; BUF_SIZE];
888        let mut body_buf = Vec::with_capacity(BUF_SIZE);
889        let mut fp_buf = Vec::with_capacity(256);
890        let mut field_decode_ctx = FieldDecodeContext::new(ctx.tick_interval);
891
892        // Skip past init (up to and including SyncTick)
893        while reader.remaining() > 0 {
894            let header = Self::read_cmd_header(&mut reader)?;
895            if header.cmd == dem::SYNC_TICK {
896                reader.skip(header.body_size as usize)?;
897                break;
898            }
899            if header.cmd == dem::STOP {
900                return Ok(());
901            }
902            reader.skip(header.body_size as usize)?;
903        }
904
905        let mut last_tick: i32 = -1;
906
907        while reader.remaining() > 0 {
908            let header = Self::read_cmd_header(&mut reader)?;
909
910            // Call callback when tick changes
911            if header.tick != last_tick && last_tick >= 0 {
912                on_tick(&ctx);
913            }
914            last_tick = header.tick;
915            ctx.tick = header.tick;
916
917            if header.cmd == dem::STOP {
918                // Final callback
919                if last_tick >= 0 {
920                    on_tick(&ctx);
921                }
922                break;
923            }
924
925            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
926
927            match header.cmd {
928                dem::FULL_PACKET => {
929                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
930
931                    if let Some(st) = cmd.string_table {
932                        ctx.string_tables.do_full_update(st);
933                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
934                    }
935
936                    if let Some(packet) = cmd.packet {
937                        let pkt_data = packet.data.unwrap_or_default();
938                        Self::process_packet_entities_filtered(
939                            &pkt_data,
940                            &mut ctx,
941                            &mut field_decode_ctx,
942                            &mut packet_buf,
943                            class_filter,
944                            &mut fp_buf,
945                        )?;
946                    }
947                }
948                dem::PACKET | dem::SIGNON_PACKET => {
949                    let cmd = CDemoPacket::decode(&body_buf[..])?;
950                    let pkt_data = cmd.data.unwrap_or_default();
951                    Self::process_packet_entities_filtered(
952                        &pkt_data,
953                        &mut ctx,
954                        &mut field_decode_ctx,
955                        &mut packet_buf,
956                        class_filter,
957                        &mut fp_buf,
958                    )?;
959                }
960                _ => {}
961            }
962        }
963
964        Ok(())
965    }
966
967    /// Parse the entire demo with entity class filtering AND event collection.
968    /// Combines `run_to_end_filtered` with `process_packet_events` in a single pass.
969    /// The callback receives both the entity context and accumulated events for the tick.
970    pub fn run_to_end_with_events_filtered<F>(
971        &self,
972        class_filter: &std::collections::HashSet<&str>,
973        mut on_tick: F,
974    ) -> Result<()>
975    where
976        F: FnMut(&Context, &[GameEvent]),
977    {
978        let mut ctx = self.parse_init()?;
979        let data = &self.data()[HEADER_SIZE..];
980        let mut reader = ByteReader::new(data);
981
982        let mut packet_buf = vec![0u8; BUF_SIZE];
983        let mut event_packet_buf = vec![0u8; BUF_SIZE];
984        let mut body_buf = Vec::with_capacity(BUF_SIZE);
985        let mut fp_buf = Vec::with_capacity(256);
986        let mut field_decode_ctx = FieldDecodeContext::new(ctx.tick_interval);
987
988        let mut descriptors: HashMap<i32, EventDescriptor> = HashMap::new();
989        let mut tick_events: Vec<GameEvent> = Vec::new();
990
991        // Skip past init (up to and including SyncTick)
992        while reader.remaining() > 0 {
993            let header = Self::read_cmd_header(&mut reader)?;
994            if header.cmd == dem::SYNC_TICK {
995                reader.skip(header.body_size as usize)?;
996                break;
997            }
998            if header.cmd == dem::STOP {
999                return Ok(());
1000            }
1001            reader.skip(header.body_size as usize)?;
1002        }
1003
1004        let mut last_tick: i32 = -1;
1005
1006        while reader.remaining() > 0 {
1007            let header = Self::read_cmd_header(&mut reader)?;
1008
1009            // Call callback when tick changes
1010            if header.tick != last_tick && last_tick >= 0 {
1011                on_tick(&ctx, &tick_events);
1012                tick_events.clear();
1013            }
1014            last_tick = header.tick;
1015            ctx.tick = header.tick;
1016
1017            if header.cmd == dem::STOP {
1018                // Final callback
1019                if last_tick >= 0 {
1020                    on_tick(&ctx, &tick_events);
1021                }
1022                break;
1023            }
1024
1025            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
1026
1027            match header.cmd {
1028                dem::FULL_PACKET => {
1029                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
1030
1031                    if let Some(st) = cmd.string_table {
1032                        ctx.string_tables.do_full_update(st);
1033                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1034                    }
1035
1036                    if let Some(packet) = cmd.packet {
1037                        let pkt_data = packet.data.unwrap_or_default();
1038                        Self::process_packet_entities_filtered(
1039                            &pkt_data,
1040                            &mut ctx,
1041                            &mut field_decode_ctx,
1042                            &mut packet_buf,
1043                            class_filter,
1044                            &mut fp_buf,
1045                        )?;
1046                        Self::process_packet_events(
1047                            &pkt_data,
1048                            header.tick,
1049                            &mut descriptors,
1050                            &mut tick_events,
1051                            &mut event_packet_buf,
1052                        )?;
1053                    }
1054                }
1055                dem::PACKET | dem::SIGNON_PACKET => {
1056                    let cmd = CDemoPacket::decode(&body_buf[..])?;
1057                    let pkt_data = cmd.data.unwrap_or_default();
1058                    Self::process_packet_entities_filtered(
1059                        &pkt_data,
1060                        &mut ctx,
1061                        &mut field_decode_ctx,
1062                        &mut packet_buf,
1063                        class_filter,
1064                        &mut fp_buf,
1065                    )?;
1066                    Self::process_packet_events(
1067                        &pkt_data,
1068                        header.tick,
1069                        &mut descriptors,
1070                        &mut tick_events,
1071                        &mut event_packet_buf,
1072                    )?;
1073                }
1074                _ => {}
1075            }
1076        }
1077
1078        Ok(())
1079    }
1080
1081    /// Process a packet's inner messages for entity updates.
1082    fn process_packet_entities(
1083        pkt_data: &[u8],
1084        ctx: &mut Context,
1085        field_decode_ctx: &mut FieldDecodeContext,
1086        packet_buf: &mut Vec<u8>,
1087        fp_buf: &mut Vec<crate::entity::field_path::FieldPath>,
1088    ) -> Result<()> {
1089        let mut br = BitReader::new(pkt_data);
1090
1091        while br.bits_remaining() > 8 {
1092            let msg_type = br.read_ubitvar()?;
1093            let size = br.read_uvarint32()? as usize;
1094
1095            if size > packet_buf.len() {
1096                packet_buf.resize(size, 0);
1097            }
1098            br.read_bytes(&mut packet_buf[..size])?;
1099            let msg_data = &packet_buf[..size];
1100
1101            match msg_type {
1102                svc::CREATE_STRING_TABLE => {
1103                    let msg = CsvcMsgCreateStringTable::decode(msg_data)?;
1104                    if ctx.string_tables.handle_create(msg)? {
1105                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1106                    }
1107                }
1108                svc::UPDATE_STRING_TABLE => {
1109                    let msg = CsvcMsgUpdateStringTable::decode(msg_data)?;
1110                    if ctx.string_tables.handle_update(msg)? {
1111                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1112                    }
1113                }
1114                svc::SERVER_INFO => {
1115                    let msg = CsvcMsgServerInfo::decode(msg_data)?;
1116                    if let Some(ti) = msg.tick_interval {
1117                        ctx.tick_interval = ti;
1118                        field_decode_ctx.tick_interval = ti;
1119                        let ratio = DEFAULT_TICK_INTERVAL / ti;
1120                        ctx.full_packet_interval = DEFAULT_FULL_PACKET_INTERVAL * ratio as i32;
1121                    }
1122                }
1123                svc::PACKET_ENTITIES => {
1124                    let msg = CsvcMsgPacketEntities::decode(msg_data)?;
1125                    ctx.entities.handle_packet_entities(
1126                        msg,
1127                        &ctx.class_info,
1128                        &ctx.serializers,
1129                        &ctx.string_tables,
1130                        field_decode_ctx,
1131                        fp_buf,
1132                    )?;
1133                }
1134                _ => {}
1135            }
1136        }
1137
1138        Ok(())
1139    }
1140
1141    /// Process a packet's inner messages with entity class filtering.
1142    fn process_packet_entities_filtered(
1143        pkt_data: &[u8],
1144        ctx: &mut Context,
1145        field_decode_ctx: &mut FieldDecodeContext,
1146        packet_buf: &mut Vec<u8>,
1147        class_filter: &std::collections::HashSet<&str>,
1148        fp_buf: &mut Vec<crate::entity::field_path::FieldPath>,
1149    ) -> Result<()> {
1150        let mut br = BitReader::new(pkt_data);
1151
1152        while br.bits_remaining() > 8 {
1153            let msg_type = br.read_ubitvar()?;
1154            let size = br.read_uvarint32()? as usize;
1155
1156            if size > packet_buf.len() {
1157                packet_buf.resize(size, 0);
1158            }
1159            br.read_bytes(&mut packet_buf[..size])?;
1160            let msg_data = &packet_buf[..size];
1161
1162            match msg_type {
1163                svc::CREATE_STRING_TABLE => {
1164                    let msg = CsvcMsgCreateStringTable::decode(msg_data)?;
1165                    if ctx.string_tables.handle_create(msg)? {
1166                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1167                    }
1168                }
1169                svc::UPDATE_STRING_TABLE => {
1170                    let msg = CsvcMsgUpdateStringTable::decode(msg_data)?;
1171                    if ctx.string_tables.handle_update(msg)? {
1172                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1173                    }
1174                }
1175                svc::SERVER_INFO => {
1176                    let msg = CsvcMsgServerInfo::decode(msg_data)?;
1177                    if let Some(ti) = msg.tick_interval {
1178                        ctx.tick_interval = ti;
1179                        field_decode_ctx.tick_interval = ti;
1180                        let ratio = DEFAULT_TICK_INTERVAL / ti;
1181                        ctx.full_packet_interval = DEFAULT_FULL_PACKET_INTERVAL * ratio as i32;
1182                    }
1183                }
1184                svc::PACKET_ENTITIES => {
1185                    let msg = CsvcMsgPacketEntities::decode(msg_data)?;
1186                    ctx.entities.handle_packet_entities_filtered(
1187                        msg,
1188                        &ctx.class_info,
1189                        &ctx.serializers,
1190                        &ctx.string_tables,
1191                        field_decode_ctx,
1192                        class_filter,
1193                        fp_buf,
1194                    )?;
1195                }
1196                _ => {}
1197            }
1198        }
1199
1200        Ok(())
1201    }
1202}