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}