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>, }
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"); const GRENADE_ORIGIN: SendPropIdentifier =
587 SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_vecOrigin");
588 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 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}