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}