Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

637 lines
20 KiB

//========= 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<void*>( &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<void*>( 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<void*>( pCurrentMap->m_aPlayerDamage.Base() ) );
CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_CLASS, ARRAYSIZE( pCurrentMap->m_aClassStats ), sizeof( pCurrentMap->m_aClassStats[0] ),
static_cast<void*>( pCurrentMap->m_aClassStats ) );
CBaseGameStats::AppendLump( MAX_LUMP_COUNT, SaveBuffer, TFSTATS_LUMP_WEAPON, ARRAYSIZE( pCurrentMap->m_aWeaponStats ), sizeof( pCurrentMap->m_aWeaponStats[0] ),
static_cast<void*>( 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<TF_Gamestats_LevelStats_t::PlayerDeathsLump_t> playerDeaths;
//playerDeaths.SetCount( iLumpCount );
//CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( TF_Gamestats_LevelStats_t::PlayerDeathsLump_t ), static_cast<void*>( playerDeaths.Base() ) );
//if ( pCurrentGame )
//{
// pCurrentGame->m_aPlayerDeaths = playerDeaths;
//}
break;
}
case TFSTATS_LUMP_MAPDAMAGE:
{
//CUtlVector<TF_Gamestats_LevelStats_t::PlayerDamageLump_t> playerDamage;
//playerDamage.SetCount( iLumpCount );
//CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( TF_Gamestats_LevelStats_t::PlayerDamageLump_t ), static_cast<void*>( 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