tf-demo-parser 0.6.4

parser for tf2 demo files
Documentation
use crate::demo::packet::datatable::{ParseSendTable, SendTableName, ServerClass};
use crate::demo::parser::MessageHandler;
use crate::demo::sendprop::{SendPropIdentifier, SendPropName};
use crate::{Demo, DemoParser, MessageType, ParserState};
use fnv::FnvHashMap;
use proc_macro2::TokenStream;
use quote::quote;
use std::collections::HashMap;

struct PropInfo {
    identifier: SendPropIdentifier,
    table_name: String,
    prop_name: String,
}

#[derive(Default)]
struct PropAnalyzer {
    prop_names: FnvHashMap<SendPropIdentifier, (SendTableName, SendPropName)>,
}

impl MessageHandler for PropAnalyzer {
    type Output = Vec<PropInfo>;

    fn does_handle(message_type: MessageType) -> bool {
        matches!(message_type, MessageType::PacketEntities)
    }

    fn handle_data_tables(
        &mut self,
        parse_tables: &[ParseSendTable],
        _server_classes: &[ServerClass],
        _state: &ParserState,
    ) {
        let mut numeric_tables: FnvHashMap<String, usize> = HashMap::default();
        for table in parse_tables {
            if table.props.iter().any(|prop| {
                prop.name == "lengthproxy" || prop.name.as_str().starts_with("lengthprop")
            }) {
                continue;
            }
            for prop_def in &table.props {
                self.prop_names.insert(
                    prop_def.identifier(),
                    (table.name.clone(), prop_def.name.clone()),
                );
                let name = prop_def.name.as_str();
                if name.len() == 3 && table.name.as_str().len() > 3 {
                    #[allow(clippy::collapsible_if)]
                    if name.parse::<u8>().is_ok() {
                        let size = match table.name.as_str() {
                            "m_nNextMapVoteOptions" => 3,
                            "m_nStreaks"
                            | "m_nNumNodeHillData"
                            | "m_nModelIndexOverrides"
                            | "m_nMinigameTeamScore"
                            | "m_iTeamBaseIcons"
                            | "m_iTeam"
                            | "m_iNumTeamMembers"
                            | "m_hProps"
                            | "m_flNextRespawnWave"
                            | "m_flEncodedController"
                            | "m_eWinningMethod"
                            | "m_chPoseIndex"
                            | "m_bTrackAlarm"
                            | "m_bTeamReady"
                            | "m_bTeamCanCap"
                            | "m_TeamRespawnWaveTimes" => 4,
                            "m_nVoteOptionCount" => 5,
                            "m_iWarnOnCap"
                            | "m_iTeamInZone"
                            | "m_iOwner"
                            | "m_iControlPointParents"
                            | "m_iCappingTeam"
                            | "m_iCPGroup"
                            | "m_hControlPointEnts"
                            | "m_flUnlockTimes"
                            | "m_flPathDistance"
                            | "m_flLazyCapPerc"
                            | "m_flCPTimerTimes"
                            | "m_bInMiniRound"
                            | "m_bCPLocked"
                            | "m_bCPIsVisible"
                            | "m_bCPCapRateScalesWithPlayers"
                            | "m_bBlocked" => 8,
                            "m_nAttachIndex" | "m_hAttachEntity" => 10,
                            "m_nMannVsMachineWaveClassFlags"
                            | "m_nMannVsMachineWaveClassFlags2"
                            | "m_nMannVsMachineWaveClassCounts2"
                            | "m_nMannVsMachineWaveClassCounts"
                            | "m_bMannVsMachineWaveClassActive2"
                            | "m_bMannVsMachineWaveClassActive" => 12,
                            "m_chCurrentSlideLists" => 16,
                            "m_bHillIsDownhill" => 20,
                            "m_chAreaPortalBits" | "m_chAreaBits" => 24,
                            "m_hMyWeapons" => 48,
                            "m_iTeamReqCappers" | "m_iTeamOverlays" | "m_iTeamIcons" => 8 * 8,
                            "m_flexWeight" | "m_flPoseParameter" => 96,
                            _ => 65,
                        };
                        numeric_tables.insert(table.name.to_string(), size);
                    }
                }
            }
        }
        for (table, size) in numeric_tables {
            for num in 0..=size {
                let prop_name = SendPropName::from(format!("{:03}", num));
                self.prop_names.insert(
                    SendPropIdentifier::new(&table, prop_name.as_str()),
                    (table.clone().into(), prop_name),
                );
            }
        }
    }

    fn into_output(self, _state: &ParserState) -> Self::Output {
        let mut props: Vec<_> = self
            .prop_names
            .into_iter()
            .map(|(identifier, (table_name, prop_name))| PropInfo {
                identifier,
                table_name: table_name.to_string(),
                prop_name: prop_name.to_string(),
            })
            .collect();
        props.sort_by(|a, b| {
            a.table_name
                .cmp(&b.table_name)
                .then(a.prop_name.cmp(&b.prop_name))
        });
        props
    }
}

pub fn generate_prop_names(demo: Demo) -> TokenStream {
    #[allow(clippy::unwrap_used)]
    let (_, props) = DemoParser::new_with_analyser(demo.get_stream(), PropAnalyzer::default())
        .parse()
        .unwrap();

    let imports = quote!(
        use crate::demo::sendprop::SendPropIdentifier;
    );

    let matches = props.into_iter().map(|prop| {
        let identifier: u64 = prop.identifier.into();
        let table_name = prop.table_name;
        let prop_name = prop.prop_name;
        quote! {
            #identifier => Some((#table_name, #prop_name))
        }
    });

    quote!(
        #imports

        pub fn get_prop_names(identifier: SendPropIdentifier) -> Option<(&'static str, &'static str)> {
            let identifier: u64 = identifier.into();
            match identifier {
                #(#matches,)*
                _ => None,
            }
        }
    )
}