Skip to content
This repository was archived by the owner on May 21, 2025. It is now read-only.

Commit cd20b5b

Browse files
committed
implement projectile collision tracking
todo: - track projectile ownership - handle bounding boxes for non-rockets
1 parent e11cfb9 commit cd20b5b

File tree

5 files changed

+258
-9
lines changed

5 files changed

+258
-9
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@
1010
heaptrack.*
1111
dhat.out.*
1212
result
13-
.direnv
13+
.direnv
14+
strings.txt
15+
models.txt

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ required-features = ["codegen"]
4040
name = "strings"
4141
path = "src/bin/strings.rs"
4242

43+
[[bin]]
44+
name = "direct_hits"
45+
path = "src/bin/direct_hits.rs"
46+
4347
[dependencies]
4448
bitbuffer = { version = "0.11.0", features = ["serde"] }
4549
num_enum = "0.7.2"

src/bin/direct_hits.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::env;
2+
use std::fs;
3+
4+
use main_error::MainError;
5+
use serde::{Deserialize, Serialize};
6+
use tf_demo_parser::demo::header::Header;
7+
use tf_demo_parser::demo::parser::analyser::MatchState;
8+
use tf_demo_parser::demo::parser::gamestateanalyser::GameStateAnalyser;
9+
pub use tf_demo_parser::{Demo, DemoParser, Parse};
10+
11+
#[cfg(feature = "jemallocator")]
12+
#[global_allocator]
13+
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
14+
15+
#[derive(Serialize, Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
struct JsonDemo {
18+
header: Header,
19+
#[serde(flatten)]
20+
state: MatchState,
21+
}
22+
23+
fn main() -> Result<(), MainError> {
24+
#[cfg(feature = "better_panic")]
25+
better_panic::install();
26+
27+
#[cfg(feature = "trace")]
28+
tracing_subscriber::fmt::init();
29+
30+
let args: Vec<_> = env::args().collect();
31+
if args.len() < 2 {
32+
println!("1 argument required");
33+
return Ok(());
34+
}
35+
let path = args[1].clone();
36+
let file = fs::read(path)?;
37+
let demo = Demo::new(&file);
38+
39+
let parser = DemoParser::new_all_with_analyser(demo.get_stream(), GameStateAnalyser::default());
40+
let (_header, state) = parser.parse()?;
41+
42+
for collision in &state.collisions {
43+
if let Some(player) = state
44+
.get_player(collision.target)
45+
.and_then(|player| player.info.as_ref())
46+
{
47+
let weapon_class = state
48+
.server_classes
49+
.get(usize::from(collision.projectile.class))
50+
.map(|class| class.name.as_str())
51+
.unwrap_or("unknown weapon");
52+
println!(
53+
"{}: {} hit by {}",
54+
collision.tick, player.name, weapon_class
55+
);
56+
}
57+
}
58+
59+
Ok(())
60+
}

src/demo/parser/gamestateanalyser.rs

Lines changed: 178 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::demo::gamevent::GameEvent;
44
use crate::demo::message::gameevent::GameEventMessage;
55
use crate::demo::message::packetentities::{EntityId, PacketEntity, UpdateType};
66
use crate::demo::message::Message;
7-
use crate::demo::packet::datatable::{ParseSendTable, ServerClass, ServerClassName};
7+
use crate::demo::packet::datatable::{ClassId, ParseSendTable, ServerClass, ServerClassName};
88
use crate::demo::packet::message::MessagePacketMeta;
99
use crate::demo::packet::stringtable::StringTableEntry;
1010
use crate::demo::parser::analyser::UserInfo;
@@ -41,6 +41,27 @@ impl PlayerState {
4141
}
4242
}
4343

44+
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
45+
pub struct Box {
46+
pub min: Vector,
47+
pub max: Vector,
48+
}
49+
50+
impl Box {
51+
pub fn new(min: Vector, max: Vector) -> Box {
52+
Box { min, max }
53+
}
54+
55+
pub fn contains(&self, point: Vector) -> bool {
56+
point.x >= self.min.x
57+
&& point.x <= self.max.x
58+
&& point.y >= self.min.y
59+
&& point.y <= self.max.y
60+
&& point.z >= self.min.z
61+
&& point.z <= self.max.z
62+
}
63+
}
64+
4465
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
4566
pub struct Player {
4667
entity: EntityId,
@@ -57,6 +78,42 @@ pub struct Player {
5778
pub simtime: u16,
5879
pub ping: u16,
5980
pub in_pvs: bool,
81+
pub bounds: Box,
82+
}
83+
84+
pub const PLAYER_BOX_DEFAULT: Box = Box {
85+
min: Vector {
86+
x: -24.0,
87+
y: -24.0,
88+
z: 0.0,
89+
},
90+
max: Vector {
91+
x: 24.0,
92+
y: 24.0,
93+
z: 82.0,
94+
},
95+
};
96+
97+
impl Player {
98+
pub fn new(entity: EntityId) -> Player {
99+
Player {
100+
entity,
101+
bounds: PLAYER_BOX_DEFAULT,
102+
..Player::default()
103+
}
104+
}
105+
106+
pub fn collides(&self, projectile: &Projectile, time_per_tick: f32) -> bool {
107+
let current_position = projectile.position;
108+
let next_position = projectile.position + (projectile.speed * time_per_tick);
109+
match projectile.bounds {
110+
Some(_) => todo!(),
111+
None => {
112+
self.bounds.contains(current_position - self.position)
113+
|| self.bounds.contains(next_position - self.position)
114+
}
115+
}
116+
}
60117
}
61118

62119
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
@@ -226,6 +283,36 @@ pub enum BuildingClass {
226283
Teleporter,
227284
}
228285

286+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287+
pub struct Projectile {
288+
pub id: EntityId,
289+
pub team: Team,
290+
pub class: ClassId,
291+
pub position: Vector,
292+
pub speed: Vector,
293+
pub bounds: Option<Box>,
294+
}
295+
296+
impl Projectile {
297+
pub fn new(id: EntityId, class: ClassId) -> Self {
298+
Projectile {
299+
id,
300+
team: Team::default(),
301+
class,
302+
position: Vector::default(),
303+
speed: Vector::default(),
304+
bounds: None,
305+
}
306+
}
307+
}
308+
309+
#[derive(Debug, PartialEq, Serialize, Deserialize)]
310+
pub struct Collision {
311+
pub tick: DemoTick,
312+
pub target: EntityId,
313+
pub projectile: Projectile,
314+
}
315+
229316
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
230317
pub struct World {
231318
pub boundary_min: Vector,
@@ -257,12 +344,20 @@ impl Kill {
257344
pub struct GameState {
258345
pub players: Vec<Player>,
259346
pub buildings: BTreeMap<EntityId, Building>,
347+
pub projectiles: BTreeMap<EntityId, Projectile>,
348+
pub collisions: Vec<Collision>,
260349
pub world: Option<World>,
261350
pub kills: Vec<Kill>,
262351
pub tick: DemoTick,
352+
pub server_classes: Vec<ServerClass>,
353+
pub interval_per_tick: f32,
263354
}
264355

265356
impl GameState {
357+
pub fn get_player(&self, id: EntityId) -> Option<&Player> {
358+
self.players.iter().find(|player| player.entity == id)
359+
}
360+
266361
pub fn get_or_create_player(&mut self, entity_id: EntityId) -> &mut Player {
267362
let index = match self
268363
.players
@@ -274,10 +369,7 @@ impl GameState {
274369
Some(index) => index,
275370
None => {
276371
let index = self.players.len();
277-
self.players.push(Player {
278-
entity: entity_id,
279-
..Player::default()
280-
});
372+
self.players.push(Player::new(entity_id));
281373
index
282374
}
283375
};
@@ -295,6 +387,32 @@ impl GameState {
295387
.or_insert_with(|| Building::new(entity_id, class))
296388
}
297389

390+
pub fn get_or_create_projectile(&mut self, id: EntityId, class: ClassId) -> &mut Projectile {
391+
self.projectiles
392+
.entry(id)
393+
.or_insert_with(|| Projectile::new(id, class))
394+
}
395+
396+
pub fn check_collision(&self, projectile: &Projectile) -> Option<&Player> {
397+
self.players
398+
.iter()
399+
.filter(|player| player.state == PlayerState::Alive)
400+
.filter(|player| player.team != projectile.team)
401+
.find(|player| player.collides(projectile, self.interval_per_tick))
402+
}
403+
404+
pub fn projectile_destroy(&mut self, id: EntityId) {
405+
if let Some(projectile) = self.projectiles.remove(&id) {
406+
if let Some(target) = self.check_collision(&projectile) {
407+
self.collisions.push(Collision {
408+
tick: self.tick,
409+
target: target.entity,
410+
projectile,
411+
})
412+
}
413+
}
414+
}
415+
298416
pub fn remove_building(&mut self, entity_id: EntityId) {
299417
self.buildings.remove(&entity_id);
300418
}
@@ -313,7 +431,7 @@ impl MessageHandler for GameStateAnalyser {
313431
fn does_handle(message_type: MessageType) -> bool {
314432
matches!(
315433
message_type,
316-
MessageType::PacketEntities | MessageType::GameEvent
434+
MessageType::PacketEntities | MessageType::GameEvent | MessageType::ServerInfo
317435
)
318436
}
319437

@@ -324,6 +442,9 @@ impl MessageHandler for GameStateAnalyser {
324442
self.handle_entity(entity, parser_state);
325443
}
326444
}
445+
Message::ServerInfo(message) => {
446+
self.state.interval_per_tick = message.interval_per_tick
447+
}
327448
Message::GameEvent(GameEventMessage { event, .. }) => match event {
328449
GameEvent::PlayerDeath(death) => {
329450
self.state.kills.push(Kill::new(self.tick, death.as_ref()))
@@ -382,7 +503,8 @@ impl MessageHandler for GameStateAnalyser {
382503
self.tick = tick;
383504
}
384505

385-
fn into_output(self, _state: &ParserState) -> Self::Output {
506+
fn into_output(mut self, state: &ParserState) -> Self::Output {
507+
self.state.server_classes = state.server_classes.clone();
386508
self.state
387509
}
388510
}
@@ -411,6 +533,9 @@ impl GameStateAnalyser {
411533
"CObjectSentrygun" => self.handle_sentry_entity(entity, parser_state),
412534
"CObjectDispenser" => self.handle_dispenser_entity(entity, parser_state),
413535
"CObjectTeleporter" => self.handle_teleporter_entity(entity, parser_state),
536+
_ if class_name.starts_with("CTFProjectile_") => {
537+
self.handle_projectile_entity(entity, parser_state)
538+
}
414539
_ => {}
415540
}
416541
}
@@ -482,6 +607,8 @@ impl GameStateAnalyser {
482607

483608
const SIMTIME_PROP: SendPropIdentifier =
484609
SendPropIdentifier::new("DT_BaseEntity", "m_flSimulationTime");
610+
const PROP_BB_MAX: SendPropIdentifier =
611+
SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled");
485612

486613
player.in_pvs = entity.in_pvs;
487614

@@ -513,6 +640,10 @@ impl GameStateAnalyser {
513640
SIMTIME_PROP => {
514641
player.simtime = i64::try_from(&prop.value).unwrap_or_default() as u16
515642
}
643+
PROP_BB_MAX => {
644+
let max = Vector::try_from(&prop.value).unwrap_or_default();
645+
player.bounds.max = max;
646+
}
516647
_ => {}
517648
}
518649
}
@@ -766,6 +897,46 @@ impl GameStateAnalyser {
766897
}
767898
}
768899

900+
pub fn handle_projectile_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
901+
const ROCKET_ORIGIN: SendPropIdentifier =
902+
SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more?
903+
const GRENADE_ORIGIN: SendPropIdentifier =
904+
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_vecOrigin");
905+
// todo: flares?
906+
const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
907+
const INITIAL_SPEED: SendPropIdentifier =
908+
SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity");
909+
910+
if entity.in_pvs {
911+
let projectile = self
912+
.state
913+
.get_or_create_projectile(entity.entity_index, entity.server_class);
914+
915+
// todo: bounds for grenades
916+
// todo: track owner
917+
918+
for prop in entity.props(parser_state) {
919+
match prop.identifier {
920+
ROCKET_ORIGIN | GRENADE_ORIGIN => {
921+
let pos = Vector::try_from(&prop.value).unwrap_or_default();
922+
projectile.position = pos
923+
}
924+
TEAM => {
925+
let team = Team::new(i64::try_from(&prop.value).unwrap_or_default());
926+
projectile.team = team;
927+
}
928+
INITIAL_SPEED => {
929+
let speed = Vector::try_from(&prop.value).unwrap_or_default();
930+
projectile.speed = speed;
931+
}
932+
_ => {}
933+
}
934+
}
935+
} else {
936+
self.state.projectile_destroy(entity.entity_index);
937+
}
938+
}
939+
769940
fn parse_user_info(
770941
&mut self,
771942
index: usize,

src/demo/vector.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::demo::sendprop::SendPropValue;
22
use bitbuffer::{BitRead, BitWrite};
33
use parse_display::Display;
44
use serde::{Deserialize, Serialize};
5-
use std::ops::{Add, Sub};
5+
use std::ops::{Add, Mul, Sub};
66

77
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88
#[derive(BitRead, BitWrite, Debug, Clone, Copy, Default, Serialize, Deserialize, Display)]
@@ -82,6 +82,18 @@ impl Sub for Vector {
8282
}
8383
}
8484

85+
impl Mul<f32> for Vector {
86+
type Output = Vector;
87+
88+
fn mul(self, rhs: f32) -> Self::Output {
89+
Vector {
90+
x: self.x * rhs,
91+
y: self.y * rhs,
92+
z: self.z * rhs,
93+
}
94+
}
95+
}
96+
8597
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8698
#[derive(BitRead, BitWrite, Debug, Clone, Copy, Default, Serialize, Deserialize, Display)]
8799
#[display("({x}, {y})")]

0 commit comments

Comments
 (0)