diff --git a/Cargo.lock b/Cargo.lock index 5fa6abe..d20d589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,11 +253,13 @@ dependencies = [ name = "cs2-demo" version = "0.0.0" dependencies = [ + "anyhow", "bitstream-io", "demo-format", "paste", "protobuf", "protobuf-codegen", + "serde", "snap", "thiserror", "tracing", diff --git a/cs2-demo/Cargo.toml b/cs2-demo/Cargo.toml index e6e16d6..e4273be 100644 --- a/cs2-demo/Cargo.toml +++ b/cs2-demo/Cargo.toml @@ -6,9 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0" bitstream-io.workspace = true demo-format = { path = "../demo-format" } protobuf = { version = "3.2.0", features = ["with-bytes"] } +serde = { version = "1.0" } snap = "1.1" thiserror = "1.0" tracing = "0.1" diff --git a/cs2-demo/src/demo_command.rs b/cs2-demo/src/demo_command.rs index e948cf4..9754a6f 100644 --- a/cs2-demo/src/demo_command.rs +++ b/cs2-demo/src/demo_command.rs @@ -1,11 +1,16 @@ -use super::packet::Packet; -use super::proto::demo::{CDemoFileHeader, CDemoPacket, CDemoSendTables}; -use super::{Error, Result}; -use crate::proto::demo::{CDemoClassInfo, CDemoFullPacket, CDemoStringTables}; -use crate::string_table::{parse_string_tables, StringTable}; +use demo_format::Tick; +use protobuf::CodedInputStream; use protobuf::Message; use std::fmt; +use crate::packet::Packet; +use crate::proto::demo::{ + CDemoClassInfo, CDemoFileHeader, CDemoFullPacket, CDemoPacket, CDemoSendTables, + CDemoStringTables, EDemoCommands, +}; +use crate::string_table::{parse_string_tables, StringTable}; +use crate::{Error, Result}; + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum DemoCommand { @@ -73,3 +78,33 @@ impl fmt::Display for DemoCommand { } } } + +pub struct DemoParser<'a> { + reader: CodedInputStream<'a>, +} + +impl<'a> DemoParser<'a> { + pub fn try_new_after_demo_type(read: &'a mut dyn std::io::Read) -> Result { + let mut reader = CodedInputStream::new(read); + reader.skip_raw_bytes(8)?; + Ok(Self { reader }) + } + + pub fn parse_next_demo_command(&mut self) -> Result> { + if self.reader.eof()? { + return Ok(None); + } + let cmd_flags = self.reader.read_raw_varint32()?; + let cmd = cmd_flags & !(EDemoCommands::DEM_IsCompressed as u32); + let compressed = (cmd_flags & (EDemoCommands::DEM_IsCompressed as u32)) != 0; + let tick = self.reader.read_raw_varint32()? as i32; + let size = self.reader.read_raw_varint32()?; + let data = self.reader.read_raw_bytes(size)?; + let data = if compressed { + snap::raw::Decoder::new().decompress_vec(data.as_slice())? + } else { + data + }; + Ok(Some((tick, DemoCommand::try_new(cmd, &data)?))) + } +} diff --git a/cs2-demo/src/entity.rs b/cs2-demo/src/entity.rs index aa242c1..9ec2b97 100644 --- a/cs2-demo/src/entity.rs +++ b/cs2-demo/src/entity.rs @@ -25,7 +25,7 @@ pub struct Entities { } impl Entities { - pub fn read_packet_entities( + pub(crate) fn read_packet_entities( &mut self, msg: CSVCMsg_PacketEntities, classes: &Classes, @@ -133,15 +133,13 @@ impl Entity { #[cfg(test)] mod tests { - use std::rc::Rc; - use super::*; use crate::testdata; #[test] fn test() -> Result<()> { let send_tables = SendTables::try_new(testdata::send_tables())?; - let classes = Classes::try_new(testdata::class_info(), Rc::new(send_tables))?; + let classes = Classes::try_new(testdata::class_info(), send_tables)?; let mut entities = Entities::default(); entities.read_packet_entities(testdata::packet_entities(), &classes)?; Ok(()) diff --git a/cs2-demo/src/entity/class.rs b/cs2-demo/src/entity/class.rs index 3f4d18c..44278fe 100644 --- a/cs2-demo/src/entity/class.rs +++ b/cs2-demo/src/entity/class.rs @@ -35,7 +35,7 @@ pub struct Classes { } impl Classes { - pub fn try_new(msg: CDemoClassInfo, send_tables: Rc) -> Result { + pub fn try_new(msg: CDemoClassInfo, send_tables: SendTables) -> Result { let serializers = send_tables .serializers .iter() @@ -71,6 +71,6 @@ mod tests { #[test] fn test() { let send_tables = SendTables::try_new(testdata::send_tables()).unwrap(); - Classes::try_new(testdata::class_info(), Rc::new(send_tables)).unwrap(); + Classes::try_new(testdata::class_info(), send_tables).unwrap(); } } diff --git a/cs2-demo/src/error.rs b/cs2-demo/src/error.rs new file mode 100644 index 0000000..e045aab --- /dev/null +++ b/cs2-demo/src/error.rs @@ -0,0 +1,36 @@ +/// Error type for this library. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Protobuf(#[from] protobuf::Error), + #[error("invalid demo type (expected: PBDEMS2, found: {found})")] + InvalidDemoType { found: String }, + #[error("unknown packet command found: {0}")] + UnknownPacketCommand(u32), + #[error(transparent)] + Decompression(#[from] snap::Error), + #[error("missing string_table from CDemoFullPacket")] + MissingStringTable, + #[error("missing packet from CDemoFullPacket")] + MissingPacket, + #[error("cannot parse string table player index")] + InvalidPlayerIndex, + #[error("cannot parse sendtables")] + InvalidSendTables, + #[error("invalid entity id in PacketEntities")] + InvalidEntityId, + #[error("missing class_id in CDemoClassInfo")] + MissingClassId, + #[error("missing class name CDemoClassInfo")] + MissingClassName, + #[error("skipped class_id in CDemoClassInfo")] + SkippedClassId, + #[error("duplicate serializer in CDemoSendTables")] + DuplicateSerializer, + #[error("packet out of order")] + PacketOutOfOrder, +} + +pub type Result = std::result::Result; diff --git a/cs2-demo/src/game_event.rs b/cs2-demo/src/game_event.rs new file mode 100644 index 0000000..f4673b1 --- /dev/null +++ b/cs2-demo/src/game_event.rs @@ -0,0 +1,76 @@ +pub mod de; + +use std::collections::HashMap; + +use crate::proto::gameevents::{ + cmsg_source1legacy_game_event_list, CMsgSource1LegacyGameEventList, +}; + +pub type GameEventDescriptors = HashMap; + +pub struct DescriptorKey { + pub type_: i32, + pub name: String, +} + +impl From for DescriptorKey { + fn from(k: cmsg_source1legacy_game_event_list::Key_t) -> Self { + Self { + name: k.name().to_string(), + type_: k.type_(), + } + } +} + +pub struct Descriptor { + pub eventid: i32, + pub name: String, + pub keys: Vec, +} + +impl From for Descriptor { + fn from(d: cmsg_source1legacy_game_event_list::Descriptor_t) -> Self { + Descriptor { + eventid: d.eventid(), + name: d.name().to_string(), + keys: d.keys.into_iter().map(DescriptorKey::from).collect(), + } + } +} + +impl std::fmt::Display for Descriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "struct {} {{", self.name)?; + for key in &self.keys { + match key.type_ { + 1 => writeln!(f, " {}: String,", key.name)?, + 2 => writeln!(f, " {}: f32,", key.name)?, + 3 => writeln!(f, " {}: i32, // long", key.name)?, + 4 => writeln!(f, " {}: i32, // short", key.name)?, + 5 => writeln!(f, " {}: i32, // byte", key.name)?, + 6 => writeln!(f, " {}: bool,", key.name)?, + 7 => writeln!(f, " {}: u64,", key.name)?, + 8 => writeln!(f, " {}: i32, // long, strict_ehandle", key.name)?, + 9 => writeln!(f, " {}: i32, // short, playercontroller", key.name)?, + t => writeln!(f, " {}: ", key.name)?, + }; + } + writeln!(f, "}}") + } +} + +#[allow(dead_code)] +pub(crate) fn dump_descriptors(descriptors: HashMap) { + let mut sorted: Vec<_> = descriptors.values().collect(); + sorted.sort_by_key(|d| &d.name); + for d in sorted { + println!("{d}"); + } +} + +pub(crate) fn parse_game_event_list(gel: CMsgSource1LegacyGameEventList) -> GameEventDescriptors { + gel.descriptors + .into_iter() + .map(|d| (d.eventid(), Descriptor::from(d))) + .collect() +} diff --git a/csdemoparser/src/game_event/de.rs b/cs2-demo/src/game_event/de.rs similarity index 96% rename from csdemoparser/src/game_event/de.rs rename to cs2-demo/src/game_event/de.rs index 9df39c6..2a183a0 100644 --- a/csdemoparser/src/game_event/de.rs +++ b/cs2-demo/src/game_event/de.rs @@ -1,12 +1,12 @@ use super::Descriptor; -use cs2_demo::proto::gameevents::{cmsg_source1legacy_game_event, CMsgSource1LegacyGameEvent}; +use crate::proto::gameevents::{cmsg_source1legacy_game_event, CMsgSource1LegacyGameEvent}; use serde::{de, forward_to_deserialize_any}; use std::fmt::Display; -pub(crate) type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] -pub(crate) enum Error { +pub enum Error { #[error("{0}")] Message(String), #[error("event {event}.{key} expected type {descriptor_type} but got type {event_type}")] @@ -25,7 +25,7 @@ pub(crate) enum Error { }, } -pub(crate) fn from_cs2_event<'a, T: serde::Deserialize<'a>>( +pub fn from_proto<'a, T: serde::Deserialize<'a>>( event: CMsgSource1LegacyGameEvent, descriptor: &'a Descriptor, ) -> Result { diff --git a/cs2-demo/src/lib.rs b/cs2-demo/src/lib.rs index 2f912f9..f1f0f9d 100644 --- a/cs2-demo/src/lib.rs +++ b/cs2-demo/src/lib.rs @@ -1,85 +1,19 @@ mod demo_command; pub mod entity; +mod error; +pub mod game_event; mod message; mod packet; pub mod proto; mod string_table; #[cfg(test)] mod testdata; +mod visit; -use crate::proto::demo::EDemoCommands; -use demo_format::Tick; -use protobuf::CodedInputStream; -use std::io; - -pub use crate::demo_command::DemoCommand; -pub use crate::message::Message; +pub use crate::error::{Error, Result}; +pub use crate::game_event::GameEventDescriptors; pub use crate::string_table::{PlayerInfo, StringTable, UserInfo}; - -/// Error type for this library. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Protobuf(#[from] protobuf::Error), - #[error("invalid demo type (expected: PBDEMS2, found: {found})")] - InvalidDemoType { found: String }, - #[error("unknown packet command found: {0}")] - UnknownPacketCommand(u32), - #[error(transparent)] - Decompression(#[from] snap::Error), - #[error("missing string_table from CDemoFullPacket")] - MissingStringTable, - #[error("missing packet from CDemoFullPacket")] - MissingPacket, - #[error("cannot parse string table player index")] - InvalidPlayerIndex, - #[error("cannot parse sendtables")] - InvalidSendTables, - #[error("invalid entity id in PacketEntities")] - InvalidEntityId, - #[error("missing class_id in CDemoClassInfo")] - MissingClassId, - #[error("missing class name CDemoClassInfo")] - MissingClassName, - #[error("skipped class_id in CDemoClassInfo")] - SkippedClassId, - #[error("duplicate serializer in CDemoSendTables")] - DuplicateSerializer, -} - -pub type Result = std::result::Result; - -pub struct DemoParser<'a> { - reader: CodedInputStream<'a>, -} - -impl<'a> DemoParser<'a> { - pub fn try_new_after_demo_type(read: &'a mut dyn io::Read) -> Result { - let mut reader = CodedInputStream::new(read); - reader.skip_raw_bytes(8)?; - Ok(Self { reader }) - } - - pub fn parse_next_demo_command(&mut self) -> Result> { - if self.reader.eof()? { - return Ok(None); - } - let cmd_flags = self.reader.read_raw_varint32()?; - let cmd = cmd_flags & !(EDemoCommands::DEM_IsCompressed as u32); - let compressed = (cmd_flags & (EDemoCommands::DEM_IsCompressed as u32)) != 0; - let tick = self.reader.read_raw_varint32()? as i32; - let size = self.reader.read_raw_varint32()?; - let data = self.reader.read_raw_bytes(size)?; - let data = if compressed { - snap::raw::Decoder::new().decompress_vec(data.as_slice())? - } else { - data - }; - Ok(Some((tick, DemoCommand::try_new(cmd, &data)?))) - } -} +pub use crate::visit::{parse_after_demo_type, Visitor}; #[allow(dead_code)] pub(crate) fn dump(msg: &M, file: &str) diff --git a/cs2-demo/src/visit.rs b/cs2-demo/src/visit.rs new file mode 100644 index 0000000..5c7b853 --- /dev/null +++ b/cs2-demo/src/visit.rs @@ -0,0 +1,111 @@ +use demo_format::Tick; +use tracing::{trace, trace_span}; + +use crate::demo_command::{DemoCommand, DemoParser}; +use crate::entity::{Classes, Entities, SendTables}; +use crate::game_event::{parse_game_event_list, GameEventDescriptors}; +use crate::message::Message; +use crate::proto::demo::CDemoFileHeader; +use crate::proto::gameevents::CMsgSource1LegacyGameEvent; +use crate::proto::netmessages::CSVCMsg_ServerInfo; +use crate::{Error, StringTable}; + +pub trait Visitor { + fn visit_file_header(&mut self, _file_header: CDemoFileHeader) -> anyhow::Result<()> { + Ok(()) + } + fn visit_server_info(&mut self, _server_info: CSVCMsg_ServerInfo) -> anyhow::Result<()> { + Ok(()) + } + fn visit_string_tables(&mut self, _string_tables: Vec) -> anyhow::Result<()> { + Ok(()) + } + fn visit_game_event( + &mut self, + _game_event: CMsgSource1LegacyGameEvent, + _tick: Tick, + ) -> anyhow::Result<()> { + Ok(()) + } + fn visit_game_event_descriptors( + &mut self, + _descriptors: GameEventDescriptors, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +pub fn parse_after_demo_type( + read: &mut dyn std::io::Read, + visitor: &mut dyn Visitor, +) -> anyhow::Result<()> { + let mut parser = DemoParser::try_new_after_demo_type(read)?; + let mut send_tables = None; + let mut classes = None; + let mut next = parser.parse_next_demo_command()?; + while let Some((tick @ -1, cmd)) = next { + trace_span!("demo_command").in_scope(|| trace!("#{tick} {cmd}")); + match cmd { + DemoCommand::SendTables(send) => send_tables = Some(SendTables::try_new(send)?), + DemoCommand::ClassInfo(ci) => { + let send_tables = send_tables.take().ok_or(Error::PacketOutOfOrder)?; + classes = Some(Classes::try_new(ci, send_tables)?) + } + DemoCommand::Packet(p) => { + for msg in p.messages { + match msg { + Message::PacketEntities(_) => Err(Error::PacketOutOfOrder)?, + Message::Unknown(_) => (), + _ => process_message(tick, msg, visitor)?, + } + } + } + _ => process_demo_command(cmd, visitor)?, + } + next = parser.parse_next_demo_command()?; + } + + let classes = classes.ok_or(Error::PacketOutOfOrder)?; + let mut entities = Entities::default(); + while let Some((tick, cmd)) = next { + trace_span!("demo_command").in_scope(|| trace!("#{tick:?} {cmd}")); + match cmd { + DemoCommand::SendTables(_) | DemoCommand::ClassInfo(_) => Err(Error::PacketOutOfOrder)?, + DemoCommand::Packet(p) => { + for msg in p.messages { + match msg { + Message::PacketEntities(pe) => { + entities.read_packet_entities(pe, &classes)? + } + Message::Unknown(_) => (), + _ => process_message(tick, msg, visitor)?, + } + } + } + DemoCommand::StringTables(st) => visitor.visit_string_tables(st)?, + _ => process_demo_command(cmd, visitor)?, + } + next = parser.parse_next_demo_command()?; + } + Ok(()) +} + +fn process_demo_command(cmd: DemoCommand, visitor: &mut dyn Visitor) -> anyhow::Result<()> { + match cmd { + DemoCommand::FileHeader(header) => visitor.visit_file_header(header), + DemoCommand::StringTables(st) => visitor.visit_string_tables(st), + _ => Ok(()), + } +} + +fn process_message(tick: Tick, msg: Message, visitor: &mut dyn Visitor) -> anyhow::Result<()> { + match msg { + Message::ServerInfo(si) => visitor.visit_server_info(si), + Message::Source1LegacyGameEventList(gel) => { + visitor.visit_game_event_descriptors(parse_game_event_list(gel)) + } + Message::Source1LegacyGameEvent(ge) => visitor.visit_game_event(ge, tick), + Message::PacketEntities(_) => unreachable!(), + Message::Unknown(_) => Ok(()), + } +} diff --git a/csdemoparser/src/cs2.rs b/csdemoparser/src/cs2.rs index fe08c2b..dd28377 100644 --- a/csdemoparser/src/cs2.rs +++ b/csdemoparser/src/cs2.rs @@ -3,116 +3,116 @@ use crate::demoinfo::{ RoundEnd, RoundStart, }; -use crate::game_event::{from_cs2_event, parse_game_event_list_impl, GameEvent}; +use crate::game_event::GameEvent; use crate::last_jump::LastJump; -use crate::{game_event, DemoInfo, Slot, UserId}; -use cs2_demo::entity::{Classes, Entities, SendTables}; +use crate::{DemoInfo, Slot, UserId}; use cs2_demo::proto::demo::CDemoFileHeader; -use cs2_demo::proto::gameevents::CMsgSource1LegacyGameEventList; -use cs2_demo::proto::netmessages::CSVCMsg_PacketEntities; -use cs2_demo::{DemoCommand, StringTable}; -use cs2_demo::{Message, UserInfo}; +use cs2_demo::proto::gameevents::CMsgSource1LegacyGameEvent; +use cs2_demo::{GameEventDescriptors, StringTable, UserInfo, Visitor}; use demo_format::Tick; -use std::cell::RefCell; use std::collections::{hash_map, HashMap}; -use std::io; -use std::rc::Rc; -use tracing::{instrument, trace, trace_span}; +use tracing::{instrument, trace}; -pub fn parse(read: &mut dyn io::Read) -> anyhow::Result { - let mut parser = cs2_demo::DemoParser::try_new_after_demo_type(read)?; +pub fn parse(read: &mut dyn std::io::Read) -> anyhow::Result { let mut state = GameState::new(); - while let Some((tick, cmd)) = parser.parse_next_demo_command()? { - trace_span!("demo_command").in_scope(|| trace!("#{tick:?} {cmd}")); - match cmd { - DemoCommand::FileHeader(header) => { - state.handle_file_header(header)?; - } - DemoCommand::Packet(p) => { - for msg in p.messages { - state.handle_packet(msg, tick)?; - } - } - DemoCommand::StringTables(st) => state.handle_string_tables(st)?, - DemoCommand::ClassInfo(ci) => state.classes = Classes::try_new(ci, Rc::clone(&state.send_tables))?, - DemoCommand::SendTables(send) => state.send_tables = Rc::new(SendTables::try_new(send)?), - _ => {} - } - } + cs2_demo::parse_after_demo_type(read, &mut state)?; state.get_info() } #[derive(Default)] struct GameState { - classes: Classes, - send_tables: Rc, - game_event_descriptors: HashMap, - entities: Entities, + game_event_descriptors: GameEventDescriptors, last_jump: LastJump, /// Maps player user_id to slot. user_id2slot: HashMap, /// Maps player slot to player info. players: HashMap, - // DemoInfo fields + demoinfo: DemoInfo, + // DemoInfo field events: Vec, - player_names: HashMap, - player_slots: HashMap, - tick_interval: f32, - demoinfo: Rc>, } -impl GameState { - fn new() -> Self { - Default::default() +impl Visitor for GameState { + fn visit_file_header(&mut self, header: CDemoFileHeader) -> anyhow::Result<()> { + self.demoinfo.servername = header.server_name().to_string(); + self.demoinfo.map = header.map_name().to_string(); + Ok(()) } - fn handle_file_header(&mut self, header: CDemoFileHeader) -> anyhow::Result<()> { - let mut demoinfo = self.demoinfo.borrow_mut(); - demoinfo.servername = header.server_name().to_string(); - demoinfo.map = header.map_name().to_string(); + fn visit_server_info( + &mut self, + server_info: cs2_demo::proto::netmessages::CSVCMsg_ServerInfo, + ) -> anyhow::Result<()> { + self.demoinfo.tickrate = server_info.tick_interval(); Ok(()) } - #[instrument(level = "trace", skip_all)] - fn handle_packet(&mut self, msg: Message, tick: Tick) -> anyhow::Result<()> { - match msg { - Message::Source1LegacyGameEvent(event) => { - if let Some(descriptor) = self.game_event_descriptors.get(&event.eventid()) { - let event = from_cs2_event(event, descriptor)?; - self.handle_game_event(event, tick)?; + fn visit_string_tables(&mut self, st: Vec) -> anyhow::Result<()> { + for table in st { + match table { + StringTable::UserInfo(table) => { + for ui in table { + self.update_players(ui); + } } } - Message::Source1LegacyGameEventList(gel) => { - self.game_event_descriptors = parse_game_event_list(gel); - } - Message::ServerInfo(si) => self.tick_interval = si.tick_interval(), - Message::PacketEntities(pe) => self.handle_packet_entities(pe, tick)?, - Message::Unknown(_) => (), } Ok(()) } - fn handle_packet_entities( + fn visit_game_event( + &mut self, + event: CMsgSource1LegacyGameEvent, + tick: Tick, + ) -> anyhow::Result<()> { + if let Some(descriptor) = self.game_event_descriptors.get(&event.eventid()) { + let event = cs2_demo::game_event::de::from_proto(event, descriptor)?; + self.handle_game_event(event, tick)?; + } + Ok(()) + } + + fn visit_game_event_descriptors( &mut self, - pe: CSVCMsg_PacketEntities, - _tick: Tick, + mut descriptors: GameEventDescriptors, ) -> anyhow::Result<()> { - self.entities.read_packet_entities(pe, &self.classes)?; + let hsbox_events = std::collections::HashSet::from([ + "bomb_defused", + "bomb_exploded", + "bot_takeover", + "game_restart", + "player_connect", + "player_death", + "player_disconnect", + "player_hurt", + "player_jump", + "player_spawn", + "round_end", + "round_officially_ended", + "round_start", + "score_changed", + "smokegrenade_detonate", + "smokegrenade_expired", + ]); + descriptors.retain(|_, ed| hsbox_events.contains(ed.name.as_str())); + self.game_event_descriptors = descriptors; Ok(()) } +} - fn get_info(self) -> anyhow::Result { - let mut demoinfo = self.demoinfo.borrow_mut(); - demoinfo.events = self +impl GameState { + fn new() -> Self { + Default::default() + } + + fn get_info(mut self) -> anyhow::Result { + self.demoinfo.events = self .events .iter() .map(serde_json::to_value) .collect::>()?; - demoinfo.tickrate = self.tick_interval; - demoinfo.player_names = self.player_names; - demoinfo.player_slots = self.player_slots; - Ok(demoinfo.clone()) + Ok(self.demoinfo) } fn add_event(&mut self, tick: Tick, event: Event) { @@ -155,7 +155,7 @@ impl GameState { let jump = self.last_jump.ticks_since_last_jump( UserId(e.attacker as u16), tick, - self.tick_interval, + self.demoinfo.tickrate, ); self.add_event( tick, @@ -228,25 +228,13 @@ impl GameState { Ok(()) } - fn handle_string_tables(&mut self, st: Vec) -> anyhow::Result<()> { - for table in st { - match table { - StringTable::UserInfo(table) => { - for ui in table { - self.update_players(ui); - } - } - } - } - Ok(()) - } - fn update_players(&mut self, ui: UserInfo) { let slot = Slot(ui.index); if let hash_map::Entry::Vacant(e) = self.players.entry(slot) { - self.player_names + self.demoinfo + .player_names .insert(ui.info.xuid.to_string(), ui.info.name.clone()); - self.player_slots + self.demoinfo.player_slots .insert(ui.info.xuid.to_string(), ui.info.user_id); self.user_id2slot .insert(UserId(ui.info.user_id as u16), slot); @@ -254,5 +242,3 @@ impl GameState { } } } - -parse_game_event_list_impl!(CMsgSource1LegacyGameEventList); diff --git a/csdemoparser/src/csgo.rs b/csdemoparser/src/csgo.rs index 333ddac..2d84086 100644 --- a/csdemoparser/src/csgo.rs +++ b/csdemoparser/src/csgo.rs @@ -1,14 +1,13 @@ +mod game_event; + use crate::entity::{Entities, Entity, EntityId, PropValue, Scalar}; use crate::entity::{ServerClasses, TrackProp}; -use crate::game_event::parse_game_event_list_impl; use crate::geometry::{through_smoke, Point}; use crate::last_jump::LastJump; use crate::string_table::{self, PlayerInfo, StringTables}; -use crate::{ - account_id_to_xuid, game_event, guid_to_xuid, maybe_get_i32, maybe_get_u16, DemoInfo, TeamScore, -}; +use crate::{account_id_to_xuid, guid_to_xuid, maybe_get_i32, maybe_get_u16, DemoInfo, TeamScore}; use anyhow::bail; -use csgo_demo::proto::netmessages::{CSVCMsg_GameEvent, CSVCMsg_GameEventList}; +use csgo_demo::proto::netmessages::CSVCMsg_GameEvent; use csgo_demo::Message; use csgo_demo::PacketContent; use csgo_demo::StringTable; @@ -78,7 +77,7 @@ pub fn parse(read: &mut dyn io::Read) -> anyhow::Result { type GameEvent = serde_json::Map; struct HeadshotBoxParser<'a> { - game_event_descriptors: HashMap, + game_event_descriptors: HashMap, string_tables: StringTables, players: HashMap, last_jump: LastJump, @@ -222,7 +221,7 @@ impl<'a> HeadshotBoxParser<'a> { Self::update_players(&mut self.players, &self.demoinfo, player_info); } } - Message::GameEventList(gel) => self.game_event_descriptors = parse_game_event_list(gel), + Message::GameEventList(gel) => self.game_event_descriptors = self::game_event::parse_game_event_list(gel), Message::GameEvent(event) => { if let Some(descriptor) = self.game_event_descriptors.get(&event.eventid()) { let attrs = self.event_map(event, descriptor, tick)?; @@ -487,8 +486,6 @@ impl<'a> HeadshotBoxParser<'a> { } } -parse_game_event_list_impl!(CSVCMsg_GameEventList); - #[cfg(test)] mod tests { use super::*; diff --git a/csdemoparser/src/csgo/game_event.rs b/csdemoparser/src/csgo/game_event.rs new file mode 100644 index 0000000..18fd5d4 --- /dev/null +++ b/csdemoparser/src/csgo/game_event.rs @@ -0,0 +1,75 @@ +use std::collections::{HashMap, HashSet}; + +use csgo_demo::proto::netmessages::CSVCMsg_GameEventList; + +pub(super) struct DescriptorKey { + pub type_: i32, + pub name: String, +} + +pub(super) struct Descriptor { + pub name: String, + pub keys: Vec, +} + +impl std::fmt::Display for Descriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "struct {} {{", self.name)?; + for key in &self.keys { + match key.type_ { + 1 => writeln!(f, " {}: String,", key.name)?, + 2 => writeln!(f, " {}: f32,", key.name)?, + 3 => writeln!(f, " {}: i32, // long", key.name)?, + 4 => writeln!(f, " {}: i32, // short", key.name)?, + 5 => writeln!(f, " {}: i32, // byte", key.name)?, + 6 => writeln!(f, " {}: bool,", key.name)?, + 7 => writeln!(f, " {}: u64,", key.name)?, + 8 => writeln!(f, " {}: i32, // long, strict_ehandle", key.name)?, + 9 => writeln!(f, " {}: i32, // short, playercontroller", key.name)?, + t => writeln!(f, " {}: ", key.name)?, + }; + } + writeln!(f, "}}") + } +} + +pub(super) fn parse_game_event_list(gel: CSVCMsg_GameEventList) -> HashMap { + let hsbox_events = HashSet::from([ + "bomb_defused", + "bomb_exploded", + "bot_takeover", + "game_restart", + "player_connect", + "player_death", + "player_disconnect", + "player_hurt", + "player_jump", + "player_spawn", + "round_end", + "round_officially_ended", + "round_start", + "score_changed", + "smokegrenade_detonate", + "smokegrenade_expired", + ]); + gel.descriptors + .into_iter() + .filter(|d| hsbox_events.contains(d.name())) + .map(|d| { + ( + d.eventid(), + Descriptor { + name: d.name().to_string(), + keys: d + .keys + .iter() + .map(|k| DescriptorKey { + name: k.name().to_string(), + type_: k.type_(), + }) + .collect(), + }, + ) + }) + .collect() +} diff --git a/csdemoparser/src/game_event.rs b/csdemoparser/src/game_event.rs index 1a6b4c5..bc8032a 100644 --- a/csdemoparser/src/game_event.rs +++ b/csdemoparser/src/game_event.rs @@ -1,10 +1,6 @@ #![allow(dead_code)] -mod de; - -pub(crate) use de::from_cs2_event; use serde::Deserialize; -use std::collections::HashMap; #[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] @@ -115,91 +111,3 @@ pub(crate) struct SmokegrenadeExpired { pub y: f32, pub z: f32, } - -pub(crate) struct DescriptorKey { - pub type_: i32, - pub name: String, -} - -pub(crate) struct Descriptor { - pub name: String, - pub keys: Vec, -} - -impl std::fmt::Display for Descriptor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "struct {} {{", self.name)?; - for key in &self.keys { - match key.type_ { - 1 => writeln!(f, " {}: String,", key.name)?, - 2 => writeln!(f, " {}: f32,", key.name)?, - 3 => writeln!(f, " {}: i32, // long", key.name)?, - 4 => writeln!(f, " {}: i32, // short", key.name)?, - 5 => writeln!(f, " {}: i32, // byte", key.name)?, - 6 => writeln!(f, " {}: bool,", key.name)?, - 7 => writeln!(f, " {}: u64,", key.name)?, - 8 => writeln!(f, " {}: i32, // long, strict_ehandle", key.name)?, - 9 => writeln!(f, " {}: i32, // short, playercontroller", key.name)?, - t => writeln!(f, " {}: ", key.name)?, - }; - } - writeln!(f, "}}") - } -} - -#[allow(dead_code)] -pub(crate) fn dump_descriptors(descriptors: HashMap) { - let mut sorted: Vec<_> = descriptors.values().collect(); - sorted.sort_by_key(|d| &d.name); - for d in sorted { - println!("{d}"); - } -} - -macro_rules! parse_game_event_list_impl { - ($game_event_list:ty) => { - fn parse_game_event_list( - gel: $game_event_list, - ) -> HashMap { - let hsbox_events = std::collections::HashSet::from([ - "bomb_defused", - "bomb_exploded", - "bot_takeover", - "game_restart", - "player_connect", - "player_death", - "player_disconnect", - "player_hurt", - "player_jump", - "player_spawn", - "round_end", - "round_officially_ended", - "round_start", - "score_changed", - "smokegrenade_detonate", - "smokegrenade_expired", - ]); - gel.descriptors - .into_iter() - .filter(|d| hsbox_events.contains(d.name())) - .map(|d| { - ( - d.eventid(), - crate::game_event::Descriptor { - name: d.name().to_string(), - keys: d - .keys - .iter() - .map(|k| game_event::DescriptorKey { - name: k.name().to_string(), - type_: k.type_(), - }) - .collect(), - }, - ) - }) - .collect() - } - }; -} -pub(crate) use parse_game_event_list_impl;