//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #include "cbase.h" #ifdef GAME_DLL #include "gamestats.h" #else #include "tf_hud_statpanel.h" #endif #include "tf_gamestats_shared.h" #ifndef NO_STEAM #include "steamworks_gamestats.h" #endif int TF_Gamestats_RoundStats_t::m_iNumRounds = 0; time_t TF_Gamestats_RoundStats_t::m_iRoundStartTime = 0; //----------------------------------------------------------------------------- const char *s_pStatStrings[ TFSTAT_TOTAL ] = { "TFSTAT_UNDEFINED", "TFSTAT_SHOTS_HIT", "TFSTAT_SHOTS_FIRED", "TFSTAT_KILLS", "TFSTAT_DEATHS", "TFSTAT_DAMAGE", "TFSTAT_CAPTURES", "TFSTAT_DEFENSES", "TFSTAT_DOMINATIONS", "TFSTAT_REVENGE", "TFSTAT_POINTSSCORED", "TFSTAT_BUILDINGSDESTROYED", "TFSTAT_HEADSHOTS", "TFSTAT_PLAYTIME", "TFSTAT_HEALING", "TFSTAT_INVULNS", "TFSTAT_KILLASSISTS", "TFSTAT_BACKSTABS", "TFSTAT_HEALTHLEACHED", "TFSTAT_BUILDINGSBUILT", "TFSTAT_MAXSENTRYKILLS", "TFSTAT_TELEPORTS", "TFSTAT_FIREDAMAGE", "TFSTAT_BONUS_POINTS", "TFSTAT_BLASTDAMAGE", "TFSTAT_DAMAGETAKEN", "TFSTAT_HEALTHKITS", "TFSTAT_AMMOKITS", "TFSTAT_CLASSCHANGES", "TFSTAT_CRITS", "TFSTAT_SUICIDES", "TFSTAT_CURRENCY_COLLECTED", "TFSTAT_DAMAGE_ASSIST", "TFSTAT_HEALING_ASSIST", "TFSTAT_DAMAGE_BOSS", "TFSTAT_DAMAGE_BLOCKED", "TFSTAT_DAMAGE_RANGED", "TFSTAT_DAMAGE_RANGED_CRIT_RANDOM", "TFSTAT_DAMAGE_RANGED_CRIT_BOOSTED", "TFSTAT_REVIVED", }; const char *s_pMapStatStrings[ TFMAPSTAT_TOTAL ] = { "TFSTAT_UNDEFINED", "TFSTAT_PLAYTIME", }; //----------------------------------------------------------------------------- // Purpose: Constructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_LevelStats_t::TF_Gamestats_LevelStats_t() { m_bInitialized = false; m_iRoundStartTime = 0; m_flRoundStartTime = 0; m_Header.m_iRoundsPlayed = 0; m_Header.m_iTotalTime = 0; m_Header.m_iBlueWins = 0; m_Header.m_iRedWins = 0; m_Header.m_iStalemates = 0; m_Header.m_iBlueSuddenDeathWins = 0; m_Header.m_iRedSuddenDeathWins = 0; Q_memset( m_aClassStats, 0, sizeof( m_aClassStats ) ); Q_memset( m_aWeaponStats, 0, sizeof( m_aWeaponStats ) ); Q_memset( m_iPeakPlayerCount, 0, sizeof( m_iPeakPlayerCount ) ); for ( int i = 0; i <= MAX_CONTROL_POINTS; i++ ) { m_Header.m_iLastCapChangedInRound[i] = 0; } } //----------------------------------------------------------------------------- // Purpose: Destructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_LevelStats_t::~TF_Gamestats_LevelStats_t() { //m_aPlayerDeaths.Purge(); //m_aPlayerDamage.Purge(); m_bIsRealServer = false; } //----------------------------------------------------------------------------- // Purpose: Copy constructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_LevelStats_t::TF_Gamestats_LevelStats_t( const TF_Gamestats_LevelStats_t &stats ) { m_bInitialized = stats.m_bInitialized; m_iRoundStartTime = stats.m_iRoundStartTime; m_flRoundStartTime = stats.m_flRoundStartTime; m_iMapStartTime = stats.m_iMapStartTime; m_Header = stats.m_Header; m_bIsRealServer = stats.m_bIsRealServer; //m_aPlayerDeaths = stats.m_aPlayerDeaths; //m_aPlayerDamage = stats.m_aPlayerDamage; } //----------------------------------------------------------------------------- // Purpose: // Input : *pszMapName - // nIPAddr - // nPort - // flStartTime - //----------------------------------------------------------------------------- void TF_Gamestats_LevelStats_t::Init( const char *pszMapName, int nMapRevision, int nIPAddr, short nPort, float flStartTime ) { Q_memset( &m_Header, 0, sizeof( m_Header ) ); // TODO: This is correct for steamworks stats, but probably breaks old stats!!! V_FileBase( pszMapName, m_Header.m_szMapName, sizeof( m_Header.m_szMapName ) ); m_Header.m_nMapRevision = nMapRevision; m_Header.m_nIPAddr = nIPAddr; m_Header.m_nPort = nPort; #ifndef NO_STEAM // Start the level timer. m_iMapStartTime = GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch(); m_iRoundStartTime = GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch(); m_flRoundStartTime = gpGlobals->curtime; #endif m_bIsRealServer = false; } //----------------------------------------------------------------------------- // Purpose: // Input : flEndTime - //----------------------------------------------------------------------------- void TF_Gamestats_LevelStats_t::Shutdown( float flEndTime ) { } //----------------------------------------------------------------------------- // Purpose: Constructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_RoundStats_t::TF_Gamestats_RoundStats_t() { Reset(); } //----------------------------------------------------------------------------- // Purpose: Destructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_RoundStats_t::~TF_Gamestats_RoundStats_t() { } //----------------------------------------------------------------------------- // Purpose: resets the state of stat tracking //----------------------------------------------------------------------------- void TF_Gamestats_RoundStats_t::Reset() { ResetSummary(); m_iRoundStartTime = 0.f; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void TF_Gamestats_RoundStats_t::ResetSummary() { Q_memset( &m_Summary, 0, sizeof( m_Summary ) ); } //----------------------------------------------------------------------------- // Purpose: Constructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_KillStats_t::TF_Gamestats_KillStats_t() { Reset(); } //----------------------------------------------------------------------------- // Purpose: Destructor // Input : - //----------------------------------------------------------------------------- TF_Gamestats_KillStats_t::~TF_Gamestats_KillStats_t() { } //----------------------------------------------------------------------------- // Purpose: resets the state of stat tracking //----------------------------------------------------------------------------- void TF_Gamestats_KillStats_t::Reset() { // Q_memset( &m_Summary, 0, sizeof( m_Summary ) ); // m_flRoundStartTime = 0.f; } //----------------------------------------------------------------------------- // Purpose: constructor //----------------------------------------------------------------------------- TFReportedStats_t::TFReportedStats_t() { Clear(); m_bValidData = false; m_pCurrentGame = NULL; } //----------------------------------------------------------------------------- // Purpose: destructor //----------------------------------------------------------------------------- TFReportedStats_t::~TFReportedStats_t() { if ( m_pCurrentGame ) { delete m_pCurrentGame; m_pCurrentGame = NULL; } } //----------------------------------------------------------------------------- // Purpose: Clears data //----------------------------------------------------------------------------- void TFReportedStats_t::Clear() { m_pCurrentGame = NULL; m_dictMapStats.Purge(); } //----------------------------------------------------------------------------- // Purpose: // Input : *szMapName - // Output : TF_Gamestats_LevelStats_t //----------------------------------------------------------------------------- TF_Gamestats_LevelStats_t *TFReportedStats_t::FindOrAddMapStats( const char *szMapName ) { int iMap = m_dictMapStats.Find( szMapName ); if( iMap == m_dictMapStats.InvalidIndex() ) { iMap = m_dictMapStats.Insert( szMapName ); } return &m_dictMapStats[iMap]; } #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: Saves data to buffer //----------------------------------------------------------------------------- void TFReportedStats_t::AppendCustomDataToSaveBuffer( CUtlBuffer &SaveBuffer ) { // save a version lump at beginning of file TF_Gamestats_Version_t versionLump; versionLump.m_iMagic = TF_GAMESTATS_MAGIC; versionLump.m_iVersion = TF_GAMESTATS_FILE_VERSION; CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_VERSION, 1, sizeof( versionLump ), &versionLump ); // Save data per map. for ( int iMap = m_dictMapStats.First(); iMap != m_dictMapStats.InvalidIndex(); iMap = m_dictMapStats.Next( iMap ) ) { // Get the current map. TF_Gamestats_LevelStats_t *pCurrentMap = &m_dictMapStats[iMap]; Assert( pCurrentMap ); // Write out the lumps. CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_MAPHEADER, 1, sizeof( TF_Gamestats_LevelStats_t::LevelHeader_t ), static_cast( &pCurrentMap->m_Header ) ); //CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_MAPDEATH, pCurrentMap->m_aPlayerDeaths.Count(), sizeof( TF_Gamestats_LevelStats_t::PlayerDeathsLump_t ), static_cast( pCurrentMap->m_aPlayerDeaths.Base() ) ); //CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_MAPDAMAGE, pCurrentMap->m_aPlayerDamage.Count(), sizeof( TF_Gamestats_LevelStats_t::PlayerDamageLump_t ), static_cast( pCurrentMap->m_aPlayerDamage.Base() ) ); CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_CLASS, ARRAYSIZE( pCurrentMap->m_aClassStats ), sizeof( pCurrentMap->m_aClassStats[0] ), static_cast( pCurrentMap->m_aClassStats ) ); CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_WEAPON, ARRAYSIZE( pCurrentMap->m_aWeaponStats ), sizeof( pCurrentMap->m_aWeaponStats[0] ), static_cast( pCurrentMap->m_aWeaponStats ) ); } // Append an end tag to verify we've reached end of file and data was sane. (Sometimes we receive stat files that start sane but become filled // with garbage partway through.) CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_ENDTAG, 1, sizeof( versionLump ), &versionLump ); } //----------------------------------------------------------------------------- // Purpose: Loads data from buffer //----------------------------------------------------------------------------- bool TFReportedStats_t::LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer ) { // read the version lump of beginning of file and verify version bool bGotEndTag = false; unsigned short iLump = 0; unsigned short iLumpCount = 0; if ( !CBaseGameStats::GetLumpHeader( MAX_LUMP_COUNT, LoadBuffer, iLump, iLumpCount ) ) return false; if ( iLump != TFSTATS_LUMP_VERSION ) { Msg( "Didn't find version header. Expected lump type TFSTATS_LUMP_VERSION, got lump type %d. Skipping file.\n", iLump ); return false; } TF_Gamestats_Version_t versionLump; CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( versionLump ), &versionLump ); if ( versionLump.m_iMagic != TF_GAMESTATS_MAGIC ) { Msg( "Incorrect magic # in version header. Expected %x, got %x. Skipping file.\n", TF_GAMESTATS_MAGIC, versionLump.m_iMagic ); return false; } if ( versionLump.m_iVersion != TF_GAMESTATS_FILE_VERSION ) { Msg( "Mismatched file version. Expected file version %d, got %d. Skipping file.\n", TF_GAMESTATS_FILE_VERSION, versionLump.m_iVersion ); return false; } TF_Gamestats_LevelStats_t *pCurrentGame = NULL; // read all the lumps in the file while( CBaseGameStats::GetLumpHeader( MAX_LUMP_COUNT, LoadBuffer, iLump, iLumpCount ) ) { switch ( iLump ) { case TFSTATS_LUMP_MAPHEADER: { TF_Gamestats_LevelStats_t::LevelHeader_t header; CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( TF_Gamestats_LevelStats_t::LevelHeader_t ), &header ); // quick sanity check on some data -- we get some stat files that start out OK but are corrupted later in the file if ( ( header.m_iRoundsPlayed < 0 ) || ( header.m_iTotalTime < 0 ) || ( header.m_iRoundsPlayed > 1000 ) ) return false; // if there's no interesting data, skip this file. (Need to have server not send it in this case.) if ( header.m_iTotalTime == 0 ) return false; pCurrentGame = FindOrAddMapStats( header.m_szMapName ); if ( pCurrentGame ) { pCurrentGame->m_Header = header; } break; } case TFSTATS_LUMP_MAPDEATH: { //CUtlVector playerDeaths; //playerDeaths.SetCount( iLumpCount ); //CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( TF_Gamestats_LevelStats_t::PlayerDeathsLump_t ), static_cast( playerDeaths.Base() ) ); //if ( pCurrentGame ) //{ // pCurrentGame->m_aPlayerDeaths = playerDeaths; //} break; } case TFSTATS_LUMP_MAPDAMAGE: { //CUtlVector playerDamage; //playerDamage.SetCount( iLumpCount ); //CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( TF_Gamestats_LevelStats_t::PlayerDamageLump_t ), static_cast( playerDamage.Base() ) ); //if ( pCurrentGame ) //{ // pCurrentGame->m_aPlayerDamage = playerDamage; //} break; } case TFSTATS_LUMP_CLASS: { Assert( pCurrentGame ); if ( !pCurrentGame ) return false; Assert ( iLumpCount == ARRAYSIZE( pCurrentGame->m_aClassStats ) ); if ( iLumpCount == ARRAYSIZE( pCurrentGame->m_aClassStats ) ) { CBaseGameStats::LoadLump( LoadBuffer, ARRAYSIZE( pCurrentGame->m_aClassStats ), sizeof( pCurrentGame->m_aClassStats[0] ), pCurrentGame->m_aClassStats ); // quick sanity check on some data -- we get some stat files that start out OK but are corrupted later in the file for ( int i = 0; i < ARRAYSIZE( pCurrentGame->m_aClassStats ); i++ ) { TF_Gamestats_ClassStats_t &classStats = pCurrentGame->m_aClassStats[i]; if ( ( classStats.iSpawns < 0 ) || ( classStats.iSpawns > 10000 ) || ( classStats.iTotalTime < 0 ) || ( classStats.iTotalTime > 36000 * 20 ) || ( classStats.iKills < 0 ) || ( classStats.iKills > 10000 ) ) { return false; } } } else { // mismatched lump size, possibly from different build, don't know how it interpret it, just skip over it return false; } break; } case TFSTATS_LUMP_WEAPON: { Assert( pCurrentGame ); if ( !pCurrentGame ) return false; Assert ( iLumpCount == ARRAYSIZE( pCurrentGame->m_aWeaponStats ) ); if ( iLumpCount == ARRAYSIZE( pCurrentGame->m_aWeaponStats ) ) { CBaseGameStats::LoadLump( LoadBuffer, ARRAYSIZE( pCurrentGame->m_aWeaponStats ), sizeof( pCurrentGame->m_aWeaponStats[0] ), pCurrentGame->m_aWeaponStats ); // quick sanity check on some data -- we get some stat files that start out OK but are corrupted later in the file if ( ( pCurrentGame->m_aWeaponStats[TF_WEAPON_MEDIGUN].iShotsFired < 0 ) || ( pCurrentGame->m_aWeaponStats[TF_WEAPON_MEDIGUN].iShotsFired > 100000 ) || ( pCurrentGame->m_aWeaponStats[TF_WEAPON_FLAMETHROWER_ROCKET].iShotsFired != 0 ) ) // check that unused weapon has 0 shots { return false; } } else { // mismatched lump size, possibly from different build, don't know how it interpret it, just skip over it return false; } break; } case TFSTATS_LUMP_ENDTAG: { // check that end tag is valid -- should be version lump again TF_Gamestats_Version_t versionLump; CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( versionLump ), &versionLump ); if ( versionLump.m_iMagic != TF_GAMESTATS_MAGIC ) { Msg( "Incorrect magic # in version header. Expected %x, got %x. Skipping file.\n", TF_GAMESTATS_MAGIC, versionLump.m_iMagic ); return false; } if ( versionLump.m_iVersion != TF_GAMESTATS_FILE_VERSION ) { Msg( "Mismatched file version. Expected file version %d, got %d. Skipping file.\n", TF_GAMESTATS_FILE_VERSION, versionLump.m_iVersion ); return false; } bGotEndTag = true; break; } } } return bGotEndTag; } #endif //----------------------------------------------------------------------------- // TF2 Beta Maps // Robot Destruction //----------------------------------------------------------------------------- RobotDestructionStats_t::RobotDestructionStats_t() { Clear(); } //----------------------------------------------------------------------------- void RobotDestructionStats_t::Clear() { V_memset( &iRobotInteraction, 0, sizeof( iRobotInteraction ) ); V_memset( &iRobotCoreInteraction, 0, sizeof( iRobotCoreInteraction ) ); V_memset( &iFlagInteraction, 0, sizeof( iFlagInteraction ) ); V_memset( &iCoresCollectedByTeam, 0, sizeof( iCoresCollectedByTeam ) ); V_memset( &iCoreCollectedByClass, 0, sizeof( iCoreCollectedByClass ) ); V_memset( &iBlueRobotsKilledByType, 0, sizeof( iBlueRobotsKilledByType ) ); V_memset( &iRedRobotsKilledByType, 0, sizeof( iRedRobotsKilledByType ) ); V_memset( &iRobotsDamageFromClass, 0, sizeof( iRobotsDamageFromClass ) ); } //----------------------------------------------------------------------------- int RobotDestructionStats_t::GetRobotInteractionCount() { int iCount = 0; for ( int i = 1; i < MAX_PLAYERS; ++i ) { if ( iRobotInteraction[i] ) { iCount++; } } return iCount; } //----------------------------------------------------------------------------- int RobotDestructionStats_t::GetRobotCoreInteractionCount() { int iCount = 0; for ( int i = 1; i < MAX_PLAYERS; ++i ) { if ( iRobotCoreInteraction[i] ) { iCount++; } } return iCount; } //----------------------------------------------------------------------------- int RobotDestructionStats_t::GetFlagInteractionCount() { int iCount = 0; for ( int i = 1; i < MAX_PLAYERS; ++i ) { if ( iFlagInteraction[i] ) { iCount++; } } return iCount; } //----------------------------------------------------------------------------- const char* g_aRoundEndReasons[] = { "round_end", "client_disconnect", "client_quit", "server_map_change", "server_shutdown", "time_limit_reached", "win_limit_reached", "win_diff_limit_reached", "round_limit_reached", "next_level_cvar", }; // Get a string describing the current game type. const char* GetGameTypeID() { ConVarRef tf_gamemode_arena( "tf_gamemode_arena" ); ConVarRef tf_gamemode_cp( "tf_gamemode_cp" ); ConVarRef tf_gamemode_ctf( "tf_gamemode_ctf" ); ConVarRef tf_gamemode_sd( "tf_gamemode_sd" ); ConVarRef tf_gamemode_payload( "tf_gamemode_payload" ); ConVarRef tf_gamemode_mvm( "tf_gamemode_mvm" ); ConVarRef tf_powerup_mode( "tf_powerup_mode" ); ConVarRef tf_gamemode_passtime( "tf_gamemode_passtime" ); const char* pszGameTypeID = NULL; if ( tf_gamemode_arena.GetBool() ) { pszGameTypeID = "arena"; } else if ( tf_gamemode_cp.GetBool() ) { pszGameTypeID = "cp"; } else if ( tf_gamemode_ctf.GetBool() ) { if ( tf_powerup_mode.GetBool() ) { pszGameTypeID = "ctf_mannpower"; } else { pszGameTypeID = "ctf"; } } else if ( tf_gamemode_sd.GetBool() ) { pszGameTypeID = "sd"; } else if ( tf_gamemode_payload.GetBool() ) { pszGameTypeID = "payload"; } else if ( tf_gamemode_mvm.GetBool() ) { pszGameTypeID = "mvm"; } else if ( tf_gamemode_passtime.GetBool() ) { pszGameTypeID = "pass"; // intentionally not "passtime" } else { pszGameTypeID = "custom"; } return pszGameTypeID; } //----------------------------------------------------------------------------- // TF2 Beta Maps // Passtime //----------------------------------------------------------------------------- void PasstimeStats_t::Clear() { memset( &summary, 0, sizeof(summary) ); memset( &classes, 0, sizeof(classes) ); } //----------------------------------------------------------------------------- void PasstimeStats_t::AddBallFracSample( float f ) { Assert( f >= 0 && f <= 1.0f ); int iBin = (uint8) Floor2Int( f * 255 ); summary.nBallFracHistSum += iBin; ++summary.arrBallFracHist[ iBin ]; ++summary.nBallFracSampleCount; } //----------------------------------------------------------------------------- void PasstimeStats_t::AddPassTravelDistSample( float f ) { if ( summary.nPassTravelDistSampleCount >= summary.k_nMaxPassTravelDistSamples ) return; Assert( f >= 0 ); summary.arrPassTravelDistSamples[ summary.nPassTravelDistSampleCount ] = (uint16) Float2Int( f ); ++summary.nPassTravelDistSampleCount; } #ifdef CLIENT_DLL MapStats_t &GetMapStats( map_identifier_t iMapID ) { return CTFStatPanel::GetMapStats( iMapID ); } #endif