diff --git a/csdemoparser/src/cs2.rs b/csdemoparser/src/cs2.rs index 8acbfe8..ede9c92 100644 --- a/csdemoparser/src/cs2.rs +++ b/csdemoparser/src/cs2.rs @@ -4,7 +4,8 @@ use crate::demoinfo::{ }; use crate::game_event::{from_cs2_event, parse_game_event_list_impl, GameEvent}; -use crate::{game_event, DemoInfo}; +use crate::last_jump::LastJump; +use crate::{game_event, DemoInfo, Slot, UserId}; use cs2_demo::proto::demo::CDemoFileHeader; use cs2_demo::proto::gameevents::CMsgSource1LegacyGameEventList; use cs2_demo::{DemoCommand, StringTable}; @@ -20,7 +21,7 @@ pub fn parse(read: &mut dyn io::Read) -> anyhow::Result { let mut parser = cs2_demo::DemoParser::try_new_after_demo_type(read)?; let mut state = GameState::new(); while let Some((tick, cmd)) = parser.parse_next_demo_command()? { - trace!("t#{tick:?} {cmd}"); + // trace!("t#{tick:?} {cmd}"); match cmd { DemoCommand::FileHeader(header) => { state.handle_file_header(header)?; @@ -37,16 +38,10 @@ pub fn parse(read: &mut dyn io::Read) -> anyhow::Result { state.get_info() } -#[derive(Eq, PartialEq, Hash, Clone, Copy)] -struct Slot(u16); -#[derive(Eq, PartialEq, Hash, Clone, Copy)] -struct UserId(u16); - #[derive(Default)] struct GameState { game_event_descriptors: HashMap, - /// Maps player user_id to last jump tick. - jumped_last: HashMap, + last_jump: LastJump, /// Maps player user_id to slot. user_id2slot: HashMap, /// Maps player slot to player info. @@ -62,9 +57,7 @@ struct GameState { impl GameState { fn new() -> Self { - GameState { - ..Default::default() - } + Default::default() } fn handle_file_header(&mut self, header: CDemoFileHeader) -> anyhow::Result<()> { @@ -109,6 +102,12 @@ impl GameState { self.events.push(EventTick { tick, event }) } + /// Returns the user XUID if available. + /// + /// userid 65535 is used as a marker for events where there is no alive player, for example: + /// - kills with no assister + /// - player disconnected + /// - player died, for example before the smoke_expired event fn maybe_xuid(&self, userid: i32) -> u64 { let Some(slot) = self.user_id2slot.get(&UserId(userid as u16)) else { return userid as u64; @@ -120,7 +119,7 @@ impl GameState { } fn handle_game_event(&mut self, ge: GameEvent, tick: Tick) -> anyhow::Result<()> { - trace!("GameEvent {:?}", ge); + trace!("#{tick} GameEvent {:?}", ge); match ge { GameEvent::BombDefused(e) => { let userid = self.maybe_xuid(e.userid); @@ -135,6 +134,11 @@ impl GameState { let userid = self.maybe_xuid(e.userid); let attacker = self.maybe_xuid(e.attacker); let assister = self.maybe_xuid(e.assister); + let jump = self.last_jump.ticks_since_last_jump( + UserId(e.attacker as u16), + tick, + self.tick_interval, + ); self.add_event( tick, Event::PlayerDeath(PlayerDeath { @@ -149,6 +153,7 @@ impl GameState { thrusmoke: e.thrusmoke, attackerblind: e.attackerblind, distance: e.distance, + jump, }), ) } @@ -173,7 +178,7 @@ impl GameState { ) } GameEvent::PlayerJump(e) => { - self.jumped_last.insert(UserId(e.userid as u16), tick); + self.last_jump.record_jump(UserId(e.userid as u16), tick); } GameEvent::PlayerSpawn(_) => { // In CS:GO, player_spawn was used to determine the team composition diff --git a/csdemoparser/src/csgo.rs b/csdemoparser/src/csgo.rs index ae0547e..333ddac 100644 --- a/csdemoparser/src/csgo.rs +++ b/csdemoparser/src/csgo.rs @@ -2,6 +2,7 @@ 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, @@ -80,7 +81,7 @@ struct HeadshotBoxParser<'a> { game_event_descriptors: HashMap, string_tables: StringTables, players: HashMap, - jumped_last: HashMap, + last_jump: LastJump, tick_interval: f32, entities: Entities<'a>, smokes: BTreeMap, @@ -159,7 +160,7 @@ impl<'a> HeadshotBoxParser<'a> { game_event_descriptors: Default::default(), string_tables: StringTables::new(), players: Default::default(), - jumped_last: HashMap::new(), + last_jump: Default::default(), tick_interval: 0.0, entities: Entities::new(server_classes), smokes: Default::default(), @@ -276,7 +277,7 @@ impl<'a> HeadshotBoxParser<'a> { match attrs.get("type").unwrap().as_str().unwrap() { "player_jump" => { if let Some(user_id) = maybe_get_i32(attrs.get("userid")) { - self.jumped_last.insert(user_id, tick); + self.last_jump.record_jump(user_id, tick); } } "smokegrenade_detonate" => { @@ -297,7 +298,11 @@ impl<'a> HeadshotBoxParser<'a> { "player_death" => { if let Some(attacker_user_id) = maybe_get_i32(attrs.get("attacker")) { if self.players.get(&attacker_user_id).is_some() { - if let Some(jump) = self.jumped_last(attacker_user_id, tick) { + if let Some(jump) = self.last_jump.ticks_since_last_jump( + attacker_user_id, + tick, + self.tick_interval, + ) { attrs.insert("jump".to_string(), json!(jump)); } } @@ -421,18 +426,6 @@ impl<'a> HeadshotBoxParser<'a> { Ok(attrs) } - fn jumped_last(&self, user_id: i32, tick: Tick) -> Option { - let &jumped_last = self.jumped_last.get(&user_id)?; - const JUMP_DURATION: f64 = 0.75; - if self.tick_interval > 0_f32 - && jumped_last as f64 >= tick as f64 - JUMP_DURATION / self.tick_interval as f64 - { - Some(tick - jumped_last) - } else { - None - } - } - fn get_player_info(&self, key: &str, attrs: &GameEvent) -> Option<&PlayerInfo> { let user_id = maybe_get_i32(attrs.get(key))?; let player_info = self.players.get(&user_id)?; diff --git a/csdemoparser/src/demoinfo.rs b/csdemoparser/src/demoinfo.rs index a1eb111..68d019d 100644 --- a/csdemoparser/src/demoinfo.rs +++ b/csdemoparser/src/demoinfo.rs @@ -83,7 +83,12 @@ pub struct PlayerDeath { pub noscope: bool, pub thrusmoke: bool, pub attackerblind: bool, + /// Distance in meters. 1 meter = 39.38 coordinate distance. pub distance: f32, + /// Number of ticks since the attacker jumped. Only set if death occurred + /// less than 0.75 seconds since the jump. + #[serde(skip_serializing_if = "Option::is_none")] + pub jump: Option, } #[derive(Serialize)] diff --git a/csdemoparser/src/last_jump.rs b/csdemoparser/src/last_jump.rs new file mode 100644 index 0000000..daedaaa --- /dev/null +++ b/csdemoparser/src/last_jump.rs @@ -0,0 +1,31 @@ +use demo_format::Tick; +use std::collections::HashMap; + +#[derive(Default)] +pub(crate) struct LastJump { + /// Maps player user_id to last jump tick. + jumped_last: HashMap, +} + +impl LastJump { + pub(crate) fn record_jump(&mut self, user_id: U, tick: Tick) { + self.jumped_last.insert(user_id, tick); + } + + pub(crate) fn ticks_since_last_jump( + &self, + user_id: U, + tick: Tick, + tick_interval: f32, + ) -> Option { + let &jumped_last = self.jumped_last.get(&user_id)?; + const JUMP_DURATION: f64 = 0.75; + if tick_interval > 0_f32 + && jumped_last as f64 >= tick as f64 - JUMP_DURATION / tick_interval as f64 + { + Some(tick - jumped_last) + } else { + None + } + } +} diff --git a/csdemoparser/src/lib.rs b/csdemoparser/src/lib.rs index db9693b..7785449 100644 --- a/csdemoparser/src/lib.rs +++ b/csdemoparser/src/lib.rs @@ -4,6 +4,7 @@ pub mod demoinfo; mod entity; mod game_event; mod geometry; +mod last_jump; mod string_table; use crate::entity::{Entity, EntityId, PropValue, Scalar}; @@ -29,6 +30,11 @@ pub fn parse(mut read: &mut dyn io::Read) -> anyhow::Result { } } +#[derive(Eq, PartialEq, Hash, Clone, Copy)] +struct Slot(u16); +#[derive(Eq, PartialEq, Hash, Clone, Copy, Default)] +struct UserId(u16); + #[derive(Default)] struct TeamScore { team_entity_id: [Option; 2], @@ -40,9 +46,16 @@ struct TeamScore { impl TeamScore { fn update(&mut self, entity: &Entity, value: &PropValue) -> bool { - let Some(pos) = self.team_entity_id.iter().position(|i| &Some(entity.id) == i) - else { return false }; - let &PropValue::Scalar(Scalar::I32(new_score)) = value else { return false }; + let Some(pos) = self + .team_entity_id + .iter() + .position(|i| &Some(entity.id) == i) + else { + return false; + }; + let &PropValue::Scalar(Scalar::I32(new_score)) = value else { + return false; + }; if new_score < self.round_start[0] && new_score < self.round_start[1] { return false; }