tf_demo_parser/demo/data/
game_state.rs

1pub use super::cond::PlayerCondition;
2use crate::demo::data::DemoTick;
3use crate::demo::gameevent_gen::PlayerDeathEvent;
4use crate::demo::gamevent::GameEvent;
5use crate::demo::message::packetentities::EntityId;
6use crate::demo::packet::datatable::{ClassId, ServerClass, ServerClassName};
7use crate::demo::parser::analyser::{Class, Team, UserId, UserInfo};
8use crate::demo::vector::Vector;
9use parse_display::Display;
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, HashMap};
12use std::ops::Rem;
13
14#[derive(Default, Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, Hash, Display)]
15pub struct Handle(pub i64);
16
17#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
18#[non_exhaustive]
19pub enum PlayerState {
20    #[default]
21    Alive = 0,
22    Dying = 1,
23    Death = 2,
24    Respawnable = 3,
25}
26
27impl PlayerState {
28    pub fn new(number: i64) -> Self {
29        match number {
30            1 => PlayerState::Dying,
31            2 => PlayerState::Death,
32            3 => PlayerState::Respawnable,
33            _ => PlayerState::Alive,
34        }
35    }
36}
37
38#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
39pub struct Box {
40    pub min: Vector,
41    pub max: Vector,
42}
43
44impl Box {
45    pub fn new(min: Vector, max: Vector) -> Box {
46        Box { min, max }
47    }
48
49    pub fn contains(&self, point: Vector) -> bool {
50        point.x >= self.min.x
51            && point.x <= self.max.x
52            && point.y >= self.min.y
53            && point.y <= self.max.y
54            && point.z >= self.min.z
55            && point.z <= self.max.z
56    }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
60#[non_exhaustive]
61pub struct Player {
62    pub entity: EntityId,
63    pub position: Vector,
64    pub health: u16,
65    pub max_health: u16,
66    pub class: Class,
67    pub team: Team,
68    pub view_angle: f32,
69    pub pitch_angle: f32,
70    pub state: PlayerState,
71    pub info: Option<UserInfo>,
72    pub charge: u8,
73    pub simtime: u16,
74    pub ping: u16,
75    pub in_pvs: bool,
76    pub bounds: Box,
77    pub weapons: [Handle; 3],
78    pub(crate) conditions: [u8; 20],
79}
80
81pub const PLAYER_BOX_DEFAULT: Box = Box {
82    min: Vector {
83        x: -24.0,
84        y: -24.0,
85        z: 0.0,
86    },
87    max: Vector {
88        x: 24.0,
89        y: 24.0,
90        z: 82.0,
91    },
92};
93
94impl Player {
95    pub fn new(entity: EntityId) -> Player {
96        Player {
97            entity,
98            bounds: PLAYER_BOX_DEFAULT,
99            ..Player::default()
100        }
101    }
102
103    pub fn collides(&self, projectile: &Projectile, time_per_tick: f32) -> bool {
104        let current_position = projectile.position;
105        let next_position = projectile.position + (projectile.initial_speed * time_per_tick);
106        match projectile.bounds {
107            Some(_) => todo!(),
108            None => {
109                self.bounds.contains(current_position - self.position)
110                    || self.bounds.contains(next_position - self.position)
111            }
112        }
113    }
114
115    pub fn conditions(&self) -> impl Iterator<Item = PlayerCondition> + '_ {
116        (1..=(PlayerCondition::MAX as u8)).filter_map(|cond_int| {
117            let byte = cond_int / 8;
118            let bit = cond_int.rem(8);
119            let cond_byte = *self.conditions.get(byte as usize)?;
120            if (cond_byte >> bit as usize) == 1 {
121                PlayerCondition::try_from(cond_byte).ok()
122            } else {
123                None
124            }
125        })
126    }
127
128    pub fn has_condition(&self, condition: PlayerCondition) -> bool {
129        let cond_int = condition as u8;
130        let byte = cond_int / 8;
131        let bit = cond_int.rem(8);
132        let cond_byte = self
133            .conditions
134            .get(byte as usize)
135            .copied()
136            .unwrap_or_default();
137        cond_byte >> bit as usize == 1
138    }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
142#[non_exhaustive]
143pub struct Sentry {
144    pub entity: EntityId,
145    pub builder: UserId,
146    pub position: Vector,
147    pub level: u8,
148    pub max_health: u16,
149    pub health: u16,
150    pub building: bool,
151    pub sapped: bool,
152    pub team: Team,
153    pub angle: f32,
154    pub player_controlled: bool,
155    pub auto_aim_target: UserId,
156    pub shells: u16,
157    pub rockets: u16,
158    pub is_mini: bool,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
162#[non_exhaustive]
163pub struct Dispenser {
164    pub entity: EntityId,
165    pub builder: UserId,
166    pub position: Vector,
167    pub level: u8,
168    pub max_health: u16,
169    pub health: u16,
170    pub building: bool,
171    pub sapped: bool,
172    pub team: Team,
173    pub angle: f32,
174    pub healing: Vec<UserId>,
175    pub metal: u16,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
179#[non_exhaustive]
180pub struct Teleporter {
181    pub entity: EntityId,
182    pub builder: UserId,
183    pub position: Vector,
184    pub level: u8,
185    pub max_health: u16,
186    pub health: u16,
187    pub building: bool,
188    pub sapped: bool,
189    pub team: Team,
190    pub angle: f32,
191    pub is_entrance: bool,
192    pub other_end: EntityId,
193    pub recharge_time: f32,
194    pub recharge_duration: f32,
195    pub times_used: u16,
196    pub yaw_to_exit: f32,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
200#[non_exhaustive]
201pub enum Building {
202    Sentry(Sentry),
203    Dispenser(Dispenser),
204    Teleporter(Teleporter),
205}
206
207impl Building {
208    pub fn new(entity_id: EntityId, class: BuildingClass) -> Building {
209        match class {
210            BuildingClass::Sentry => Building::Sentry(Sentry {
211                entity: entity_id,
212                ..Sentry::default()
213            }),
214            BuildingClass::Dispenser => Building::Dispenser(Dispenser {
215                entity: entity_id,
216                ..Dispenser::default()
217            }),
218            BuildingClass::Teleporter => Building::Teleporter(Teleporter {
219                entity: entity_id,
220                ..Teleporter::default()
221            }),
222        }
223    }
224
225    pub fn entity_id(&self) -> EntityId {
226        match self {
227            Building::Sentry(Sentry { entity, .. })
228            | Building::Dispenser(Dispenser { entity, .. })
229            | Building::Teleporter(Teleporter { entity, .. }) => *entity,
230        }
231    }
232
233    pub fn level(&self) -> u8 {
234        match self {
235            Building::Sentry(Sentry { level, .. })
236            | Building::Dispenser(Dispenser { level, .. })
237            | Building::Teleporter(Teleporter { level, .. }) => *level,
238        }
239    }
240
241    pub fn position(&self) -> Vector {
242        match self {
243            Building::Sentry(Sentry { position, .. })
244            | Building::Dispenser(Dispenser { position, .. })
245            | Building::Teleporter(Teleporter { position, .. }) => *position,
246        }
247    }
248
249    pub fn builder(&self) -> UserId {
250        match self {
251            Building::Sentry(Sentry { builder, .. })
252            | Building::Dispenser(Dispenser { builder, .. })
253            | Building::Teleporter(Teleporter { builder, .. }) => *builder,
254        }
255    }
256
257    pub fn angle(&self) -> f32 {
258        match self {
259            Building::Sentry(Sentry { angle, .. })
260            | Building::Dispenser(Dispenser { angle, .. })
261            | Building::Teleporter(Teleporter { angle, .. }) => *angle,
262        }
263    }
264
265    pub fn max_health(&self) -> u16 {
266        match self {
267            Building::Sentry(Sentry { max_health, .. })
268            | Building::Dispenser(Dispenser { max_health, .. })
269            | Building::Teleporter(Teleporter { max_health, .. }) => *max_health,
270        }
271    }
272
273    pub fn health(&self) -> u16 {
274        match self {
275            Building::Sentry(Sentry { health, .. })
276            | Building::Dispenser(Dispenser { health, .. })
277            | Building::Teleporter(Teleporter { health, .. }) => *health,
278        }
279    }
280
281    pub fn sapped(&self) -> bool {
282        match self {
283            Building::Sentry(Sentry { sapped, .. })
284            | Building::Dispenser(Dispenser { sapped, .. })
285            | Building::Teleporter(Teleporter { sapped, .. }) => *sapped,
286        }
287    }
288
289    pub fn team(&self) -> Team {
290        match self {
291            Building::Sentry(Sentry { team, .. })
292            | Building::Dispenser(Dispenser { team, .. })
293            | Building::Teleporter(Teleporter { team, .. }) => *team,
294        }
295    }
296
297    pub fn class(&self) -> BuildingClass {
298        match self {
299            Building::Sentry(_) => BuildingClass::Sentry,
300            Building::Dispenser(_) => BuildingClass::Sentry,
301            Building::Teleporter(_) => BuildingClass::Teleporter,
302        }
303    }
304}
305
306#[non_exhaustive]
307pub enum BuildingClass {
308    Sentry,
309    Dispenser,
310    Teleporter,
311}
312
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314#[non_exhaustive]
315pub struct Projectile {
316    pub id: EntityId,
317    pub team: Team,
318    pub class: ClassId,
319    pub position: Vector,
320    pub rotation: Vector,
321    pub initial_speed: Vector,
322    pub bounds: Option<Box>,
323    pub launcher: Handle,
324    pub ty: ProjectileType,
325    pub critical: bool,
326}
327
328impl Projectile {
329    pub fn new(id: EntityId, class: ClassId, class_name: &ServerClassName) -> Self {
330        Projectile {
331            id,
332            team: Team::default(),
333            class,
334            position: Vector::default(),
335            rotation: Vector::default(),
336            initial_speed: Vector::default(),
337            bounds: None,
338            launcher: Handle::default(),
339            ty: ProjectileType::new(class_name, None),
340            critical: false,
341        }
342    }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
346#[non_exhaustive]
347pub enum PipeType {
348    Regular = 0,
349    Sticky = 1,
350    StickyJumper = 2,
351    LooseCannon = 3,
352}
353
354impl PipeType {
355    pub fn new(number: i64) -> Self {
356        match number {
357            1 => PipeType::Sticky,
358            2 => PipeType::StickyJumper,
359            3 => PipeType::LooseCannon,
360            _ => PipeType::Regular,
361        }
362    }
363
364    pub fn is_sticky(&self) -> bool {
365        match self {
366            PipeType::Regular | PipeType::LooseCannon => false,
367            PipeType::Sticky | PipeType::StickyJumper => true,
368        }
369    }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
373#[repr(u8)]
374pub enum ProjectileType {
375    Rocket = 0,
376    HealingArrow = 1,
377    Sticky = 2,
378    Pipe = 3,
379    Flare = 4,
380    LooseCannon = 5,
381    #[default]
382    Unknown = 7,
383}
384
385impl ProjectileType {
386    pub fn new(class: &ServerClassName, pipe_type: Option<PipeType>) -> Self {
387        match (class.as_str(), pipe_type) {
388            ("CTFGrenadePipebombProjectile", Some(PipeType::Sticky | PipeType::StickyJumper)) => {
389                ProjectileType::Sticky
390            }
391            ("CTFGrenadePipebombProjectile", Some(PipeType::LooseCannon)) => {
392                ProjectileType::LooseCannon
393            }
394            ("CTFGrenadePipebombProjectile", _) => ProjectileType::Pipe,
395            ("CTFProjectile_SentryRocket" | "CTFProjectile_Rocket", _) => ProjectileType::Rocket,
396            ("CTFProjectile_Flare", _) => ProjectileType::Flare,
397            ("CTFProjectile_HealingBolt", _) => ProjectileType::HealingArrow,
398            _ => ProjectileType::Unknown,
399        }
400    }
401}
402
403impl From<u8> for ProjectileType {
404    fn from(value: u8) -> Self {
405        match value {
406            0 => ProjectileType::Rocket,
407            1 => ProjectileType::HealingArrow,
408            2 => ProjectileType::Sticky,
409            3 => ProjectileType::Pipe,
410            4 => ProjectileType::Flare,
411            5 => ProjectileType::LooseCannon,
412            _ => ProjectileType::Unknown,
413        }
414    }
415}
416
417#[derive(Debug, PartialEq, Serialize, Deserialize)]
418#[non_exhaustive]
419pub struct Collision {
420    pub tick: DemoTick,
421    pub target: EntityId,
422    pub projectile: Projectile,
423}
424
425#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
426#[non_exhaustive]
427pub struct World {
428    pub boundary_min: Vector,
429    pub boundary_max: Vector,
430}
431
432#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
433#[non_exhaustive]
434pub struct Kill {
435    pub attacker_id: u16,
436    pub assister_id: u16,
437    pub victim_id: u16,
438    pub weapon: String,
439    pub tick: DemoTick,
440}
441
442impl Kill {
443    pub fn new(tick: DemoTick, death: &PlayerDeathEvent) -> Self {
444        Kill {
445            attacker_id: death.attacker,
446            assister_id: death.assister,
447            victim_id: death.user_id,
448            weapon: death.weapon.to_string(),
449            tick,
450        }
451    }
452}
453
454#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
455pub struct Cart {
456    pub position: Vector,
457}
458
459#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
460pub struct ControlPoint {
461    pub owner: Team,
462    pub cap_percentage: f32,
463}
464
465#[derive(Debug, Serialize, Deserialize, PartialEq)]
466pub enum Objective {
467    Cart(Cart),
468    ControlPoint(ControlPoint),
469}
470
471impl Objective {
472    pub fn as_cart(&self) -> Option<&Cart> {
473        match self {
474            Objective::Cart(cart) => Some(cart),
475            _ => None,
476        }
477    }
478}
479
480#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
481#[non_exhaustive]
482pub struct GameState {
483    pub players: Vec<Player>,
484    pub buildings: BTreeMap<EntityId, Building>,
485    pub projectiles: BTreeMap<EntityId, Projectile>,
486    pub collisions: Vec<Collision>,
487    pub world: Option<World>,
488    pub kills: Vec<Kill>,
489    pub tick: DemoTick,
490    pub server_classes: Vec<ServerClass>,
491    pub interval_per_tick: f32,
492    pub outer_map: HashMap<Handle, EntityId>,
493    pub events: Vec<(DemoTick, GameEvent)>,
494    pub objectives: BTreeMap<EntityId, Objective>,
495}
496
497impl GameState {
498    pub fn get_player(&self, id: EntityId) -> Option<&Player> {
499        self.players.iter().find(|player| player.entity == id)
500    }
501
502    pub fn get_or_create_player(&mut self, entity_id: EntityId) -> &mut Player {
503        let index = match self
504            .players
505            .iter()
506            .enumerate()
507            .find(|(_index, player)| player.entity == entity_id)
508            .map(|(index, _)| index)
509        {
510            Some(index) => index,
511            None => {
512                let index = self.players.len();
513                self.players.push(Player::new(entity_id));
514                index
515            }
516        };
517
518        #[allow(clippy::indexing_slicing)]
519        &mut self.players[index]
520    }
521    pub fn get_or_create_building(
522        &mut self,
523        entity_id: EntityId,
524        class: BuildingClass,
525    ) -> &mut Building {
526        self.buildings
527            .entry(entity_id)
528            .or_insert_with(|| Building::new(entity_id, class))
529    }
530
531    pub fn check_collision(&self, projectile: &Projectile) -> Option<&Player> {
532        self.players
533            .iter()
534            .filter(|player| player.state == PlayerState::Alive)
535            .filter(|player| player.team != projectile.team)
536            .find(|player| player.collides(projectile, self.interval_per_tick))
537    }
538
539    pub fn projectile_destroy(&mut self, id: EntityId) {
540        if let Some(projectile) = self.projectiles.remove(&id) {
541            if let Some(target) = self.check_collision(&projectile) {
542                self.collisions.push(Collision {
543                    tick: self.tick,
544                    target: target.entity,
545                    projectile,
546                })
547            }
548        }
549    }
550
551    pub fn remove_building(&mut self, entity_id: EntityId) {
552        self.buildings.remove(&entity_id);
553    }
554}