diff --git a/src/bin/direct_hits.rs b/src/bin/direct_hits.rs index 26f11af..cb2f5c8 100644 --- a/src/bin/direct_hits.rs +++ b/src/bin/direct_hits.rs @@ -49,10 +49,29 @@ fn main() -> Result<(), MainError> { .get(usize::from(collision.projectile.class)) .map(|class| class.name.as_str()) .unwrap_or("unknown weapon"); - println!( - "{}: {} hit by {}", - collision.tick, player.name, weapon_class - ); + + let shooter = state + .players + .iter() + .find(|player| { + player + .weapons + .iter() + .any(|weapon| collision.projectile.launcher == *weapon) + }) + .and_then(|player| player.info.as_ref()); + + if let Some(shooter) = shooter { + println!( + "{}: {} hit by {} from {}", + collision.tick, player.name, weapon_class, shooter.name + ); + } else { + println!( + "{}: {} hit by {} from unknown player {}", + collision.tick, player.name, weapon_class, collision.projectile.launcher + ); + } } } diff --git a/src/demo/data/game_state.rs b/src/demo/data/game_state.rs index d847bdc..85fc8f9 100644 --- a/src/demo/data/game_state.rs +++ b/src/demo/data/game_state.rs @@ -1,11 +1,15 @@ use crate::demo::data::DemoTick; use crate::demo::gameevent_gen::PlayerDeathEvent; use crate::demo::message::packetentities::EntityId; -use crate::demo::packet::datatable::{ClassId, ServerClass}; +use crate::demo::packet::datatable::{ClassId, ServerClass, ServerClassName}; use crate::demo::parser::analyser::{Class, Team, UserId, UserInfo}; use crate::demo::vector::Vector; +use parse_display::Display; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; + +#[derive(Default, Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash, Display)] +pub struct Handle(pub i64); #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)] pub enum PlayerState { @@ -65,6 +69,7 @@ pub struct Player { pub ping: u16, pub in_pvs: bool, pub bounds: Box, + pub weapons: [Handle; 3], } pub const PLAYER_BOX_DEFAULT: Box = Box { @@ -91,7 +96,7 @@ impl Player { pub fn collides(&self, projectile: &Projectile, time_per_tick: f32) -> bool { let current_position = projectile.position; - let next_position = projectile.position + (projectile.speed * time_per_tick); + let next_position = projectile.position + (projectile.initial_speed * time_per_tick); match projectile.bounds { Some(_) => todo!(), None => { @@ -275,19 +280,96 @@ pub struct Projectile { pub team: Team, pub class: ClassId, pub position: Vector, - pub speed: Vector, + pub rotation: Vector, + pub initial_speed: Vector, pub bounds: Option, + pub launcher: Handle, + pub ty: ProjectileType, } impl Projectile { - pub fn new(id: EntityId, class: ClassId) -> Self { + pub fn new(id: EntityId, class: ClassId, class_name: &ServerClassName) -> Self { Projectile { id, team: Team::default(), class, position: Vector::default(), - speed: Vector::default(), + rotation: Vector::default(), + initial_speed: Vector::default(), bounds: None, + launcher: Handle::default(), + ty: ProjectileType::new(class_name, None), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum PipeType { + Regular = 0, + Sticky = 1, + StickyJumper = 2, + LooseCannon = 3, +} + +impl PipeType { + pub fn new(number: i64) -> Self { + match number { + 1 => PipeType::Sticky, + 2 => PipeType::StickyJumper, + 3 => PipeType::LooseCannon, + _ => PipeType::Regular, + } + } + + pub fn is_sticky(&self) -> bool { + match self { + PipeType::Regular | PipeType::LooseCannon => false, + PipeType::Sticky | PipeType::StickyJumper => true, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] +#[repr(u8)] +pub enum ProjectileType { + Rocket = 0, + HealingArrow = 1, + Sticky = 2, + Pipe = 3, + Flare = 4, + LooseCannon = 5, + #[default] + Unknown = 7, +} + +impl ProjectileType { + pub fn new(class: &ServerClassName, pipe_type: Option) -> Self { + match (class.as_str(), pipe_type) { + ("CTFGrenadePipebombProjectile", Some(PipeType::Sticky | PipeType::StickyJumper)) => { + ProjectileType::Sticky + } + ("CTFGrenadePipebombProjectile", Some(PipeType::LooseCannon)) => { + ProjectileType::LooseCannon + } + ("CTFGrenadePipebombProjectile", _) => ProjectileType::Pipe, + ("CTFProjectile_SentryRocket" | "CTFProjectile_Rocket", _) => ProjectileType::Rocket, + ("CTFProjectile_Flare", _) => ProjectileType::Flare, + ("CTFProjectile_HealingBolt", _) => ProjectileType::HealingArrow, + _ => ProjectileType::Unknown, + } + } +} + +impl From for ProjectileType { + fn from(value: u8) -> Self { + match value { + 0 => ProjectileType::Rocket, + 1 => ProjectileType::HealingArrow, + 2 => ProjectileType::Sticky, + 3 => ProjectileType::Pipe, + 4 => ProjectileType::Flare, + 5 => ProjectileType::LooseCannon, + _ => ProjectileType::Unknown, } } } @@ -337,6 +419,7 @@ pub struct GameState { pub tick: DemoTick, pub server_classes: Vec, pub interval_per_tick: f32, + pub outer_map: HashMap, } impl GameState { @@ -373,12 +456,6 @@ impl GameState { .or_insert_with(|| Building::new(entity_id, class)) } - pub fn get_or_create_projectile(&mut self, id: EntityId, class: ClassId) -> &mut Projectile { - self.projectiles - .entry(id) - .or_insert_with(|| Projectile::new(id, class)) - } - pub fn check_collision(&self, projectile: &Projectile) -> Option<&Player> { self.players .iter() diff --git a/src/demo/parser/gamestateanalyser.rs b/src/demo/parser/gamestateanalyser.rs index 4b043db..f7138a1 100644 --- a/src/demo/parser/gamestateanalyser.rs +++ b/src/demo/parser/gamestateanalyser.rs @@ -1,6 +1,7 @@ pub use crate::demo::data::game_state::{ Building, BuildingClass, Dispenser, GameState, Kill, PlayerState, Sentry, Teleporter, World, }; +use crate::demo::data::game_state::{Handle, PipeType, Projectile, ProjectileType}; use crate::demo::data::DemoTick; use crate::demo::gameevent_gen::ObjectDestroyedEvent; use crate::demo::gamevent::GameEvent; @@ -44,6 +45,10 @@ impl MessageHandler for GameStateAnalyser { for entity in &message.entities { self.handle_entity(entity, parser_state); } + for id in &message.removed_entities { + self.state.projectile_destroy(*id); + self.state.remove_building(*id); + } } Message::ServerInfo(message) => { self.state.interval_per_tick = message.interval_per_tick @@ -124,19 +129,32 @@ impl GameStateAnalyser { } pub fn handle_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) { - let class_name: &str = self - .class_names - .get(usize::from(entity.server_class)) - .map(|class_name| class_name.as_str()) - .unwrap_or(""); - match class_name { + const OUTER: SendPropIdentifier = + SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter"); + + let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else { + return; + }; + + for prop in &entity.props { + if prop.identifier == OUTER { + let outer = i64::try_from(&prop.value).unwrap_or_default(); + self.state + .outer_map + .insert(Handle(outer), entity.entity_index); + } + } + + match class_name.as_str() { "CTFPlayer" => self.handle_player_entity(entity, parser_state), "CTFPlayerResource" => self.handle_player_resource(entity, parser_state), "CWorld" => self.handle_world_entity(entity, parser_state), "CObjectSentrygun" => self.handle_sentry_entity(entity, parser_state), "CObjectDispenser" => self.handle_dispenser_entity(entity, parser_state), "CObjectTeleporter" => self.handle_teleporter_entity(entity, parser_state), - _ if class_name.starts_with("CTFProjectile_") => { + _ if class_name.starts_with("CTFProjectile_") + || class_name.as_str() == "CTFGrenadePipebombProjectile" => + { self.handle_projectile_entity(entity, parser_state) } _ => {} @@ -213,6 +231,10 @@ impl GameStateAnalyser { const PROP_BB_MAX: SendPropIdentifier = SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled"); + const WEAPON_0: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "000"); + const WEAPON_1: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "001"); + const WEAPON_2: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "002"); + player.in_pvs = entity.in_pvs; for prop in entity.props(parser_state) { @@ -247,6 +269,18 @@ impl GameStateAnalyser { let max = Vector::try_from(&prop.value).unwrap_or_default(); player.bounds.max = max; } + WEAPON_0 => { + let handle = Handle(i64::try_from(&prop.value).unwrap_or_default()); + player.weapons[0] = handle; + } + WEAPON_1 => { + let handle = Handle(i64::try_from(&prop.value).unwrap_or_default()); + player.weapons[1] = handle; + } + WEAPON_2 => { + let handle = Handle(i64::try_from(&prop.value).unwrap_or_default()); + player.weapons[2] = handle; + } _ => {} } } @@ -501,6 +535,10 @@ impl GameStateAnalyser { } pub fn handle_projectile_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) { + let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else { + return; + }; + const ROCKET_ORIGIN: SendPropIdentifier = SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more? const GRENADE_ORIGIN: SendPropIdentifier = @@ -509,34 +547,62 @@ impl GameStateAnalyser { const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum"); const INITIAL_SPEED: SendPropIdentifier = SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity"); + const LAUNCHER: SendPropIdentifier = + SendPropIdentifier::new("DT_BaseProjectile", "m_hOriginalLauncher"); + const PIPE_TYPE: SendPropIdentifier = + SendPropIdentifier::new("DT_TFProjectile_Pipebomb", "m_iType"); + const ROCKET_ROTATION: SendPropIdentifier = + SendPropIdentifier::new("DT_TFBaseRocket", "m_angRotation"); + const GRENADE_ROTATION: SendPropIdentifier = + SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_angRotation"); - if entity.in_pvs { - let projectile = self - .state - .get_or_create_projectile(entity.entity_index, entity.server_class); + if entity.update_type == UpdateType::Delete { + self.state.projectile_destroy(entity.entity_index); + return; + } - // todo: bounds for grenades - // todo: track owner + let projectile = self + .state + .projectiles + .entry(entity.entity_index) + .or_insert_with(|| { + Projectile::new(entity.entity_index, entity.server_class, class_name) + }); - for prop in entity.props(parser_state) { - match prop.identifier { - ROCKET_ORIGIN | GRENADE_ORIGIN => { - let pos = Vector::try_from(&prop.value).unwrap_or_default(); - projectile.position = pos - } - TEAM => { - let team = Team::new(i64::try_from(&prop.value).unwrap_or_default()); - projectile.team = team; - } - INITIAL_SPEED => { - let speed = Vector::try_from(&prop.value).unwrap_or_default(); - projectile.speed = speed; + // todo: bounds for grenades + + for prop in entity.props(parser_state) { + match prop.identifier { + ROCKET_ORIGIN | GRENADE_ORIGIN => { + let pos = Vector::try_from(&prop.value).unwrap_or_default(); + projectile.position = pos + } + TEAM => { + let team = Team::new(i64::try_from(&prop.value).unwrap_or_default()); + projectile.team = team; + } + INITIAL_SPEED => { + let speed = Vector::try_from(&prop.value).unwrap_or_default(); + projectile.initial_speed = speed; + } + LAUNCHER => { + let launcher = Handle(i64::try_from(&prop.value).unwrap_or_default()); + projectile.launcher = launcher; + } + PIPE_TYPE => { + let pipe_type = PipeType::new(i64::try_from(&prop.value).unwrap_or_default()); + if let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) + { + let ty = ProjectileType::new(class_name, Some(pipe_type)); + projectile.ty = ty; } - _ => {} } + ROCKET_ROTATION | GRENADE_ROTATION => { + let rotation = Vector::try_from(&prop.value).unwrap_or_default(); + projectile.rotation = rotation; + } + _ => {} } - } else { - self.state.projectile_destroy(entity.entity_index); } }