mirror of
https://codeberg.org/demostf/parser.git
synced 2026-06-03 18:24:05 +02:00
split up gamestateanalyser
This commit is contained in:
parent
a229ed5f11
commit
b93390d1e4
7 changed files with 874 additions and 847 deletions
|
|
@ -85,6 +85,7 @@ pub struct Player {
|
||||||
pub in_pvs: bool,
|
pub in_pvs: bool,
|
||||||
pub bounds: Box,
|
pub bounds: Box,
|
||||||
pub weapons: [Handle; 3],
|
pub weapons: [Handle; 3],
|
||||||
|
pub handle: Handle,
|
||||||
pub(crate) conditions: [u8; 20],
|
pub(crate) conditions: [u8; 20],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -625,4 +626,16 @@ impl GameState {
|
||||||
pub fn remove_building(&mut self, entity_id: EntityId) {
|
pub fn remove_building(&mut self, entity_id: EntityId) {
|
||||||
self.buildings.remove(&entity_id);
|
self.buildings.remove(&entity_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_player_by_weapon_handle(&mut self, handle: Handle) -> Option<&mut Player> {
|
||||||
|
self.players
|
||||||
|
.iter_mut()
|
||||||
|
.find(|player| player.weapons.contains(&handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player_by_handle(&mut self, handle: Handle) -> Option<&mut Player> {
|
||||||
|
self.players
|
||||||
|
.iter_mut()
|
||||||
|
.find(|player| player.handle == handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,847 +0,0 @@
|
||||||
use crate::demo::data::attributes::{has_attribute, Attribute};
|
|
||||||
pub use crate::demo::data::game_state::{
|
|
||||||
Building, BuildingClass, Dispenser, GameState, Kill, PlayerState, Sentry, Teleporter, World,
|
|
||||||
};
|
|
||||||
use crate::demo::data::game_state::{
|
|
||||||
Cart, Handle, MedigunType, Objective, PipeType, Player, PlayerClassData, Projectile,
|
|
||||||
ProjectileType,
|
|
||||||
};
|
|
||||||
use crate::demo::data::DemoTick;
|
|
||||||
use crate::demo::gameevent_gen::ObjectDestroyedEvent;
|
|
||||||
use crate::demo::gamevent::GameEvent;
|
|
||||||
use crate::demo::message::gameevent::GameEventMessage;
|
|
||||||
use crate::demo::message::packetentities::{EntityId, PacketEntity, UpdateType};
|
|
||||||
use crate::demo::message::Message;
|
|
||||||
use crate::demo::packet::datatable::{ParseSendTable, ServerClass, ServerClassName};
|
|
||||||
use crate::demo::packet::message::MessagePacketMeta;
|
|
||||||
use crate::demo::packet::stringtable::StringTableEntry;
|
|
||||||
pub use crate::demo::parser::analyser::{Class, Team, UserId};
|
|
||||||
use crate::demo::parser::handler::BorrowMessageHandler;
|
|
||||||
use crate::demo::parser::MessageHandler;
|
|
||||||
use crate::demo::sendprop::{SendProp, SendPropIdentifier, SendPropValue};
|
|
||||||
use crate::demo::vector::{Vector, VectorXY};
|
|
||||||
use crate::{MessageType, ParserState, ReadResult, Stream};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
pub struct CachedEntities {}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct GameStateAnalyser {
|
|
||||||
pub state: GameState,
|
|
||||||
tick: DemoTick,
|
|
||||||
class_names: Vec<ServerClassName>, // indexed by ClassId
|
|
||||||
outer_map: HashMap<Handle, EntityId>,
|
|
||||||
outer_map_rev: HashMap<EntityId, Handle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageHandler for GameStateAnalyser {
|
|
||||||
type Output = GameState;
|
|
||||||
|
|
||||||
fn does_handle(message_type: MessageType) -> bool {
|
|
||||||
matches!(
|
|
||||||
message_type,
|
|
||||||
MessageType::PacketEntities | MessageType::GameEvent | MessageType::ServerInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_message(&mut self, message: &Message, _tick: DemoTick, parser_state: &ParserState) {
|
|
||||||
match message {
|
|
||||||
Message::PacketEntities(message) => {
|
|
||||||
for entity in &message.entities {
|
|
||||||
self.handle_entity(entity, parser_state);
|
|
||||||
}
|
|
||||||
for id in &message.removed_entities {
|
|
||||||
self.state.projectile_destroy(*id);
|
|
||||||
self.state.remove_building(*id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::ServerInfo(message) => {
|
|
||||||
self.state.interval_per_tick = message.interval_per_tick
|
|
||||||
}
|
|
||||||
Message::GameEvent(GameEventMessage { event, .. }) => {
|
|
||||||
self.state.events.push((self.tick, event.clone()));
|
|
||||||
match event {
|
|
||||||
GameEvent::PlayerDeath(death) => {
|
|
||||||
self.state.kills.push(Kill::new(self.tick, death.as_ref()))
|
|
||||||
}
|
|
||||||
GameEvent::RoundStart(_) => {
|
|
||||||
self.state.buildings.clear();
|
|
||||||
self.state.projectiles.clear();
|
|
||||||
}
|
|
||||||
GameEvent::TeamPlayRoundStart(_) => {
|
|
||||||
self.state.buildings.clear();
|
|
||||||
self.state.projectiles.clear();
|
|
||||||
}
|
|
||||||
GameEvent::ObjectDestroyed(ObjectDestroyedEvent { index, .. }) => {
|
|
||||||
self.state.remove_building((*index as u32).into());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_string_entry(
|
|
||||||
&mut self,
|
|
||||||
table: &str,
|
|
||||||
index: usize,
|
|
||||||
entry: &StringTableEntry,
|
|
||||||
_parser_state: &ParserState,
|
|
||||||
) {
|
|
||||||
if table == "userinfo" {
|
|
||||||
let _ = self.parse_user_info(
|
|
||||||
index,
|
|
||||||
entry.text.as_ref().map(|s| s.as_ref()),
|
|
||||||
entry.extra_data.as_ref().map(|data| data.data.clone()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_data_tables(
|
|
||||||
&mut self,
|
|
||||||
_parse_tables: &[ParseSendTable],
|
|
||||||
server_classes: &[ServerClass],
|
|
||||||
_parser_state: &ParserState,
|
|
||||||
) {
|
|
||||||
self.class_names = server_classes
|
|
||||||
.iter()
|
|
||||||
.map(|class| &class.name)
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_packet_meta(
|
|
||||||
&mut self,
|
|
||||||
tick: DemoTick,
|
|
||||||
_meta: &MessagePacketMeta,
|
|
||||||
_parser_state: &ParserState,
|
|
||||||
) {
|
|
||||||
self.state.tick = tick;
|
|
||||||
self.tick = tick;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_output(mut self, state: &ParserState) -> Self::Output {
|
|
||||||
self.state.server_classes = state.server_classes.clone();
|
|
||||||
self.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorrowMessageHandler for GameStateAnalyser {
|
|
||||||
fn borrow_output(&self, _state: &ParserState) -> &Self::Output {
|
|
||||||
&self.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameStateAnalyser {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
const OUTER: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
|
|
||||||
const OUTER2: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_AttributeManager", "m_hOuter");
|
|
||||||
|
|
||||||
let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
for prop in &entity.props {
|
|
||||||
if prop.identifier == OUTER || prop.identifier == OUTER2 {
|
|
||||||
let outer = Handle::try_from(&prop.value).unwrap_or_default();
|
|
||||||
self.outer_map.insert(outer, entity.entity_index);
|
|
||||||
self.outer_map_rev.insert(entity.entity_index, outer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match class_name.as_str() {
|
|
||||||
"CTFPlayer" => self.handle_player_entity(entity, parser_state),
|
|
||||||
"CTFPlayerResource" => self.handle_player_resource(entity, parser_state),
|
|
||||||
"CWorld" => self.handle_world_entity(entity, parser_state),
|
|
||||||
"CObjectSentrygun" => self.handle_sentry_entity(entity, parser_state),
|
|
||||||
"CObjectDispenser" => self.handle_dispenser_entity(entity, parser_state),
|
|
||||||
"CObjectTeleporter" => self.handle_teleporter_entity(entity, parser_state),
|
|
||||||
"CFuncTrackTrain" => self.handle_train_entity(entity, parser_state),
|
|
||||||
"CWeaponMedigun" => self.handle_medigun_entity(entity, parser_state),
|
|
||||||
_ if class_name.starts_with("CTFProjectile_")
|
|
||||||
|| class_name.as_str() == "CTFGrenadePipebombProjectile" =>
|
|
||||||
{
|
|
||||||
self.handle_projectile_entity(entity, parser_state)
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_player_resource(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
if let Some((table_name, prop_name)) = prop.identifier.names() {
|
|
||||||
if let Ok(player_id) = u32::from_str(prop_name.as_str()) {
|
|
||||||
let entity_id = EntityId::from(player_id);
|
|
||||||
if let Some(player) = self
|
|
||||||
.state
|
|
||||||
.players
|
|
||||||
.iter_mut()
|
|
||||||
.find(|player| player.entity == entity_id)
|
|
||||||
{
|
|
||||||
match table_name.as_str() {
|
|
||||||
"m_iTeam" => {
|
|
||||||
player.team =
|
|
||||||
Team::new(i64::try_from(&prop.value).unwrap_or_default())
|
|
||||||
}
|
|
||||||
"m_iMaxHealth" => {
|
|
||||||
player.max_health =
|
|
||||||
i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
"m_iPlayerClass" => {
|
|
||||||
let class =
|
|
||||||
Class::new(i64::try_from(&prop.value).unwrap_or_default());
|
|
||||||
if player.class != class {
|
|
||||||
player.class = class;
|
|
||||||
player.class_data = PlayerClassData::default_for_class(class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"m_iChargeLevel" => {
|
|
||||||
if let PlayerClassData::Medic { charge, .. } =
|
|
||||||
&mut player.class_data
|
|
||||||
{
|
|
||||||
*charge = i64::try_from(&prop.value).unwrap_or_default() as u8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"m_iPing" => {
|
|
||||||
player.ping = i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_player_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
let player = self.state.get_or_create_player(entity.entity_index);
|
|
||||||
|
|
||||||
const HEALTH_PROP: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BasePlayer", "m_iHealth");
|
|
||||||
const MAX_HEALTH_PROP: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BasePlayer", "m_iMaxHealth");
|
|
||||||
const LIFE_STATE_PROP: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BasePlayer", "m_lifeState");
|
|
||||||
|
|
||||||
const LOCAL_ORIGIN: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin");
|
|
||||||
const NON_LOCAL_ORIGIN: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin");
|
|
||||||
const LOCAL_ORIGIN_Z: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin[2]");
|
|
||||||
const NON_LOCAL_ORIGIN_Z: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin[2]");
|
|
||||||
const LOCAL_EYE_ANGLES: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[1]");
|
|
||||||
const NON_LOCAL_EYE_ANGLES: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
|
|
||||||
const LOCAL_PITCH_ANGLES: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[0]");
|
|
||||||
const NON_LOCAL_PITCH_ANGLES: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[0]");
|
|
||||||
|
|
||||||
const SIMTIME_PROP: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseEntity", "m_flSimulationTime");
|
|
||||||
const PROP_BB_MAX: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled");
|
|
||||||
const PLAYER_COND: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCond");
|
|
||||||
const PLAYER_COND_EX1: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx");
|
|
||||||
const PLAYER_COND_EX2: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx2");
|
|
||||||
const PLAYER_COND_EX3: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx3");
|
|
||||||
const PLAYER_COND_EX4: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx4");
|
|
||||||
const PLAYER_COND_BITS: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerConditionListExclusive", "_condition_bits");
|
|
||||||
|
|
||||||
const WEAPON_0: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "000");
|
|
||||||
const WEAPON_1: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "001");
|
|
||||||
const WEAPON_2: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "002");
|
|
||||||
|
|
||||||
const DISGUISE_TEAM: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nDisguiseTeam");
|
|
||||||
const DISGUISE_CLASS: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_nDisguiseClass");
|
|
||||||
const CLOAK_LEVEL: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFPlayerShared", "m_flCloakMeter");
|
|
||||||
|
|
||||||
player.in_pvs = entity.in_pvs;
|
|
||||||
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
match prop.identifier {
|
|
||||||
HEALTH_PROP => {
|
|
||||||
player.health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
MAX_HEALTH_PROP => {
|
|
||||||
player.max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
LIFE_STATE_PROP => {
|
|
||||||
player.state = PlayerState::new(i64::try_from(&prop.value).unwrap_or_default())
|
|
||||||
}
|
|
||||||
LOCAL_ORIGIN | NON_LOCAL_ORIGIN => {
|
|
||||||
let pos_xy = VectorXY::try_from(&prop.value).unwrap_or_default();
|
|
||||||
player.position.x = pos_xy.x;
|
|
||||||
player.position.y = pos_xy.y;
|
|
||||||
}
|
|
||||||
LOCAL_ORIGIN_Z | NON_LOCAL_ORIGIN_Z => {
|
|
||||||
player.position.z = f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
LOCAL_EYE_ANGLES | NON_LOCAL_EYE_ANGLES => {
|
|
||||||
player.view_angle = f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
LOCAL_PITCH_ANGLES | NON_LOCAL_PITCH_ANGLES => {
|
|
||||||
player.pitch_angle = f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
SIMTIME_PROP => {
|
|
||||||
player.simulation_time = i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
PROP_BB_MAX => {
|
|
||||||
let max = Vector::try_from(&prop.value).unwrap_or_default();
|
|
||||||
player.bounds.max = max;
|
|
||||||
}
|
|
||||||
WEAPON_0 => {
|
|
||||||
let handle = Handle::try_from(&prop.value).unwrap_or_default();
|
|
||||||
player.weapons[0] = handle;
|
|
||||||
}
|
|
||||||
WEAPON_1 => {
|
|
||||||
let handle = Handle::try_from(&prop.value).unwrap_or_default();
|
|
||||||
player.weapons[1] = handle;
|
|
||||||
}
|
|
||||||
WEAPON_2 => {
|
|
||||||
let handle = Handle::try_from(&prop.value).unwrap_or_default();
|
|
||||||
player.weapons[2] = handle;
|
|
||||||
}
|
|
||||||
PLAYER_COND | PLAYER_COND_BITS => {
|
|
||||||
player.conditions[0..4].copy_from_slice(
|
|
||||||
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
PLAYER_COND_EX1 => {
|
|
||||||
player.conditions[4..8].copy_from_slice(
|
|
||||||
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
PLAYER_COND_EX2 => {
|
|
||||||
player.conditions[8..12].copy_from_slice(
|
|
||||||
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
PLAYER_COND_EX3 => {
|
|
||||||
player.conditions[12..16].copy_from_slice(
|
|
||||||
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
PLAYER_COND_EX4 => {
|
|
||||||
player.conditions[16..20].copy_from_slice(
|
|
||||||
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
DISGUISE_TEAM => {
|
|
||||||
if let PlayerClassData::Spy { disguise_team, .. } = &mut player.class_data {
|
|
||||||
*disguise_team = Team::new(i64::try_from(&prop.value).unwrap_or_default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DISGUISE_CLASS => {
|
|
||||||
if let PlayerClassData::Spy { disguise_class, .. } = &mut player.class_data {
|
|
||||||
*disguise_class =
|
|
||||||
Class::new(i64::try_from(&prop.value).unwrap_or_default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CLOAK_LEVEL => {
|
|
||||||
if let PlayerClassData::Spy { cloak, .. } = &mut player.class_data {
|
|
||||||
*cloak = f32::try_from(&prop.value).unwrap_or_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_medigun_entity(&mut self, entity: &PacketEntity, _parser_state: &ParserState) {
|
|
||||||
const OUTER: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
|
|
||||||
const TARGET: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_WeaponMedigun", "m_hHealingTarget");
|
|
||||||
|
|
||||||
if entity.update_type == UpdateType::Enter {
|
|
||||||
let mut ty = MedigunType::Uber;
|
|
||||||
if has_attribute(&entity.props, Attribute::MedigunChargeIsCritBoost) {
|
|
||||||
ty = MedigunType::Kritzkrieg;
|
|
||||||
}
|
|
||||||
if has_attribute(&entity.props, Attribute::MedigunChargeIsMegaHeal) {
|
|
||||||
ty = MedigunType::Quickfix;
|
|
||||||
}
|
|
||||||
if has_attribute(&entity.props, Attribute::MedigunChargeIsResists) {
|
|
||||||
ty = MedigunType::Vaccinator;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handle) = entity.get_own_prop_value_by_identifier(OUTER) {
|
|
||||||
if let Some(player) = self
|
|
||||||
.state
|
|
||||||
.players
|
|
||||||
.iter_mut()
|
|
||||||
.find(|player| player.weapons.contains(&handle))
|
|
||||||
{
|
|
||||||
if let PlayerClassData::Medic { medigun, .. } = &mut player.class_data {
|
|
||||||
*medigun = ty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(target_handle) = entity.get_own_prop_value_by_identifier::<Handle>(TARGET) {
|
|
||||||
let target_id = self
|
|
||||||
.get_player_by_handle(target_handle)
|
|
||||||
.map(|target| target.entity);
|
|
||||||
let medic = self
|
|
||||||
.outer_map_rev
|
|
||||||
.get(&entity.entity_index)
|
|
||||||
.copied()
|
|
||||||
.and_then(|self_handle| self.get_player_by_weapon_handle(self_handle));
|
|
||||||
|
|
||||||
if let Some(medic) = medic {
|
|
||||||
if let PlayerClassData::Medic { target, .. } = &mut medic.class_data {
|
|
||||||
*target = target_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_world_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
if let (
|
|
||||||
Some(SendProp {
|
|
||||||
value: SendPropValue::Vector(boundary_min),
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
Some(SendProp {
|
|
||||||
value: SendPropValue::Vector(boundary_max),
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
) = (
|
|
||||||
entity.get_prop_by_name("DT_WORLD", "m_WorldMins", parser_state),
|
|
||||||
entity.get_prop_by_name("DT_WORLD", "m_WorldMaxs", parser_state),
|
|
||||||
) {
|
|
||||||
self.state.world = Some(World {
|
|
||||||
boundary_min,
|
|
||||||
boundary_max,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_sentry_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
const ANGLE: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
|
|
||||||
const MINI: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseObject", "m_bMiniBuilding");
|
|
||||||
const CONTROLLED: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectSentrygun", "m_bPlayerControlled");
|
|
||||||
const TARGET: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectSentrygun", "m_hAutoAimTarget");
|
|
||||||
const SHELLS: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoShells");
|
|
||||||
const ROCKETS: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoRockets");
|
|
||||||
|
|
||||||
if entity.update_type == UpdateType::Delete {
|
|
||||||
self.state.remove_building(entity.entity_index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.handle_building(entity, parser_state, BuildingClass::Sentry);
|
|
||||||
|
|
||||||
let building = self
|
|
||||||
.state
|
|
||||||
.get_or_create_building(entity.entity_index, BuildingClass::Sentry);
|
|
||||||
|
|
||||||
if let Building::Sentry(sentry) = building {
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
match prop.identifier {
|
|
||||||
ANGLE => sentry.angle = f32::try_from(&prop.value).unwrap_or_default(),
|
|
||||||
MINI => sentry.is_mini = i64::try_from(&prop.value).unwrap_or_default() > 0,
|
|
||||||
CONTROLLED => {
|
|
||||||
sentry.player_controlled =
|
|
||||||
i64::try_from(&prop.value).unwrap_or_default() > 0
|
|
||||||
}
|
|
||||||
TARGET => {
|
|
||||||
sentry.auto_aim_target =
|
|
||||||
UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
|
|
||||||
}
|
|
||||||
SHELLS => sentry.shells = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
|
||||||
ROCKETS => {
|
|
||||||
sentry.rockets = i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_teleporter_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
const RECHARGE_TIME: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectTeleporter", "m_flRechargeTime");
|
|
||||||
const RECHARGE_DURATION: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectTeleporter", "m_flCurrentRechargeDuration");
|
|
||||||
const TIMES_USED: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectTeleporter", "m_iTimesUsed");
|
|
||||||
const OTHER_END: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectTeleporter", "m_bMatchBuilding");
|
|
||||||
const YAW_TO_EXIT: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectTeleporter", "m_flYawToExit");
|
|
||||||
const IS_ENTRANCE: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseObject", "m_iObjectMode");
|
|
||||||
|
|
||||||
if entity.update_type == UpdateType::Delete {
|
|
||||||
self.state.remove_building(entity.entity_index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.handle_building(entity, parser_state, BuildingClass::Teleporter);
|
|
||||||
|
|
||||||
let building = self
|
|
||||||
.state
|
|
||||||
.get_or_create_building(entity.entity_index, BuildingClass::Teleporter);
|
|
||||||
|
|
||||||
if let Building::Teleporter(teleporter) = building {
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
match prop.identifier {
|
|
||||||
RECHARGE_TIME => {
|
|
||||||
teleporter.recharge_time = f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
RECHARGE_DURATION => {
|
|
||||||
teleporter.recharge_duration =
|
|
||||||
f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
TIMES_USED => {
|
|
||||||
teleporter.times_used =
|
|
||||||
i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
OTHER_END => {
|
|
||||||
teleporter.other_end =
|
|
||||||
EntityId::from(i64::try_from(&prop.value).unwrap_or_default() as u32)
|
|
||||||
}
|
|
||||||
YAW_TO_EXIT => {
|
|
||||||
teleporter.yaw_to_exit = f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
IS_ENTRANCE => {
|
|
||||||
teleporter.is_entrance = i64::try_from(&prop.value).unwrap_or_default() == 0
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_dispenser_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
const AMMO: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectDispenser", "m_iAmmoMetal");
|
|
||||||
const HEALING: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_ObjectDispenser", "healing_array");
|
|
||||||
|
|
||||||
if entity.update_type == UpdateType::Delete {
|
|
||||||
self.state.remove_building(entity.entity_index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.handle_building(entity, parser_state, BuildingClass::Dispenser);
|
|
||||||
|
|
||||||
let building = self
|
|
||||||
.state
|
|
||||||
.get_or_create_building(entity.entity_index, BuildingClass::Dispenser);
|
|
||||||
|
|
||||||
if let Building::Dispenser(dispenser) = building {
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
match prop.identifier {
|
|
||||||
AMMO => dispenser.metal = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
|
||||||
HEALING => {
|
|
||||||
let values = match &prop.value {
|
|
||||||
SendPropValue::Array(vec) => vec.as_slice(),
|
|
||||||
_ => Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
dispenser.healing = values
|
|
||||||
.iter()
|
|
||||||
.map(|val| UserId::from(i64::try_from(val).unwrap_or_default() as u16))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_building(
|
|
||||||
&mut self,
|
|
||||||
entity: &PacketEntity,
|
|
||||||
_parser_state: &ParserState,
|
|
||||||
class: BuildingClass,
|
|
||||||
) {
|
|
||||||
let building = self
|
|
||||||
.state
|
|
||||||
.get_or_create_building(entity.entity_index, class);
|
|
||||||
|
|
||||||
const LOCAL_ORIGIN: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
|
|
||||||
const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
|
|
||||||
const ANGLE: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_angRotation");
|
|
||||||
const SAPPED: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_bHasSapper");
|
|
||||||
const BUILDING: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseObject", "m_bBuilding");
|
|
||||||
const LEVEL: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseObject", "m_iUpgradeLevel");
|
|
||||||
const BUILDER: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_hBuilder");
|
|
||||||
const MAX_HEALTH: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseObject", "m_iMaxHealth");
|
|
||||||
const HEALTH: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_iHealth");
|
|
||||||
const PROGRESS: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseObject", "m_flPercentageConstructed");
|
|
||||||
|
|
||||||
match building {
|
|
||||||
Building::Sentry(Sentry {
|
|
||||||
position,
|
|
||||||
team,
|
|
||||||
angle,
|
|
||||||
sapped,
|
|
||||||
builder,
|
|
||||||
level,
|
|
||||||
building,
|
|
||||||
max_health,
|
|
||||||
health,
|
|
||||||
construction_progress,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
| Building::Dispenser(Dispenser {
|
|
||||||
position,
|
|
||||||
team,
|
|
||||||
angle,
|
|
||||||
sapped,
|
|
||||||
builder,
|
|
||||||
level,
|
|
||||||
building,
|
|
||||||
max_health,
|
|
||||||
health,
|
|
||||||
construction_progress,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
| Building::Teleporter(Teleporter {
|
|
||||||
position,
|
|
||||||
team,
|
|
||||||
angle,
|
|
||||||
sapped,
|
|
||||||
builder,
|
|
||||||
level,
|
|
||||||
building,
|
|
||||||
max_health,
|
|
||||||
health,
|
|
||||||
construction_progress,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
// picked up
|
|
||||||
if entity.update_type == UpdateType::Leave {
|
|
||||||
*health = 0;
|
|
||||||
}
|
|
||||||
for prop in &entity.props {
|
|
||||||
match prop.identifier {
|
|
||||||
LOCAL_ORIGIN => {
|
|
||||||
*position = Vector::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
TEAM => *team = Team::new(i64::try_from(&prop.value).unwrap_or_default()),
|
|
||||||
ANGLE => {
|
|
||||||
*angle = Vector::try_from(&prop.value)
|
|
||||||
.map(|v| v.y)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
SAPPED => *sapped = i64::try_from(&prop.value).unwrap_or_default() > 0,
|
|
||||||
BUILDING => *building = i64::try_from(&prop.value).unwrap_or_default() > 0,
|
|
||||||
LEVEL => *level = i64::try_from(&prop.value).unwrap_or_default() as u8,
|
|
||||||
BUILDER => {
|
|
||||||
*builder =
|
|
||||||
UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
|
|
||||||
}
|
|
||||||
MAX_HEALTH => {
|
|
||||||
*max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
|
||||||
}
|
|
||||||
HEALTH => *health = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
|
||||||
PROGRESS => {
|
|
||||||
*construction_progress = f32::try_from(&prop.value).unwrap_or_default()
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_projectile_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ROCKET_ORIGIN: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more?
|
|
||||||
const GRENADE_ORIGIN: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_vecOrigin");
|
|
||||||
// todo: flares?
|
|
||||||
const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
|
|
||||||
const INITIAL_SPEED: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity");
|
|
||||||
const LAUNCHER: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseProjectile", "m_hOriginalLauncher");
|
|
||||||
const PIPE_TYPE: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFProjectile_Pipebomb", "m_iType");
|
|
||||||
const ROCKET_ROTATION: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFBaseRocket", "m_angRotation");
|
|
||||||
const GRENADE_ROTATION: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_angRotation");
|
|
||||||
const CRITICAL_GRENADE: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_bCritical");
|
|
||||||
const CRITICAL_ROCKET: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFProjectile_Rocket", "m_bCritical");
|
|
||||||
const CRITICAL_FLARE: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFProjectile_Flare", "m_bCritical");
|
|
||||||
const CRITICAL_ARROW: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_TFProjectile_Arrow", "m_bCritical");
|
|
||||||
|
|
||||||
if entity.update_type == UpdateType::Delete {
|
|
||||||
self.state.projectile_destroy(entity.entity_index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let projectile = self
|
|
||||||
.state
|
|
||||||
.projectiles
|
|
||||||
.entry(entity.entity_index)
|
|
||||||
.or_insert_with(|| {
|
|
||||||
Projectile::new(entity.entity_index, entity.server_class, class_name)
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo: bounds for grenades
|
|
||||||
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
match prop.identifier {
|
|
||||||
ROCKET_ORIGIN | GRENADE_ORIGIN => {
|
|
||||||
let pos = Vector::try_from(&prop.value).unwrap_or_default();
|
|
||||||
projectile.position = pos
|
|
||||||
}
|
|
||||||
TEAM => {
|
|
||||||
let team = Team::new(i64::try_from(&prop.value).unwrap_or_default());
|
|
||||||
projectile.team = team;
|
|
||||||
}
|
|
||||||
INITIAL_SPEED => {
|
|
||||||
let speed = Vector::try_from(&prop.value).unwrap_or_default();
|
|
||||||
projectile.initial_speed = speed;
|
|
||||||
}
|
|
||||||
LAUNCHER => {
|
|
||||||
let launcher = Handle(i64::try_from(&prop.value).unwrap_or_default());
|
|
||||||
projectile.launcher = launcher;
|
|
||||||
}
|
|
||||||
PIPE_TYPE => {
|
|
||||||
let pipe_type = PipeType::new(i64::try_from(&prop.value).unwrap_or_default());
|
|
||||||
if let Some(class_name) = self.class_names.get(usize::from(entity.server_class))
|
|
||||||
{
|
|
||||||
let ty = ProjectileType::new(class_name, Some(pipe_type));
|
|
||||||
projectile.ty = ty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ROCKET_ROTATION | GRENADE_ROTATION => {
|
|
||||||
let rotation = Vector::try_from(&prop.value).unwrap_or_default();
|
|
||||||
projectile.rotation = rotation;
|
|
||||||
}
|
|
||||||
CRITICAL_GRENADE | CRITICAL_ROCKET | CRITICAL_FLARE | CRITICAL_ARROW => {
|
|
||||||
let critical = bool::try_from(&prop.value).unwrap_or_default();
|
|
||||||
projectile.critical = critical;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_train_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
const POSITION: SendPropIdentifier =
|
|
||||||
SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
|
|
||||||
|
|
||||||
let objective = self
|
|
||||||
.state
|
|
||||||
.objectives
|
|
||||||
.entry(entity.entity_index)
|
|
||||||
.or_insert_with(|| Objective::Cart(Cart::default()));
|
|
||||||
|
|
||||||
#[allow(irrefutable_let_patterns)]
|
|
||||||
if let Objective::Cart(cart) = objective {
|
|
||||||
for prop in entity.props(parser_state) {
|
|
||||||
if prop.identifier == POSITION {
|
|
||||||
let pos = Vector::try_from(&prop.value).unwrap_or_default();
|
|
||||||
cart.position = pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code, unused_variables)]
|
|
||||||
pub fn handle_cp_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
|
||||||
const OWNERS: [SendPropIdentifier; 5] = [
|
|
||||||
SendPropIdentifier::new("m_iOwner", "000"),
|
|
||||||
SendPropIdentifier::new("m_iOwner", "001"),
|
|
||||||
SendPropIdentifier::new("m_iOwner", "002"),
|
|
||||||
SendPropIdentifier::new("m_iOwner", "003"),
|
|
||||||
SendPropIdentifier::new("m_iOwner", "004"),
|
|
||||||
];
|
|
||||||
const CAP_PERCENTAGE: [SendPropIdentifier; 5] = [
|
|
||||||
SendPropIdentifier::new("m_flLazyCapPerc", "000"),
|
|
||||||
SendPropIdentifier::new("m_flLazyCapPerc", "001"),
|
|
||||||
SendPropIdentifier::new("m_flLazyCapPerc", "002"),
|
|
||||||
SendPropIdentifier::new("m_flLazyCapPerc", "003"),
|
|
||||||
SendPropIdentifier::new("m_flLazyCapPerc", "004"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let objective = self
|
|
||||||
.state
|
|
||||||
.objectives
|
|
||||||
.entry(entity.entity_index)
|
|
||||||
.or_insert_with(|| Objective::Cart(Cart::default()));
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_user_info(
|
|
||||||
&mut self,
|
|
||||||
index: usize,
|
|
||||||
text: Option<&str>,
|
|
||||||
data: Option<Stream>,
|
|
||||||
) -> ReadResult<()> {
|
|
||||||
if let Some(user_info) =
|
|
||||||
crate::demo::data::UserInfo::parse_from_string_table(index as u16, text, data)?
|
|
||||||
{
|
|
||||||
let id = user_info.entity_id;
|
|
||||||
self.state.get_or_create_player(id).info = Some(user_info.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_player_by_weapon_handle(&mut self, handle: Handle) -> Option<&mut Player> {
|
|
||||||
self.state
|
|
||||||
.players
|
|
||||||
.iter_mut()
|
|
||||||
.find(|player| player.weapons.contains(&handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_player_by_handle(&mut self, handle: Handle) -> Option<&mut Player> {
|
|
||||||
let entity_id = self.outer_map.get(&handle)?;
|
|
||||||
self.state
|
|
||||||
.players
|
|
||||||
.iter_mut()
|
|
||||||
.find(|player| player.entity == *entity_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
243
src/demo/parser/gamestateanalyser/building.rs
Normal file
243
src/demo/parser/gamestateanalyser/building.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
use crate::demo::data::game_state::{
|
||||||
|
Building, BuildingClass, Dispenser, GameState, Sentry, Teleporter,
|
||||||
|
};
|
||||||
|
use crate::demo::message::{EntityId, PacketEntity, UpdateType};
|
||||||
|
use crate::demo::parser::analyser::{Team, UserId};
|
||||||
|
use crate::demo::sendprop::{SendPropIdentifier, SendPropValue};
|
||||||
|
use crate::demo::vector::Vector;
|
||||||
|
use crate::ParserState;
|
||||||
|
|
||||||
|
pub fn handle_sentry_entity(
|
||||||
|
state: &mut GameState,
|
||||||
|
entity: &PacketEntity,
|
||||||
|
parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
const ANGLE: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
|
||||||
|
const MINI: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_bMiniBuilding");
|
||||||
|
const CONTROLLED: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectSentrygun", "m_bPlayerControlled");
|
||||||
|
const TARGET: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectSentrygun", "m_hAutoAimTarget");
|
||||||
|
const SHELLS: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoShells");
|
||||||
|
const ROCKETS: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectSentrygun", "m_iAmmoRockets");
|
||||||
|
|
||||||
|
if entity.update_type == UpdateType::Delete {
|
||||||
|
state.remove_building(entity.entity_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_building(state, entity, parser_state, BuildingClass::Sentry);
|
||||||
|
|
||||||
|
let building = state.get_or_create_building(entity.entity_index, BuildingClass::Sentry);
|
||||||
|
|
||||||
|
if let Building::Sentry(sentry) = building {
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
match prop.identifier {
|
||||||
|
ANGLE => sentry.angle = f32::try_from(&prop.value).unwrap_or_default(),
|
||||||
|
MINI => sentry.is_mini = i64::try_from(&prop.value).unwrap_or_default() > 0,
|
||||||
|
CONTROLLED => {
|
||||||
|
sentry.player_controlled = i64::try_from(&prop.value).unwrap_or_default() > 0
|
||||||
|
}
|
||||||
|
TARGET => {
|
||||||
|
sentry.auto_aim_target =
|
||||||
|
UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
|
||||||
|
}
|
||||||
|
SHELLS => sentry.shells = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
||||||
|
ROCKETS => sentry.rockets = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_teleporter_entity(
|
||||||
|
state: &mut GameState,
|
||||||
|
entity: &PacketEntity,
|
||||||
|
parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
const RECHARGE_TIME: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectTeleporter", "m_flRechargeTime");
|
||||||
|
const RECHARGE_DURATION: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectTeleporter", "m_flCurrentRechargeDuration");
|
||||||
|
const TIMES_USED: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectTeleporter", "m_iTimesUsed");
|
||||||
|
const OTHER_END: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectTeleporter", "m_bMatchBuilding");
|
||||||
|
const YAW_TO_EXIT: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectTeleporter", "m_flYawToExit");
|
||||||
|
const IS_ENTRANCE: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BaseObject", "m_iObjectMode");
|
||||||
|
|
||||||
|
if entity.update_type == UpdateType::Delete {
|
||||||
|
state.remove_building(entity.entity_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_building(state, entity, parser_state, BuildingClass::Teleporter);
|
||||||
|
|
||||||
|
let building = state.get_or_create_building(entity.entity_index, BuildingClass::Teleporter);
|
||||||
|
|
||||||
|
if let Building::Teleporter(teleporter) = building {
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
match prop.identifier {
|
||||||
|
RECHARGE_TIME => {
|
||||||
|
teleporter.recharge_time = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
RECHARGE_DURATION => {
|
||||||
|
teleporter.recharge_duration = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
TIMES_USED => {
|
||||||
|
teleporter.times_used = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
OTHER_END => {
|
||||||
|
teleporter.other_end =
|
||||||
|
EntityId::from(i64::try_from(&prop.value).unwrap_or_default() as u32)
|
||||||
|
}
|
||||||
|
YAW_TO_EXIT => {
|
||||||
|
teleporter.yaw_to_exit = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
IS_ENTRANCE => {
|
||||||
|
teleporter.is_entrance = i64::try_from(&prop.value).unwrap_or_default() == 0
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_dispenser_entity(
|
||||||
|
state: &mut GameState,
|
||||||
|
entity: &PacketEntity,
|
||||||
|
parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
const AMMO: SendPropIdentifier = SendPropIdentifier::new("DT_ObjectDispenser", "m_iAmmoMetal");
|
||||||
|
const HEALING: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_ObjectDispenser", "healing_array");
|
||||||
|
|
||||||
|
if entity.update_type == UpdateType::Delete {
|
||||||
|
state.remove_building(entity.entity_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_building(state, entity, parser_state, BuildingClass::Dispenser);
|
||||||
|
|
||||||
|
let building = state.get_or_create_building(entity.entity_index, BuildingClass::Dispenser);
|
||||||
|
|
||||||
|
if let Building::Dispenser(dispenser) = building {
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
match prop.identifier {
|
||||||
|
AMMO => dispenser.metal = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
||||||
|
HEALING => {
|
||||||
|
let values = match &prop.value {
|
||||||
|
SendPropValue::Array(vec) => vec.as_slice(),
|
||||||
|
_ => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
dispenser.healing = values
|
||||||
|
.iter()
|
||||||
|
.map(|val| UserId::from(i64::try_from(val).unwrap_or_default() as u16))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_building(
|
||||||
|
state: &mut GameState,
|
||||||
|
entity: &PacketEntity,
|
||||||
|
parser_state: &ParserState,
|
||||||
|
class: BuildingClass,
|
||||||
|
) {
|
||||||
|
let building = state.get_or_create_building(entity.entity_index, class);
|
||||||
|
|
||||||
|
const LOCAL_ORIGIN: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
|
||||||
|
const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
|
||||||
|
const ANGLE: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_angRotation");
|
||||||
|
const SAPPED: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_bHasSapper");
|
||||||
|
const BUILDING: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_bBuilding");
|
||||||
|
const LEVEL: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_iUpgradeLevel");
|
||||||
|
const BUILDER: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_hBuilder");
|
||||||
|
const MAX_HEALTH: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_iMaxHealth");
|
||||||
|
const HEALTH: SendPropIdentifier = SendPropIdentifier::new("DT_BaseObject", "m_iHealth");
|
||||||
|
const PROGRESS: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BaseObject", "m_flPercentageConstructed");
|
||||||
|
|
||||||
|
match building {
|
||||||
|
Building::Sentry(Sentry {
|
||||||
|
position,
|
||||||
|
team,
|
||||||
|
angle,
|
||||||
|
sapped,
|
||||||
|
builder,
|
||||||
|
level,
|
||||||
|
building,
|
||||||
|
max_health,
|
||||||
|
health,
|
||||||
|
construction_progress,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
| Building::Dispenser(Dispenser {
|
||||||
|
position,
|
||||||
|
team,
|
||||||
|
angle,
|
||||||
|
sapped,
|
||||||
|
builder,
|
||||||
|
level,
|
||||||
|
building,
|
||||||
|
max_health,
|
||||||
|
health,
|
||||||
|
construction_progress,
|
||||||
|
..
|
||||||
|
})
|
||||||
|
| Building::Teleporter(Teleporter {
|
||||||
|
position,
|
||||||
|
team,
|
||||||
|
angle,
|
||||||
|
sapped,
|
||||||
|
builder,
|
||||||
|
level,
|
||||||
|
building,
|
||||||
|
max_health,
|
||||||
|
health,
|
||||||
|
construction_progress,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
// picked up
|
||||||
|
if entity.update_type == UpdateType::Leave {
|
||||||
|
*health = 0;
|
||||||
|
}
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
match prop.identifier {
|
||||||
|
LOCAL_ORIGIN => *position = Vector::try_from(&prop.value).unwrap_or_default(),
|
||||||
|
TEAM => *team = Team::new(i64::try_from(&prop.value).unwrap_or_default()),
|
||||||
|
ANGLE => {
|
||||||
|
*angle = Vector::try_from(&prop.value)
|
||||||
|
.map(|v| v.y)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
SAPPED => *sapped = i64::try_from(&prop.value).unwrap_or_default() > 0,
|
||||||
|
BUILDING => *building = i64::try_from(&prop.value).unwrap_or_default() > 0,
|
||||||
|
LEVEL => *level = i64::try_from(&prop.value).unwrap_or_default() as u8,
|
||||||
|
BUILDER => {
|
||||||
|
*builder =
|
||||||
|
UserId::from(i64::try_from(&prop.value).unwrap_or_default() as u16)
|
||||||
|
}
|
||||||
|
MAX_HEALTH => {
|
||||||
|
*max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
HEALTH => *health = i64::try_from(&prop.value).unwrap_or_default() as u16,
|
||||||
|
PROGRESS => {
|
||||||
|
*construction_progress = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
270
src/demo/parser/gamestateanalyser/mod.rs
Normal file
270
src/demo/parser/gamestateanalyser/mod.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
mod building;
|
||||||
|
mod player;
|
||||||
|
mod projectile;
|
||||||
|
mod weapon;
|
||||||
|
|
||||||
|
pub use crate::demo::data::game_state::{
|
||||||
|
Building, BuildingClass, Dispenser, GameState, Kill, PlayerState, Sentry, Teleporter, World,
|
||||||
|
};
|
||||||
|
use crate::demo::data::game_state::{Cart, Handle, Objective};
|
||||||
|
use crate::demo::data::DemoTick;
|
||||||
|
use crate::demo::gameevent_gen::ObjectDestroyedEvent;
|
||||||
|
use crate::demo::gamevent::GameEvent;
|
||||||
|
use crate::demo::message::gameevent::GameEventMessage;
|
||||||
|
use crate::demo::message::packetentities::{EntityId, PacketEntity};
|
||||||
|
use crate::demo::message::Message;
|
||||||
|
use crate::demo::packet::datatable::{ParseSendTable, ServerClass, ServerClassName};
|
||||||
|
use crate::demo::packet::message::MessagePacketMeta;
|
||||||
|
use crate::demo::packet::stringtable::StringTableEntry;
|
||||||
|
pub use crate::demo::parser::analyser::{Class, Team, UserId};
|
||||||
|
use crate::demo::parser::gamestateanalyser::building::{
|
||||||
|
handle_dispenser_entity, handle_sentry_entity, handle_teleporter_entity,
|
||||||
|
};
|
||||||
|
use crate::demo::parser::gamestateanalyser::player::{
|
||||||
|
handle_player_entity, handle_player_resource,
|
||||||
|
};
|
||||||
|
use crate::demo::parser::gamestateanalyser::projectile::handle_projectile_entity;
|
||||||
|
use crate::demo::parser::gamestateanalyser::weapon::handle_medigun_entity;
|
||||||
|
use crate::demo::parser::handler::BorrowMessageHandler;
|
||||||
|
use crate::demo::parser::MessageHandler;
|
||||||
|
use crate::demo::sendprop::{SendProp, SendPropIdentifier, SendPropValue};
|
||||||
|
use crate::demo::vector::Vector;
|
||||||
|
use crate::{MessageType, ParserState, ReadResult, Stream};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
pub struct CachedEntities {}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct GameStateAnalyser {
|
||||||
|
pub state: GameState,
|
||||||
|
tick: DemoTick,
|
||||||
|
class_names: Vec<ServerClassName>, // indexed by ClassId
|
||||||
|
outer_map: HashMap<Handle, EntityId>,
|
||||||
|
outer_map_rev: HashMap<EntityId, Handle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageHandler for GameStateAnalyser {
|
||||||
|
type Output = GameState;
|
||||||
|
|
||||||
|
fn does_handle(message_type: MessageType) -> bool {
|
||||||
|
matches!(
|
||||||
|
message_type,
|
||||||
|
MessageType::PacketEntities | MessageType::GameEvent | MessageType::ServerInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(&mut self, message: &Message, _tick: DemoTick, parser_state: &ParserState) {
|
||||||
|
match message {
|
||||||
|
Message::PacketEntities(message) => {
|
||||||
|
for entity in &message.entities {
|
||||||
|
self.handle_entity(entity, parser_state);
|
||||||
|
}
|
||||||
|
for id in &message.removed_entities {
|
||||||
|
self.state.projectile_destroy(*id);
|
||||||
|
self.state.remove_building(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::ServerInfo(message) => {
|
||||||
|
self.state.interval_per_tick = message.interval_per_tick
|
||||||
|
}
|
||||||
|
Message::GameEvent(GameEventMessage { event, .. }) => {
|
||||||
|
self.state.events.push((self.tick, event.clone()));
|
||||||
|
match event {
|
||||||
|
GameEvent::PlayerDeath(death) => {
|
||||||
|
self.state.kills.push(Kill::new(self.tick, death.as_ref()))
|
||||||
|
}
|
||||||
|
GameEvent::RoundStart(_) => {
|
||||||
|
self.state.buildings.clear();
|
||||||
|
self.state.projectiles.clear();
|
||||||
|
}
|
||||||
|
GameEvent::TeamPlayRoundStart(_) => {
|
||||||
|
self.state.buildings.clear();
|
||||||
|
self.state.projectiles.clear();
|
||||||
|
}
|
||||||
|
GameEvent::ObjectDestroyed(ObjectDestroyedEvent { index, .. }) => {
|
||||||
|
self.state.remove_building((*index as u32).into());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_string_entry(
|
||||||
|
&mut self,
|
||||||
|
table: &str,
|
||||||
|
index: usize,
|
||||||
|
entry: &StringTableEntry,
|
||||||
|
_parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
if table == "userinfo" {
|
||||||
|
let _ = self.parse_user_info(
|
||||||
|
index,
|
||||||
|
entry.text.as_ref().map(|s| s.as_ref()),
|
||||||
|
entry.extra_data.as_ref().map(|data| data.data.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_data_tables(
|
||||||
|
&mut self,
|
||||||
|
_parse_tables: &[ParseSendTable],
|
||||||
|
server_classes: &[ServerClass],
|
||||||
|
_parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
self.class_names = server_classes
|
||||||
|
.iter()
|
||||||
|
.map(|class| &class.name)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_packet_meta(
|
||||||
|
&mut self,
|
||||||
|
tick: DemoTick,
|
||||||
|
_meta: &MessagePacketMeta,
|
||||||
|
_parser_state: &ParserState,
|
||||||
|
) {
|
||||||
|
self.state.tick = tick;
|
||||||
|
self.tick = tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_output(mut self, state: &ParserState) -> Self::Output {
|
||||||
|
self.state.server_classes = state.server_classes.clone();
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorrowMessageHandler for GameStateAnalyser {
|
||||||
|
fn borrow_output(&self, _state: &ParserState) -> &Self::Output {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameStateAnalyser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
const OUTER: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
|
||||||
|
const OUTER2: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_AttributeManager", "m_hOuter");
|
||||||
|
|
||||||
|
let Some(class_name) = self.class_names.get(usize::from(entity.server_class)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for prop in &entity.props {
|
||||||
|
if prop.identifier == OUTER || prop.identifier == OUTER2 {
|
||||||
|
let outer = Handle::try_from(&prop.value).unwrap_or_default();
|
||||||
|
self.outer_map.insert(outer, entity.entity_index);
|
||||||
|
self.outer_map_rev.insert(entity.entity_index, outer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match class_name.as_str() {
|
||||||
|
"CTFPlayer" => handle_player_entity(&mut self.state, entity, parser_state),
|
||||||
|
"CTFPlayerResource" => handle_player_resource(&mut self.state, entity, parser_state),
|
||||||
|
"CWorld" => self.handle_world_entity(entity, parser_state),
|
||||||
|
"CObjectSentrygun" => handle_sentry_entity(&mut self.state, entity, parser_state),
|
||||||
|
"CObjectDispenser" => handle_dispenser_entity(&mut self.state, entity, parser_state),
|
||||||
|
"CObjectTeleporter" => handle_teleporter_entity(&mut self.state, entity, parser_state),
|
||||||
|
"CFuncTrackTrain" => self.handle_train_entity(entity, parser_state),
|
||||||
|
"CWeaponMedigun" => handle_medigun_entity(&mut self.state, entity, &self.outer_map_rev),
|
||||||
|
_ if class_name.starts_with("CTFProjectile_")
|
||||||
|
|| class_name.as_str() == "CTFGrenadePipebombProjectile" =>
|
||||||
|
{
|
||||||
|
handle_projectile_entity(&mut self.state, entity, parser_state, &self.class_names)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_world_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
if let (
|
||||||
|
Some(SendProp {
|
||||||
|
value: SendPropValue::Vector(boundary_min),
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
Some(SendProp {
|
||||||
|
value: SendPropValue::Vector(boundary_max),
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
) = (
|
||||||
|
entity.get_prop_by_name("DT_WORLD", "m_WorldMins", parser_state),
|
||||||
|
entity.get_prop_by_name("DT_WORLD", "m_WorldMaxs", parser_state),
|
||||||
|
) {
|
||||||
|
self.state.world = Some(World {
|
||||||
|
boundary_min,
|
||||||
|
boundary_max,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_train_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
const POSITION: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BaseEntity", "m_vecOrigin");
|
||||||
|
|
||||||
|
let objective = self
|
||||||
|
.state
|
||||||
|
.objectives
|
||||||
|
.entry(entity.entity_index)
|
||||||
|
.or_insert_with(|| Objective::Cart(Cart::default()));
|
||||||
|
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
|
if let Objective::Cart(cart) = objective {
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
if prop.identifier == POSITION {
|
||||||
|
let pos = Vector::try_from(&prop.value).unwrap_or_default();
|
||||||
|
cart.position = pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code, unused_variables)]
|
||||||
|
pub fn handle_cp_entity(&mut self, entity: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
const OWNERS: [SendPropIdentifier; 5] = [
|
||||||
|
SendPropIdentifier::new("m_iOwner", "000"),
|
||||||
|
SendPropIdentifier::new("m_iOwner", "001"),
|
||||||
|
SendPropIdentifier::new("m_iOwner", "002"),
|
||||||
|
SendPropIdentifier::new("m_iOwner", "003"),
|
||||||
|
SendPropIdentifier::new("m_iOwner", "004"),
|
||||||
|
];
|
||||||
|
const CAP_PERCENTAGE: [SendPropIdentifier; 5] = [
|
||||||
|
SendPropIdentifier::new("m_flLazyCapPerc", "000"),
|
||||||
|
SendPropIdentifier::new("m_flLazyCapPerc", "001"),
|
||||||
|
SendPropIdentifier::new("m_flLazyCapPerc", "002"),
|
||||||
|
SendPropIdentifier::new("m_flLazyCapPerc", "003"),
|
||||||
|
SendPropIdentifier::new("m_flLazyCapPerc", "004"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let objective = self
|
||||||
|
.state
|
||||||
|
.objectives
|
||||||
|
.entry(entity.entity_index)
|
||||||
|
.or_insert_with(|| Objective::Cart(Cart::default()));
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_user_info(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
text: Option<&str>,
|
||||||
|
data: Option<Stream>,
|
||||||
|
) -> ReadResult<()> {
|
||||||
|
if let Some(user_info) =
|
||||||
|
crate::demo::data::UserInfo::parse_from_string_table(index as u16, text, data)?
|
||||||
|
{
|
||||||
|
let id = user_info.entity_id;
|
||||||
|
self.state.get_or_create_player(id).info = Some(user_info.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
205
src/demo/parser/gamestateanalyser/player.rs
Normal file
205
src/demo/parser/gamestateanalyser/player.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
use crate::demo::data::game_state::{GameState, Handle, PlayerClassData, PlayerState};
|
||||||
|
use crate::demo::message::{EntityId, PacketEntity};
|
||||||
|
use crate::demo::parser::analyser::{Class, Team};
|
||||||
|
use crate::demo::sendprop::SendPropIdentifier;
|
||||||
|
use crate::demo::vector::{Vector, VectorXY};
|
||||||
|
use crate::ParserState;
|
||||||
|
|
||||||
|
pub fn handle_player_entity(state: &mut GameState, entity: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
let player = state.get_or_create_player(entity.entity_index);
|
||||||
|
|
||||||
|
const OUTER: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
|
||||||
|
|
||||||
|
const HEALTH_PROP: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BasePlayer", "m_iHealth");
|
||||||
|
const MAX_HEALTH_PROP: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BasePlayer", "m_iMaxHealth");
|
||||||
|
const LIFE_STATE_PROP: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BasePlayer", "m_lifeState");
|
||||||
|
|
||||||
|
const LOCAL_ORIGIN: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin");
|
||||||
|
const NON_LOCAL_ORIGIN: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin");
|
||||||
|
const LOCAL_ORIGIN_Z: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_vecOrigin[2]");
|
||||||
|
const NON_LOCAL_ORIGIN_Z: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_vecOrigin[2]");
|
||||||
|
const LOCAL_EYE_ANGLES: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[1]");
|
||||||
|
const NON_LOCAL_EYE_ANGLES: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[1]");
|
||||||
|
const LOCAL_PITCH_ANGLES: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFLocalPlayerExclusive", "m_angEyeAngles[0]");
|
||||||
|
const NON_LOCAL_PITCH_ANGLES: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFNonLocalPlayerExclusive", "m_angEyeAngles[0]");
|
||||||
|
|
||||||
|
const SIMTIME_PROP: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BaseEntity", "m_flSimulationTime");
|
||||||
|
const PROP_BB_MAX: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_CollisionProperty", "m_vecMaxsPreScaled");
|
||||||
|
const PLAYER_COND: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCond");
|
||||||
|
const PLAYER_COND_EX1: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx");
|
||||||
|
const PLAYER_COND_EX2: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx2");
|
||||||
|
const PLAYER_COND_EX3: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx3");
|
||||||
|
const PLAYER_COND_EX4: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nPlayerCondEx4");
|
||||||
|
const PLAYER_COND_BITS: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerConditionListExclusive", "_condition_bits");
|
||||||
|
|
||||||
|
const WEAPON_0: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "000");
|
||||||
|
const WEAPON_1: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "001");
|
||||||
|
const WEAPON_2: SendPropIdentifier = SendPropIdentifier::new("m_hMyWeapons", "002");
|
||||||
|
|
||||||
|
const DISGUISE_TEAM: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nDisguiseTeam");
|
||||||
|
const DISGUISE_CLASS: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_nDisguiseClass");
|
||||||
|
const CLOAK_LEVEL: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFPlayerShared", "m_flCloakMeter");
|
||||||
|
|
||||||
|
player.in_pvs = entity.in_pvs;
|
||||||
|
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
match prop.identifier {
|
||||||
|
OUTER => {
|
||||||
|
player.handle = Handle::try_from(&prop.value).unwrap_or_default();
|
||||||
|
}
|
||||||
|
HEALTH_PROP => {
|
||||||
|
player.health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
MAX_HEALTH_PROP => {
|
||||||
|
player.max_health = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
LIFE_STATE_PROP => {
|
||||||
|
player.state = PlayerState::new(i64::try_from(&prop.value).unwrap_or_default())
|
||||||
|
}
|
||||||
|
LOCAL_ORIGIN | NON_LOCAL_ORIGIN => {
|
||||||
|
let pos_xy = VectorXY::try_from(&prop.value).unwrap_or_default();
|
||||||
|
player.position.x = pos_xy.x;
|
||||||
|
player.position.y = pos_xy.y;
|
||||||
|
}
|
||||||
|
LOCAL_ORIGIN_Z | NON_LOCAL_ORIGIN_Z => {
|
||||||
|
player.position.z = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
LOCAL_EYE_ANGLES | NON_LOCAL_EYE_ANGLES => {
|
||||||
|
player.view_angle = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
LOCAL_PITCH_ANGLES | NON_LOCAL_PITCH_ANGLES => {
|
||||||
|
player.pitch_angle = f32::try_from(&prop.value).unwrap_or_default()
|
||||||
|
}
|
||||||
|
SIMTIME_PROP => {
|
||||||
|
player.simulation_time = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
PROP_BB_MAX => {
|
||||||
|
let max = Vector::try_from(&prop.value).unwrap_or_default();
|
||||||
|
player.bounds.max = max;
|
||||||
|
}
|
||||||
|
WEAPON_0 => {
|
||||||
|
let handle = Handle::try_from(&prop.value).unwrap_or_default();
|
||||||
|
player.weapons[0] = handle;
|
||||||
|
}
|
||||||
|
WEAPON_1 => {
|
||||||
|
let handle = Handle::try_from(&prop.value).unwrap_or_default();
|
||||||
|
player.weapons[1] = handle;
|
||||||
|
}
|
||||||
|
WEAPON_2 => {
|
||||||
|
let handle = Handle::try_from(&prop.value).unwrap_or_default();
|
||||||
|
player.weapons[2] = handle;
|
||||||
|
}
|
||||||
|
PLAYER_COND | PLAYER_COND_BITS => {
|
||||||
|
player.conditions[0..4].copy_from_slice(
|
||||||
|
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PLAYER_COND_EX1 => {
|
||||||
|
player.conditions[4..8].copy_from_slice(
|
||||||
|
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PLAYER_COND_EX2 => {
|
||||||
|
player.conditions[8..12].copy_from_slice(
|
||||||
|
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PLAYER_COND_EX3 => {
|
||||||
|
player.conditions[12..16].copy_from_slice(
|
||||||
|
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PLAYER_COND_EX4 => {
|
||||||
|
player.conditions[16..20].copy_from_slice(
|
||||||
|
&i64::try_from(&prop.value).unwrap_or_default().to_le_bytes()[0..4],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DISGUISE_TEAM => {
|
||||||
|
if let PlayerClassData::Spy { disguise_team, .. } = &mut player.class_data {
|
||||||
|
*disguise_team = Team::new(i64::try_from(&prop.value).unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DISGUISE_CLASS => {
|
||||||
|
if let PlayerClassData::Spy { disguise_class, .. } = &mut player.class_data {
|
||||||
|
*disguise_class =
|
||||||
|
Class::new(i64::try_from(&prop.value).unwrap_or_default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CLOAK_LEVEL => {
|
||||||
|
if let PlayerClassData::Spy { cloak, .. } = &mut player.class_data {
|
||||||
|
*cloak = f32::try_from(&prop.value).unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_player_resource(state: &mut GameState, entity: &PacketEntity, parser_state: &ParserState) {
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
if let Some((table_name, prop_name)) = prop.identifier.names() {
|
||||||
|
if let Ok(player_id) = u32::from_str(prop_name.as_str()) {
|
||||||
|
let entity_id = EntityId::from(player_id);
|
||||||
|
if let Some(player) = state
|
||||||
|
.players
|
||||||
|
.iter_mut()
|
||||||
|
.find(|player| player.entity == entity_id)
|
||||||
|
{
|
||||||
|
match table_name.as_str() {
|
||||||
|
"m_iTeam" => {
|
||||||
|
player.team =
|
||||||
|
Team::new(i64::try_from(&prop.value).unwrap_or_default())
|
||||||
|
}
|
||||||
|
"m_iMaxHealth" => {
|
||||||
|
player.max_health =
|
||||||
|
i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
"m_iPlayerClass" => {
|
||||||
|
let class =
|
||||||
|
Class::new(i64::try_from(&prop.value).unwrap_or_default());
|
||||||
|
if player.class != class {
|
||||||
|
player.class = class;
|
||||||
|
player.class_data = PlayerClassData::default_for_class(class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"m_iChargeLevel" => {
|
||||||
|
if let PlayerClassData::Medic { charge, .. } =
|
||||||
|
&mut player.class_data
|
||||||
|
{
|
||||||
|
*charge = i64::try_from(&prop.value).unwrap_or_default() as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"m_iPing" => {
|
||||||
|
player.ping = i64::try_from(&prop.value).unwrap_or_default() as u16
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/demo/parser/gamestateanalyser/projectile.rs
Normal file
90
src/demo/parser/gamestateanalyser/projectile.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
use crate::demo::data::game_state::{GameState, Handle, PipeType, Projectile, ProjectileType};
|
||||||
|
use crate::demo::message::{PacketEntity, UpdateType};
|
||||||
|
use crate::demo::packet::datatable::ServerClassName;
|
||||||
|
use crate::demo::parser::analyser::Team;
|
||||||
|
use crate::demo::sendprop::SendPropIdentifier;
|
||||||
|
use crate::demo::vector::Vector;
|
||||||
|
use crate::ParserState;
|
||||||
|
|
||||||
|
pub fn handle_projectile_entity(state: &mut GameState, entity: &PacketEntity, parser_state: &ParserState, class_names: &[ServerClassName]) {
|
||||||
|
let Some(class_name) = class_names.get(usize::from(entity.server_class)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROCKET_ORIGIN: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFBaseRocket", "m_vecOrigin"); // rockets, arrows, more?
|
||||||
|
const GRENADE_ORIGIN: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_vecOrigin");
|
||||||
|
// todo: flares?
|
||||||
|
const TEAM: SendPropIdentifier = SendPropIdentifier::new("DT_BaseEntity", "m_iTeamNum");
|
||||||
|
const INITIAL_SPEED: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFBaseRocket", "m_vInitialVelocity");
|
||||||
|
const LAUNCHER: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_BaseProjectile", "m_hOriginalLauncher");
|
||||||
|
const PIPE_TYPE: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFProjectile_Pipebomb", "m_iType");
|
||||||
|
const ROCKET_ROTATION: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFBaseRocket", "m_angRotation");
|
||||||
|
const GRENADE_ROTATION: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_angRotation");
|
||||||
|
const CRITICAL_GRENADE: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFWeaponBaseGrenadeProj", "m_bCritical");
|
||||||
|
const CRITICAL_ROCKET: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFProjectile_Rocket", "m_bCritical");
|
||||||
|
const CRITICAL_FLARE: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFProjectile_Flare", "m_bCritical");
|
||||||
|
const CRITICAL_ARROW: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_TFProjectile_Arrow", "m_bCritical");
|
||||||
|
|
||||||
|
if entity.update_type == UpdateType::Delete {
|
||||||
|
state.projectile_destroy(entity.entity_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let projectile = state
|
||||||
|
.projectiles
|
||||||
|
.entry(entity.entity_index)
|
||||||
|
.or_insert_with(|| {
|
||||||
|
Projectile::new(entity.entity_index, entity.server_class, class_name)
|
||||||
|
});
|
||||||
|
|
||||||
|
// todo: bounds for grenades
|
||||||
|
|
||||||
|
for prop in entity.props(parser_state) {
|
||||||
|
match prop.identifier {
|
||||||
|
ROCKET_ORIGIN | GRENADE_ORIGIN => {
|
||||||
|
let pos = Vector::try_from(&prop.value).unwrap_or_default();
|
||||||
|
projectile.position = pos
|
||||||
|
}
|
||||||
|
TEAM => {
|
||||||
|
let team = Team::new(i64::try_from(&prop.value).unwrap_or_default());
|
||||||
|
projectile.team = team;
|
||||||
|
}
|
||||||
|
INITIAL_SPEED => {
|
||||||
|
let speed = Vector::try_from(&prop.value).unwrap_or_default();
|
||||||
|
projectile.initial_speed = speed;
|
||||||
|
}
|
||||||
|
LAUNCHER => {
|
||||||
|
let launcher = Handle(i64::try_from(&prop.value).unwrap_or_default());
|
||||||
|
projectile.launcher = launcher;
|
||||||
|
}
|
||||||
|
PIPE_TYPE => {
|
||||||
|
let pipe_type = PipeType::new(i64::try_from(&prop.value).unwrap_or_default());
|
||||||
|
if let Some(class_name) = class_names.get(usize::from(entity.server_class))
|
||||||
|
{
|
||||||
|
let ty = ProjectileType::new(class_name, Some(pipe_type));
|
||||||
|
projectile.ty = ty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ROCKET_ROTATION | GRENADE_ROTATION => {
|
||||||
|
let rotation = Vector::try_from(&prop.value).unwrap_or_default();
|
||||||
|
projectile.rotation = rotation;
|
||||||
|
}
|
||||||
|
CRITICAL_GRENADE | CRITICAL_ROCKET | CRITICAL_FLARE | CRITICAL_ARROW => {
|
||||||
|
let critical = bool::try_from(&prop.value).unwrap_or_default();
|
||||||
|
projectile.critical = critical;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/demo/parser/gamestateanalyser/weapon.rs
Normal file
53
src/demo/parser/gamestateanalyser/weapon.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::demo::data::attributes::{has_attribute, Attribute};
|
||||||
|
use crate::demo::data::game_state::{GameState, Handle, MedigunType, PlayerClassData};
|
||||||
|
use crate::demo::message::{EntityId, PacketEntity, UpdateType};
|
||||||
|
use crate::demo::sendprop::SendPropIdentifier;
|
||||||
|
|
||||||
|
pub fn handle_medigun_entity(state: &mut GameState, entity: &PacketEntity, outer_map_rev: &HashMap<EntityId, Handle>) {
|
||||||
|
const OUTER: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_AttributeContainer", "m_hOuter");
|
||||||
|
const TARGET: SendPropIdentifier =
|
||||||
|
SendPropIdentifier::new("DT_WeaponMedigun", "m_hHealingTarget");
|
||||||
|
|
||||||
|
if entity.update_type == UpdateType::Enter {
|
||||||
|
let mut ty = MedigunType::Uber;
|
||||||
|
if has_attribute(&entity.props, Attribute::MedigunChargeIsCritBoost) {
|
||||||
|
ty = MedigunType::Kritzkrieg;
|
||||||
|
}
|
||||||
|
if has_attribute(&entity.props, Attribute::MedigunChargeIsMegaHeal) {
|
||||||
|
ty = MedigunType::Quickfix;
|
||||||
|
}
|
||||||
|
if has_attribute(&entity.props, Attribute::MedigunChargeIsResists) {
|
||||||
|
ty = MedigunType::Vaccinator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(handle) = entity.get_own_prop_value_by_identifier(OUTER) {
|
||||||
|
if let Some(player) = state
|
||||||
|
.players
|
||||||
|
.iter_mut()
|
||||||
|
.find(|player| player.weapons.contains(&handle))
|
||||||
|
{
|
||||||
|
if let PlayerClassData::Medic { medigun, .. } = &mut player.class_data {
|
||||||
|
*medigun = ty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(target_handle) = entity.get_own_prop_value_by_identifier::<Handle>(TARGET) {
|
||||||
|
let target_id = state
|
||||||
|
.get_player_by_handle(target_handle)
|
||||||
|
.map(|target| target.entity);
|
||||||
|
let medic = outer_map_rev
|
||||||
|
.get(&entity.entity_index)
|
||||||
|
.copied()
|
||||||
|
.and_then(|self_handle| state.get_player_by_weapon_handle(self_handle));
|
||||||
|
|
||||||
|
if let Some(medic) = medic {
|
||||||
|
if let PlayerClassData::Medic { target, .. } = &mut medic.class_data {
|
||||||
|
*target = target_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue