Skip to main content

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