1
0
Fork 0
mirror of https://codeberg.org/demostf/parser.git synced 2026-06-03 10:14:06 +02:00

more projectile tracking work

This commit is contained in:
Robin Appelman 2024-12-05 19:36:07 +01:00
commit e0ee7c87d2
3 changed files with 207 additions and 45 deletions

View file

@ -49,10 +49,29 @@ fn main() -> Result<(), MainError> {
.get(usize::from(collision.projectile.class)) .get(usize::from(collision.projectile.class))
.map(|class| class.name.as_str()) .map(|class| class.name.as_str())
.unwrap_or("unknown weapon"); .unwrap_or("unknown weapon");
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!( println!(
"{}: {} hit by {}", "{}: {} hit by {} from {}",
collision.tick, player.name, weapon_class collision.tick, player.name, weapon_class, shooter.name
); );
} else {
println!(
"{}: {} hit by {} from unknown player {}",
collision.tick, player.name, weapon_class, collision.projectile.launcher
);
}
} }
} }

View file

@ -1,11 +1,15 @@
use crate::demo::data::DemoTick; use crate::demo::data::DemoTick;
use crate::demo::gameevent_gen::PlayerDeathEvent; use crate::demo::gameevent_gen::PlayerDeathEvent;
use crate::demo::message::packetentities::EntityId; 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::parser::analyser::{Class, Team, UserId, UserInfo};
use crate::demo::vector::Vector; use crate::demo::vector::Vector;
use parse_display::Display;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
pub enum PlayerState { pub enum PlayerState {
@ -65,6 +69,7 @@ pub struct Player {
pub ping: u16, pub ping: u16,
pub in_pvs: bool, pub in_pvs: bool,
pub bounds: Box, pub bounds: Box,
pub weapons: [Handle; 3],
} }
pub const PLAYER_BOX_DEFAULT: Box = Box { pub const PLAYER_BOX_DEFAULT: Box = Box {
@ -91,7 +96,7 @@ impl Player {
pub fn collides(&self, projectile: &Projectile, time_per_tick: f32) -> bool { pub fn collides(&self, projectile: &Projectile, time_per_tick: f32) -> bool {
let current_position = projectile.position; 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 { match projectile.bounds {
Some(_) => todo!(), Some(_) => todo!(),
None => { None => {
@ -275,19 +280,96 @@ pub struct Projectile {
pub team: Team, pub team: Team,
pub class: ClassId, pub class: ClassId,
pub position: Vector, pub position: Vector,
pub speed: Vector, pub rotation: Vector,
pub initial_speed: Vector,
pub bounds: Option<Box>, pub bounds: Option<Box>,
pub launcher: Handle,
pub ty: ProjectileType,
} }
impl Projectile { impl Projectile {
pub fn new(id: EntityId, class: ClassId) -> Self { pub fn new(id: EntityId, class: ClassId, class_name: &ServerClassName) -> Self {
Projectile { Projectile {
id, id,
team: Team::default(), team: Team::default(),
class, class,
position: Vector::default(), position: Vector::default(),
speed: Vector::default(), rotation: Vector::default(),
initial_speed: Vector::default(),
bounds: None, 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<PipeType>) -> 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<u8> 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 tick: DemoTick,
pub server_classes: Vec<ServerClass>, pub server_classes: Vec<ServerClass>,
pub interval_per_tick: f32, pub interval_per_tick: f32,
pub outer_map: HashMap<Handle, EntityId>,
} }
impl GameState { impl GameState {
@ -373,12 +456,6 @@ impl GameState {
.or_insert_with(|| Building::new(entity_id, class)) .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> { pub fn check_collision(&self, projectile: &Projectile) -> Option<&Player> {
self.players self.players
.iter() .iter()

View file

@ -1,6 +1,7 @@
pub use crate::demo::data::game_state::{ pub use crate::demo::data::game_state::{
Building, BuildingClass, Dispenser, GameState, Kill, PlayerState, Sentry, Teleporter, World, 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::data::DemoTick;
use crate::demo::gameevent_gen::ObjectDestroyedEvent; use crate::demo::gameevent_gen::ObjectDestroyedEvent;
use crate::demo::gamevent::GameEvent; use crate::demo::gamevent::GameEvent;
@ -44,6 +45,10 @@ impl MessageHandler for GameStateAnalyser {
for entity in &message.entities { for entity in &message.entities {
self.handle_entity(entity, parser_state); 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) => { Message::ServerInfo(message) => {
self.state.interval_per_tick = message.interval_per_tick 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) { pub fn handle_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
let class_name: &str = self const OUTER: SendPropIdentifier =
.class_names SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
.get(usize::from(entity.server_class))
.map(|class_name| class_name.as_str()) let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
.unwrap_or(""); return;
match class_name { };
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), "CTFPlayer" => self.handle_player_entity(entity, parser_state),
"CTFPlayerResource" => self.handle_player_resource(entity, parser_state), "CTFPlayerResource" => self.handle_player_resource(entity, parser_state),
"CWorld" => self.handle_world_entity(entity, parser_state), "CWorld" => self.handle_world_entity(entity, parser_state),
"CObjectSentrygun" => self.handle_sentry_entity(entity, parser_state), "CObjectSentrygun" => self.handle_sentry_entity(entity, parser_state),
"CObjectDispenser" => self.handle_dispenser_entity(entity, parser_state), "CObjectDispenser" => self.handle_dispenser_entity(entity, parser_state),
"CObjectTeleporter" => self.handle_teleporter_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) self.handle_projectile_entity(entity, parser_state)
} }
_ => {} _ => {}
@ -213,6 +231,10 @@ impl GameStateAnalyser {
const PROP_BB_MAX: SendPropIdentifier = const PROP_BB_MAX: SendPropIdentifier =
SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled"); 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; player.in_pvs = entity.in_pvs;
for prop in entity.props(parser_state) { for prop in entity.props(parser_state) {
@ -247,6 +269,18 @@ impl GameStateAnalyser {
let max = Vector::try_from(&prop.value).unwrap_or_default(); let max = Vector::try_from(&prop.value).unwrap_or_default();
player.bounds.max = max; 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) { 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 = const ROCKET_ORIGIN: SendPropIdentifier =
SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more? SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more?
const GRENADE_ORIGIN: SendPropIdentifier = const GRENADE_ORIGIN: SendPropIdentifier =
@ -509,14 +547,29 @@ impl GameStateAnalyser {
const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum"); const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
const INITIAL_SPEED: SendPropIdentifier = const INITIAL_SPEED: SendPropIdentifier =
SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity"); 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.update_type == UpdateType::Delete {
self.state.projectile_destroy(entity.entity_index);
return;
}
if entity.in_pvs {
let projectile = self let projectile = self
.state .state
.get_or_create_projectile(entity.entity_index, entity.server_class); .projectiles
.entry(entity.entity_index)
.or_insert_with(|| {
Projectile::new(entity.entity_index, entity.server_class, class_name)
});
// todo: bounds for grenades // todo: bounds for grenades
// todo: track owner
for prop in entity.props(parser_state) { for prop in entity.props(parser_state) {
match prop.identifier { match prop.identifier {
@ -530,14 +583,27 @@ impl GameStateAnalyser {
} }
INITIAL_SPEED => { INITIAL_SPEED => {
let speed = Vector::try_from(&prop.value).unwrap_or_default(); let speed = Vector::try_from(&prop.value).unwrap_or_default();
projectile.speed = speed; 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);
}
} }
fn parse_user_info( fn parse_user_info(