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                ctx.string_tables.clear_dirty();
821                ctx.entities.clear_updated();
822            }
823            last_tick = header.tick;
824            ctx.tick = header.tick;
825
826            if header.cmd == dem::STOP {
827                // Final callback
828                if last_tick >= 0 {
829                    on_tick(&ctx);
830                }
831                break;
832            }
833
834            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
835
836            match header.cmd {
837                dem::FULL_PACKET => {
838                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
839
840                    if let Some(st) = cmd.string_table {
841                        ctx.string_tables.do_full_update(st);
842                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
843                    }
844
845                    if let Some(packet) = cmd.packet {
846                        let pkt_data = packet.data.unwrap_or_default();
847                        Self::process_packet_entities(
848                            &pkt_data,
849                            &mut ctx,
850                            &mut field_decode_ctx,
851                            &mut packet_buf,
852                            &mut fp_buf,
853                        )?;
854                    }
855                }
856                dem::PACKET | dem::SIGNON_PACKET => {
857                    let cmd = CDemoPacket::decode(&body_buf[..])?;
858                    let pkt_data = cmd.data.unwrap_or_default();
859                    Self::process_packet_entities(
860                        &pkt_data,
861                        &mut ctx,
862                        &mut field_decode_ctx,
863                        &mut packet_buf,
864                        &mut fp_buf,
865                    )?;
866                }
867                _ => {}
868            }
869        }
870
871        Ok(())
872    }
873
874    /// Parse the entire demo with entity class filtering.
875    /// Only entities with classes in the filter are fully tracked.
876    /// This is much faster when you only need specific entity types.
877    pub fn run_to_end_filtered<F>(
878        &self,
879        class_filter: &std::collections::HashSet<&str>,
880        mut on_tick: F,
881    ) -> Result<()>
882    where
883        F: FnMut(&Context),
884    {
885        let mut ctx = self.parse_init()?;
886        let data = &self.data()[HEADER_SIZE..];
887        let mut reader = ByteReader::new(data);
888
889        let mut packet_buf = vec![0u8; BUF_SIZE];
890        let mut body_buf = Vec::with_capacity(BUF_SIZE);
891        let mut fp_buf = Vec::with_capacity(256);
892        let mut field_decode_ctx = FieldDecodeContext::new(ctx.tick_interval);
893
894        // Skip past init (up to and including SyncTick)
895        while reader.remaining() > 0 {
896            let header = Self::read_cmd_header(&mut reader)?;
897            if header.cmd == dem::SYNC_TICK {
898                reader.skip(header.body_size as usize)?;
899                break;
900            }
901            if header.cmd == dem::STOP {
902                return Ok(());
903            }
904            reader.skip(header.body_size as usize)?;
905        }
906
907        let mut last_tick: i32 = -1;
908
909        while reader.remaining() > 0 {
910            let header = Self::read_cmd_header(&mut reader)?;
911
912            // Call callback when tick changes
913            if header.tick != last_tick && last_tick >= 0 {
914                on_tick(&ctx);
915                ctx.string_tables.clear_dirty();
916                ctx.entities.clear_updated();
917            }
918            last_tick = header.tick;
919            ctx.tick = header.tick;
920
921            if header.cmd == dem::STOP {
922                // Final callback
923                if last_tick >= 0 {
924                    on_tick(&ctx);
925                }
926                break;
927            }
928
929            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
930
931            match header.cmd {
932                dem::FULL_PACKET => {
933                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
934
935                    if let Some(st) = cmd.string_table {
936                        ctx.string_tables.do_full_update(st);
937                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
938                    }
939
940                    if let Some(packet) = cmd.packet {
941                        let pkt_data = packet.data.unwrap_or_default();
942                        Self::process_packet_entities_filtered(
943                            &pkt_data,
944                            &mut ctx,
945                            &mut field_decode_ctx,
946                            &mut packet_buf,
947                            class_filter,
948                            &mut fp_buf,
949                        )?;
950                    }
951                }
952                dem::PACKET | dem::SIGNON_PACKET => {
953                    let cmd = CDemoPacket::decode(&body_buf[..])?;
954                    let pkt_data = cmd.data.unwrap_or_default();
955                    Self::process_packet_entities_filtered(
956                        &pkt_data,
957                        &mut ctx,
958                        &mut field_decode_ctx,
959                        &mut packet_buf,
960                        class_filter,
961                        &mut fp_buf,
962                    )?;
963                }
964                _ => {}
965            }
966        }
967
968        Ok(())
969    }
970
971    /// Parse the entire demo with entity class filtering AND event collection.
972    /// Combines `run_to_end_filtered` with `process_packet_events` in a single pass.
973    /// The callback receives both the entity context and accumulated events for the tick.
974    pub fn run_to_end_with_events_filtered<F>(
975        &self,
976        class_filter: &std::collections::HashSet<&str>,
977        mut on_tick: F,
978    ) -> Result<()>
979    where
980        F: FnMut(&Context, &[GameEvent]),
981    {
982        let mut ctx = self.parse_init()?;
983        let data = &self.data()[HEADER_SIZE..];
984        let mut reader = ByteReader::new(data);
985
986        let mut packet_buf = vec![0u8; BUF_SIZE];
987        let mut event_packet_buf = vec![0u8; BUF_SIZE];
988        let mut body_buf = Vec::with_capacity(BUF_SIZE);
989        let mut fp_buf = Vec::with_capacity(256);
990        let mut field_decode_ctx = FieldDecodeContext::new(ctx.tick_interval);
991
992        let mut descriptors: HashMap<i32, EventDescriptor> = HashMap::new();
993        let mut tick_events: Vec<GameEvent> = Vec::new();
994
995        // Skip past init (up to and including SyncTick)
996        while reader.remaining() > 0 {
997            let header = Self::read_cmd_header(&mut reader)?;
998            if header.cmd == dem::SYNC_TICK {
999                reader.skip(header.body_size as usize)?;
1000                break;
1001            }
1002            if header.cmd == dem::STOP {
1003                return Ok(());
1004            }
1005            reader.skip(header.body_size as usize)?;
1006        }
1007
1008        let mut last_tick: i32 = -1;
1009
1010        while reader.remaining() > 0 {
1011            let header = Self::read_cmd_header(&mut reader)?;
1012
1013            // Call callback when tick changes
1014            if header.tick != last_tick && last_tick >= 0 {
1015                on_tick(&ctx, &tick_events);
1016                tick_events.clear();
1017                ctx.string_tables.clear_dirty();
1018                ctx.entities.clear_updated();
1019            }
1020            last_tick = header.tick;
1021            ctx.tick = header.tick;
1022
1023            if header.cmd == dem::STOP {
1024                // Final callback
1025                if last_tick >= 0 {
1026                    on_tick(&ctx, &tick_events);
1027                }
1028                break;
1029            }
1030
1031            Self::read_cmd_body(&mut reader, &header, &mut body_buf)?;
1032
1033            match header.cmd {
1034                dem::FULL_PACKET => {
1035                    let cmd = CDemoFullPacket::decode(&body_buf[..])?;
1036
1037                    if let Some(st) = cmd.string_table {
1038                        ctx.string_tables.do_full_update(st);
1039                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1040                    }
1041
1042                    if let Some(packet) = cmd.packet {
1043                        let pkt_data = packet.data.unwrap_or_default();
1044                        Self::process_packet_entities_filtered(
1045                            &pkt_data,
1046                            &mut ctx,
1047                            &mut field_decode_ctx,
1048                            &mut packet_buf,
1049                            class_filter,
1050                            &mut fp_buf,
1051                        )?;
1052                        Self::process_packet_events(
1053                            &pkt_data,
1054                            header.tick,
1055                            &mut descriptors,
1056                            &mut tick_events,
1057                            &mut event_packet_buf,
1058                        )?;
1059                    }
1060                }
1061                dem::PACKET | dem::SIGNON_PACKET => {
1062                    let cmd = CDemoPacket::decode(&body_buf[..])?;
1063                    let pkt_data = cmd.data.unwrap_or_default();
1064                    Self::process_packet_entities_filtered(
1065                        &pkt_data,
1066                        &mut ctx,
1067                        &mut field_decode_ctx,
1068                        &mut packet_buf,
1069                        class_filter,
1070                        &mut fp_buf,
1071                    )?;
1072                    Self::process_packet_events(
1073                        &pkt_data,
1074                        header.tick,
1075                        &mut descriptors,
1076                        &mut tick_events,
1077                        &mut event_packet_buf,
1078                    )?;
1079                }
1080                _ => {}
1081            }
1082        }
1083
1084        Ok(())
1085    }
1086
1087    /// Process a packet's inner messages for entity updates.
1088    fn process_packet_entities(
1089        pkt_data: &[u8],
1090        ctx: &mut Context,
1091        field_decode_ctx: &mut FieldDecodeContext,
1092        packet_buf: &mut Vec<u8>,
1093        fp_buf: &mut Vec<crate::entity::field_path::FieldPath>,
1094    ) -> Result<()> {
1095        let mut br = BitReader::new(pkt_data);
1096
1097        while br.bits_remaining() > 8 {
1098            let msg_type = br.read_ubitvar()?;
1099            let size = br.read_uvarint32()? as usize;
1100
1101            // Only these four message types are consumed below; advance past
1102            // everything else without copying its bytes out of the bitstream.
1103            if !matches!(
1104                msg_type,
1105                svc::CREATE_STRING_TABLE
1106                    | svc::UPDATE_STRING_TABLE
1107                    | svc::SERVER_INFO
1108                    | svc::PACKET_ENTITIES
1109            ) {
1110                br.skip_bits(size * 8)?;
1111                continue;
1112            }
1113
1114            if size > packet_buf.len() {
1115                packet_buf.resize(size, 0);
1116            }
1117            br.read_bytes(&mut packet_buf[..size])?;
1118            let msg_data = &packet_buf[..size];
1119
1120            match msg_type {
1121                svc::CREATE_STRING_TABLE => {
1122                    let msg = CsvcMsgCreateStringTable::decode(msg_data)?;
1123                    if ctx.string_tables.handle_create(msg)? {
1124                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1125                    }
1126                }
1127                svc::UPDATE_STRING_TABLE => {
1128                    let msg = CsvcMsgUpdateStringTable::decode(msg_data)?;
1129                    if ctx.string_tables.handle_update(msg)? {
1130                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1131                    }
1132                }
1133                svc::SERVER_INFO => {
1134                    let msg = CsvcMsgServerInfo::decode(msg_data)?;
1135                    if let Some(ti) = msg.tick_interval {
1136                        ctx.tick_interval = ti;
1137                        field_decode_ctx.tick_interval = ti;
1138                        let ratio = DEFAULT_TICK_INTERVAL / ti;
1139                        ctx.full_packet_interval = DEFAULT_FULL_PACKET_INTERVAL * ratio as i32;
1140                    }
1141                }
1142                svc::PACKET_ENTITIES => {
1143                    let msg = CsvcMsgPacketEntities::decode(msg_data)?;
1144                    ctx.entities.handle_packet_entities(
1145                        msg,
1146                        &ctx.class_info,
1147                        &ctx.serializers,
1148                        &ctx.string_tables,
1149                        field_decode_ctx,
1150                        fp_buf,
1151                    )?;
1152                }
1153                _ => {}
1154            }
1155        }
1156
1157        Ok(())
1158    }
1159
1160    /// Process a packet's inner messages with entity class filtering.
1161    fn process_packet_entities_filtered(
1162        pkt_data: &[u8],
1163        ctx: &mut Context,
1164        field_decode_ctx: &mut FieldDecodeContext,
1165        packet_buf: &mut Vec<u8>,
1166        class_filter: &std::collections::HashSet<&str>,
1167        fp_buf: &mut Vec<crate::entity::field_path::FieldPath>,
1168    ) -> Result<()> {
1169        let mut br = BitReader::new(pkt_data);
1170
1171        while br.bits_remaining() > 8 {
1172            let msg_type = br.read_ubitvar()?;
1173            let size = br.read_uvarint32()? as usize;
1174
1175            // Only these four message types are consumed below; advance past
1176            // everything else without copying its bytes out of the bitstream.
1177            if !matches!(
1178                msg_type,
1179                svc::CREATE_STRING_TABLE
1180                    | svc::UPDATE_STRING_TABLE
1181                    | svc::SERVER_INFO
1182                    | svc::PACKET_ENTITIES
1183            ) {
1184                br.skip_bits(size * 8)?;
1185                continue;
1186            }
1187
1188            if size > packet_buf.len() {
1189                packet_buf.resize(size, 0);
1190            }
1191            br.read_bytes(&mut packet_buf[..size])?;
1192            let msg_data = &packet_buf[..size];
1193
1194            match msg_type {
1195                svc::CREATE_STRING_TABLE => {
1196                    let msg = CsvcMsgCreateStringTable::decode(msg_data)?;
1197                    if ctx.string_tables.handle_create(msg)? {
1198                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1199                    }
1200                }
1201                svc::UPDATE_STRING_TABLE => {
1202                    let msg = CsvcMsgUpdateStringTable::decode(msg_data)?;
1203                    if ctx.string_tables.handle_update(msg)? {
1204                        ctx.string_tables.update_instance_baselines(&ctx.class_info);
1205                    }
1206                }
1207                svc::SERVER_INFO => {
1208                    let msg = CsvcMsgServerInfo::decode(msg_data)?;
1209                    if let Some(ti) = msg.tick_interval {
1210                        ctx.tick_interval = ti;
1211                        field_decode_ctx.tick_interval = ti;
1212                        let ratio = DEFAULT_TICK_INTERVAL / ti;
1213                        ctx.full_packet_interval = DEFAULT_FULL_PACKET_INTERVAL * ratio as i32;
1214                    }
1215                }
1216                svc::PACKET_ENTITIES => {
1217                    let msg = CsvcMsgPacketEntities::decode(msg_data)?;
1218                    ctx.entities.handle_packet_entities_filtered(
1219                        msg,
1220                        &ctx.class_info,
1221                        &ctx.serializers,
1222                        &ctx.string_tables,
1223                        field_decode_ctx,
1224                        class_filter,
1225                        fp_buf,
1226                    )?;
1227                }
1228                _ => {}
1229            }
1230        }
1231
1232        Ok(())
1233    }
1234}