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::{Handle, 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            _ if class_name.starts_with("CTFProjectile_")
161                || class_name.as_str() == "CTFGrenadePipebombProjectile" =>
162            {
163                self.handle_projectile_entity(entity, parser_state)
164            }
165            _ => {}
166        }
167    }
168
169    pub fn handle_player_resource(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
170        for prop in entity.props(parser_state) {
171            if let Some((table_name, prop_name)) = prop.identifier.names() {
172                if let Ok(player_id) = u32::from_str(prop_name.as_str()) {
173                    let entity_id = EntityId::from(player_id);
174                    if let Some(player) = self
175                        .state
176                        .players
177                        .iter_mut()
178                        .find(|player| player.entity == entity_id)
179                    {
180                        match table_name.as_str() {
181                            "m_iTeam" => {
182                                player.team =
183                                    Team::new(i64::try_from(&prop.value).unwrap_or_default())
184                            }
185                            "m_iMaxHealth" => {
186                                player.max_health =
187                                    i64::try_from(&prop.value).unwrap_or_default() as u16
188                            }
189                            "m_iPlayerClass" => {
190                                player.class =
191                                    Class::new(i64::try_from(&prop.value).unwrap_or_default())
192                            }
193                            "m_iChargeLevel" => {
194                                player.charge = i64::try_from(&prop.value).unwrap_or_default() as u8
195                            }
196                            "m_iPing" => {
197                                player.ping = i64::try_from(&prop.value).unwrap_or_default() as u16
198                            }
199                            _ => {}
200                        }
201                    }
202                }
203            }
204        }
205    }
206
207    pub fn handle_player_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
208        let player = self.state.get_or_create_player(entity.entity_index);
209
210        const HEALTH_PROP: SendPropIdentifier =
211            SendPropIdentifier::new("DT_BasePlayer", "m_iHealth");
212        const MAX_HEALTH_PROP: SendPropIdentifier =
213            SendPropIdentifier::new("DT_BasePlayer", "m_iMaxHealth");
214        const LIFE_STATE_PROP: SendPropIdentifier =
215            SendPropIdentifier::new("DT_BasePlayer", "m_lifeState");
216
217        const LOCAL_ORIGIN: SendPropIdentifier =
218            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin");
219        const NON_LOCAL_ORIGIN: SendPropIdentifier =
220            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin");
221        const LOCAL_ORIGIN_Z: SendPropIdentifier =
222            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin[2]");
223        const NON_LOCAL_ORIGIN_Z: SendPropIdentifier =
224            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin[2]");
225        const LOCAL_EYE_ANGLES: SendPropIdentifier =
226            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[1]");
227        const NON_LOCAL_EYE_ANGLES: SendPropIdentifier =
228            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
229        const LOCAL_PITCH_ANGLES: SendPropIdentifier =
230            SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[0]");
231        const NON_LOCAL_PITCH_ANGLES: SendPropIdentifier =
232            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[0]");
233
234        const SIMTIME_PROP: SendPropIdentifier =
235            SendPropIdentifier::new("DT_BaseEntity", "m_flSimulationTime");
236        const PROP_BB_MAX: SendPropIdentifier =
237            SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled");
238        const PLAYER_COND: SendPropIdentifier =
239            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCond");
240        const PLAYER_COND_EX1: SendPropIdentifier =
241            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx");
242        const PLAYER_COND_EX2: SendPropIdentifier =
243            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx2");
244        const PLAYER_COND_EX3: SendPropIdentifier =
245            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx3");
246        const PLAYER_COND_EX4: SendPropIdentifier =
247            SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx4");
248        const PLAYER_COND_BITS: SendPropIdentifier =
249            SendPropIdentifier::new("DT_TFPlayerConditionListExclusive", "_condition_bits");
250
251        const WEAPON_0: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "000");
252        const WEAPON_1: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "001");
253        const WEAPON_2: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "002");
254
255        player.in_pvs = entity.in_pvs;
256
257        for prop in entity.props(parser_state) {
258            match prop.identifier {
259                HEALTH_PROP => {
260                    player.health = i64::try_from(&prop.value).unwrap_or_default() as u16
261                }
262                MAX_HEALTH_PROP => {
263                    player.max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
264                }
265                LIFE_STATE_PROP => {
266                    player.state = PlayerState::new(i64::try_from(&prop.value).unwrap_or_default())
267                }
268                LOCAL_ORIGIN | NON_LOCAL_ORIGIN => {
269                    let pos_xy = VectorXY::try_from(&prop.value).unwrap_or_default();
270                    player.position.x = pos_xy.x;
271                    player.position.y = pos_xy.y;
272                }
273                LOCAL_ORIGIN_Z | NON_LOCAL_ORIGIN_Z => {
274                    player.position.z = f32::try_from(&prop.value).unwrap_or_default()
275                }
276                LOCAL_EYE_ANGLES | NON_LOCAL_EYE_ANGLES => {
277                    player.view_angle = f32::try_from(&prop.value).unwrap_or_default()
278                }
279                LOCAL_PITCH_ANGLES | NON_LOCAL_PITCH_ANGLES => {
280                    player.pitch_angle = f32::try_from(&prop.value).unwrap_or_default()
281                }
282                SIMTIME_PROP => {
283                    player.simtime = i64::try_from(&prop.value).unwrap_or_default() as u16
284                }
285                PROP_BB_MAX => {
286                    let max = Vector::try_from(&prop.value).unwrap_or_default();
287                    player.bounds.max = max;
288                }
289                WEAPON_0 => {
290                    let handle = Handle(i64::try_from(&prop.value).unwrap_or_default());
291                    player.weapons[0] = handle;
292                }
293                WEAPON_1 => {
294                    let handle = Handle(i64::try_from(&prop.value).unwrap_or_default());
295                    player.weapons[1] = handle;
296                }
297                WEAPON_2 => {
298                    let handle = Handle(i64::try_from(&prop.value).unwrap_or_default());
299                    player.weapons[2] = handle;
300                }
301                PLAYER_COND | PLAYER_COND_BITS => {
302                    player.conditions[0..4].copy_from_slice(
303                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
304                    );
305                }
306                PLAYER_COND_EX1 => {
307                    player.conditions[4..8].copy_from_slice(
308                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
309                    );
310                }
311                PLAYER_COND_EX2 => {
312                    player.conditions[8..12].copy_from_slice(
313                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
314                    );
315                }
316                PLAYER_COND_EX3 => {
317                    player.conditions[12..16].copy_from_slice(
318                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
319                    );
320                }
321                PLAYER_COND_EX4 => {
322                    player.conditions[16..20].copy_from_slice(
323                        &i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
324                    );
325                }
326                _ => {}
327            }
328        }
329    }
330
331    pub fn handle_world_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
332        if let (
333            Some(SendProp {
334                value: SendPropValue::Vector(boundary_min),
335                ..
336            }),
337            Some(SendProp {
338                value: SendPropValue::Vector(boundary_max),
339                ..
340            }),
341        ) = (
342            entity.get_prop_by_name("DT_WORLD", "m_WorldMins", parser_state),
343            entity.get_prop_by_name("DT_WORLD", "m_WorldMaxs", parser_state),
344        ) {
345            self.state.world = Some(World {
346                boundary_min,
347                boundary_max,
348            })
349        }
350    }
351
352    pub fn handle_sentry_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
353        const ANGLE: SendPropIdentifier =
354            SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
355        const MINI: SendPropIdentifier =
356            SendPropIdentifier::new("DT_BaseObject", "m_bMiniBuilding");
357        const CONTROLLED: SendPropIdentifier =
358            SendPropIdentifier::new("DT_ObjectSentrygun", "m_bPlayerControlled");
359        const TARGET: SendPropIdentifier =
360            SendPropIdentifier::new("DT_ObjectSentrygun", "m_hAutoAimTarget");
361        const SHELLS: SendPropIdentifier =
362            SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoShells");
363        const ROCKETS: SendPropIdentifier =
364            SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoRockets");
365
366        if entity.update_type == UpdateType::Delete {
367            self.state.remove_building(entity.entity_index);
368            return;
369        }
370
371        self.handle_building(entity, parser_state, BuildingClass::Sentry);
372
373        let building = self
374            .state
375            .get_or_create_building(entity.entity_index, BuildingClass::Sentry);
376
377        if let Building::Sentry(sentry) = building {
378            for prop in entity.props(parser_state) {
379                match prop.identifier {
380                    ANGLE => sentry.angle = f32::try_from(&prop.value).unwrap_or_default(),
381                    MINI => sentry.is_mini = i64::try_from(&prop.value).unwrap_or_default() > 0,
382                    CONTROLLED => {
383                        sentry.player_controlled =
384                            i64::try_from(&prop.value).unwrap_or_default() > 0
385                    }
386                    TARGET => {
387                        sentry.auto_aim_target =
388                            UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
389                    }
390                    SHELLS => sentry.shells = i64::try_from(&prop.value).unwrap_or_default() as u16,
391                    ROCKETS => {
392                        sentry.rockets = i64::try_from(&prop.value).unwrap_or_default() as u16
393                    }
394                    _ => {}
395                }
396            }
397        }
398    }
399
400    pub fn handle_teleporter_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
401        const RECHARGE_TIME: SendPropIdentifier =
402            SendPropIdentifier::new("DT_ObjectTeleporter", "m_flRechargeTime");
403        const RECHARGE_DURATION: SendPropIdentifier =
404            SendPropIdentifier::new("DT_ObjectTeleporter", "m_flCurrentRechargeDuration");
405        const TIMES_USED: SendPropIdentifier =
406            SendPropIdentifier::new("DT_ObjectTeleporter", "m_iTimesUsed");
407        const OTHER_END: SendPropIdentifier =
408            SendPropIdentifier::new("DT_ObjectTeleporter", "m_bMatchBuilding");
409        const YAW_TO_EXIT: SendPropIdentifier =
410            SendPropIdentifier::new("DT_ObjectTeleporter", "m_flYawToExit");
411        const IS_ENTRANCE: SendPropIdentifier =
412            SendPropIdentifier::new("DT_BaseObject", "m_iObjectMode");
413
414        if entity.update_type == UpdateType::Delete {
415            self.state.remove_building(entity.entity_index);
416            return;
417        }
418
419        self.handle_building(entity, parser_state, BuildingClass::Teleporter);
420
421        let building = self
422            .state
423            .get_or_create_building(entity.entity_index, BuildingClass::Teleporter);
424
425        if let Building::Teleporter(teleporter) = building {
426            for prop in entity.props(parser_state) {
427                match prop.identifier {
428                    RECHARGE_TIME => {
429                        teleporter.recharge_time = f32::try_from(&prop.value).unwrap_or_default()
430                    }
431                    RECHARGE_DURATION => {
432                        teleporter.recharge_duration =
433                            f32::try_from(&prop.value).unwrap_or_default()
434                    }
435                    TIMES_USED => {
436                        teleporter.times_used =
437                            i64::try_from(&prop.value).unwrap_or_default() as u16
438                    }
439                    OTHER_END => {
440                        teleporter.other_end =
441                            EntityId::from(i64::try_from(&prop.value).unwrap_or_default() as u32)
442                    }
443                    YAW_TO_EXIT => {
444                        teleporter.yaw_to_exit = f32::try_from(&prop.value).unwrap_or_default()
445                    }
446                    IS_ENTRANCE => {
447                        teleporter.is_entrance = i64::try_from(&prop.value).unwrap_or_default() == 0
448                    }
449                    _ => {}
450                }
451            }
452        }
453    }
454
455    pub fn handle_dispenser_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
456        const AMMO: SendPropIdentifier =
457            SendPropIdentifier::new("DT_ObjectDispenser", "m_iAmmoMetal");
458        const HEALING: SendPropIdentifier =
459            SendPropIdentifier::new("DT_ObjectDispenser", "healing_array");
460
461        if entity.update_type == UpdateType::Delete {
462            self.state.remove_building(entity.entity_index);
463            return;
464        }
465
466        self.handle_building(entity, parser_state, BuildingClass::Dispenser);
467
468        let building = self
469            .state
470            .get_or_create_building(entity.entity_index, BuildingClass::Dispenser);
471
472        if let Building::Dispenser(dispenser) = building {
473            for prop in entity.props(parser_state) {
474                match prop.identifier {
475                    AMMO => dispenser.metal = i64::try_from(&prop.value).unwrap_or_default() as u16,
476                    HEALING => {
477                        let values = match &prop.value {
478                            SendPropValue::Array(vec) => vec.as_slice(),
479                            _ => Default::default(),
480                        };
481
482                        dispenser.healing = values
483                            .iter()
484                            .map(|val| UserId::from(i64::try_from(val).unwrap_or_default() as u16))
485                            .collect()
486                    }
487                    _ => {}
488                }
489            }
490        }
491    }
492
493    fn handle_building(
494        &mut self,
495        entity: &PacketEntity,
496        parser_state: &ParserState,
497        class: BuildingClass,
498    ) {
499        let building = self
500            .state
501            .get_or_create_building(entity.entity_index, class);
502
503        const LOCAL_ORIGIN: SendPropIdentifier =
504            SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
505        const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
506        const ANGLE: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_angRotation");
507        const SAPPED: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_bHasSapper");
508        const BUILDING: SendPropIdentifier =
509            SendPropIdentifier::new("DT_BaseObject", "m_bBuilding");
510        const LEVEL: SendPropIdentifier =
511            SendPropIdentifier::new("DT_BaseObject", "m_iUpgradeLevel");
512        const BUILDER: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_hBuilder");
513        const MAX_HEALTH: SendPropIdentifier =
514            SendPropIdentifier::new("DT_BaseObject", "m_iMaxHealth");
515        const HEALTH: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_iHealth");
516
517        match building {
518            Building::Sentry(Sentry {
519                position,
520                team,
521                angle,
522                sapped,
523                builder,
524                level,
525                building,
526                max_health,
527                health,
528                ..
529            })
530            | Building::Dispenser(Dispenser {
531                position,
532                team,
533                angle,
534                sapped,
535                builder,
536                level,
537                building,
538                max_health,
539                health,
540                ..
541            })
542            | Building::Teleporter(Teleporter {
543                position,
544                team,
545                angle,
546                sapped,
547                builder,
548                level,
549                building,
550                max_health,
551                health,
552                ..
553            }) => {
554                for prop in entity.props(parser_state) {
555                    match prop.identifier {
556                        LOCAL_ORIGIN => {
557                            *position = Vector::try_from(&prop.value).unwrap_or_default()
558                        }
559                        TEAM => *team = Team::new(i64::try_from(&prop.value).unwrap_or_default()),
560                        ANGLE => *angle = f32::try_from(&prop.value).unwrap_or_default(),
561                        SAPPED => *sapped = i64::try_from(&prop.value).unwrap_or_default() > 0,
562                        BUILDING => *building = i64::try_from(&prop.value).unwrap_or_default() > 0,
563                        LEVEL => *level = i64::try_from(&prop.value).unwrap_or_default() as u8,
564                        BUILDER => {
565                            *builder =
566                                UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
567                        }
568                        MAX_HEALTH => {
569                            *max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
570                        }
571                        HEALTH => *health = i64::try_from(&prop.value).unwrap_or_default() as u16,
572                        _ => {}
573                    }
574                }
575            }
576        }
577    }
578
579    pub fn handle_projectile_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
580        let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
581            return;
582        };
583
584        const ROCKET_ORIGIN: SendPropIdentifier =
585            SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more?
586        const GRENADE_ORIGIN: SendPropIdentifier =
587            SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_vecOrigin");
588        // todo: flares?
589        const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
590        const INITIAL_SPEED: SendPropIdentifier =
591            SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity");
592        const LAUNCHER: SendPropIdentifier =
593            SendPropIdentifier::new("DT_BaseProjectile", "m_hOriginalLauncher");
594        const PIPE_TYPE: SendPropIdentifier =
595            SendPropIdentifier::new("DT_TFProjectile_Pipebomb", "m_iType");
596        const ROCKET_ROTATION: SendPropIdentifier =
597            SendPropIdentifier::new("DT_TFBaseRocket", "m_angRotation");
598        const GRENADE_ROTATION: SendPropIdentifier =
599            SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_angRotation");
600
601        if entity.update_type == UpdateType::Delete {
602            self.state.projectile_destroy(entity.entity_index);
603            return;
604        }
605
606        let projectile = self
607            .state
608            .projectiles
609            .entry(entity.entity_index)
610            .or_insert_with(|| {
611                Projectile::new(entity.entity_index, entity.server_class, class_name)
612            });
613
614        // todo: bounds for grenades
615
616        for prop in entity.props(parser_state) {
617            match prop.identifier {
618                ROCKET_ORIGIN | GRENADE_ORIGIN => {
619                    let pos = Vector::try_from(&prop.value).unwrap_or_default();
620                    projectile.position = pos
621                }
622                TEAM => {
623                    let team = Team::new(i64::try_from(&prop.value).unwrap_or_default());
624                    projectile.team = team;
625                }
626                INITIAL_SPEED => {
627                    let speed = Vector::try_from(&prop.value).unwrap_or_default();
628                    projectile.initial_speed = speed;
629                }
630                LAUNCHER => {
631                    let launcher = Handle(i64::try_from(&prop.value).unwrap_or_default());
632                    projectile.launcher = launcher;
633                }
634                PIPE_TYPE => {
635                    let pipe_type = PipeType::new(i64::try_from(&prop.value).unwrap_or_default());
636                    if let Some(class_name) = self.class_names.get(usize::from(entity.server_class))
637                    {
638                        let ty = ProjectileType::new(class_name, Some(pipe_type));
639                        projectile.ty = ty;
640                    }
641                }
642                ROCKET_ROTATION | GRENADE_ROTATION => {
643                    let rotation = Vector::try_from(&prop.value).unwrap_or_default();
644                    projectile.rotation = rotation;
645                }
646                _ => {}
647            }
648        }
649    }
650
651    fn parse_user_info(
652        &mut self,
653        index: usize,
654        text: Option<&str>,
655        data: Option<Stream>,
656    ) -> ReadResult<()> {
657        if let Some(user_info) =
658            crate::demo::data::UserInfo::parse_from_string_table(index as u16, text, data)?
659        {
660            let id = user_info.entity_id;
661            self.state.get_or_create_player(id).info = Some(user_info.into());
662        }
663
664        Ok(())
665    }
666}