Skip to content

Commit

Permalink
more projectile tracking work
Browse files Browse the repository at this point in the history
  • Loading branch information
icewind1991 committed Dec 5, 2024
1 parent a9c0313 commit e0ee7c8
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 45 deletions.
27 changes: 23 additions & 4 deletions src/bin/direct_hits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
}

Expand Down
101 changes: 89 additions & 12 deletions src/demo/data/game_state.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 => {
Expand Down Expand Up @@ -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<Box>,
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<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,
}
}
}
Expand Down Expand Up @@ -337,6 +419,7 @@ pub struct GameState {
pub tick: DemoTick,
pub server_classes: Vec<ServerClass>,
pub interval_per_tick: f32,
pub outer_map: HashMap<Handle, EntityId>,
}

impl GameState {
Expand Down Expand Up @@ -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()
Expand Down
124 changes: 95 additions & 29 deletions src/demo/parser/gamestateanalyser.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
_ => {}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
_ => {}
}
}
Expand Down Expand Up @@ -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 =
Expand All @@ -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);
}
}

Expand Down

0 comments on commit e0ee7c8

Please sign in to comment.