Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
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.
 
 
 
 
 
 

624 lines
18 KiB

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: game stat gathering
//
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "tf_stats.h"
#include "tf_shareddefs.h"
#include "tf_team.h"
#include "tf_player.h"
#include "utlbuffer.h"
#include "filesystem.h"
#include "igamesystem.h"
#include "textstatsmgr.h"
#include "info_act.h"
static ConVar tf_stats( "tf_stats", "0", 0, "Enable stat gathering for TF2." );
//-----------------------------------------------------------------------------
// Collect stats every N seconds
//-----------------------------------------------------------------------------
#define TF_STATS_COLLECTION_TIME 1
#define TF_STAT_FILE "tf_stat_total"
#define TF_TEAM_STAT_FILE "tf_stat_team"
#define TF_PLAYER_STAT_FILE "tf_stat_class"
static char s_pStatFile[MAX_PATH];
static char s_pTeamStatFile[MAX_PATH];
static char s_pPlayerStatFile[MAX_PATH];
//-----------------------------------------------------------------------------
// Strings assocaited with the stats
//-----------------------------------------------------------------------------
static const char *s_pStatStrings[TF_STAT_COUNT] =
{
"Ferry Control", // TF_STAT_FERRY_CONTROL
"Resource Chunks Spawned", // TF_STAT_RESOURCE_CHUNKS_SPAWNED,
"Resource Gems Spawned", // TF_STAT_RESOURCE_PROCESSED_CHUNKS_SPAWNED,
"Resource Chunks Retired", // TF_STAT_RESOURCE_CHUNKS_RETIRED,
};
// These are the strings for the team stats above TFCLASS_CLASS_COUNT.
static const char *s_pNonClassTeamStatStrings[TF_TEAM_STAT_COUNT-TFCLASS_CLASS_COUNT] =
{
"Player Count", // TF_TEAM_STAT_PLAYER_COUNT,
"Resources Collected", // TF_TEAM_STAT_RESOURCES_COLLECTED,
"Resources Harvested", // TF_TEAM_STAT_RESOURCES_HARVESTED,
"Chunks Dropped", // TF_TEAM_STAT_RESOURCE_CHUNKS_DROPPED,
"Chunks Collected", // TF_TEAM_STAT_RESOURCE_CHUNKS_COLLECTED,
"Kill Count", // TF_TEAM_STAT_KILL_COUNT,
"Destroyed Objects", // TF_TEAM_STAT_DESTROYED_OBJECT_COUNT,
"Ferry Control Time", // TF_TEAM_STAT_FERRY_CONTROL_TIME,
};
// These are initialized in the first call to GetTeamStatString().
static const char *s_pTeamStatStrings[TF_TEAM_STAT_COUNT];
static bool s_bTeamStatStringsInitted = false;
static const char *s_pPlayerStatStrings[TF_PLAYER_STAT_COUNT] =
{
"Player Count", // TF_PLAYER_STAT_PLAYER_COUNT
"Player Seconds", // TF_PLAYER_STAT_PLAYER_SECONDS
"Seconds At Least One Existed", // TF_PLAYER_STAT_EXISTING_SECONDS
"Resources Acquired", // TF_PLAYER_STAT_RESOURCES_ACQUIRED
"Resources Acquired From Chunks", // TF_PLAYER_STAT_RESOURCES_ACQUIRED_FROM_CHUNKS
"Resources Carried", // TF_PLAYER_STAT_RESOURCES_CARRIED,
"Resources Spent", // TF_PLAYER_STAT_RESOURCES_SPENT,
"Object Value", // TF_PLAYER_STAT_CURRENT_OBJECT_VALUE
"Objects Owned", // TF_PLAYER_STAT_OBJECT_COUNT,
"Kill Count", // TF_PLAYER_STAT_KILL_COUNT,
"Objects Destroyed", // TF_PLAYER_STAT_DESTROYED_OBJECT_COUNT,
"Health Given", // TF_PLAYER_STAT_HEALTH_GIVEN,
"Animation Idle Time", // TF_PLAYER_STAT_ANIMATION_IDLE,
"Animation Walk Time", // TF_PLAYER_STAT_ANIMATION_WALKING,
"Animation Run Time", // TF_PLAYER_STAT_ANIMATION_RUNNING,
"Animation Crouch Time",// TF_PLAYER_STAT_ANIMATION_CROUCHING,
"Animation Jump Time", // TF_PLAYER_STAT_ANIMATION_JUMPING,
"Animation Other Time", // TF_PLAYER_STAT_ANIMATION_OTHER,
};
static const char *GetStatString( int stat )
{
if (stat < TF_STAT_FIRST_OBJECT_BUILT)
return s_pStatStrings[stat];
static char s_TempBuf[256];
Q_snprintf( s_TempBuf, sizeof( s_TempBuf ), "%s Count Built", GetObjectInfo( stat - TF_STAT_FIRST_OBJECT_BUILT )->m_pClassName );
return s_TempBuf;
}
static const char *GetTeamStatString( int stat )
{
if ( !s_bTeamStatStringsInitted )
{
s_bTeamStatStringsInitted = true;
// Go through and fill in the strings.
for ( int i=0; i < TFCLASS_CLASS_COUNT; i++ )
s_pTeamStatStrings[i] = GetTFClassInfo( i )->m_pClassName;
for ( i=TFCLASS_CLASS_COUNT; i < TF_TEAM_STAT_COUNT; i++ )
s_pTeamStatStrings[i] = s_pNonClassTeamStatStrings[i - TFCLASS_CLASS_COUNT];
}
return s_pTeamStatStrings[stat];
}
static const char *GetPlayerStatString( int stat )
{
return s_pPlayerStatStrings[stat];
}
//-----------------------------------------------------------------------------
// Implementation of the TF stats class
//-----------------------------------------------------------------------------
class CTFStats : public CAutoGameSystem, public ITFStats
{
public:
CTFStats();
// Inherited from IAutoServerSystem
virtual void LevelInitPreEntity();
virtual void FrameUpdatePostEntityThink( );
// Clear out the stats + their history
void ResetStats();
void IncrementStat( TFStatId_t stat, int nIncrement );
void SetStat( TFStatId_t stat, int nAmount );
void IncrementTeamStat( int nTeam, TFTeamStatId_t stat, int nIncrement );
void SetTeamStat( int nTeam, TFTeamStatId_t stat, int nAmount );
void IncrementPlayerStat( CBaseEntity *pPlayer, TFPlayerStatId_t stat, int nIncrement );
void ClearPlayerStat( int nTeam, TFPlayerStatId_t stat );
// We need to be ticked once a frame
void FrameUpdate( );
private:
struct Stat_t
{
int m_nCount;
};
typedef const char * (*StatNameFunc_t)( int stat );
// Collects frame-based stats
void CollectFrameStats( );
void CollectStats( );
int GetStat( TFStatId_t stat ) const { return m_Stats[stat].m_nCount; }
int GetTeamStat( int nTeam, TFTeamStatId_t stat ) const { return m_TeamStats[nTeam][stat].m_nCount; }
void WriteHeader( CUtlBuffer &buf, const char *pPrefix, int nCount, StatNameFunc_t func, bool bTerminate = true );
void WriteStatLine( CUtlBuffer &buf, int nCount, Stat_t *pStats, bool bTerminate = true );
void WriteAvgStatLine( CUtlBuffer &buf );
void WriteStats();
void WriteTeamStats();
void WritePlayerStats( );
void EraseFile( const char *pFileName );
void AppendToFile( const char *pFileName, CUtlBuffer &buf );
void ComputeFileNames();
void ClearStats();
// Compute class-based stats from the player stats
int ComputeClassStats( int nTeam, TFClass classType, Stat_t *pStats );
bool m_bWrittenHeader;
int m_nLastWriteTime;
Stat_t m_Stats[TF_STAT_COUNT];
Stat_t m_TeamStats[MAX_TF_TEAMS][TF_TEAM_STAT_COUNT];
Stat_t m_ClassStats[MAX_TF_TEAMS][TFCLASS_CLASS_COUNT][TF_PLAYER_STAT_COUNT];
};
//-----------------------------------------------------------------------------
// Accessor method
//-----------------------------------------------------------------------------
static CTFStats s_TFStats;
ITFStats *TFStats()
{
return &s_TFStats;
}
//-----------------------------------------------------------------------------
// Constructor, destructor
//-----------------------------------------------------------------------------
CTFStats::CTFStats()
{
ResetStats();
}
//-----------------------------------------------------------------------------
// Clear out the stats + their history
//-----------------------------------------------------------------------------
void CTFStats::ClearStats()
{
int i;
for (i = 0; i < TF_STAT_COUNT; ++i)
{
SetStat( (TFStatId_t)i, 0 );
}
for (i = 0; i < MAX_TF_TEAMS; ++i)
{
for (int j = 0; j < TF_TEAM_STAT_COUNT; ++j)
{
SetTeamStat(i, (TFTeamStatId_t)j, 0);
}
}
for (int nTeam = 0; nTeam < MAX_TF_TEAMS; ++nTeam)
{
for (i = 0; i < TFCLASS_CLASS_COUNT; ++i)
{
for (int j = 0; j < TF_PLAYER_STAT_COUNT; ++j)
{
m_ClassStats[nTeam][i][(TFPlayerStatId_t)j].m_nCount = 0;
}
}
}
}
//-----------------------------------------------------------------------------
// Clear out the stats + their history
//-----------------------------------------------------------------------------
void CTFStats::ResetStats()
{
ClearStats();
m_bWrittenHeader = false;
m_nLastWriteTime = -9999;
}
//-----------------------------------------------------------------------------
// Inherited from IAutoServerSystem
//-----------------------------------------------------------------------------
void CTFStats::LevelInitPreEntity()
{
ResetStats();
}
//-----------------------------------------------------------------------------
// Update stats...
//-----------------------------------------------------------------------------
void CTFStats::IncrementStat( TFStatId_t stat, int nIncrement )
{
m_Stats[stat].m_nCount += nIncrement;
}
void CTFStats::SetStat( TFStatId_t stat, int nAmount )
{
m_Stats[stat].m_nCount = nAmount;
}
void CTFStats::IncrementTeamStat( int nTeam, TFTeamStatId_t stat, int nIncrement )
{
m_TeamStats[nTeam][stat].m_nCount += nIncrement;
}
void CTFStats::SetTeamStat( int nTeam, TFTeamStatId_t stat, int nAmount )
{
m_TeamStats[nTeam][stat].m_nCount = nAmount;
}
void CTFStats::IncrementPlayerStat( CBaseEntity *pEntity, TFPlayerStatId_t stat, int nIncrement )
{
if (!pEntity->IsPlayer())
return;
CBaseTFPlayer *pTFPlayer = static_cast<CBaseTFPlayer*>(pEntity);
int nTeam = pTFPlayer->GetTeamNumber();
CPlayerClass *pPlayerClass = pTFPlayer->GetPlayerClass();
int nClass = pPlayerClass ? pPlayerClass->GetTFClass() : TFCLASS_UNDECIDED;
m_ClassStats[nTeam][nClass][stat].m_nCount += nIncrement;
}
void CTFStats::ClearPlayerStat( int nTeam, TFPlayerStatId_t stat )
{
for (int i = 0; i < TFCLASS_CLASS_COUNT; ++i)
{
m_ClassStats[nTeam][i][stat].m_nCount = 0;
}
}
//-----------------------------------------------------------------------------
// We need to be ticked once a frame
//-----------------------------------------------------------------------------
void CTFStats::CollectFrameStats( )
{
// This collects a bunch of polled stats so we don't have to pollute
// a bunch of code
for (int i = 0; i < MAX_TF_TEAMS; ++i)
{
CTFTeam *pTeam = GetGlobalTFTeam(i);
for (int j = pTeam->GetNumPlayers(); --j >= 0; )
{
CBaseTFPlayer *pPlayer = static_cast<CBaseTFPlayer*>(pTeam->GetPlayer(j));
if (!pPlayer)
continue;
int nTimeMS = 1000 * gpGlobals->frametime;
switch( pPlayer->GetActivity() )
{
case ACT_IDLE:
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_ANIMATION_IDLE, nTimeMS );
break;
case ACT_WALK:
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_ANIMATION_WALKING, nTimeMS );
break;
case ACT_RUN:
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_ANIMATION_RUNNING, nTimeMS );
break;
case ACT_JUMP:
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_ANIMATION_JUMPING, nTimeMS );
break;
case ACT_CROUCHIDLE:
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_ANIMATION_CROUCHING, nTimeMS );
break;
default:
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_ANIMATION_OTHER, nTimeMS );
break;
}
}
}
}
//-----------------------------------------------------------------------------
// We need to be ticked once a frame
//-----------------------------------------------------------------------------
void CTFStats::CollectStats( )
{
// This collects a bunch of polled stats so we don't have to pollute
// a bunch of code
bool bAtLeastOnePlayer = false;
for (int i = 0; i < MAX_TF_TEAMS; ++i)
{
CTFTeam *pTeam = GetGlobalTFTeam(i);
for ( int iClass=0; iClass < TFCLASS_CLASS_COUNT; iClass++ )
{
SetTeamStat( i, (TFTeamStatId_t)iClass, pTeam->GetNumOfClass( (TFClass)iClass ) );
}
SetTeamStat( i, TF_TEAM_STAT_PLAYER_COUNT, pTeam->GetNumPlayers() );
if ( GetStat( TF_STAT_FERRY_CONTROL ) == i )
{
IncrementTeamStat( i, TF_TEAM_STAT_FERRY_CONTROL_TIME, TF_STATS_COLLECTION_TIME );
}
ClearPlayerStat( i, TF_PLAYER_STAT_OBJECT_COUNT );
ClearPlayerStat( i, TF_PLAYER_STAT_RESOURCES_CARRIED );
ClearPlayerStat( i, TF_PLAYER_STAT_CURRENT_OBJECT_VALUE );
ClearPlayerStat( i, TF_PLAYER_STAT_PLAYER_COUNT );
bool bClassEncountered[TFCLASS_CLASS_COUNT];
memset( bClassEncountered, 0, TFCLASS_CLASS_COUNT * sizeof(bool) );
for (int j = pTeam->GetNumPlayers(); --j >= 0; )
{
bAtLeastOnePlayer = true;
CBaseTFPlayer *pPlayer = static_cast<CBaseTFPlayer*>(pTeam->GetPlayer(j));
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_OBJECT_COUNT, pPlayer->GetObjectCount() );
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_RESOURCES_CARRIED, pPlayer->GetBankResources() );
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_PLAYER_COUNT, 1 );
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_PLAYER_SECONDS, 1 );
CPlayerClass *pPlayerClass = pPlayer->GetPlayerClass();
int nClass = pPlayerClass ? pPlayerClass->GetTFClass() : TFCLASS_UNDECIDED;
if (!bClassEncountered[nClass])
{
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_EXISTING_SECONDS, 1 );
bClassEncountered[nClass] = true;
}
// Count up the cost of all current objects..
int nCost = 0;
int pObjectCount[OBJ_LAST];
memset( pObjectCount, 0, OBJ_LAST * sizeof(int) );
for (int k = pPlayer->GetObjectCount(); --k >= 0; )
{
CBaseObject *pObject = pPlayer->GetObject(k);
if (pObject)
{
int nType = pObject->GetType();
nCost += CalculateObjectCost( nType, pObjectCount[nType], pPlayer->GetTeamNumber(), false );
++pObjectCount[nType];
}
}
IncrementPlayerStat( pPlayer, TF_PLAYER_STAT_CURRENT_OBJECT_VALUE, nCost );
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : char const
//-----------------------------------------------------------------------------
void CTFStats::ComputeFileNames()
{
Q_snprintf( s_pStatFile, sizeof( s_pStatFile ), "%s.txt", TF_STAT_FILE );
Q_snprintf( s_pTeamStatFile, sizeof( s_pTeamStatFile ), "%s.txt", TF_TEAM_STAT_FILE );
Q_snprintf( s_pPlayerStatFile, sizeof( s_pPlayerStatFile ), "%s.txt", TF_PLAYER_STAT_FILE );
}
//-----------------------------------------------------------------------------
// File access.
//-----------------------------------------------------------------------------
void CTFStats::EraseFile( const char *pFileName )
{
filesystem->RemoveFile( pFileName, "GAME" );
}
void CTFStats::AppendToFile( const char *pFileName, CUtlBuffer &buf )
{
FileHandle_t fh = filesystem->Open( pFileName, "a", "LOGDIR" );
if (fh != FILESYSTEM_INVALID_HANDLE)
{
filesystem->Write( buf.Base(), buf.TellPut(), fh );
filesystem->Close( fh );
}
}
//-----------------------------------------------------------------------------
// Write out a header.
//-----------------------------------------------------------------------------
void CTFStats::WriteHeader( CUtlBuffer &buf, const char *pPrefix, int nCount, StatNameFunc_t func, bool bTerminate )
{
for (int i = 0; i < nCount-1; ++i)
{
buf.Printf("%s %s\t", pPrefix, func(i) );
}
buf.Printf( bTerminate ? "%s %s\n" : "%s %s\t", pPrefix, func(nCount-1) );
}
void CTFStats::WriteStatLine( CUtlBuffer &buf, int nCount, Stat_t *pStats, bool bTerminate )
{
for (int i = 0; i < nCount-1; ++i)
{
buf.Printf("%d\t", pStats[i].m_nCount );
}
buf.Printf( bTerminate ? "%d\n" : "%d\t", pStats[nCount-1].m_nCount );
}
void CTFStats::WriteAvgStatLine( CUtlBuffer &buf )
{
int nTeam, i, j;
for ( nTeam = 0; nTeam < MAX_TF_TEAMS; ++nTeam )
{
for ( i = 0; i < TF_PLAYER_STAT_COUNT; ++i )
{
for ( j = 1; j < TFCLASS_CLASS_COUNT; ++j )
{
if (!GetTFClassInfo(j)->m_pCurrentlyActive)
continue;
buf.Printf("%d\t", m_ClassStats[nTeam][j][i].m_nCount);
}
}
}
// Blat out that last tab and replace with a \n
buf.SeekPut( CUtlBuffer::SEEK_CURRENT, -1 );
buf.Printf("\n");
}
//-----------------------------------------------------------------------------
// Write out total stats...
//-----------------------------------------------------------------------------
void CTFStats::WriteStats()
{
CUtlBuffer buf( 0, 1024, CUtlBuffer::TEXT_BUFFER );
if (!m_bWrittenHeader)
{
EraseFile( s_pStatFile );
WriteHeader( buf, "", TF_STAT_COUNT, GetStatString );
}
WriteStatLine( buf, TF_STAT_COUNT, m_Stats );
AppendToFile( s_pStatFile, buf );
}
//-----------------------------------------------------------------------------
// Write out total stats...
//-----------------------------------------------------------------------------
void CTFStats::WriteTeamStats()
{
CUtlBuffer buf( 0, 1024, CUtlBuffer::TEXT_BUFFER );
int i,j;
if (!m_bWrittenHeader)
{
EraseFile( s_pTeamStatFile );
for ( i = 0; i < TF_TEAM_STAT_COUNT; ++i )
{
for ( j = 0; j < MAX_TF_TEAMS; ++j )
{
buf.Printf("Team %d %s\t", j, GetTeamStatString(i) );
}
}
// Blat out that last tab and replace with a \n
buf.SeekPut( CUtlBuffer::SEEK_CURRENT, -1 );
buf.Printf("\n");
}
for ( i = 0; i < TF_TEAM_STAT_COUNT; ++i )
{
for ( j = 0; j < MAX_TF_TEAMS; ++j )
{
buf.Printf("%d\t", m_TeamStats[j][i].m_nCount );
}
}
// Blat out that last tab and replace with a \n
buf.SeekPut( CUtlBuffer::SEEK_CURRENT, -1 );
buf.Printf("\n");
AppendToFile( s_pTeamStatFile, buf );
}
//-----------------------------------------------------------------------------
// Write out total stats...
//-----------------------------------------------------------------------------
void CTFStats::WritePlayerStats( )
{
CUtlBuffer buf( 0, 1024, CUtlBuffer::TEXT_BUFFER );
int i, j, nTeam;
if (!m_bWrittenHeader)
{
EraseFile( s_pPlayerStatFile );
for ( nTeam = 0; nTeam < MAX_TF_TEAMS; ++nTeam )
{
for ( i = 0; i < TF_PLAYER_STAT_COUNT; ++i )
{
for ( j = 1; j < TFCLASS_CLASS_COUNT; ++j )
{
if (!GetTFClassInfo(j)->m_pCurrentlyActive)
continue;
buf.Printf("Team %d %s %s\t", nTeam, GetTFClassInfo( j )->m_pClassName, GetPlayerStatString(i) );
}
}
}
// Blat out that last tab and replace with a \n
buf.SeekPut( CUtlBuffer::SEEK_CURRENT, -1 );
buf.Printf("\n");
}
WriteAvgStatLine( buf );
AppendToFile( s_pPlayerStatFile, buf );
}
//-----------------------------------------------------------------------------
// We need to be ticked once a frame
//-----------------------------------------------------------------------------
void CTFStats::FrameUpdatePostEntityThink( )
{
if (!tf_stats.GetBool())
return;
// Don't stat gather during waiting acts
if ( CurrentActIsAWaitingAct() )
return;
CollectFrameStats();
// NOTE: We could keep track of the history here if we wanted for later
// display when the map ends
// Record the history every so often
if (gpGlobals->curtime - m_nLastWriteTime < TF_STATS_COLLECTION_TIME)
return;
if (!m_bWrittenHeader)
{
ComputeFileNames();
}
CollectStats();
WriteStats();
WriteTeamStats();
WritePlayerStats();
ClearStats();
// By this point, we've written the header for each file
m_bWrittenHeader = true;
m_nLastWriteTime = gpGlobals->curtime;
}