From cfff05a82fc2dc7144a45fd45ea7879c0cb5513d Mon Sep 17 00:00:00 2001 From: Lol3rrr Date: Sun, 15 Sep 2024 16:13:40 +0200 Subject: [PATCH] Add some more types and improve typing for player events --- src/game_event.rs | 247 ++++++++++++++++++++++++++++++---------------- src/lib.rs | 3 + src/parser.rs | 44 ++++++--- src/values.rs | 71 +++++++++++++ tests/parse.rs | 24 ++++- 5 files changed, 291 insertions(+), 98 deletions(-) create mode 100644 src/values.rs diff --git a/src/game_event.rs b/src/game_event.rs index 8a24c20..5f58b64 100644 --- a/src/game_event.rs +++ b/src/game_event.rs @@ -1,60 +1,4 @@ -use crate::csgo_proto; - -#[derive(Debug)] -pub enum RawValue { - String(String), - F32(f32), - I32(i32), - Bool(bool), - U64(u64), -} - -impl TryFrom for RawValue { - type Error = (); - - fn try_from(value: crate::csgo_proto::c_msg_source1_legacy_game_event::KeyT) -> Result { - match value.r#type() { - 1 if value.val_string.is_some() => Ok(Self::String(value.val_string.unwrap())), - 2 if value.val_float.is_some() => Ok(Self::F32(value.val_float.unwrap())), - 3 if value.val_long.is_some() => Ok(Self::I32(value.val_long.unwrap())), - 4 if value.val_short.is_some() => Ok(Self::I32(value.val_short.unwrap() as i32)), - 5 if value.val_byte.is_some() => Ok(Self::I32(value.val_byte.unwrap() as i32)), - 6 if value.val_bool.is_some() => Ok(Self::Bool(value.val_bool.unwrap())), - 7 if value.val_uint64.is_some() => Ok(Self::U64(value.val_uint64.unwrap())), - 8 if value.val_long.is_some() => Ok(Self::I32(value.val_long.unwrap())), - 9 if value.val_short.is_some() => Ok(Self::I32(value.val_short.unwrap() as i32)), - _ => Err(()), - } - } -} - -impl TryFrom for i32 { - type Error = (); - fn try_from(value: RawValue) -> Result { - match value { - RawValue::I32(v) => Ok(v), - _ => Err(()), - } - } -} -impl TryFrom for bool { - type Error = (); - fn try_from(value: RawValue) -> Result { - match value { - RawValue::Bool(v) => Ok(v), - _ => Err(()), - } - } -} -impl TryFrom for String { - type Error = (); - fn try_from(value: RawValue) -> Result { - match value { - RawValue::String(v) => Ok(v), - _ => Err(()), - } - } -} +use crate::{csgo_proto, RawValue, UserId}; macro_rules! define_event { ($name:ident, $target:path $(, ($field:ident, $field_ty:ty))*) => { @@ -87,12 +31,48 @@ macro_rules! define_event { define_event!(HltvVersionInfo, GameEvent::HltvVersionInfo); -define_event!(ItemEquip, GameEvent::ItemEquip, (userid, i32), (hassilencer, bool), (hastracers, bool), (item, String), (issilenced, bool), (canzoom, bool), (ispainted, bool), (weptype, i32), (defindex, i32)); -define_event!(ItemPickup, GameEvent::ItemPickup, (userid, RawValue), (item, RawValue), (silent, RawValue), (defindex, RawValue)); +define_event!( + ItemEquip, + GameEvent::ItemEquip, + (userid, UserId), + (hassilencer, bool), + (hastracers, bool), + (item, String), + (issilenced, bool), + (canzoom, bool), + (ispainted, bool), + (weptype, i32), + (defindex, i32) +); +define_event!( + ItemPickup, + GameEvent::ItemPickup, + (userid, UserId), + (item, RawValue), + (silent, RawValue), + (defindex, RawValue) +); -define_event!(WeaponReload, GameEvent::WeaponReload, (userid, RawValue), (userid_pawn, RawValue)); -define_event!(WeaponZoom, GameEvent::WeaponZoom, (userid, RawValue), (userid_pawn, RawValue)); -define_event!(WeaponFire, GameEvent::WeaponFire, (userid, RawValue), (weapon, RawValue), (silenced, RawValue), (userid_pawn, RawValue)); +define_event!( + WeaponReload, + GameEvent::WeaponReload, + (userid, UserId), + (userid_pawn, RawValue) +); +define_event!( + WeaponZoom, + GameEvent::WeaponZoom, + (userid, UserId), + (userid_pawn, RawValue) +); +define_event!( + WeaponFire, + GameEvent::WeaponFire, + (userid, UserId), + (weapon, RawValue), + (silenced, RawValue), + (userid_pawn, RawValue) +); define_event!(SmokeGrenadeDetonate, GameEvent::SmokeGrenadeDetonate); define_event!(SmokeGrenadeExpired, GameEvent::SmokeGrenadeExpired); @@ -103,16 +83,106 @@ define_event!(FlashbangDetonate, GameEvent::FlashbangDetonate); define_event!(DecoyStarted, GameEvent::DecoyStarted); define_event!(DecoyDetonate, GameEvent::DecoyDetonate); -define_event!(PlayerConnect, GameEvent::PlayerConnect, (address, RawValue), (bot, RawValue), (name, RawValue), (userid, RawValue), (networkid, RawValue), (xuid, RawValue)); -define_event!(PlayerConnectFull, GameEvent::PlayerConnectFull, (userid, RawValue)); -define_event!(PlayerDisconnect, GameEvent::PlayerDisconnect, (userid, RawValue), (reason, RawValue), (name, RawValue), (networkid, RawValue), (xuid, RawValue)); -define_event!(PlayerFootstep, GameEvent::PlayerFootstep); -define_event!(PlayerJump, GameEvent::PlayerJump); -define_event!(PlayerHurt, GameEvent::PlayerHurt); -define_event!(PlayerDeath, GameEvent::PlayerDeath); -define_event!(PlayerSpawn, GameEvent::PlayerSpawn); -define_event!(PlayerBlind, GameEvent::PlayerBlind); -define_event!(PlayerTeam, GameEvent::PlayerTeam, (userid, RawValue), (team, RawValue), (oldteam, RawValue), (disconnect, RawValue), (silent, RawValue), (isbot, RawValue), (userid_pawn, RawValue)); +define_event!( + PlayerConnect, + GameEvent::PlayerConnect, + (address, RawValue), + (bot, RawValue), + (name, RawValue), + (userid, i32), + (networkid, RawValue), + (xuid, RawValue) +); +define_event!( + PlayerConnectFull, + GameEvent::PlayerConnectFull, + (userid, UserId) +); +define_event!( + PlayerDisconnect, + GameEvent::PlayerDisconnect, + (userid, UserId), + (reason, RawValue), + (name, RawValue), + (networkid, RawValue), + (xuid, RawValue) +); +define_event!( + PlayerFootstep, + GameEvent::PlayerFootstep, + (userid, UserId), + (userid_pawn, RawValue) +); +define_event!(PlayerJump, GameEvent::PlayerJump, (userid, i32)); +define_event!( + PlayerHurt, + GameEvent::PlayerHurt, + (userid, UserId), + (attacker, UserId), + (health, RawValue), + (armor, RawValue), + (weapon, String), + (dmg_health, RawValue), + (dmg_armor, RawValue), + (hitgroup, RawValue), + (userid_pawn, RawValue), + (attacker_pawn, RawValue) +); +define_event!( + PlayerDeath, + GameEvent::PlayerDeath, + (userid, UserId), + (attacker, UserId), + (assister, UserId), + (assistedflash, bool), + (weapon, String), + (weapon_itemid, String), + (weapon_fauxitemid, String), + (weapon_originalowner_xuid, String), + (headshot, bool), + (dominated, RawValue), + (revenge, RawValue), + (wipe, RawValue), + (penetrated, RawValue), + (noreplay, bool), + (noscope, bool), + (thrusmoke, bool), + (attackerblind, bool), + (distance, RawValue), + (userid_pawn, RawValue), + (attacker_pawn, RawValue), + (assister_pawn, RawValue), + (dmg_health, RawValue), + (dmg_armor, RawValue), + (hitgroup, RawValue), + (attackerinair, bool) +); +define_event!( + PlayerSpawn, + GameEvent::PlayerSpawn, + (userid, UserId), + (inrestart, RawValue), + (userid_pawn, RawValue) +); +define_event!( + PlayerBlind, + GameEvent::PlayerBlind, + (userid, UserId), + (attacker, RawValue), + (entityid, RawValue), + (blind_duration, RawValue) +); +define_event!( + PlayerTeam, + GameEvent::PlayerTeam, + (userid, UserId), + (team, RawValue), + (oldteam, RawValue), + (disconnect, RawValue), + (silent, RawValue), + (isbot, RawValue), + (userid_pawn, RawValue) +); define_event!(BulletDamage, GameEvent::BulletDamage); @@ -138,12 +208,18 @@ define_event!(RoundPreRestart, GameEvent::RoundPreRestart); define_event!(RoundTimeWarning, GameEvent::RoundTimeWarning); define_event!(RoundFinalBeep, GameEvent::RoundFinalBeep); define_event!(BuyTimeEnded, GameEvent::BuyTimeEnded); -define_event!(RoundAnnounceLastRoundHalf, GameEvent::RoundAnnounceLastRoundHalf); +define_event!( + RoundAnnounceLastRoundHalf, + GameEvent::RoundAnnounceLastRoundHalf +); define_event!(AnnouncePhaseEnd, GameEvent::AnnouncePhaseEnd); define_event!(WinPanelMatch, GameEvent::WinPanelMatch); -type ParseFn = fn(keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], event: csgo_proto::CMsgSource1LegacyGameEvent) -> Result; +type ParseFn = fn( + keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], + event: csgo_proto::CMsgSource1LegacyGameEvent, +) -> Result; #[derive(Debug)] #[allow(dead_code)] @@ -203,7 +279,7 @@ pub enum GameEvent { BuyTimeEnded(BuyTimeEnded), RoundAnnounceLastRoundHalf(RoundAnnounceLastRoundHalf), AnnouncePhaseEnd(AnnouncePhaseEnd), - + WinPanelMatch(WinPanelMatch), } @@ -217,7 +293,7 @@ pub static EVENT_PARSERS: phf::Map<&'static str, GameEventParser> = phf::phf_map "item_equip" => GameEventParser::new(ItemEquip::parse), "item_pickup" => GameEventParser::new(ItemPickup::parse), - + "weapon_reload" => GameEventParser::new(WeaponReload::parse), "weapon_zoom" => GameEventParser::new(WeaponZoom::parse), "weapon_fire" => GameEventParser::new(WeaponFire::parse), @@ -230,7 +306,7 @@ pub static EVENT_PARSERS: phf::Map<&'static str, GameEventParser> = phf::phf_map "flashbang_detonate" => GameEventParser::new(FlashbangDetonate::parse), "decoy_started" => GameEventParser::new(DecoyStarted::parse), "decoy_detonate" => GameEventParser::new(DecoyDetonate::parse), - + "player_connect" => GameEventParser::new(PlayerConnect::parse), "player_connect_full" => GameEventParser::new(PlayerConnectFull::parse), "player_disconnect" => GameEventParser::new(PlayerDisconnect::parse), @@ -241,7 +317,7 @@ pub static EVENT_PARSERS: phf::Map<&'static str, GameEventParser> = phf::phf_map "player_spawn" => GameEventParser::new(PlayerSpawn::parse), "player_blind" => GameEventParser::new(PlayerBlind::parse), "player_team" => GameEventParser::new(PlayerTeam::parse), - + "bullet_damage" => GameEventParser::new(BulletDamage::parse), "other_death" => GameEventParser::new(OtherDeath::parse), @@ -273,17 +349,22 @@ pub static EVENT_PARSERS: phf::Map<&'static str, GameEventParser> = phf::phf_map }; pub struct GameEventParser { - inner: fn(keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], event: csgo_proto::CMsgSource1LegacyGameEvent) -> Result, + inner: fn( + keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], + event: csgo_proto::CMsgSource1LegacyGameEvent, + ) -> Result, } impl GameEventParser { pub const fn new(func: ParseFn) -> Self { - Self { - inner: func, - } + Self { inner: func } } - pub fn parse(&self, keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], event: csgo_proto::CMsgSource1LegacyGameEvent) -> Result { + pub fn parse( + &self, + keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], + event: csgo_proto::CMsgSource1LegacyGameEvent, + ) -> Result { if keys.len() != event.keys.len() { return Err(ParseGameEventError::MismatchedKeysFields); } diff --git a/src/lib.rs b/src/lib.rs index 51681c0..ce06f7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,9 @@ mod packet; pub use packet::DemoEvent; pub mod game_event; +mod values; +pub use values::*; + pub mod parser; pub mod csgo_proto { diff --git a/src/parser.rs b/src/parser.rs index 010ed74..3d9cc3e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,4 +1,4 @@ -use crate::{packet::DemoEvent, DemoCommand, Frame}; +use crate::{packet::DemoEvent, DemoCommand, Frame, UserId}; #[derive(Debug)] pub enum FirstPassError { @@ -8,7 +8,7 @@ pub enum FirstPassError { MissingFileHeader, MissingFileInfo, Bitreader(crate::bitreader::BitReadError), - ParseGameEventError(crate::game_event::ParseGameEventError) + ParseGameEventError(crate::game_event::ParseGameEventError), } impl From for FirstPassError { @@ -38,12 +38,18 @@ pub struct FirstPassOutput { pub header: crate::csgo_proto::CDemoFileHeader, pub info: crate::csgo_proto::CDemoFileInfo, pub events: Vec, - pub player_info: std::collections::HashMap, + pub player_info: std::collections::HashMap, } #[derive(Debug)] struct GameEventMapping { - mapping: std::collections::HashMap)>, + mapping: std::collections::HashMap< + i32, + ( + String, + Vec, + ), + >, } pub fn parse<'b, FI>(frames: FI) -> Result @@ -87,14 +93,19 @@ where let header = header.ok_or(FirstPassError::MissingFileHeader)?; let info = file_info.ok_or(FirstPassError::MissingFileInfo)?; - Ok(FirstPassOutput { header, info, events, player_info }) + Ok(FirstPassOutput { + header, + info, + events, + player_info, + }) } fn parse_fullpacket( data: &[u8], events: &mut Vec, event_mapper: &mut GameEventMapping, - player_info: &mut std::collections::HashMap, + player_info: &mut std::collections::HashMap, ) -> Result<(), FirstPassError> { let raw: crate::csgo_proto::CDemoFullPacket = prost::Message::decode(data)?; @@ -115,7 +126,7 @@ fn parse_packet( data: &[u8], events: &mut Vec, event_mapper: &mut GameEventMapping, - player_info: &mut std::collections::HashMap, + player_info: &mut std::collections::HashMap, ) -> Result<(), FirstPassError> { let raw: crate::csgo_proto::CDemoPacket = prost::Message::decode(data)?; @@ -128,7 +139,7 @@ fn inner_parse_packet( raw: &crate::csgo_proto::CDemoPacket, events: &mut Vec, event_mapper: &mut GameEventMapping, - player_info: &mut std::collections::HashMap, + player_info: &mut std::collections::HashMap, ) -> Result<(), FirstPassError> { let mut bitreader = crate::bitreader::Bitreader::new(raw.data()); @@ -167,7 +178,8 @@ fn inner_parse_packet( events.push(DemoEvent::ServerInfo(raw)); } crate::netmessagetypes::NetmessageType::net_SignonState => { - let raw: crate::csgo_proto::CnetMsgSignonState = prost::Message::decode(msg_bytes.as_slice())?; + let raw: crate::csgo_proto::CnetMsgSignonState = + prost::Message::decode(msg_bytes.as_slice())?; dbg!(raw); } crate::netmessagetypes::NetmessageType::net_Tick => { @@ -228,13 +240,17 @@ fn inner_parse_packet( crate::netmessagetypes::NetmessageType::TE_EffectDispatch => {} crate::netmessagetypes::NetmessageType::CS_UM_PlayerStatsUpdate => {} crate::netmessagetypes::NetmessageType::CS_UM_EndOfMatchAllPlayersData => { - let raw: crate::csgo_proto::CcsUsrMsgEndOfMatchAllPlayersData = prost::Message::decode(msg_bytes.as_slice())?; + let raw: crate::csgo_proto::CcsUsrMsgEndOfMatchAllPlayersData = + prost::Message::decode(msg_bytes.as_slice())?; for data in raw.allplayerdata { - player_info.insert(data.slot(), Player { - name: data.name.unwrap(), - xuid: data.xuid.unwrap(), - }); + player_info.insert( + UserId(data.slot()), + Player { + name: data.name.unwrap(), + xuid: data.xuid.unwrap(), + }, + ); } } crate::netmessagetypes::NetmessageType::TE_PhysicsProp => {} diff --git a/src/values.rs b/src/values.rs new file mode 100644 index 0000000..bc5eff0 --- /dev/null +++ b/src/values.rs @@ -0,0 +1,71 @@ +#[derive(Debug)] +pub enum RawValue { + String(String), + F32(f32), + I32(i32), + Bool(bool), + U64(u64), +} + +impl TryFrom for RawValue { + type Error = (); + + fn try_from( + value: crate::csgo_proto::c_msg_source1_legacy_game_event::KeyT, + ) -> Result { + match value.r#type() { + 1 if value.val_string.is_some() => Ok(Self::String(value.val_string.unwrap())), + 2 if value.val_float.is_some() => Ok(Self::F32(value.val_float.unwrap())), + 3 if value.val_long.is_some() => Ok(Self::I32(value.val_long.unwrap())), + 4 if value.val_short.is_some() => Ok(Self::I32(value.val_short.unwrap() as i32)), + 5 if value.val_byte.is_some() => Ok(Self::I32(value.val_byte.unwrap() as i32)), + 6 if value.val_bool.is_some() => Ok(Self::Bool(value.val_bool.unwrap())), + 7 if value.val_uint64.is_some() => Ok(Self::U64(value.val_uint64.unwrap())), + 8 if value.val_long.is_some() => Ok(Self::I32(value.val_long.unwrap())), + 9 if value.val_short.is_some() => Ok(Self::I32(value.val_short.unwrap() as i32)), + _ => Err(()), + } + } +} + +impl TryFrom for i32 { + type Error = (); + fn try_from(value: RawValue) -> Result { + match value { + RawValue::I32(v) => Ok(v), + _ => Err(()), + } + } +} +impl TryFrom for bool { + type Error = (); + fn try_from(value: RawValue) -> Result { + match value { + RawValue::Bool(v) => Ok(v), + _ => Err(()), + } + } +} +impl TryFrom for String { + type Error = (); + fn try_from(value: RawValue) -> Result { + match value { + RawValue::String(v) => Ok(v), + _ => Err(()), + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct UserId(pub(crate) i32); + +impl TryFrom for UserId { + type Error = (); + + fn try_from(value: RawValue) -> Result { + match value { + RawValue::I32(v) => Ok(Self(v)), + _ => Err(()), + } + } +} diff --git a/tests/parse.rs b/tests/parse.rs index 7495222..3c0c855 100644 --- a/tests/parse.rs +++ b/tests/parse.rs @@ -1,3 +1,5 @@ +use csdemo::{game_event::GameEvent, DemoEvent}; + #[test] fn mirage_1() { let content = std::fs::read("testfiles/mirage.dem").unwrap(); @@ -11,7 +13,27 @@ fn mirage_1() { assert_eq!("de_mirage", output.header.map_name()); - todo!() + for event in output.events.iter() { + match event { + DemoEvent::GameEvent(gevent) => match gevent { + GameEvent::PlayerDeath(death) => { + assert!( + death.remaining.is_empty(), + "Remaining for PlayerDeath: {:?}", + death.remaining + ); + + let died_user = output + .player_info + .get(death.userid.as_ref().unwrap()) + .unwrap(); + dbg!(died_user); + } + _ => {} + }, + _ => {} + }; + } } #[test]