tf_demo_parser/demo/parser/
gamestateanalyser.rs

1pub use crate::demo::data::game_state::{
2    Building, BuildingClass, Dispenser, GameState, Kill, PlayerState, Sentry, Teleporter, World,
3};
4use crate::demo::data::game_state::{Cart, Handle, Objective, PipeType, Projectile, ProjectileType};
5use crate::demo::data::DemoTick;
6use crate::demo::gameevent_gen::ObjectDestroyedEvent;
7use crate::demo::gamevent::GameEvent;
8use crate::demo::message::gameevent::GameEventMessage;
9use crate::demo::message::packetentities::{EntityId, PacketEntity, UpdateType};
10use crate::demo::message::Message;
11use crate::demo::packet::datatable::{ParseSendTable, ServerClass, ServerClassName};
12use crate::demo::packet::message::MessagePacketMeta;
13use crate::demo::packet::stringtable::StringTableEntry;
14pub use crate::demo::parser::analyser::{Class, Team, UserId};
15use crate::demo::parser::handler::BorrowMessageHandler;
16use crate::demo::parser::MessageHandler;
17use crate::demo::sendprop::{SendProp, SendPropIdentifier, SendPropValue};
18use crate::demo::vector::{Vector, VectorXY};
19use crate::{MessageType, ParserState, ReadResult, Stream};
20use std::convert::TryFrom;
21use std::str::FromStr;
22
23pub struct CachedEntities {}
24
25#[derive(Default, Debug)]
26pub struct GameStateAnalyser {
27    pub state: GameState,
28    tick: DemoTick,
29    class_names: Vec<ServerClassName>, // indexed by ClassId
30}
31
32impl MessageHandler for GameStateAnalyser {
33    type Output = GameState;
34
35    fn does_handle(message_type: MessageType) -> bool {
36        matches!(
37            message_type,
38            MessageType::PacketEntities | MessageType::GameEvent | MessageType::ServerInfo
39        )
40    }
41
42    fn handle_message(&mut self, message: &Message, _tick: DemoTick, parser_state: &ParserState) {
43        match message {
44            Message::PacketEntities(message) => {
45                for entity in &message.entities {
46                    self.handle_entity(entity, parser_state);
47                }
48                for id in &message.removed_entities {
49                    self.state.projectile_destroy(*id);
50                    self.state.remove_building(*id);
51                }
52            }
53            Message::ServerInfo(message) => {
54                self.state.interval_per_tick = message.interval_per_tick
55            }
56            Message::GameEvent(GameEventMessage { event, .. }) => {
57                self.state.events.push((self.tick, event.clone()));
58                match event {
59                    GameEvent::PlayerDeath(death) => {
60                        self.state.kills.push(Kill::new(self.tick, death.as_ref()))
61                    }
62                    GameEvent::RoundStart(_) => {
63                        self.state.buildings.clear();
64                        self.state.projectiles.clear();
65                    }
66                    GameEvent::TeamPlayRoundStart(_) => {
67                        self.state.buildings.clear();
68                        self.state.projectiles.clear();
69                    }
70                    GameEvent::ObjectDestroyed(ObjectDestroyedEvent { index, .. }) => {
71                        self.state.remove_building((*index as u32).into());
72                    }
73                    _ => {}
74                }
75            }
76            _ => {}
77        }
78    }
79
80    fn handle_string_entry(
81        &mut self,
82        table: &str,
83        index: usize,
84        entry: &StringTableEntry,
85        _parser_state: &ParserState,
86    ) {
87        if table == "userinfo" {
88            let _ = self.parse_user_info(
89                index,
90                entry.text.as_ref().map(|s| s.as_ref()),
91                entry.extra_data.as_ref().map(|data| data.data.clone()),
92            );
93        }
94    }
95
96    fn handle_data_tables(
97        &mut self,
98        _parse_tables: &[ParseSendTable],
99        server_classes: &[ServerClass],
100        _parser_state: &ParserState,
101    ) {
102        self.class_names = server_classes
103            .iter()
104            .map(|class| &class.name)
105            .cloned()
106            .collect();
107    }
108
109    fn handle_packet_meta(
110        &mut self,
111        tick: DemoTick,
112        _meta: &MessagePacketMeta,
113        _parser_state: &ParserState,
114    ) {
115        self.state.tick = tick;
116        self.tick = tick;
117    }
118
119    fn into_output(mut self, state: &ParserState) -> Self::Output {
120        self.state.server_classes = state.server_classes.clone();
121        self.state
122    }
123}
124
125impl BorrowMessageHandler for GameStateAnalyser {
126    fn borrow_output(&self, _state: &ParserState) -> &Self::Output {
127        &self.state
128    }
129}
130
131impl GameStateAnalyser {
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    pub fn handle_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
137        const OUTER: SendPropIdentifier =
138            SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
139
140        let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
141            return;
142        };
143
144        for prop in &entity.props {
145            if prop.identifier == OUTER {
146                let outer = i64::try_from(&prop.value).unwrap_or_default();
147                self.state
148                    .outer_map
149                    .insert(Handle(outer), entity.entity_index);
150            }
151        }
152
153        match class_name.as_str() {
154            "CTFPlayer" => self.handle_player_entity(entity, parser_state),
155            "CTFPlayerResource" => self.handle_player_resource(entity, parser_state),
156            "CWorld" => self.handle_world_entity(entity, parser_state),
157            "CObjectSentrygun" => self.handle_sentry_entity(entity, parser_state),
158            "CObjectDispenser" => self.handle_dispenser_entity(entity, parser_state),
159            "CObjectTeleporter" => self.handle_teleporter_entity(entity, parser_state),
160            "CFuncTrackTrain" => self.handle_train_entity(entity, parser_state),
161            _ if class_name.starts_with("CTFProjectile_")
162                || class_name.as_str() == "CTFGrenadePipebombProjectile" =>
163            {
164                self.handle_projectile_entity(entity, parser_state)
165            }
166            _ => {}
167        }
168    }
169
170    pub fn handle_player_resource(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
171        for prop in entity.props(parser_state) {
172            if let Some((table_name, prop_name)) = prop.identifier.names() {
173                if let Ok(player_id) = u32::from_str(prop_name.as_str()) {
174                    let entity_id = EntityId::from(player_id);
175                    if let Some(player) = self
176                        .state
177                        .players
178                        .iter_mut()
179                        .find(|player| player.entity == entity_id)
180                    {
181                        match table_name.as_str() {
182                            "m_iTeam" => {
183                                player.team =
184                                    Team::new(i64::try_from(&prop.value).unwrap_or_default())
185                            }
186                            "m_iMaxHealth" => {
187                                player.max_health =
188                                    i64::try_from(&prop.value).unwrap_or_default() as u16
189                            }
190                            "m_iPlayerClass" => {
191                                player.class =
192                                    Class::new(i64::try_from(&prop.value).unwrap_or_default())
193                            }
194                            "m_iChargeLevel" => {
195                                player.charge = i64::try_from(&prop.value).unwrap_or_default() as u8
196                            }
197                            "m_iPing" => {
198                                player.ping = i64::try_from(&prop.value).unwrap_or_default() as u16
199                            }
200                            _ => {}
201                        }
202                    }
203                }
204            }
205        }
206    }
207
208    pub fn handle_player_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
209        let player = self.state.get_or_create_player(entity.entity_index);
210
211        const HEALTH_PROP: SendPropIdentifier =
212            SendPropIdentifier::new("DT_BasePlayer", "m_iHealth");
213        const MAX_HEALTH_PROP: SendPropIdentifier =
214            SendPropIdentifier::new("DT_BasePlayer", "m_iMaxHealth");
215        const LIFE_STATE_PROP: SendPropIdentifier =
216            SendPropIdentifier::new("DT_BasePlayer", "m_lifeState");
217
218        const LOCAL_ORIGIN: SendPropIdentifier =
219            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin");
220        const NON_LOCAL_ORIGIN: SendPropIdentifier =
221            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin");
222        const LOCAL_ORIGIN_Z: SendPropIdentifier =
223            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin[2]");
224        const NON_LOCAL_ORIGIN_Z: SendPropIdentifier =
225            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin[2]");
226        const LOCAL_EYE_ANGLES: SendPropIdentifier =
227            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[1]");
228        const NON_LOCAL_EYE_ANGLES: SendPropIdentifier =
229            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
230        const LOCAL_PITCH_ANGLES: SendPropIdentifier =
231            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[0]");
232        const NON_LOCAL_PITCH_ANGLES: SendPropIdentifier =
233            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[0]");
234
235        const SIMTIME_PROP: SendPropIdentifier =
236            SendPropIdentifier::new("DT_BaseEntity", "m_flSimulationTime");
237        const PROP_BB_MAX: SendPropIdentifier =
238            SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled");
239        const PLAYER_COND: SendPropIdentifier =
240            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCond");
241        const PLAYER_COND_EX1: SendPropIdentifier =
242            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx");
243        const PLAYER_COND_EX2: SendPropIdentifier =
244            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx2");
245        const PLAYER_COND_EX3: SendPropIdentifier =
246            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx3");
247        const PLAYER_COND_EX4: SendPropIdentifier =
248            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx4");
249        const PLAYER_COND_BITS: SendPropIdentifier =
250            SendPropIdentifier::new("DT_TFPlayerConditionListExclusive", "_condition_bits");
251
252        const WEAPON_0: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "000");
253        const WEAPON_1: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "001");
254        const WEAPON_2: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "002");
255
256        player.in_pvs = entity.in_pvs;
257
258        for prop in entity.props(parser_state) {
259            match prop.identifier {
260                HEALTH_PROP => {
261                    player.health = i64::try_from(&prop.value).unwrap_or_default() as u16
262                }
263                MAX_HEALTH_PROP => {
264                    player.max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
265                }
266                LIFE_STATE_PROP => {
267                    player.state = PlayerState::new(i64::try_from(&prop.value).unwrap_or_default())
268                }
269                LOCAL_ORIGIN | NON_LOCAL_ORIGIN => {
270                    let pos_xy = VectorXY::try_from(&prop.value).unwrap_or_default();
271                    player.position.x = pos_xy.x;
272                    player.position.y = pos_xy.y;
273                }
274                LOCAL_ORIGIN_Z | NON_LOCAL_ORIGIN_Z => {
275                    player.position.z = f32::try_from(&prop.value).unwrap_or_default()
276                }
277                LOCAL_EYE_ANGLES | NON_LOCAL_EYE_ANGLES => {
278                    player.view_angle = f32::try_from(&prop.value).unwrap_or_default()
279                }
280                LOCAL_PITCH_ANGLES | NON_LOCAL_PITCH_ANGLES => {
281                    player.pitch_angle = f32::try_from(&prop.value).unwrap_or_default()
282                }
283                SIMTIME_PROP => {
284                    player.simtime = i64::try_from(&prop.value).unwrap_or_default() as u16
285                }
286                PROP_BB_MAX => {
287                    let max = Vector::try_from(&prop.value).unwrap_or_default();
288                    player.bounds.max = max;
289                }
290                WEAPON_0 => {
291                    let handle = Handle(i64::try_from(&prop.value).unwrap_or_default());
292                    player.weapons[0] = handle;
293                }
294                WEAPON_1 => {
295                    let handle = Handle(i64::try_from(&prop.value).unwrap_or_default());
296                    player.weapons[1] = handle;
297                }
298                WEAPON_2 => {
299                    let handle = Handle(i64::try_from(&prop.value).unwrap_or_default());
300                    player.weapons[2] = handle;
301                }
302                PLAYER_COND | PLAYER_COND_BITS => {
303                    player.conditions[0..4].copy_from_slice(
304                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
305                    );
306                }
307                PLAYER_COND_EX1 => {
308                    player.conditions[4..8].copy_from_slice(
309                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
310                    );
311                }
312                PLAYER_COND_EX2 => {
313                    player.conditions[8..12].copy_from_slice(
314                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
315                    );
316                }
317                PLAYER_COND_EX3 => {
318                    player.conditions[12..16].copy_from_slice(
319                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
320                    );
321                }
322                PLAYER_COND_EX4 => {
323                    player.conditions[16..20].copy_from_slice(
324                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
325                    );
326                }
327                _ => {}
328            }
329        }
330    }
331
332    pub fn handle_world_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
333        if let (
334            Some(SendProp {
335                value: SendPropValue::Vector(boundary_min),
336                ..
337            }),
338            Some(SendProp {
339                value: SendPropValue::Vector(boundary_max),
340                ..
341            }),
342        ) = (
343            entity.get_prop_by_name("DT_WORLD", "m_WorldMins", parser_state),
344            entity.get_prop_by_name("DT_WORLD", "m_WorldMaxs", parser_state),
345        ) {
346            self.state.world = Some(World {
347                boundary_min,
348                boundary_max,
349            })
350        }
351    }
352
353    pub fn handle_sentry_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
354        const ANGLE: SendPropIdentifier =
355            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
356        const MINI: SendPropIdentifier =
357            SendPropIdentifier::new("DT_BaseObject", "m_bMiniBuilding");
358        const CONTROLLED: SendPropIdentifier =
359            SendPropIdentifier::new("DT_ObjectSentrygun", "m_bPlayerControlled");
360        const TARGET: SendPropIdentifier =
361            SendPropIdentifier::new("DT_ObjectSentrygun", "m_hAutoAimTarget");
362        const SHELLS: SendPropIdentifier =
363            SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoShells");
364        const ROCKETS: SendPropIdentifier =
365            SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoRockets");
366
367        if entity.update_type == UpdateType::Delete {
368            self.state.remove_building(entity.entity_index);
369            return;
370        }
371
372        self.handle_building(entity, parser_state, BuildingClass::Sentry);
373
374        let building = self
375            .state
376            .get_or_create_building(entity.entity_index, BuildingClass::Sentry);
377
378        if let Building::Sentry(sentry) = building {
379            for prop in entity.props(parser_state) {
380                match prop.identifier {
381                    ANGLE => sentry.angle = f32::try_from(&prop.value).unwrap_or_default(),
382                    MINI => sentry.is_mini = i64::try_from(&prop.value).unwrap_or_default() > 0,
383                    CONTROLLED => {
384                        sentry.player_controlled =
385                            i64::try_from(&prop.value).unwrap_or_default() > 0
386                    }
387                    TARGET => {
388                        sentry.auto_aim_target =
389                            UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
390                    }
391                    SHELLS => sentry.shells = i64::try_from(&prop.value).unwrap_or_default() as u16,
392                    ROCKETS => {
393                        sentry.rockets = i64::try_from(&prop.value).unwrap_or_default() as u16
394                    }
395                    _ => {}
396                }
397            }
398        }
399    }
400
401    pub fn handle_teleporter_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
402        const RECHARGE_TIME: SendPropIdentifier =
403            SendPropIdentifier::new("DT_ObjectTeleporter", "m_flRechargeTime");
404        const RECHARGE_DURATION: SendPropIdentifier =
405            SendPropIdentifier::new("DT_ObjectTeleporter", "m_flCurrentRechargeDuration");
406        const TIMES_USED: SendPropIdentifier =
407            SendPropIdentifier::new("DT_ObjectTeleporter", "m_iTimesUsed");
408        const OTHER_END: SendPropIdentifier =
409            SendPropIdentifier::new("DT_ObjectTeleporter", "m_bMatchBuilding");
410        const YAW_TO_EXIT: SendPropIdentifier =
411            SendPropIdentifier::new("DT_ObjectTeleporter", "m_flYawToExit");
412        const IS_ENTRANCE: SendPropIdentifier =
413            SendPropIdentifier::new("DT_BaseObject", "m_iObjectMode");
414
415        if entity.update_type == UpdateType::Delete {
416            self.state.remove_building(entity.entity_index);
417            return;
418        }
419
420        self.handle_building(entity, parser_state, BuildingClass::Teleporter);
421
422        let building = self
423            .state
424            .get_or_create_building(entity.entity_index, BuildingClass::Teleporter);
425
426        if let Building::Teleporter(teleporter) = building {
427            for prop in entity.props(parser_state) {
428                match prop.identifier {
429                    RECHARGE_TIME => {
430                        teleporter.recharge_time = f32::try_from(&prop.value).unwrap_or_default()
431                    }
432                    RECHARGE_DURATION => {
433                        teleporter.recharge_duration =
434                            f32::try_from(&prop.value).unwrap_or_default()
435                    }
436                    TIMES_USED => {
437                        teleporter.times_used =
438                            i64::try_from(&prop.value).unwrap_or_default() as u16
439                    }
440                    OTHER_END => {
441                        teleporter.other_end =
442                            EntityId::from(i64::try_from(&prop.value).unwrap_or_default() as u32)
443                    }
444                    YAW_TO_EXIT => {
445                        teleporter.yaw_to_exit = f32::try_from(&prop.value).unwrap_or_default()
446                    }
447                    IS_ENTRANCE => {
448                        teleporter.is_entrance = i64::try_from(&prop.value).unwrap_or_default() == 0
449                    }
450                    _ => {}
451                }
452            }
453        }
454    }
455
456    pub fn handle_dispenser_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
457        const AMMO: SendPropIdentifier =
458            SendPropIdentifier::new("DT_ObjectDispenser", "m_iAmmoMetal");
459        const HEALING: SendPropIdentifier =
460            SendPropIdentifier::new("DT_ObjectDispenser", "healing_array");
461
462        if entity.update_type == UpdateType::Delete {
463            self.state.remove_building(entity.entity_index);
464            return;
465        }
466
467        self.handle_building(entity, parser_state, BuildingClass::Dispenser);
468
469        let building = self
470            .state
471            .get_or_create_building(entity.entity_index, BuildingClass::Dispenser);
472
473        if let Building::Dispenser(dispenser) = building {
474            for prop in entity.props(parser_state) {
475                match prop.identifier {
476                    AMMO => dispenser.metal = i64::try_from(&prop.value).unwrap_or_default() as u16,
477                    HEALING => {
478                        let values = match &prop.value {
479                            SendPropValue::Array(vec) => vec.as_slice(),
480                            _ => Default::default(),
481                        };
482
483                        dispenser.healing = values
484                            .iter()
485                            .map(|val| UserId::from(i64::try_from(val).unwrap_or_default() as u16))
486                            .collect()
487                    }
488                    _ => {}
489                }
490            }
491        }
492    }
493
494    fn handle_building(
495        &mut self,
496        entity: &PacketEntity,
497        parser_state: &ParserState,
498        class: BuildingClass,
499    ) {
500        let building = self
501            .state
502            .get_or_create_building(entity.entity_index, class);
503
504        const LOCAL_ORIGIN: SendPropIdentifier =
505            SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
506        const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
507        const ANGLE: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_angRotation");
508        const SAPPED: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_bHasSapper");
509        const BUILDING: SendPropIdentifier =
510            SendPropIdentifier::new("DT_BaseObject", "m_bBuilding");
511        const LEVEL: SendPropIdentifier =
512            SendPropIdentifier::new("DT_BaseObject", "m_iUpgradeLevel");
513        const BUILDER: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_hBuilder");
514        const MAX_HEALTH: SendPropIdentifier =
515            SendPropIdentifier::new("DT_BaseObject", "m_iMaxHealth");
516        const HEALTH: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_iHealth");
517
518        match building {
519            Building::Sentry(Sentry {
520                position,
521                team,
522                angle,
523                sapped,
524                builder,
525                level,
526                building,
527                max_health,
528                health,
529                ..
530            })
531            | Building::Dispenser(Dispenser {
532                position,
533                team,
534                angle,
535                sapped,
536                builder,
537                level,
538                building,
539                max_health,
540                health,
541                ..
542            })
543            | Building::Teleporter(Teleporter {
544                position,
545                team,
546                angle,
547                sapped,
548                builder,
549                level,
550                building,
551                max_health,
552                health,
553                ..
554            }) => {
555                for prop in entity.props(parser_state) {
556                    match prop.identifier {
557                        LOCAL_ORIGIN => {
558                            *position = Vector::try_from(&prop.value).unwrap_or_default()
559                        }
560                        TEAM => *team = Team::new(i64::try_from(&prop.value).unwrap_or_default()),
561                        ANGLE => *angle = f32::try_from(&prop.value).unwrap_or_default(),
562                        SAPPED => *sapped = i64::try_from(&prop.value).unwrap_or_default() > 0,
563                        BUILDING => *building = i64::try_from(&prop.value).unwrap_or_default() > 0,
564                        LEVEL => *level = i64::try_from(&prop.value).unwrap_or_default() as u8,
565                        BUILDER => {
566                            *builder =
567                                UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
568                        }
569                        MAX_HEALTH => {
570                            *max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
571                        }
572                        HEALTH => *health = i64::try_from(&prop.value).unwrap_or_default() as u16,
573                        _ => {}
574                    }
575                }
576            }
577        }
578    }
579
580    pub fn handle_projectile_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
581        let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
582            return;
583        };
584
585        const ROCKET_ORIGIN: SendPropIdentifier =
586            SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more?
587        const GRENADE_ORIGIN: SendPropIdentifier =
588            SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_vecOrigin");
589        // todo: flares?
590        const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
591        const INITIAL_SPEED: SendPropIdentifier =
592            SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity");
593        const LAUNCHER: SendPropIdentifier =
594            SendPropIdentifier::new("DT_BaseProjectile", "m_hOriginalLauncher");
595        const PIPE_TYPE: SendPropIdentifier =
596            SendPropIdentifier::new("DT_TFProjectile_Pipebomb", "m_iType");
597        const ROCKET_ROTATION: SendPropIdentifier =
598            SendPropIdentifier::new("DT_TFBaseRocket", "m_angRotation");
599        const GRENADE_ROTATION: SendPropIdentifier =
600            SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_angRotation");
601        const CRITICAL_GRENADE: SendPropIdentifier =
602            SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_bCritical");
603        const CRITICAL_ROCKET: SendPropIdentifier =
604            SendPropIdentifier::new("DT_TFProjectile_Rocket", "m_bCritical");
605        const CRITICAL_FLARE: SendPropIdentifier =
606            SendPropIdentifier::new("DT_TFProjectile_Flare", "m_bCritical");
607        const CRITICAL_ARROW: SendPropIdentifier =
608            SendPropIdentifier::new("DT_TFProjectile_Arrow", "m_bCritical");
609
610        if entity.update_type == UpdateType::Delete {
611            self.state.projectile_destroy(entity.entity_index);
612            return;
613        }
614
615        let projectile = self
616            .state
617            .projectiles
618            .entry(entity.entity_index)
619            .or_insert_with(|| {
620                Projectile::new(entity.entity_index, entity.server_class, class_name)
621            });
622
623        // todo: bounds for grenades
624
625        for prop in entity.props(parser_state) {
626            match prop.identifier {
627                ROCKET_ORIGIN | GRENADE_ORIGIN => {
628                    let pos = Vector::try_from(&prop.value).unwrap_or_default();
629                    projectile.position = pos
630                }
631                TEAM => {
632                    let team = Team::new(i64::try_from(&prop.value).unwrap_or_default());
633                    projectile.team = team;
634                }
635                INITIAL_SPEED => {
636                    let speed = Vector::try_from(&prop.value).unwrap_or_default();
637                    projectile.initial_speed = speed;
638                }
639                LAUNCHER => {
640                    let launcher = Handle(i64::try_from(&prop.value).unwrap_or_default());
641                    projectile.launcher = launcher;
642                }
643                PIPE_TYPE => {
644                    let pipe_type = PipeType::new(i64::try_from(&prop.value).unwrap_or_default());
645                    if let Some(class_name) = self.class_names.get(usize::from(entity.server_class))
646                    {
647                        let ty = ProjectileType::new(class_name, Some(pipe_type));
648                        projectile.ty = ty;
649                    }
650                }
651                ROCKET_ROTATION | GRENADE_ROTATION => {
652                    let rotation = Vector::try_from(&prop.value).unwrap_or_default();
653                    projectile.rotation = rotation;
654                }
655                CRITICAL_GRENADE | CRITICAL_ROCKET | CRITICAL_FLARE | CRITICAL_ARROW => {
656                    let critical = bool::try_from(&prop.value).unwrap_or_default();
657                    projectile.critical = critical;
658                }
659                _ => {}
660            }
661        }
662    }
663
664    pub fn handle_train_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
665        const POSITION: SendPropIdentifier =
666            SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
667
668        let objective = self
669            .state
670            .objectives
671            .entry(entity.entity_index)
672            .or_insert_with(|| {
673                Objective::Cart(Cart::default())
674            });
675
676        #[allow(irrefutable_let_patterns)]
677        if let Objective::Cart(cart) = objective {
678            for prop in entity.props(parser_state) {
679                if prop.identifier == POSITION {
680                    let pos = Vector::try_from(&prop.value).unwrap_or_default();
681                    cart.position = pos
682                }
683            }
684        }
685    }
686
687    #[allow(dead_code, unused_variables)]
688    pub fn handle_cp_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
689        const OWNERS: [SendPropIdentifier; 5] = [
690            SendPropIdentifier::new("m_iOwner", "000"),
691            SendPropIdentifier::new("m_iOwner", "001"),
692            SendPropIdentifier::new("m_iOwner", "002"),
693            SendPropIdentifier::new("m_iOwner", "003"),
694            SendPropIdentifier::new("m_iOwner", "004"),
695        ];
696        const CAP_PERCENTAGE: [SendPropIdentifier; 5] = [
697            SendPropIdentifier::new("m_flLazyCapPerc", "000"),
698            SendPropIdentifier::new("m_flLazyCapPerc", "001"),
699            SendPropIdentifier::new("m_flLazyCapPerc", "002"),
700            SendPropIdentifier::new("m_flLazyCapPerc", "003"),
701            SendPropIdentifier::new("m_flLazyCapPerc", "004"),
702        ];
703
704        let objective = self
705            .state
706            .objectives
707            .entry(entity.entity_index)
708            .or_insert_with(|| {
709                Objective::Cart(Cart::default())
710            });
711
712        todo!()
713    }
714
715    fn parse_user_info(
716        &mut self,
717        index: usize,
718        text: Option<&str>,
719        data: Option<Stream>,
720    ) -> ReadResult<()> {
721        if let Some(user_info) =
722            crate::demo::data::UserInfo::parse_from_string_table(index as u16, text, data)?
723        {
724            let id = user_info.entity_id;
725            self.state.get_or_create_player(id).info = Some(user_info.into());
726        }
727
728        Ok(())
729    }
730}