summaryrefslogtreecommitdiff
path: root/game/server/tf/tf_gamestats.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/tf_gamestats.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/server/tf/tf_gamestats.cpp')
-rw-r--r--game/server/tf/tf_gamestats.cpp3592
1 files changed, 3592 insertions, 0 deletions
diff --git a/game/server/tf/tf_gamestats.cpp b/game/server/tf/tf_gamestats.cpp
new file mode 100644
index 0000000..5664759
--- /dev/null
+++ b/game/server/tf/tf_gamestats.cpp
@@ -0,0 +1,3592 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+//=============================================================================//
+
+#include "cbase.h"
+#include "tf_gamerules.h"
+#include "tf_gamestats.h"
+#include "tf_obj_sentrygun.h"
+#include "tf_obj_dispenser.h"
+#include "tf_obj_sapper.h"
+#include "tf_team.h"
+#include "usermessages.h"
+#include "player_resource.h"
+#include "team.h"
+#include "achievementmgr.h"
+#include "hl2orange.spa.h"
+#include "tf_weapon_medigun.h"
+#include "NextBot/NextBotManager.h"
+#include "team_control_point_master.h"
+#include "steamworks_gamestats.h"
+#include "vote_controller.h"
+#include "tf_mann_vs_machine_stats.h"
+
+#include "tf_passtime_logic.h"
+#include "filesystem.h" // for temp passtime local stats logging
+#include "passtime_convars.h"
+
+#include "tf_matchmaking_shared.h"
+
+#include "gc_clientsystem.h"
+#include "tf_gcmessages.h"
+#include "rtime.h"
+#include "team_train_watcher.h"
+#include "tf_gc_api.h"
+
+extern ConVar tf_mm_trusted;
+
+// Must run with -gamestats to be able to turn on/off stats with ConVar below.
+static ConVar tf_stats_nogameplaycheck( "tf_stats_nogameplaycheck", "0", FCVAR_NONE , "Disable normal check for valid gameplay, send stats regardless." );
+//static ConVar tf_stats_track( "tf_stats_track", "1", FCVAR_NONE, "Turn on//off tf stats tracking." );
+//static ConVar tf_stats_verbose( "tf_stats_verbose", "0", FCVAR_NONE, "Turn on//off verbose logging of stats." );
+
+CTFGameStats CTF_GameStats;
+
+const char *g_aClassNames[] =
+{
+ "TF_CLASS_UNDEFINED",
+ "TF_CLASS_SCOUT",
+ "TF_CLASS_SNIPER",
+ "TF_CLASS_SOLDIER",
+ "TF_CLASS_DEMOMAN",
+ "TF_CLASS_MEDIC",
+ "TF_CLASS_HEAVYWEAPONS",
+ "TF_CLASS_PYRO",
+ "TF_CLASS_SPY",
+ "TF_CLASS_ENGINEER",
+ "TF_CLASS_CIVILIAN",
+};
+
+//-----------------------------------------------------------------------------
+// Purpose: Constructor
+// Input : -
+//-----------------------------------------------------------------------------
+CTFGameStats::CTFGameStats()
+{
+ gamestats = this;
+ Clear();
+
+ SetDefLessFunc( m_MapsPlaytime );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Destructor
+// Input : -
+//-----------------------------------------------------------------------------
+CTFGameStats::~CTFGameStats()
+{
+ Clear();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Clear out game stats
+// Input : -
+//-----------------------------------------------------------------------------
+void CTFGameStats::Clear( void )
+{
+ m_bRoundActive = false;
+ m_iRoundsPlayed = 0;
+ m_iEvents = 0;
+ m_iKillCount = 0;
+ m_iPlayerUpdates = 0;
+ m_reportedStats.Clear();
+ Q_memset( m_aPlayerStats, 0, sizeof( m_aPlayerStats ) );
+ m_rdStats.Clear();
+ m_passtimeStats.Clear();
+ m_iLoadoutChangesCount = 1;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Adds our data to the gamestats data that gets uploaded.
+// Returns true if we added data, false if we didn't
+//-----------------------------------------------------------------------------
+bool CTFGameStats::AddDataForSend( KeyValues *pKV, StatSendType_t sendType )
+{
+ // we only have data to send at level shutdown
+ if ( sendType != STATSEND_LEVELSHUTDOWN )
+ return false;
+
+ // do we have anything to report?
+ if ( !m_reportedStats.m_bValidData )
+ return false;
+
+ // Save data per map.
+
+ CUtlDict<TF_Gamestats_LevelStats_t, unsigned short> &dictMapStats = m_reportedStats.m_dictMapStats;
+ Assert( dictMapStats.Count() <= 1 );
+
+ for ( int iMap = dictMapStats.First(); iMap != dictMapStats.InvalidIndex(); iMap = dictMapStats.Next( iMap ) )
+ {
+ // Get the current map.
+ TF_Gamestats_LevelStats_t *pCurrentMap = &dictMapStats[iMap];
+ Assert( pCurrentMap );
+
+ KeyValues *pKVData = new KeyValues( "tf_mapdata" );
+ pKVData->SetInt( "RoundsPlayed", pCurrentMap->m_Header.m_iRoundsPlayed );
+ pKVData->SetInt( "TotalTime", pCurrentMap->m_Header.m_iTotalTime );
+ pKVData->SetInt( "BlueWins", pCurrentMap->m_Header.m_iBlueWins );
+ pKVData->SetInt( "RedWins", pCurrentMap->m_Header.m_iRedWins );
+ pKVData->SetInt( "Stalemates", pCurrentMap->m_Header.m_iStalemates );
+ pKVData->SetInt( "BlueSuddenDeathWins", pCurrentMap->m_Header.m_iBlueSuddenDeathWins );
+ pKVData->SetInt( "RedSuddenDeathWins", pCurrentMap->m_Header.m_iRedSuddenDeathWins );
+ for ( int i = 0; i <= MAX_CONTROL_POINTS; i++ )
+ {
+ if ( pCurrentMap->m_Header.m_iLastCapChangedInRound[i] )
+ {
+ pKVData->SetInt( UTIL_VarArgs("RoundsEndingOnCP%d",i), pCurrentMap->m_Header.m_iLastCapChangedInRound[i] );
+ }
+ }
+ pKV->AddSubKey( pKVData );
+
+ // save class stats
+ for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass <= TF_LAST_NORMAL_CLASS; iClass ++ )
+ {
+ TF_Gamestats_ClassStats_t &classStats = pCurrentMap->m_aClassStats[iClass];
+ if ( classStats.iTotalTime > 0 )
+ {
+ pKVData = new KeyValues( "tf_classdata" );
+ pKVData->SetInt( "Class", iClass );
+ pKVData->SetInt( "Spawns", classStats.iSpawns );
+ pKVData->SetInt( "TotalTime", classStats.iTotalTime );
+ pKVData->SetInt( "Score", classStats.iScore );
+ pKVData->SetInt( "Kills", classStats.iKills );
+ pKVData->SetInt( "Deaths", classStats.iDeaths );
+ pKVData->SetInt( "Assists", classStats.iAssists );
+ pKVData->SetInt( "Captures", classStats.iCaptures );
+ pKVData->SetInt( "ClassChanges", classStats.iClassChanges );
+ pKV->AddSubKey( pKVData );
+ }
+ }
+
+ // save weapon stats
+ for ( int iWeapon = TF_WEAPON_NONE+1; iWeapon < TF_WEAPON_COUNT; iWeapon++ )
+ {
+ TF_Gamestats_WeaponStats_t &weaponStats = pCurrentMap->m_aWeaponStats[iWeapon];
+ if ( weaponStats.iShotsFired > 0 )
+ {
+ pKVData = new KeyValues( "tf_weapondata" );
+ pKVData->SetInt( "WeaponID", iWeapon );
+ pKVData->SetInt( "ShotsFired", weaponStats.iShotsFired );
+ pKVData->SetInt( "ShotsFiredCrit", weaponStats.iCritShotsFired );
+ pKVData->SetInt( "ShotsHit", weaponStats.iHits );
+ pKVData->SetInt( "DamageTotal", weaponStats.iTotalDamage );
+ pKVData->SetInt( "HitsWithKnownDistance", weaponStats.iHitsWithKnownDistance );
+ pKVData->SetInt( "DistanceTotal", weaponStats.iTotalDistance );
+ pKV->AddSubKey( pKVData );
+ }
+ }
+
+ //// save deaths
+ //for ( int i = 0; i < pCurrentMap->m_aPlayerDeaths.Count(); i++ )
+ //{
+ // TF_Gamestats_LevelStats_t::PlayerDeathsLump_t &playerDeath = pCurrentMap->m_aPlayerDeaths[i];
+
+ // pKVData = new KeyValues( "tf_deaths" );
+ // pKVData->SetInt( "DeathIndex", i );
+ // pKVData->SetInt( "X", playerDeath.nPosition[0] );
+ // pKVData->SetInt( "Y", playerDeath.nPosition[1] );
+ // pKVData->SetInt( "Z", playerDeath.nPosition[2] );
+ // pKV->AddSubKey( pKVData );
+ //}
+ }
+
+ // clear stats since we've now reported these
+ m_reportedStats.Clear();
+
+ return true;
+}
+
+extern CBaseGameStats_Driver CBGSDriver;
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFGameStats::Init( void )
+{
+ ListenForGameEvent( "teamplay_round_start" );
+ ListenForGameEvent( "tf_game_over" );
+ ListenForGameEvent( "teamplay_game_over" );
+
+ // CTF Gameplay Events
+ ListenForGameEvent( "teamplay_flag_event" );
+
+ // CP Gameplay Events
+ //ListenForGameEvent( "teamplay_point_startcapture" );
+ ListenForGameEvent( "teamplay_point_captured" );
+ //ListenForGameEvent( "teamplay_capture_blocked" );
+
+ // Player Event
+ ListenForGameEvent( "player_disconnect" );
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void StripNewlineFromString( char *string )
+{
+ int nStrlength = strlen( string ) - 1;
+ if ( nStrlength >= 0 )
+ {
+ if ( string[nStrlength] == '\n' || string[nStrlength] == '\r' )
+ string[nStrlength] = 0;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_LevelInit( void )
+{
+ ClearCurrentGameData();
+
+ // Get the host ip and port.
+ int nIPAddr = 0;
+ short nPort = 0;
+ ConVar *hostip = cvar->FindVar( "hostip" );
+ if ( hostip )
+ {
+ nIPAddr = hostip->GetInt();
+ }
+
+ ConVar *hostport = cvar->FindVar( "hostip" );
+ if ( hostport )
+ {
+ nPort = hostport->GetInt();
+ }
+
+ m_reportedStats.m_pCurrentGame->Init( STRING( gpGlobals->mapname ), gpGlobals->mapversion, nIPAddr, nPort, gpGlobals->curtime );
+ m_reportedStats.m_bValidData = false;
+
+ TF_Gamestats_LevelStats_t *map = m_reportedStats.FindOrAddMapStats( STRING( gpGlobals->mapname ) );
+ map->Init( STRING( gpGlobals->mapname ), gpGlobals->mapversion, nIPAddr, nPort, gpGlobals->curtime );
+
+ m_currentRoundRed.m_iNumRounds = 0;
+
+ m_currentMap.Init( STRING( gpGlobals->mapname ), gpGlobals->mapversion, nIPAddr, nPort, gpGlobals->curtime );
+
+ if ( !g_pStringTableServerMapCycle )
+ return;
+
+ // Parse the server's mapcycle for playtime tracking (used in voting)
+ if ( !m_MapsPlaytime.Count() )
+ {
+ int index = g_pStringTableServerMapCycle->FindStringIndex( "ServerMapCycle" );
+ if ( index != ::INVALID_STRING_INDEX )
+ {
+ int nLength = 0;
+ const char *pszMapCycle = (const char *)g_pStringTableServerMapCycle->GetStringUserData( index, &nLength );
+ if ( pszMapCycle && pszMapCycle[0] )
+ {
+ if ( pszMapCycle && nLength )
+ {
+ CUtlVector< char * > vecMapCycle;
+ V_SplitString( pszMapCycle, "\n", vecMapCycle );
+
+ if ( vecMapCycle.Count() )
+ {
+ for ( int index = 0; index < vecMapCycle.Count(); index++ )
+ {
+ // Initialize the list with random playtimes - to vary the first vote options
+ int nSeed = RandomInt( 1, 300 );
+ StripNewlineFromString( vecMapCycle[index] );
+
+ // Canonicalize map name and ensure we know of it
+ char szMapName[ 64 ] = { 0 };
+ V_strncpy( szMapName, vecMapCycle[index], sizeof( szMapName ) );
+ IVEngineServer::eFindMapResult eResult = engine->FindMap( szMapName, sizeof( szMapName ) );
+ switch ( eResult )
+ {
+ case IVEngineServer::eFindMap_Found:
+ case IVEngineServer::eFindMap_NonCanonical:
+ case IVEngineServer::eFindMap_FuzzyMatch:
+ m_MapsPlaytime.Insert( CUtlConstString( szMapName ), nSeed );
+ break;
+ case IVEngineServer::eFindMap_NotFound:
+ // Don't know the canonical map name for stats here :-/
+ case IVEngineServer::eFindMap_PossiblyAvailable:
+ default:
+ break;
+ }
+ }
+ }
+
+ vecMapCycle.PurgeAndDeleteElements();
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Opens a new server session when a level is started.
+//-----------------------------------------------------------------------------
+void CTFGameStats::LevelInitPreEntity()
+{
+ // Start the server session.
+ GetSteamWorksSGameStatsUploader().StartSession();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Closes the server session when the level is shutdown.
+//-----------------------------------------------------------------------------
+void CTFGameStats::LevelShutdownPreClearSteamAPIContext()
+{
+ // Write server specific end session rows.
+ SW_WriteHostsRow();
+
+ // End the server session.
+ GetSteamWorksSGameStatsUploader().EndSession();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_LevelShutdown( float flElapsed )
+{
+ if ( m_reportedStats.m_pCurrentGame )
+ {
+ flElapsed = gpGlobals->curtime - m_reportedStats.m_pCurrentGame->m_flRoundStartTime;
+ m_reportedStats.m_pCurrentGame->m_Header.m_iTotalTime += (int) flElapsed;
+ }
+
+ // Store data for the vote system (for issues that present choices based on stats)
+ AccumulateVoteData();
+
+ // add current game data in to data for this level
+ AccumulateGameData();
+
+ if ( m_bRoundActive )
+ {
+ m_bRoundActive = false;
+ m_iRoundsPlayed++;
+ SW_GameStats_WriteRound( -1, false, m_bServerShutdown ? RE_SERVER_SHUTDOWN : RE_SERVER_MAP_CHANGE );
+ for ( int iPlayerIndex=1; iPlayerIndex<=MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( pPlayer )
+ {
+ SW_GameStats_WritePlayer( pPlayer );
+ }
+ }
+ }
+
+ SW_GameStats_WriteMap();
+
+ if ( m_bServerShutdown )
+ {
+ StopListeningForAllEvents();
+ }
+
+ m_bServerShutdown = false;
+ m_bRoundActive = false;
+ m_iRoundsPlayed = 0;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Resets all stats for this player
+//-----------------------------------------------------------------------------
+void CTFGameStats::ResetPlayerStats( CTFPlayer *pPlayer )
+{
+ PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()];
+ // reset the stats on this player
+ stats.Reset();
+ // reset the matrix of who killed whom with respect to this player
+ ResetKillHistory( pPlayer );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Resets the kill history for this player
+//-----------------------------------------------------------------------------
+void CTFGameStats::ResetKillHistory( CTFPlayer *pPlayer )
+{
+ int iPlayerIndex = pPlayer->entindex();
+
+ // for every other player, set all all the kills with respect to this player to 0
+ for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ )
+ {
+ PlayerStats_t &statsOther = m_aPlayerStats[i];
+ statsOther.statsKills.iNumKilled[iPlayerIndex] = 0;
+ statsOther.statsKills.iNumKilledBy[iPlayerIndex] = 0;
+ statsOther.statsKills.iNumKilledByUnanswered[iPlayerIndex] = 0;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Resets per-round stats for all players
+//-----------------------------------------------------------------------------
+void CTFGameStats::ResetRoundStats()
+{
+ for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ )
+ {
+ m_aPlayerStats[i].statsCurrentRound.Reset();
+ }
+ m_currentRoundRed.Reset();
+ m_currentRoundBlue.Reset();
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "stats_resetround" );
+ if ( event )
+ {
+ gameeventmanager->FireEvent( event );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Increments specified stat for specified player by specified amount
+//-----------------------------------------------------------------------------
+void CTFGameStats::IncrementStat( CTFPlayer *pPlayer, TFStatType_t statType, int iValue )
+{
+ if ( TFGameRules() && TFGameRules()->IsCompetitiveMode() && TFGameRules()->State_Get() != GR_STATE_RND_RUNNING )
+ return;
+
+ PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()];
+ stats.statsCurrentLife.m_iStat[statType] += iValue;
+ stats.statsCurrentRound.m_iStat[statType] += iValue;
+ stats.mapStatsCurrentLife.m_iStat[statType] += iValue;
+ stats.mapStatsCurrentRound.m_iStat[statType] += iValue;
+ stats.statsAccumulated.m_iStat[statType] += iValue;
+ stats.mapStatsAccumulated.m_iStat[statType] += iValue;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::SendStatsToPlayer( CTFPlayer *pPlayer, bool bIsAlive )
+{
+ PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()];
+
+ // set the play time for the round
+ stats.statsCurrentLife.m_iStat[TFSTAT_PLAYTIME] = (int) gpGlobals->curtime - pPlayer->GetSpawnTime();
+ stats.statsCurrentLife.m_iStat[TFSTAT_POINTSSCORED] = TFGameRules()->CalcPlayerScore( &stats.statsCurrentLife, pPlayer );
+ stats.statsCurrentLife.m_iStat[TFSTAT_MAXSENTRYKILLS] = pPlayer->GetMaxSentryKills();
+ stats.mapStatsCurrentLife.m_iStat[TFMAPSTAT_PLAYTIME] = (int) gpGlobals->curtime - pPlayer->GetSpawnTime();
+
+ // make a bit field of all the stats we want to send (all with non-zero values)
+ int iStat;
+ int iSendBits = 0;
+ for ( iStat = TFSTAT_FIRST; iStat <= TFSTAT_LAST; iStat++ )
+ {
+ if ( stats.statsCurrentLife.m_iStat[iStat] > 0 )
+ {
+ iSendBits |= ( 1 << ( iStat - TFSTAT_FIRST ) );
+ }
+ }
+
+ iStat = TFSTAT_FIRST;
+ CSingleUserRecipientFilter filter( pPlayer );
+ filter.MakeReliable();
+ UserMessageBegin( filter, "PlayerStatsUpdate" );
+ WRITE_BYTE( pPlayer->GetPlayerClass()->GetClassIndex() ); // write the class
+ WRITE_BYTE( bIsAlive ); // write if the player is currently alive
+ WRITE_LONG( iSendBits ); // write the bit mask of which stats follow in the message
+
+ // write all the non-zero stats according to the bit mask
+ while ( iSendBits > 0 )
+ {
+ if ( iSendBits & 1 )
+ {
+ WRITE_LONG( stats.statsCurrentLife.m_iStat[iStat] );
+ }
+ iSendBits >>= 1;
+ iStat ++;
+ }
+ MessageEnd();
+
+ for ( int i = 0; i < GetItemSchema()->GetMapCount(); i++ )
+ {
+ const MapDef_t *pMapDef = GetItemSchema()->GetMasterMapDefByIndex( i );
+ if ( V_strcmp( pMapDef->pszMapName, gpGlobals->mapname.ToCStr() ) == 0 )
+ {
+ iStat = 0;
+ iSendBits = 0;
+
+ for ( iStat = TFMAPSTAT_FIRST; iStat <= TFMAPSTAT_LAST; iStat++ )
+ {
+ if ( stats.mapStatsCurrentLife.m_iStat[iStat] > 0 )
+ {
+ iSendBits |= ( 1 << ( iStat - TFMAPSTAT_FIRST ) );
+ }
+ }
+
+ iStat = TFMAPSTAT_FIRST;
+
+ UserMessageBegin( filter, "MapStatsUpdate" );
+
+ WRITE_UBITLONG( pMapDef->GetStatsIdentifier(), 32 ); // write the mapid
+ WRITE_LONG( iSendBits ); // write the bit mask of which stats follow in the message
+
+ // write all the non-zero stats according to the bit mask
+ while ( iSendBits > 0 )
+ {
+ if ( iSendBits & 1 )
+ {
+ WRITE_LONG( stats.mapStatsCurrentLife.m_iStat[iStat] );
+ }
+ iSendBits >>= 1;
+ iStat ++;
+ }
+
+ MessageEnd();
+ break;
+ }
+ }
+
+ AccumulateAndResetPerLifeStats( pPlayer );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::AccumulateAndResetPerLifeStats( CTFPlayer *pPlayer )
+{
+ int iClass = pPlayer->GetPlayerClass()->GetClassIndex();
+
+ PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()];
+
+ // add score from previous life and reset current life stats
+ int iScore = TFGameRules()->CalcPlayerScore( &stats.statsCurrentLife, pPlayer );
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iScore += iScore;
+ }
+ stats.statsCurrentRound.m_iStat[TFSTAT_POINTSSCORED] += iScore;
+ stats.statsAccumulated.m_iStat[TFSTAT_POINTSSCORED] += iScore;
+ stats.statsCurrentLife.Reset();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerConnected( CBasePlayer *pPlayer )
+{
+ ResetPlayerStats( ToTFPlayer( pPlayer ) );
+
+ PlayerStats_t &stats = CTF_GameStats.m_aPlayerStats[pPlayer->entindex()];
+ stats.iConnectTime = GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerDisconnectedTF( CTFPlayer *pTFPlayer )
+{
+ if ( !pTFPlayer )
+ return;
+
+ PlayerStats_t &stats = CTF_GameStats.m_aPlayerStats[pTFPlayer->entindex()];
+ stats.iDisconnectTime = GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch();
+ SW_GameStats_WritePlayer( pTFPlayer, true );
+
+ ResetPlayerStats( pTFPlayer );
+
+ if ( pTFPlayer->IsAlive() )
+ {
+ int iClass = pTFPlayer->GetPlayerClass()->GetClassIndex();
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iTotalTime += (int) ( gpGlobals->curtime - pTFPlayer->GetSpawnTime() );
+ }
+ }
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pTFPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iTeamQuit++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerChangedClass( CTFPlayer *pPlayer, int iOldClass, int iNewClass )
+{
+ if ( iNewClass >= TF_FIRST_NORMAL_CLASS && iNewClass <= TF_LAST_NORMAL_CLASS )
+ {
+ if ( m_reportedStats.m_pCurrentGame )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iNewClass].iClassChanges += 1;
+ }
+ IncrementStat( pPlayer, TFSTAT_CLASSCHANGES, 1 );
+
+ // Record this in steamworks stats also...
+ SW_ClassChange( pPlayer, iOldClass, iNewClass );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerSpawned( CTFPlayer *pPlayer )
+{
+ // if player is spawning as a member of valid team, increase the spawn count for his class
+ int iTeam = pPlayer->GetTeamNumber();
+ int iClass = pPlayer->GetPlayerClass()->GetClassIndex();
+ if ( TEAM_UNASSIGNED != iTeam && TEAM_SPECTATOR != iTeam )
+ {
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iSpawns++;
+ }
+ }
+
+ TF_Gamestats_LevelStats_t *map = m_reportedStats.m_pCurrentGame;
+ if ( !map )
+ return;
+
+ // calculate peak player count on each team
+ for ( iTeam = FIRST_GAME_TEAM; iTeam < TF_TEAM_COUNT; iTeam++ )
+ {
+ int iPlayerCount = GetGlobalTeam( iTeam )->GetNumPlayers();
+ if ( iPlayerCount > map->m_iPeakPlayerCount[iTeam] )
+ {
+ map->m_iPeakPlayerCount[iTeam] = iPlayerCount;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerForceRespawn( CTFPlayer *pPlayer )
+{
+ if ( pPlayer->IsAlive() && !TFGameRules()->PrevRoundWasWaitingForPlayers() )
+ {
+ // send stats to player
+ SendStatsToPlayer( pPlayer, true );
+
+ // if player is alive before respawn, add time from this life to class stat
+ int iClass = pPlayer->GetPlayerClass()->GetClassIndex();
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iTotalTime += (int) ( gpGlobals->curtime - pPlayer->GetSpawnTime() );
+ }
+ }
+
+ AccumulateAndResetPerLifeStats( pPlayer );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerLeachedHealth( CTFPlayer *pPlayer, bool bDispenserHeal, float amount )
+{
+ // make sure value is sane
+ Assert( amount >= 0 );
+ Assert( amount < 1000 );
+
+ if ( !bDispenserHeal )
+ {
+ // If this was a heal by enemy medic and the first such heal that the server is aware of for this player,
+ // send an achievement event to client. On the client, it will award achievement if player doesn't have it yet
+ PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()];
+ if ( 0 == stats.statsAccumulated.m_iStat[TFSTAT_HEALTHLEACHED] )
+ {
+ pPlayer->AwardAchievement( ACHIEVEMENT_TF_GET_HEALED_BYENEMY );
+ }
+ }
+
+ IncrementStat( pPlayer, TFSTAT_HEALTHLEACHED, (int) amount );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+TF_Gamestats_RoundStats_t* CTFGameStats::GetRoundStatsForTeam( int iTeamNumber )
+{
+ if ( iTeamNumber == TF_TEAM_BLUE )
+ return &m_currentRoundBlue;
+ else if ( iTeamNumber == TF_TEAM_RED )
+ return &m_currentRoundRed;
+ else
+ return NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerHealedOther( CTFPlayer *pPlayer, float amount )
+{
+ // make sure value is sane
+ int iAmount = (int) amount;
+ Assert( iAmount >= 0 );
+ Assert( iAmount <= 1000 );
+ if ( iAmount < 0 || iAmount > 1000 )
+ {
+ DevMsg( "CTFGameStats: bogus healing value of %d reported, ignoring\n", iAmount );
+ return;
+ }
+ IncrementStat( pPlayer, TFSTAT_HEALING, (int) amount );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iHealingDone += amount;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: How much health effects like mad milk generate - awarded to the provider
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerHealedOtherAssist( CTFPlayer *pPlayer, float amount )
+{
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pPlayer && pMatch )
+ {
+ // Anti-farming in official matchmaking modes
+ if ( gpGlobals->curtime - pPlayer->GetLastDamageReceivedTime() > 90.f ||
+ gpGlobals->curtime - pPlayer->GetLastEntityDamagedTime() > 90.f )
+ {
+ return;
+ }
+ }
+
+ Assert ( pPlayer );
+
+ // make sure value is sane
+ int iAmount = (int) amount;
+ Assert( iAmount >= 0 );
+ Assert( iAmount <= 1000 );
+ if ( iAmount < 0 || iAmount > 1000 )
+ {
+ DevMsg( "CTFGameStats: bogus healing value of %d reported, ignoring\n", iAmount );
+ return;
+ }
+ IncrementStat( pPlayer, TFSTAT_HEALING_ASSIST, (int) amount );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Raw damage blocked due to effects like invuln, projectile shields, etc
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerBlockedDamage( CTFPlayer *pPlayer, int nAmount )
+{
+ Assert( pPlayer && nAmount > 0 && nAmount < 3000 );
+ if ( nAmount < 0 || nAmount > 3000 )
+ {
+ DevMsg( "CTFGameStats: bogus blocked damage value of %d reported, ignoring\n", nAmount );
+ return;
+ }
+ IncrementStat( pPlayer, TFSTAT_DAMAGE_BLOCKED, nAmount );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_AssistKill( CTFPlayer *pAttacker, CBaseEntity *pVictim )
+{
+ // increment player's stat
+ IncrementStat( pAttacker, TFSTAT_KILLASSISTS, 1 );
+ // increment reported class stats
+ int iClass = pAttacker->GetPlayerClass()->GetClassIndex();
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iAssists++;
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pAttacker->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iAssists++;
+ }
+ }
+
+ if ( pVictim->IsPlayer() )
+ {
+ // keep track of how many times every player kills every other player
+ CTFPlayer *pPlayerVictim = ToTFPlayer( pVictim );
+ TrackKillStats( pAttacker, pPlayerVictim );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerInvulnerable( CTFPlayer *pPlayer )
+{
+ IncrementStat( pPlayer, TFSTAT_INVULNS, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iInvulns++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerCreatedBuilding( CTFPlayer *pPlayer, CBaseObject *pBuilding )
+{
+ // sappers are buildings from the code's point of view but not from the player's, don't count them
+ CObjectSapper *pSapper = dynamic_cast<CObjectSapper *>( pBuilding );
+ if ( pSapper )
+ return;
+
+ IncrementStat( pPlayer, TFSTAT_BUILDINGSBUILT, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iBuildingsBuilt++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerDestroyedBuilding( CTFPlayer *pPlayer, CBaseObject *pBuilding )
+{
+ // sappers are buildings from the code's point of view but not from the player's, don't count them
+ CObjectSapper *pSapper = dynamic_cast<CObjectSapper *>( pBuilding );
+ if ( pSapper )
+ return;
+
+ IncrementStat( pPlayer, TFSTAT_BUILDINGSDESTROYED, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iBuildingsDestroyed++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_AssistDestroyBuilding( CTFPlayer *pPlayer, CBaseObject *pBuilding )
+{
+ // sappers are buildings from the code's point of view but not from the player's, don't count them
+ CObjectSapper *pSapper = dynamic_cast<CObjectSapper *>( pBuilding );
+ if ( pSapper )
+ return;
+
+ IncrementStat( pPlayer, TFSTAT_KILLASSISTS, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_Headshot( CTFPlayer *pKiller, bool bBowShot )
+{
+ IncrementStat( pKiller, TFSTAT_HEADSHOTS, 1 );
+ if ( bBowShot ) // Extra points for a bow shot.
+ {
+ IncrementStat( pKiller, TFSTAT_BONUS_POINTS, 5 ); // Extra point.
+ }
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pKiller->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iHeadshots++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_Backstab( CTFPlayer *pKiller )
+{
+ IncrementStat( pKiller, TFSTAT_BACKSTABS, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pKiller->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iBackstabs++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerStunBall( CTFPlayer *pAttacker, bool bSpecial )
+{
+ if ( bSpecial )
+ {
+ IncrementStat( pAttacker, TFSTAT_BONUS_POINTS, 20 );
+ }
+ else
+ {
+ IncrementStat( pAttacker, TFSTAT_BONUS_POINTS, 10 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerAwardBonusPoints( CTFPlayer *pPlayer, CBaseEntity *pSource, int nCount )
+{
+ CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
+ if ( pPlayer && pMatch )
+ {
+ // Anti-farming in official matchmaking modes
+ if ( gpGlobals->curtime - pPlayer->GetLastDamageReceivedTime() > 90.f ||
+ gpGlobals->curtime - pPlayer->GetLastEntityDamagedTime() > 90.f )
+ {
+ return;
+ }
+ }
+
+ IncrementStat( pPlayer, TFSTAT_BONUS_POINTS, nCount );
+
+ // This event ends up drawing a combattext number
+ if ( pSource )
+ {
+ if ( nCount >= 10 )
+ {
+ CSingleUserRecipientFilter filter( pPlayer );
+ filter.MakeReliable();
+ UserMessageBegin( filter, "PlayerBonusPoints" );
+ WRITE_BYTE( nCount );
+ WRITE_BYTE( pPlayer->entindex() );
+ WRITE_BYTE( pSource->entindex() );
+ MessageEnd();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerHealthkitPickup( CTFPlayer *pTFPlayer )
+{
+ IncrementStat( pTFPlayer, TFSTAT_HEALTHKITS, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerAmmokitPickup( CTFPlayer *pTFPlayer )
+{
+ IncrementStat( pTFPlayer, TFSTAT_AMMOKITS, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerUsedTeleport( CTFPlayer *pTeleportOwner, CTFPlayer *pTeleportingPlayer )
+{
+ // We don't count the builder's teleports
+ if ( pTeleportOwner && pTeleportOwner != pTeleportingPlayer )
+ {
+ IncrementStat( pTeleportOwner, TFSTAT_TELEPORTS, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pTeleportOwner->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iTeleports++;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerFiredWeapon( CTFPlayer *pPlayer, bool bCritical )
+{
+ // If normal gameplay state, track weapon stats.
+ if ( TFGameRules()->State_Get() == GR_STATE_RND_RUNNING )
+ {
+ CTFWeaponBase *pTFWeapon = pPlayer->GetActiveTFWeapon();
+ if ( pTFWeapon )
+ {
+ // record shots fired in reported per-weapon stats
+ int iWeaponID = pTFWeapon->GetWeaponID();
+
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ TF_Gamestats_WeaponStats_t *pWeaponStats = &m_reportedStats.m_pCurrentGame->m_aWeaponStats[iWeaponID];
+ pWeaponStats->iShotsFired++;
+ if ( bCritical )
+ {
+ pWeaponStats->iCritShotsFired++;
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iCrits++;
+ }
+ IncrementStat( pPlayer, TFSTAT_CRITS, 1 );
+ }
+ }
+
+ // need a better place to do this
+ pPlayer->OnMyWeaponFired( pTFWeapon );
+
+ // inform the bots
+ TheNextBots().OnWeaponFired( pPlayer, pTFWeapon );
+ }
+ }
+
+ IncrementStat( pPlayer, TFSTAT_SHOTS_FIRED, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info, int iDamageTaken )
+{
+ // defensive guard against insanely huge damage values that apparently get into the stats system once in a while -- ignore insane values
+ const int INSANE_PLAYER_DAMAGE = TFGameRules()->IsMannVsMachineMode() ? 5000 : 1500;
+
+ Assert( iDamageTaken >= 0 );
+ if ( ( iDamageTaken < 0 ) || ( iDamageTaken > INSANE_PLAYER_DAMAGE ) )
+ return;
+
+ CObjectSentrygun *pSentry = NULL;
+ CTFPlayer *pTarget = ToTFPlayer( pBasePlayer );
+ CTFPlayer *pAttacker = ToTFPlayer( info.GetAttacker() );
+ if ( !pAttacker )
+ {
+ pSentry = dynamic_cast< CObjectSentrygun * >( info.GetAttacker() );
+ if ( !pSentry )
+ return;
+
+ pAttacker = pSentry->GetOwner();
+ }
+ // don't count damage to yourself
+ if ( pTarget == pAttacker )
+ return;
+
+ if ( pAttacker )
+ {
+ IncrementStat( pAttacker, TFSTAT_DAMAGE, iDamageTaken );
+
+ if ( info.GetDamageType() & (DMG_BURN | DMG_IGNITE) )
+ {
+ IncrementStat( pAttacker, TFSTAT_FIREDAMAGE, iDamageTaken );
+ }
+ if ( info.GetDamageType() & DMG_BLAST )
+ {
+ IncrementStat( pAttacker, TFSTAT_BLASTDAMAGE, iDamageTaken );
+ }
+ // Ranged stats
+ if ( !( info.GetDamageType() & DMG_MELEE ) )
+ {
+ IncrementStat( pAttacker, TFSTAT_DAMAGE_RANGED, iDamageTaken );
+
+ if ( info.GetDamageType() & DMG_CRITICAL )
+ {
+ if ( pAttacker->m_Shared.IsCritBoosted() )
+ {
+ IncrementStat( pAttacker, TFSTAT_DAMAGE_RANGED_CRIT_BOOSTED, iDamageTaken );
+
+ }
+ else
+ {
+ IncrementStat( pAttacker, TFSTAT_DAMAGE_RANGED_CRIT_RANDOM, iDamageTaken );
+ }
+ }
+ }
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pAttacker->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iDamageDone += iDamageTaken;
+ }
+
+ //Report MvM damage to bots
+ if ( TFGameRules()->IsMannVsMachineMode() )
+ {
+ CMannVsMachineStats *pStats = MannVsMachineStats_GetInstance();
+ if ( pStats )
+ {
+ if ( pTarget && pTarget->IsBot() )
+ {
+ if ( pTarget->IsMiniBoss() )
+ {
+ pStats->PlayerEvent_DealtDamageToGiants( pAttacker, iDamageTaken );
+ }
+ else
+ {
+ pStats->PlayerEvent_DealtDamageToBots( pAttacker, iDamageTaken );
+ }
+ }
+ }
+ }
+ }
+
+ if ( pTarget )
+ {
+ IncrementStat( pTarget, TFSTAT_DAMAGETAKEN, iDamageTaken );
+ }
+
+ TF_Gamestats_LevelStats_t::PlayerDamageLump_t damage;
+ Vector killerOrg(0, 0, 0);
+
+ // set the location where the target was hit
+ const Vector &org = pTarget->GetAbsOrigin();
+ damage.nTargetPosition[ 0 ] = static_cast<int>( org.x );
+ damage.nTargetPosition[ 1 ] = static_cast<int>( org.y );
+ damage.nTargetPosition[ 2 ] = static_cast<int>( org.z );
+
+ // set the class of the attacker
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CBasePlayer *pScorer = TFGameRules()->GetDeathScorer( pAttacker, pInflictor, pTarget );
+
+ if ( !pSentry )
+ {
+ pSentry = dynamic_cast< CObjectSentrygun * >( pInflictor );
+ }
+
+ if ( pSentry != NULL )
+ {
+ killerOrg = pSentry->GetAbsOrigin();
+ damage.iAttackClass = TF_CLASS_ENGINEER;
+ damage.iWeapon = ( info.GetDamageType() & DMG_BLAST ) ? TF_WEAPON_SENTRY_ROCKET : TF_WEAPON_SENTRY_BULLET;
+ }
+ else if ( dynamic_cast<CObjectDispenser *>( pInflictor ) )
+ {
+ damage.iAttackClass = TF_CLASS_ENGINEER;
+ damage.iWeapon = TF_WEAPON_DISPENSER;
+ }
+ else
+ {
+ CTFPlayer *pTFAttacker = ToTFPlayer( pScorer );
+ if ( pTFAttacker )
+ {
+ CTFPlayerClass *pAttackerClass = pTFAttacker->GetPlayerClass();
+ damage.iAttackClass = ( !pAttackerClass ) ? TF_CLASS_UNDEFINED : pAttackerClass->GetClassIndex();
+ killerOrg = pTFAttacker->GetAbsOrigin();
+ }
+ else
+ {
+ damage.iAttackClass = TF_CLASS_UNDEFINED;
+ killerOrg = org;
+ }
+
+ // find the weapon the killer used
+ damage.iWeapon = GetWeaponFromDamage( info );
+ }
+
+ // If normal gameplay state, track weapon stats.
+ if ( ( TFGameRules()->State_Get() == GR_STATE_RND_RUNNING ) && ( damage.iWeapon != TF_WEAPON_NONE ) )
+ {
+ // record hits & damage in reported per-weapon stats
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ TF_Gamestats_WeaponStats_t *pWeaponStats = &m_reportedStats.m_pCurrentGame->m_aWeaponStats[damage.iWeapon];
+ pWeaponStats->iHits++;
+ pWeaponStats->iTotalDamage += iDamageTaken;
+
+ // Try and figure out where the damage is coming from
+ Vector vecDamageOrigin = info.GetReportedPosition();
+ // If we didn't get an origin to use, try using the attacker's origin
+ if ( vecDamageOrigin == vec3_origin )
+ {
+ if ( pSentry )
+ {
+ vecDamageOrigin = pSentry->GetAbsOrigin();
+ }
+ else
+ {
+ vecDamageOrigin = killerOrg;
+ }
+ }
+ if ( vecDamageOrigin != vec3_origin )
+ {
+ pWeaponStats->iHitsWithKnownDistance++;
+ int iDistance = (int) vecDamageOrigin.DistTo( pBasePlayer->GetAbsOrigin() );
+ // Msg( "Damage distance: %d\n", iDistance );
+ pWeaponStats->iTotalDistance += iDistance;
+ }
+ }
+ }
+
+ Assert( damage.iAttackClass != TF_CLASS_UNDEFINED );
+
+ // record the time the damage occurred
+ damage.fTime = gpGlobals->curtime;
+
+ // store the attacker's position
+ damage.nAttackerPosition[ 0 ] = static_cast<int>( killerOrg.x );
+ damage.nAttackerPosition[ 1 ] = static_cast<int>( killerOrg.y );
+ damage.nAttackerPosition[ 2 ] = static_cast<int>( killerOrg.z );
+
+ // set the class of the target
+ CTFPlayer *pTFPlayer = ToTFPlayer( pTarget );
+ CTFPlayerClass *pTargetClass = ( pTFPlayer ) ? pTFPlayer->GetPlayerClass() : NULL;
+ damage.iTargetClass = ( !pTargetClass ) ? TF_CLASS_UNDEFINED : pTargetClass->GetClassIndex();
+
+ Assert( damage.iTargetClass != TF_CLASS_UNDEFINED );
+
+ // record the damage done
+ damage.iDamage = info.GetDamage();
+
+ // record if it was a crit
+ damage.iCrit = ( ( info.GetDamageType() & DMG_CRITICAL ) != 0 );
+
+ // record if it was a kill
+ damage.iKill = ( pTarget->GetHealth() <= 0 );
+
+ // add it to the list of damages
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ //m_reportedStats.m_pCurrentGame->m_aPlayerDamage.AddToTail( damage );
+ m_reportedStats.m_pCurrentGame->m_bIsRealServer = true;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: How much damage effects like jarate add - awarded to the provider
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerDamageAssist( CBasePlayer *pProvider, int iBonusDamage )
+{
+ Assert( pProvider );
+
+ const int INSANE_PLAYER_DAMAGE = 5000;
+ Assert( iBonusDamage >= 0 );
+ Assert( iBonusDamage <= INSANE_PLAYER_DAMAGE );
+ if ( iBonusDamage < 0 || iBonusDamage > INSANE_PLAYER_DAMAGE )
+ return;
+
+ CTFPlayer *pTFProvider = ToTFPlayer( pProvider );
+ if ( pTFProvider )
+ {
+ IncrementStat( pTFProvider, TFSTAT_DAMAGE_ASSIST, iBonusDamage );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Damage done to all boss types
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_BossDamage( CBasePlayer *pAttacker, int iDamage )
+{
+ const int INSANE_DAMAGE = 5000;
+ Assert( iDamage >= 0 );
+ Assert( iDamage <= INSANE_DAMAGE );
+ if ( iDamage < 0 || iDamage > INSANE_DAMAGE )
+ return;
+
+ CTFPlayer *pTFAttacker = ToTFPlayer( pAttacker );
+ if ( pTFAttacker )
+ {
+ IncrementStat( pTFAttacker, TFSTAT_DAMAGE_BOSS, iDamage );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerSuicide( CBasePlayer *pPlayer )
+{
+ CTFPlayer *pTFPlayer = dynamic_cast<CTFPlayer*>( pPlayer );
+ if ( pTFPlayer )
+ {
+ IncrementStat( pTFPlayer, TFSTAT_SUICIDES, 1 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info )
+{
+ // This also gets called when the victim is a building. That gets tracked separately as building destruction, don't count it here
+ if ( !pVictim->IsPlayer() )
+ return;
+
+ CTFPlayer *pPlayerAttacker = static_cast< CTFPlayer * >( pAttacker );
+
+ IncrementStat( pPlayerAttacker, TFSTAT_KILLS, 1 );
+
+ // keep track of how many times every player kills every other player
+ CTFPlayer *pPlayerVictim = ToTFPlayer( pVictim );
+ TrackKillStats( pAttacker, pPlayerVictim );
+
+ int iClass = pPlayerAttacker->GetPlayerClass()->GetClassIndex();
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iKills++;
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pAttacker->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iKills++;
+ }
+ }
+
+ // Throwable Kill tracking
+ if ( info.GetDamageCustom() == TF_DMG_CUSTOM_THROWABLE )
+ {
+ CTFPlayer *pTFAttacker = ToTFPlayer( pAttacker );
+ if ( pTFAttacker )
+ {
+ Event_PlayerThrowableKill( pTFAttacker );
+ }
+ }
+
+ // Scouts get additional points for killing medics that were actively healing.
+ if ( (iClass == TF_CLASS_SCOUT) && pPlayerVictim && (pPlayerVictim->GetPlayerClass()->GetClassIndex() == TF_CLASS_MEDIC) )
+ {
+ // Determine if the medic was using their (now holstered) heal gun.
+ CWeaponMedigun *pMedigun = (CWeaponMedigun *) pPlayerVictim->Weapon_OwnsThisID( TF_WEAPON_MEDIGUN );
+ if ( pMedigun && pMedigun->m_bWasHealingBeforeDeath )
+ {
+ IncrementStat( pPlayerAttacker, TFSTAT_BONUS_POINTS, 10 );
+ }
+ }
+
+ // Players get points for killing a Rune carrier
+ if ( pPlayerVictim->m_Shared.IsCarryingRune() )
+ {
+ IncrementStat( pPlayerAttacker, TFSTAT_KILLS_RUNECARRIER, 1 );
+ }
+
+}
+
+void CTFGameStats::Event_KillDetail( CTFPlayer* pKiller, CTFPlayer* pVictim, CTFPlayer* pAssister, IGameEvent* event /*player_death*/, const CTakeDamageInfo &info )
+{
+ SW_GameStats_WriteKill( pKiller, pVictim, pAssister, event, info );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_RoundStart()
+{
+ m_iGameEndReason = 0;
+ m_currentRoundRed.Reset();
+ m_currentRoundBlue.Reset();
+ m_bRoundActive = true;
+ m_iKillCount = 0;
+ m_iPlayerUpdates = 0;
+ m_currentRoundRed.m_iRoundStartTime = GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch();
+
+ m_rdStats.Clear();
+ m_passtimeStats.Clear();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_RoundEnd( int iWinningTeam, bool bFullRound, float flRoundTime, bool bWasSuddenDeathWin )
+{
+ TF_Gamestats_LevelStats_t *map = m_reportedStats.m_pCurrentGame;
+ Assert( map );
+ if ( !map )
+ return;
+
+ m_reportedStats.m_pCurrentGame->m_Header.m_iTotalTime += (int) flRoundTime;
+ m_reportedStats.m_pCurrentGame->m_flRoundStartTime = gpGlobals->curtime;
+
+// if ( !bFullRound )
+// return;
+
+ map->m_Header.m_iRoundsPlayed++;
+ switch ( iWinningTeam )
+ {
+ case TF_TEAM_RED:
+ map->m_Header.m_iRedWins++;
+ if ( bWasSuddenDeathWin )
+ {
+ map->m_Header.m_iRedSuddenDeathWins++;
+ }
+ break;
+ case TF_TEAM_BLUE:
+ map->m_Header.m_iBlueWins++;
+ if ( bWasSuddenDeathWin )
+ {
+ map->m_Header.m_iBlueSuddenDeathWins++;
+ }
+ break;
+ case TEAM_UNASSIGNED:
+ map->m_Header.m_iStalemates++;
+ break;
+ default:
+ Assert( false );
+ break;
+ }
+
+ // Determine which control point was last captured
+ if ( TFGameRules() && ( TFGameRules()->GetGameType() == TF_GAMETYPE_CP || TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT || TFGameRules()->GetGameType() == TF_GAMETYPE_ARENA ) )
+ {
+ int iLastCP = TFGameRules()->GetLastCapPointChanged();
+ if ( iLastCP >= 0 && iLastCP <= MAX_CONTROL_POINTS )
+ {
+ map->m_Header.m_iLastCapChangedInRound[iLastCP]++;
+ }
+ }
+
+ // add current game data in to data for this level
+ AccumulateGameData();
+
+ m_bRoundActive = false;
+ m_iRoundsPlayed++;
+ SW_GameStats_WriteRound( iWinningTeam, bFullRound, RE_ROUND_END );
+ for ( int iPlayerIndex=1; iPlayerIndex<=MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( pPlayer )
+ {
+ SW_GameStats_WritePlayer( pPlayer );
+ }
+ }
+
+ SW_PasstimeRoundEnded();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_GameEnd()
+{
+ // Send a stats update to all players who are still alive. (Dead one have already
+ // received theirs when they died.)
+ for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) );
+ if ( pPlayer && pPlayer->IsConnected() && pPlayer->IsAlive() )
+ {
+ SendStatsToPlayer( pPlayer, true );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerCapturedPoint( CTFPlayer *pPlayer )
+{
+ // increment player stats
+ IncrementStat( pPlayer, TFSTAT_CAPTURES, 1 );
+ // increment reported stats
+ int iClass = pPlayer->GetPlayerClass()->GetClassIndex();
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iCaptures++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerReturnedFlag( CTFPlayer *pPlayer )
+{
+ // increment player stats
+ IncrementStat( pPlayer, TFSTAT_FLAGRETURNS, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerScoresEscortPoints( CTFPlayer *pPlayer, int iPoints )
+{
+ // increment player stats
+ IncrementStat( pPlayer, TFSTAT_CAPTURES, iPoints );
+
+ SW_GameEvent( pPlayer, "escort_scored", iPoints );
+
+ // increment reported stats
+ //int iClass = pPlayer->GetPlayerClass()->GetClassIndex();
+ //if ( m_reportedStats.m_pCurrentGame != NULL )
+ //{
+ // m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iCaptures++;
+ //}
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerDefendedPoint( CTFPlayer *pPlayer )
+{
+ IncrementStat( pPlayer, TFSTAT_DEFENSES, 1 );
+
+ ConVarRef tf_gamemode_cp( "tf_gamemode_cp" );
+ ConVarRef tf_gamemode_payload( "tf_gamemode_payload" );
+ if ( tf_gamemode_cp.GetInt() == 1 )
+ {
+ SW_GameEvent( pPlayer, "point_blocked", 1 );
+ }
+ else if ( tf_gamemode_payload.GetInt() == 1 )
+ {
+ SW_GameEvent( pPlayer, "escort_blocked", 1 );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerDominatedOther( CTFPlayer *pAttacker )
+{
+ IncrementStat( pAttacker, TFSTAT_DOMINATIONS, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pAttacker->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iDominations++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerRevenge( CTFPlayer *pAttacker )
+{
+ IncrementStat( pAttacker, TFSTAT_REVENGE, 1 );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pAttacker->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iRevenges++;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info )
+{
+ Assert( pPlayer );
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+
+ IncrementStat( pTFPlayer, TFSTAT_DEATHS, 1 );
+ SendStatsToPlayer( pTFPlayer, false );
+
+ // TF_Gamestats_LevelStats_t::PlayerDeathsLump_t death;
+ Vector killerOrg;
+
+ // set the location where the target died
+ const Vector &org = pPlayer->GetAbsOrigin();
+ // death.nPosition[ 0 ] = static_cast<int>( org.x );
+ // death.nPosition[ 1 ] = static_cast<int>( org.y );
+ // death.nPosition[ 2 ] = static_cast<int>( org.z );
+
+ // set the class of the attacker
+ CBaseEntity *pInflictor = info.GetInflictor();
+ CBaseEntity *pKiller = info.GetAttacker();
+ CTFPlayer *pScorer = ToTFPlayer( TFGameRules()->GetDeathScorer( pKiller, pInflictor, pPlayer ) );
+
+ if ( pInflictor && pInflictor->IsBaseObject() && dynamic_cast< CObjectSentrygun * >( pInflictor ) != NULL )
+ {
+ killerOrg = pInflictor->GetAbsOrigin();
+ }
+ else
+ {
+ if ( pScorer )
+ {
+ // CTFPlayerClass *pAttackerClass = pScorer->GetPlayerClass();
+ // death.iAttackClass = ( !pAttackerClass ) ? TF_CLASS_UNDEFINED : pAttackerClass->GetClassIndex();
+ killerOrg = pScorer->GetAbsOrigin();
+ }
+ else
+ {
+ // death.iAttackClass = TF_CLASS_UNDEFINED;
+ killerOrg = org;
+ }
+ }
+
+ // set the class of the target
+ // CTFPlayerClass *pTargetClass = ( pTFPlayer ) ? pTFPlayer->GetPlayerClass() : NULL;
+ // death.iTargetClass = ( !pTargetClass ) ? TF_CLASS_UNDEFINED : pTargetClass->GetClassIndex();
+
+ // find the weapon the killer used
+ // death.iWeapon = GetWeaponFromDamage( info );
+
+ // calculate the distance to the killer
+ // death.iDistance = static_cast<unsigned short>( ( killerOrg - org ).Length() );
+
+ // add it to the list of deaths
+ TF_Gamestats_LevelStats_t *map = m_reportedStats.m_pCurrentGame;
+ if ( map )
+ {
+ //const int MAX_REPORTED_DEATH_COORDS = 2000; // limit list of death coords reported so it doesn't grow unbounded.
+ //if ( map->m_aPlayerDeaths.Count() < MAX_REPORTED_DEATH_COORDS )
+ //{
+ // map->m_aPlayerDeaths.AddToTail( death );
+ //}
+
+ int iClass = ToTFPlayer( pPlayer )->GetPlayerClass()->GetClassIndex();
+
+ if ( m_reportedStats.m_pCurrentGame != NULL )
+ {
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iDeaths++;
+ m_reportedStats.m_pCurrentGame->m_aClassStats[iClass].iTotalTime += (int) ( gpGlobals->curtime - pTFPlayer->GetSpawnTime() );
+
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pPlayer->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iDeaths++;
+ }
+ if ( pKiller != NULL && pKiller == pPlayer )
+ {
+ TF_Gamestats_RoundStats_t* round = GetRoundStatsForTeam( pKiller->GetTeamNumber() );
+ if ( round )
+ {
+ round->m_Summary.iSuicides++;
+ }
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Event handler
+//-----------------------------------------------------------------------------
+void CTFGameStats::FireGameEvent( IGameEvent *event )
+{
+ const char *pEventName = event->GetName();
+
+ if ( Q_strcmp( "tf_game_over", pEventName ) == 0 )
+ {
+ StoreGameEndReason( event->GetString( "reason" ) );
+
+ Event_GameEnd();
+ }
+ else if ( Q_strcmp( "teamplay_game_over", pEventName ) == 0 )
+ {
+ StoreGameEndReason( event->GetString( "reason" ) );
+
+ Event_GameEnd();
+ }
+ else if ( Q_strcmp( "teamplay_round_start", pEventName ) == 0 )
+ {
+ Event_RoundStart();
+ }
+ else if ( Q_strcmp( "teamplay_flag_event", pEventName ) == 0 )
+ {
+ SW_FlagEvent( event->GetInt( "player" ), event->GetInt( "carrier"), event->GetInt( "eventtype") );
+ }
+ else if ( Q_strcmp( "teamplay_point_startcapture", pEventName ) == 0 )
+ {
+ // Not sure this is necessary to track, since it's only sent when a cap is freshly started.
+ // Should probably see what the community needs are or what our needs are after we release the initial set of stats.
+ /*
+ const char *cappers = event->GetString( "cappers" );
+ for ( int i = 0; i < Q_strlen( cappers ); i++ )
+ {
+ SW_CapEvent( event->GetInt( "cp" ), cappers[i], "point_start_capture", 0 );
+ }
+ */
+ }
+ else if ( Q_strcmp( "teamplay_point_captured", pEventName ) == 0 )
+ {
+ const char *cappers = event->GetString( "cappers" );
+ for ( int i = 0; i < Q_strlen( cappers ); i++ )
+ {
+ SW_CapEvent( event->GetInt( "cp" ), cappers[i], "point_captured", 1 );
+ }
+ }
+ else if ( Q_strcmp( "player_disconnect", pEventName ) == 0 )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByUserId( event->GetInt("userid") ) );
+ if ( pPlayer )
+ {
+ CTF_GameStats.Event_PlayerLoadoutChanged( pPlayer, true );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Adds data from current game into accumulated data for this level.
+//-----------------------------------------------------------------------------
+void CTFGameStats::StoreGameEndReason( const char* reason )
+{
+ if ( Q_strcmp( reason, "Reached Time Limit" ) == 0 )
+ {
+ m_iGameEndReason = RE_TIME_LIMIT;
+ }
+ else if ( Q_strcmp( reason, "Reached Win Limit" ) == 0 )
+ {
+ m_iGameEndReason = RE_WIN_LIMIT;
+ }
+ else if ( Q_strcmp( reason, "Reached Win Difference Limit" ) == 0 )
+ {
+ m_iGameEndReason = RE_WIN_DIFF_LIMIT;
+ }
+ else if ( Q_strcmp( reason, "Reached Round Limit" ) == 0 )
+ {
+ m_iGameEndReason = RE_ROUND_LIMIT;
+ }
+ else if ( Q_strcmp( reason, "NextLevel CVAR" ) == 0 )
+ {
+ m_iGameEndReason = RE_NEXT_LEVEL_CVAR;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Adds data from current game into accumulated data for this level.
+//-----------------------------------------------------------------------------
+void CTFGameStats::AccumulateGameData()
+{
+ // find or add a bucket for this level
+ TF_Gamestats_LevelStats_t *map = m_reportedStats.FindOrAddMapStats( STRING( gpGlobals->mapname ) );
+ // get current game data
+ TF_Gamestats_LevelStats_t *game = m_reportedStats.m_pCurrentGame;
+ if ( !map || !game )
+ return;
+
+ if ( IsRealGameplay( game ) )
+ {
+ // if this looks like real game play, add it to stats
+ map->Accumulate( game );
+ m_reportedStats.m_bValidData = true;
+ }
+ m_currentMap.Accumulate( game ); // Steamworks stats always accumulate.
+
+ ClearCurrentGameData();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::AccumulateVoteData( void )
+{
+ if ( !g_voteController )
+ return;
+
+ if ( !g_pStringTableServerMapCycle )
+ return;
+
+ // Find the current map and update playtime
+ int iIndex = m_MapsPlaytime.Find( CUtlConstString( STRING( gpGlobals->mapname ) ) );
+ if ( iIndex != m_MapsPlaytime.InvalidIndex() )
+ {
+ TF_Gamestats_LevelStats_t *CurrentGame = m_reportedStats.m_pCurrentGame;
+ //Msg( "%s -- old: %i ", STRING( gpGlobals->mapname ), m_MapsPlaytime[iIndex] );
+ m_MapsPlaytime[iIndex] += CurrentGame->m_Header.m_iTotalTime;
+ //Msg( "new: %i\n", STRING( gpGlobals->mapname ), m_MapsPlaytime[iIndex] );
+ }
+}
+
+struct MapNameAndPlaytime_t
+{
+ const char* szName;
+ int nTime;
+};
+
+// Returns negative if elem2 > elem1, positive if elem2 < elem1, and zero if elem 1 == elem2
+static int __cdecl SortMapPlaytime( const void *elem1, const void *elem2 )
+{
+ int time1 = static_cast< const MapNameAndPlaytime_t * >( elem1 )->nTime;
+ int time2 = static_cast< const MapNameAndPlaytime_t * >( elem2 )->nTime;
+
+ if ( time2 < time1 )
+ return -1;
+
+ if ( time2 > time1 )
+ return 1;
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Method used by the vote system to retrieve various data, like map playtime
+//-----------------------------------------------------------------------------
+bool CTFGameStats::GetVoteData( const char *szIssueName, int nNumOptions, CUtlVector <const char*> &vecNames )
+{
+ // Feeds lowest playtime maps to the vote system to present as options
+ if ( Q_strcmp( szIssueName, "NextLevel" ) == 0 )
+ {
+ // This can only happen if we don't get any maps from the mapcycle file
+ if ( !m_MapsPlaytime.Count() )
+ return false;
+
+ vecNames.EnsureCapacity( MIN( nNumOptions, (int) m_MapsPlaytime.Count() ) );
+
+ // What's the next map in the mapcycle? Place that first in the output
+ m_szNextMap[0] = '\0';
+ CMultiplayRules *pRules = dynamic_cast< CMultiplayRules * >( GameRules() );
+ if ( pRules )
+ {
+ pRules->GetNextLevelName( m_szNextMap, sizeof( m_szNextMap ) );
+ if ( m_szNextMap[0] )
+ {
+ vecNames.AddToTail( m_szNextMap );
+ }
+ }
+
+ CUtlVector< MapNameAndPlaytime_t > vecMapsAndPlaytime;
+ vecMapsAndPlaytime.EnsureCapacity( m_MapsPlaytime.Count() );
+
+ // Feed the maps into a vector for sorting
+ FOR_EACH_MAP_FAST( m_MapsPlaytime, iIndex )
+ {
+ const char *szItemName = m_MapsPlaytime.Key( iIndex ).Get();
+ int nItemTime = m_MapsPlaytime.Element( iIndex );
+ // Exclude the next map (already added) and the current map (omitted)
+ if ( Q_strcmp( szItemName, m_szNextMap ) != 0 &&
+ Q_strcmp( szItemName, STRING( gpGlobals->mapname ) ) != 0 )
+ {
+ int iVec = vecMapsAndPlaytime.AddToTail();
+ vecMapsAndPlaytime[ iVec ].szName = szItemName;
+ vecMapsAndPlaytime[ iVec ].nTime = nItemTime;
+ }
+ }
+ qsort( vecMapsAndPlaytime.Base(), vecMapsAndPlaytime.Count(), sizeof( MapNameAndPlaytime_t ), SortMapPlaytime );
+
+ // Copy sorted maps to output until we have got enough options
+ FOR_EACH_VEC( vecMapsAndPlaytime, iVec )
+ {
+ if ( vecNames.Count() >= nNumOptions )
+ break;
+ vecNames.AddToTail( vecMapsAndPlaytime[ iVec ].szName );
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Clears data for current game
+//-----------------------------------------------------------------------------
+void CTFGameStats::ClearCurrentGameData()
+{
+ if ( m_reportedStats.m_pCurrentGame )
+ {
+ delete m_reportedStats.m_pCurrentGame;
+ }
+ m_reportedStats.m_pCurrentGame = new TF_Gamestats_LevelStats_t;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Updates the stats of who has killed whom
+//-----------------------------------------------------------------------------
+void CTFGameStats::TrackKillStats( CBasePlayer *pAttacker, CBasePlayer *pVictim )
+{
+ int iPlayerIndexAttacker = pAttacker->entindex();
+ int iPlayerIndexVictim = pVictim->entindex();
+
+ PlayerStats_t &statsAttacker = m_aPlayerStats[iPlayerIndexAttacker];
+ PlayerStats_t &statsVictim = m_aPlayerStats[iPlayerIndexVictim];
+
+ statsVictim.statsKills.iNumKilledBy[iPlayerIndexAttacker]++;
+ statsVictim.statsKills.iNumKilledByUnanswered[iPlayerIndexAttacker]++;
+ statsAttacker.statsKills.iNumKilled[iPlayerIndexVictim]++;
+ statsAttacker.statsKills.iNumKilledByUnanswered[iPlayerIndexVictim] = 0;
+}
+
+struct PlayerStats_t *CTFGameStats::FindPlayerStats( CBasePlayer *pPlayer )
+{
+ if ( !pPlayer )
+ return NULL;
+
+ return &m_aPlayerStats[pPlayer->entindex()];
+}
+
+bool CTFGameStats::IsRealGameplay( TF_Gamestats_LevelStats_t *game )
+{
+ // Sanity-check that this looks like real game play -- must have minimum # of players on both teams,
+ // minimum time and some damage to players must have occurred
+ if ( tf_stats_nogameplaycheck.GetInt() )
+ return true;
+
+ bool bIsRealGameplay = (
+ ( game->m_iPeakPlayerCount[TF_TEAM_RED] >= TFGameRules()->GetStatsMinimumPlayers() ) &&
+ ( game->m_iPeakPlayerCount[TF_TEAM_BLUE] >= TFGameRules()->GetStatsMinimumPlayers() ) &&
+ ( game->m_Header.m_iTotalTime >= TFGameRules()->GetStatsMinimumPlayedTime() ) && ( game->m_bIsRealServer )
+ );
+
+ return bIsRealGameplay;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: //Deprecated
+//-----------------------------------------------------------------------------
+//static void CC_ListDeaths( const CCommand &args )
+//{
+// if ( !UTIL_IsCommandIssuedByServerAdmin() )
+// return;
+//
+// Msg( "Command Deprecated");
+//
+// //TF_Gamestats_LevelStats_t *map = CTF_GameStats.m_reportedStats.m_pCurrentGame;
+// //if ( !map )
+// // return;
+//
+// //for( int i = 0; i < map->m_aPlayerDeaths.Count(); i++ )
+// //{
+// // Msg( "%s killed %s with %s at (%d,%d,%d), distance %d\n",
+// // g_aClassNames[ map->m_aPlayerDeaths[ i ].iAttackClass ],
+// // g_aClassNames[ map->m_aPlayerDeaths[ i ].iTargetClass ],
+// // WeaponIdToAlias( map->m_aPlayerDeaths[ i ].iWeapon ),
+// // map->m_aPlayerDeaths[ i ].nPosition[ 0 ],
+// // map->m_aPlayerDeaths[ i ].nPosition[ 1 ],
+// // map->m_aPlayerDeaths[ i ].nPosition[ 2 ],
+// // map->m_aPlayerDeaths[ i ].iDistance );
+// //}
+//
+// //Msg( "\n---------------------------------\n\n" );
+//
+// //for( int i = 0; i < map->m_aPlayerDamage.Count(); i++ )
+// //{
+// // Msg( "%.2f : %s at (%d,%d,%d) caused %d damage to %s with %s at (%d,%d,%d)%s%s\n",
+// // map->m_aPlayerDamage[ i ].fTime,
+// // g_aClassNames[ map->m_aPlayerDamage[ i ].iAttackClass ],
+// // map->m_aPlayerDamage[ i ].nAttackerPosition[ 0 ],
+// // map->m_aPlayerDamage[ i ].nAttackerPosition[ 1 ],
+// // map->m_aPlayerDamage[ i ].nAttackerPosition[ 2 ],
+// // map->m_aPlayerDamage[ i ].iDamage,
+// // g_aClassNames[ map->m_aPlayerDamage[ i ].iTargetClass ],
+// // WeaponIdToAlias( map->m_aPlayerDamage[ i ].iWeapon ),
+// // map->m_aPlayerDamage[ i ].nTargetPosition[ 0 ],
+// // map->m_aPlayerDamage[ i ].nTargetPosition[ 1 ],
+// // map->m_aPlayerDamage[ i ].nTargetPosition[ 2 ],
+// // map->m_aPlayerDamage[ i ].iCrit ? ", CRIT!" : "",
+// // map->m_aPlayerDamage[ i ].iKill ? ", KILL" : "" );
+// //}
+//
+// //Msg( "\n---------------------------------\n\n" );
+// //Msg( "listed %d deaths\n", map->m_aPlayerDeaths.Count() );
+// //Msg( "listed %d damages\n\n", map->m_aPlayerDamage.Count() );
+//}
+//
+//static ConCommand listDeaths("listdeaths", CC_ListDeaths, "lists player deaths", FCVAR_DEVELOPMENTONLY );
+
+CON_COMMAND_F( tf_dumpplayerstats, "Dumps current player stats", FCVAR_DEVELOPMENTONLY )
+{
+ if ( !UTIL_IsCommandIssuedByServerAdmin() )
+ return;
+
+ for ( int i = 1; i <= gpGlobals->maxClients; i++ )
+ {
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
+ if ( pPlayer && pPlayer->IsConnected() )
+ {
+ PlayerStats_t &stats = CTF_GameStats.m_aPlayerStats[pPlayer->entindex()];
+ Msg( "%s:\n", pPlayer->GetPlayerName() );
+ for ( int iStat = TFSTAT_FIRST; iStat <= TFSTAT_LAST; iStat++ )
+ {
+ Msg( " Stat %d = %d (round), %d (map)\n", iStat, stats.statsCurrentRound.m_iStat[iStat], stats.statsAccumulated.m_iStat[iStat] );
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: New Steamworks Database Map Data
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_GameStats_WriteMap()
+{
+#if !defined(NO_STEAM)
+ KeyValues* pKVData = new KeyValues( "TF2ServerMaps" );
+ pKVData->SetInt( "MapIndex", CBGSDriver.m_iNumLevels );
+ pKVData->SetInt( "StartTime", m_currentMap.m_iMapStartTime );
+ pKVData->SetInt( "EndTime", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+ pKVData->SetString( "MapID", m_currentMap.m_Header.m_szMapName );
+
+ const char* pszGameTypeID = GetGameTypeID();
+ if ( pszGameTypeID )
+ {
+ pKVData->SetString( "GameTypeID", pszGameTypeID );
+ }
+
+ pKVData->SetInt( "RoundsPlayed", m_iRoundsPlayed );
+
+ if ( m_currentMap.m_Header.m_iBlueWins > 0 )
+ {
+ pKVData->SetInt( "BlueWins", m_currentMap.m_Header.m_iBlueWins );
+ }
+
+ if ( m_currentMap.m_Header.m_iRedWins > 0 )
+ {
+ pKVData->SetInt( "RedWins", m_currentMap.m_Header.m_iRedWins );
+ }
+
+ int iRedScore = GetGlobalTFTeam( TF_TEAM_RED )->GetScore();
+ if ( iRedScore )
+ {
+ pKVData->SetInt( "RedScore", iRedScore );
+ }
+
+ int iBlueScore = GetGlobalTFTeam( TF_TEAM_BLUE )->GetScore();
+ if ( iBlueScore )
+ {
+ pKVData->SetInt( "BlueScore", iBlueScore );
+ }
+
+ if ( m_currentMap.m_Header.m_iStalemates > 0 )
+ {
+ pKVData->SetInt( "Stalemates", m_currentMap.m_Header.m_iStalemates );
+ }
+
+ if ( m_currentMap.m_Header.m_iBlueSuddenDeathWins > 0 )
+ {
+ pKVData->SetInt( "BlueSuddenDeathWins", m_currentMap.m_Header.m_iBlueSuddenDeathWins );
+ }
+
+ if ( m_currentMap.m_Header.m_iRedSuddenDeathWins > 0 )
+ {
+ pKVData->SetInt( "RedSuddenDeathWins", m_currentMap.m_Header.m_iRedSuddenDeathWins );
+ }
+
+ if ( m_bServerShutdown )
+ {
+ m_iGameEndReason = RE_SERVER_SHUTDOWN;
+ }
+ else if ( !m_iGameEndReason )
+ {
+ m_iGameEndReason = RE_SERVER_MAP_CHANGE;
+ }
+ int iReason = clamp( m_iGameEndReason, 0, MAX_ROUND_END_REASON-1 );
+ pKVData->SetString( "EndReason", g_aRoundEndReasons[iReason] );
+
+ pKVData->SetInt( "MapVersion", m_currentMap.m_Header.m_nMapRevision );
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: New Steamworks Database Round Data
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_GameStats_WriteRound( int iWinningTeam, bool bFullRound, int iEndReason )
+{
+#if !defined(NO_STEAM)
+ // Flush data gathered so far...
+ GetSteamWorksSGameStatsUploader().FlushStats();
+
+ // Round info.
+ KeyValues* pKVData = new KeyValues( "TF2ServerRounds" );
+ pKVData->SetInt( "MapIndex", 0 );
+ pKVData->SetInt( "RoundIndex", m_iRoundsPlayed );
+
+ const char* pszGameTypeID = GetGameTypeID();
+ if ( pszGameTypeID )
+ {
+ pKVData->SetString( "GameTypeID", pszGameTypeID );
+ }
+
+ pKVData->SetInt( "EndTime", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+ pKVData->SetInt( "StartTime", m_currentRoundRed.m_iRoundStartTime );
+
+ pKVData->SetString( "EndReason", ClampedArrayElement( g_aRoundEndReasons, iEndReason ) );
+
+ iWinningTeam = clamp( iWinningTeam, 0, TF_TEAM_COUNT - 1 );
+ pKVData->SetString( "WinningTeam", ClampedArrayElement( g_aTeamNames, iWinningTeam ) );
+
+ if ( bFullRound )
+ {
+ pKVData->SetInt( "FullRound", bFullRound );
+ }
+
+ int nRoundsRemaining = 0;
+ if ( ( TFGameRules()->GetGameType() == TF_GAMETYPE_ESCORT ) && TFGameRules()->HasMultipleTrains() )
+ {
+ if ( g_hControlPointMasters.Count() && g_hControlPointMasters[0] && g_hControlPointMasters[0]->PlayingMiniRounds() )
+ {
+ nRoundsRemaining = g_hControlPointMasters[0]->NumPlayableControlPointRounds();
+ }
+ }
+ else
+ {
+ if ( g_hControlPointMasters.Count() && g_hControlPointMasters[0] )
+ {
+ nRoundsRemaining = g_hControlPointMasters[0]->NumPlayableControlPointRounds();
+ }
+ }
+ if ( nRoundsRemaining )
+ {
+ pKVData->SetInt( "RoundsRemaining", nRoundsRemaining );
+ }
+
+ int iRedScore = GetGlobalTFTeam( TF_TEAM_RED )->GetScore();
+ if ( iRedScore )
+ {
+ pKVData->SetInt( "RedScore", iRedScore );
+ }
+
+ int iBlueScore = GetGlobalTFTeam( TF_TEAM_BLUE )->GetScore();
+ if ( iBlueScore )
+ {
+ pKVData->SetInt( "BlueScore", iBlueScore );
+ }
+
+ int iRedFlags = GetGlobalTFTeam( TF_TEAM_RED )->GetFlagCaptures();
+ if ( iRedFlags )
+ {
+ pKVData->SetInt( "RedFlagCaps", iRedFlags );
+ }
+
+ int iBlueFlags = GetGlobalTFTeam( TF_TEAM_BLUE )->GetFlagCaptures();
+ if ( iBlueFlags )
+ {
+ pKVData->SetInt( "BlueFlagCaps", iBlueFlags );
+ }
+
+ for ( int iTeam = FIRST_GAME_TEAM; iTeam < TF_TEAM_COUNT; iTeam++ )
+ {
+ int iPlayerCount = GetGlobalTeam( iTeam )->GetNumPlayers();
+ if ( iPlayerCount == 0 )
+ continue;
+ switch ( iTeam )
+ {
+ case TF_TEAM_BLUE:
+ pKVData->SetInt( "BluePlayerCount", iPlayerCount );
+ break;
+ case TF_TEAM_RED:
+ pKVData->SetInt( "RedPlayerCount", iPlayerCount );
+ break;
+ }
+ }
+
+ bool bStalemate = iWinningTeam == TEAM_UNASSIGNED;
+ if ( bStalemate )
+ {
+ pKVData->SetInt( "Stalemate", bStalemate );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iTeamQuit > 0 )
+ {
+ pKVData->SetInt( "RedTeamQuit", m_currentRoundRed.m_Summary.iTeamQuit );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iTeamQuit > 0 )
+ {
+ pKVData->SetInt( "BlueTeamQuit", m_currentRoundBlue.m_Summary.iTeamQuit );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iKills > 0 )
+ {
+ pKVData->SetInt( "RedKills", m_currentRoundRed.m_Summary.iKills );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iKills > 0 )
+ {
+ pKVData->SetInt( "BlueKills", m_currentRoundBlue.m_Summary.iKills );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iDeaths > 0 )
+ {
+ pKVData->SetInt( "RedDeaths", m_currentRoundRed.m_Summary.iDeaths );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iDeaths > 0 )
+ {
+ pKVData->SetInt( "BlueDeaths", m_currentRoundBlue.m_Summary.iDeaths );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iSuicides > 0 )
+ {
+ pKVData->SetInt( "RedSuicides", m_currentRoundRed.m_Summary.iSuicides );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iSuicides > 0 )
+ {
+ pKVData->SetInt( "BlueSuicides", m_currentRoundBlue.m_Summary.iSuicides );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iAssists > 0 )
+ {
+ pKVData->SetInt( "RedAssists", m_currentRoundRed.m_Summary.iAssists );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iAssists > 0 )
+ {
+ pKVData->SetInt( "BlueAssists", m_currentRoundBlue.m_Summary.iAssists );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iBuildingsBuilt > 0 )
+ {
+ pKVData->SetInt( "RedBuildingsBuilt", m_currentRoundRed.m_Summary.iBuildingsBuilt );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iBuildingsBuilt > 0 )
+ {
+ pKVData->SetInt( "BlueBuildingsBuilt", m_currentRoundBlue.m_Summary.iBuildingsBuilt );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iBuildingsDestroyed > 0 )
+ {
+ pKVData->SetInt( "RedBuildingsDestroyed", m_currentRoundRed.m_Summary.iBuildingsDestroyed );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iBuildingsDestroyed > 0 )
+ {
+ pKVData->SetInt( "BlueBuildingsDestroyed", m_currentRoundBlue.m_Summary.iBuildingsDestroyed );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iHeadshots > 0 )
+ {
+ pKVData->SetInt( "RedHeadshots", m_currentRoundRed.m_Summary.iHeadshots );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iHeadshots > 0 )
+ {
+ pKVData->SetInt( "BlueHeadshots", m_currentRoundBlue.m_Summary.iHeadshots );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iDominations > 0 )
+ {
+ pKVData->SetInt( "RedDominations", m_currentRoundRed.m_Summary.iDominations );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iDominations > 0 )
+ {
+ pKVData->SetInt( "BlueDominations", m_currentRoundBlue.m_Summary.iDominations );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iRevenges > 0 )
+ {
+ pKVData->SetInt( "RedRevenges", m_currentRoundRed.m_Summary.iRevenges );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iRevenges > 0 )
+ {
+ pKVData->SetInt( "BlueRevenges", m_currentRoundBlue.m_Summary.iRevenges );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iInvulns > 0 )
+ {
+ pKVData->SetInt( "RedInvulns", m_currentRoundRed.m_Summary.iInvulns );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iInvulns > 0 )
+ {
+ pKVData->SetInt( "BlueInvulns", m_currentRoundBlue.m_Summary.iInvulns );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iTeleports > 0 )
+ {
+ pKVData->SetInt( "RedTeleports", m_currentRoundRed.m_Summary.iTeleports );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iTeleports > 0 )
+ {
+ pKVData->SetInt( "BlueTeleports", m_currentRoundBlue.m_Summary.iTeleports );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iDamageDone > 0 )
+ {
+ pKVData->SetInt( "RedDamageDone", m_currentRoundRed.m_Summary.iDamageDone );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iDamageDone > 0 )
+ {
+ pKVData->SetInt( "BlueDamageDone", m_currentRoundBlue.m_Summary.iDamageDone );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iHealingDone > 0 )
+ {
+ pKVData->SetInt( "RedHealingDone", m_currentRoundRed.m_Summary.iHealingDone );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iHealingDone > 0 )
+ {
+ pKVData->SetInt( "BlueHealingDone", m_currentRoundBlue.m_Summary.iHealingDone );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iCrits > 0 )
+ {
+ pKVData->SetInt( "RedCrits", m_currentRoundRed.m_Summary.iCrits );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iCrits > 0 )
+ {
+ pKVData->SetInt( "BlueCrits", m_currentRoundBlue.m_Summary.iCrits );
+ }
+
+ if ( m_currentRoundRed.m_Summary.iBackstabs > 0 )
+ {
+ pKVData->SetInt( "RedBackstabs", m_currentRoundRed.m_Summary.iBackstabs );
+ }
+
+ if ( m_currentRoundBlue.m_Summary.iBackstabs > 0 )
+ {
+ pKVData->SetInt( "BlueBackstabs", m_currentRoundBlue.m_Summary.iBackstabs );
+ }
+
+ const char *pszReg = GameCoordinator_GetRegistrationString();
+ bool bOfficial = pszReg && V_strstr( pszReg, "'Gordon'" ) && tf_mm_trusted.GetBool();
+ pKVData->SetInt( "IsTrustedServer", bOfficial );
+ //INT_FIELD( nIsOfficial, IsTrustedServer, int8 )
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: New Steamworks Database Player Data
+// Player reports are sent every round and when a player disconnects.
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_GameStats_WritePlayer( CTFPlayer *pPlayer, bool bDisconnected )
+{
+ // Everytime we write out a player we also want to write out their loadout stats
+ Event_PlayerLoadoutChanged( pPlayer, true );
+#if !defined(NO_STEAM)
+ if ( !pPlayer )
+ return;
+
+ if ( pPlayer->IsBot() )
+ return;
+
+ CSteamID steamIDForPlayer;
+ if ( !pPlayer->GetSteamID( &steamIDForPlayer ) )
+ return;
+
+ PlayerStats_t &stats = CTF_GameStats.m_aPlayerStats[pPlayer->entindex()];
+
+ // Player info.
+ KeyValues* pKVData = new KeyValues( "TF2ServerPlayers" );
+ pKVData->SetInt( "MapIndex", 0 );
+
+ int iRoundIndex = m_iRoundsPlayed;
+ if ( bDisconnected && m_bRoundActive )
+ iRoundIndex++; // Increment so we report the current round if the player disconnected before the round was over.
+ pKVData->SetInt( "RoundIndex", iRoundIndex );
+
+ pKVData->SetInt( "PlayerUpdates", ++m_iPlayerUpdates );
+
+ pKVData->SetUint64( "AccountIDPlayer", steamIDForPlayer.ConvertToUint64() );
+
+ pKVData->SetInt( "ConnectTime", stats.iConnectTime );
+ pKVData->SetInt( "DisconnectTime", MAX( stats.iDisconnectTime, 0 ) );
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_KILLS] > 0 )
+ {
+ pKVData->SetInt( "Kills", stats.statsCurrentRound.m_iStat[TFSTAT_KILLS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_DEATHS] > 0 )
+ {
+ pKVData->SetInt( "Deaths", stats.statsCurrentRound.m_iStat[TFSTAT_DEATHS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_SUICIDES] > 0 )
+ {
+ pKVData->SetInt( "Suicides", stats.statsCurrentRound.m_iStat[TFSTAT_SUICIDES] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_KILLASSISTS] > 0 )
+ {
+ pKVData->SetInt( "Assists", stats.statsCurrentRound.m_iStat[TFSTAT_KILLASSISTS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_BUILDINGSBUILT] > 0 )
+ {
+ pKVData->SetInt( "BuildingsBuilt", stats.statsCurrentRound.m_iStat[TFSTAT_BUILDINGSBUILT] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_BUILDINGSDESTROYED] > 0 )
+ {
+ pKVData->SetInt( "BuildingsDestroyed", stats.statsCurrentRound.m_iStat[TFSTAT_BUILDINGSDESTROYED] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_HEADSHOTS] > 0 )
+ {
+ pKVData->SetInt( "Headshots", stats.statsCurrentRound.m_iStat[TFSTAT_HEADSHOTS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_DOMINATIONS] > 0 )
+ {
+ pKVData->SetInt( "Dominations", stats.statsCurrentRound.m_iStat[TFSTAT_DOMINATIONS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_REVENGE] > 0 )
+ {
+ pKVData->SetInt( "Revenges", stats.statsCurrentRound.m_iStat[TFSTAT_REVENGE] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_DAMAGE] > 0 )
+ {
+ pKVData->SetInt( "DamageDone", stats.statsCurrentRound.m_iStat[TFSTAT_DAMAGE] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_DAMAGETAKEN] > 0 )
+ {
+ pKVData->SetInt( "DamageTaken", stats.statsCurrentRound.m_iStat[TFSTAT_DAMAGETAKEN] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_HEALING] > 0 )
+ {
+ pKVData->SetInt( "HealingDone", stats.statsCurrentRound.m_iStat[TFSTAT_HEALING] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_HEALTHKITS] > 0 )
+ {
+ pKVData->SetInt( "HealthKits", stats.statsCurrentRound.m_iStat[TFSTAT_HEALTHKITS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_AMMOKITS] > 0 )
+ {
+ pKVData->SetInt( "AmmoKits", stats.statsCurrentRound.m_iStat[TFSTAT_AMMOKITS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_CLASSCHANGES] > 0 )
+ {
+ pKVData->SetInt( "ClassChanges", stats.statsCurrentRound.m_iStat[TFSTAT_CLASSCHANGES] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_INVULNS] > 0 )
+ {
+ pKVData->SetInt( "Invulns", stats.statsCurrentRound.m_iStat[TFSTAT_INVULNS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_CRITS] > 0 )
+ {
+ pKVData->SetInt( "Crits", stats.statsCurrentRound.m_iStat[TFSTAT_CRITS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_BACKSTABS] > 0 )
+ {
+ pKVData->SetInt( "Backstabs", stats.statsCurrentRound.m_iStat[TFSTAT_BACKSTABS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_TELEPORTS] > 0 )
+ {
+ pKVData->SetInt( "Teleports", stats.statsCurrentRound.m_iStat[TFSTAT_TELEPORTS] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_SHOTS_HIT] > 0 )
+ {
+ pKVData->SetInt( "ShotsHit", stats.statsCurrentRound.m_iStat[TFSTAT_SHOTS_HIT] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_SHOTS_FIRED] > 0 )
+ {
+ pKVData->SetInt( "ShotsFired", stats.statsCurrentRound.m_iStat[TFSTAT_SHOTS_FIRED] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_POINTSSCORED] > 0 )
+ {
+ pKVData->SetInt( "PointsScored", stats.statsCurrentRound.m_iStat[TFSTAT_POINTSSCORED] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_CAPTURES] > 0 )
+ {
+ pKVData->SetInt( "Captures", stats.statsCurrentRound.m_iStat[TFSTAT_CAPTURES] );
+ }
+
+ if ( stats.statsCurrentRound.m_iStat[TFSTAT_DEFENSES] > 0 )
+ {
+ pKVData->SetInt( "Defenses", stats.statsCurrentRound.m_iStat[TFSTAT_DEFENSES] );
+ }
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: New Steamworks Database Kill Data
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_GameStats_WriteKill( CTFPlayer* pKiller, CTFPlayer* pVictim, CTFPlayer* pAssister, IGameEvent* event /*player_death*/, const CTakeDamageInfo &info )
+{
+ if ( !pKiller || !pVictim )
+ return; // Recorded kills must have a killer and a victim.
+
+ if ( pKiller->IsBot() && pVictim->IsBot() )
+ {
+ if ( !pAssister || pAssister->IsBot() )
+ return; // Don't record kills that only involve bots.
+ }
+
+ // 08/26/2010 - For now, don't record kills involving bots at all.
+ // This is to work around an issue with inserting duplicate keys.
+ if ( pKiller->IsBot() || pVictim->IsBot() )
+ return;
+ if ( pAssister && pAssister->IsBot() )
+ return;
+
+ if ( TFGameRules()->State_Get() != GR_STATE_RND_RUNNING )
+ return; // Only record kills during an active round.
+
+ if ( !m_bRoundActive )
+ return;
+
+ // Kills info.
+ KeyValues* pKVData = new KeyValues( "TF2ServerKills" );
+ pKVData->SetInt( "MapIndex", 0 );
+ pKVData->SetInt( "RoundIndex", m_iRoundsPlayed+1 );
+
+ CSteamID steamIDForPlayer;
+ pVictim->GetSteamID( &steamIDForPlayer );
+ if ( !pVictim->IsBot() )
+ {
+ pKVData->SetUint64( "AccountIDVictim", steamIDForPlayer.ConvertToUint64() );
+ }
+ else
+ {
+ pKVData->SetUint64( "AccountIDVictim", 1 );
+ }
+
+ pKiller->GetSteamID( &steamIDForPlayer );
+ if ( !pKiller->IsBot() )
+ {
+ pKVData->SetUint64( "AccountIDKiller", steamIDForPlayer.ConvertToUint64() );
+ }
+ else
+ {
+ pKVData->SetUint64( "AccountIDKiller", 1 );
+ }
+
+ if ( pAssister && !pAssister->IsBot() )
+ {
+ pAssister->GetSteamID( &steamIDForPlayer );
+ pKVData->SetUint64( "AccountIDAssister", steamIDForPlayer.ConvertToUint64() );
+ }
+ else
+ {
+ if ( !pAssister )
+ {
+ pKVData->SetUint64( "AccountIDAssister", 0 );
+ }
+ else if ( pAssister->IsBot() )
+ {
+ pKVData->SetUint64( "AccountIDAssister", 1 );
+ }
+ }
+
+ pKVData->SetInt( "KillCount", ++m_iKillCount );
+
+ pKVData->SetInt( "KillTime", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+
+ // Participant Details
+ const Vector& victimPos = pVictim->GetAbsOrigin();
+ pKVData->SetString( "VictimClass", ClampedArrayElement( g_aPlayerClassNames_NonLocalized, pVictim->GetPlayerClass()->GetClassIndex() ) );
+ pKVData->SetFloat( "VictimLocationX", victimPos.x );
+ pKVData->SetFloat( "VictimLocationY", victimPos.y );
+ pKVData->SetFloat( "VictimLocationZ", victimPos.z );
+
+ const Vector& killerPos = pKiller->GetAbsOrigin();
+ pKVData->SetString( "KillerClass", ClampedArrayElement( g_aPlayerClassNames_NonLocalized, pKiller->GetPlayerClass()->GetClassIndex() ) );
+ pKVData->SetFloat( "KillerLocationX", killerPos.x );
+ pKVData->SetFloat( "KillerLocationY", killerPos.y );
+ pKVData->SetFloat( "KillerLocationZ", killerPos.z );
+
+ if ( pAssister )
+ {
+ const Vector& assisterPos = pAssister->GetAbsOrigin();
+ pKVData->SetString( "AssisterClass", ClampedArrayElement( g_aPlayerClassNames_NonLocalized, pAssister->GetPlayerClass()->GetClassIndex() ) );
+ pKVData->SetFloat( "AssisterLocationX", assisterPos.x );
+ pKVData->SetFloat( "AssisterLocationY", assisterPos.y );
+ pKVData->SetFloat( "AssisterLocationZ", assisterPos.z );
+ }
+
+ int damageBits = event->GetInt( "damagebits" );
+
+ const char* log_name = event->GetString( "weapon_logclassname" );
+ pKVData->SetString( "WeaponLogNameID", log_name );
+
+ pKVData->SetString( "WeaponID", ClampedArrayElement( g_aWeaponNames, event->GetInt( "weaponid" ) ) );
+
+ ETFDmgCustom iCustomKill = (ETFDmgCustom)event->GetInt( "customkill" );
+ if ( iCustomKill > 0 )
+ {
+ pKVData->SetString( "CustomDamageInfo", GetCustomDamageName( iCustomKill ) );
+ }
+
+ int16 damage = clamp( RoundFloatToInt( info.GetDamage() ), 32767, -32767 );
+ pKVData->SetInt( "Damage", damage );
+
+ pKVData->SetFloat( "Distance", victimPos.DistTo( killerPos ) );
+
+ Vector damagePos = info.GetDamagePosition();
+ pKVData->SetFloat( "DamageSourceX", damagePos.x );
+ pKVData->SetFloat( "DamageSourceY", damagePos.y );
+ pKVData->SetFloat( "DamageSourceZ", damagePos.z );
+
+ bool bTest = ( damageBits & DMG_CRITICAL ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsCrit", bTest );
+ }
+
+ // Break down the death flags to make this data easier to handle.
+ int deathFlags = event->GetInt( "death_flags" );
+
+ bTest = ( deathFlags & TF_DEATH_DOMINATION ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsDomination", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_REVENGE ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsRevenge", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_ASSISTER_DOMINATION ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsAssisterDomination", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_ASSISTER_REVENGE ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsAssisterRevenge", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_FIRST_BLOOD ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsFirstBlood", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_FEIGN_DEATH ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsFeignDeath", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_INTERRUPTED ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsInterrupted", bTest );
+ }
+
+ bTest = ( deathFlags & TF_DEATH_GIBBED ) > 0;
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsGibbed", bTest );
+ }
+
+ bTest = ( pKiller == pVictim );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsSuicide", bTest );
+ }
+
+ // Break down the interesting condition flags.
+ int stunFlags = event->GetInt( "stun_flags" );
+
+ bTest = ( pVictim->m_Shared.InCond( TF_COND_STUNNED ) &&
+ ( (stunFlags & TF_STUN_LOSER_STATE) || (stunFlags & TF_STUN_CONTROLS) ) );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimBallStunned", bTest );
+ }
+
+ bTest = pVictim->m_Shared.InCond( TF_COND_URINE );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimJarated", bTest );
+ }
+
+ bTest = pVictim->m_Shared.InCond( TF_COND_BLEEDING );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimBleeding", bTest );
+ }
+
+ bTest = pVictim->m_Shared.InCond( TF_COND_BURNING );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimBurning", bTest );
+ }
+
+ bTest = pVictim->m_Shared.InCond( TF_COND_DISGUISED );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimDisguised", bTest );
+ }
+
+ bTest = pVictim->m_Shared.InCond( TF_COND_STEALTHED );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimStealthed", bTest );
+ }
+
+ bTest = pVictim->m_Shared.InCond( TF_COND_ZOOMED );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsVictimZoomed", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_CRITBOOSTED );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerCritBoosted", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_CRITBOOSTED_RAGE_BUFF );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerRageCritBoosted", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_OFFENSEBUFF );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerSoldierBuffed", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_DEFENSEBUFF );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerSoldierDefenseBuffed", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_REGENONDAMAGEBUFF );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerSoldierRegenBuffed", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_SHIELD_CHARGE );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerShieldCharging", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_DEMO_BUFF );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerEyelanderBuffed", bTest );
+ }
+
+ bTest = pKiller->m_Shared.InCond( TF_COND_ZOOMED );
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerZoomed", bTest );
+ }
+
+ bTest = pKiller->m_Shared.IsInvulnerable();
+ if ( bTest )
+ {
+ pKVData->SetInt( "IsKillerInvulnerable", bTest );
+ }
+
+ int16 victim_health = clamp( pVictim->GetHealthBefore(), 32767, -32767 );
+ pKVData->SetInt( "VictimHealth", victim_health );
+
+ int16 killer_health = clamp( pKiller->GetHealth(), 32767, -32767 );
+ pKVData->SetInt( "KillerHealth", killer_health );
+
+ if ( pAssister )
+ {
+ int16 assister_health = clamp( pAssister->GetHealth(), 32767, -32767 );
+ pKVData->SetInt( "AssisterHealth", assister_health );
+ }
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Records player team activity during a round.
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_TeamChange( CTFPlayer* pPlayer, int oldTeam, int newTeam )
+{
+ if ( pPlayer->IsBot() )
+ return;
+
+ CSteamID steamIDForPlayer;
+ if ( !pPlayer->GetSteamID( &steamIDForPlayer ) )
+ return;
+
+ if ( oldTeam == newTeam )
+ return;
+
+// if ( oldTeam == 0 || newTeam == 0 )
+// return;
+
+#if !defined(NO_STEAM)
+ KeyValues* pKVData = new KeyValues( "TF2ServerTeamChanges" );
+ pKVData->SetInt( "MapIndex", 0 );
+ pKVData->SetInt( "RoundIndex", m_iRoundsPlayed+1 );
+// pKVData->SetInt( "TimeSubmitted", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+
+ pKVData->SetString( "OldTeam", ClampedArrayElement( g_aTeamNames, oldTeam ) );
+ pKVData->SetString( "NewTeam", ClampedArrayElement( g_aTeamNames, newTeam ) );
+
+ pKVData->SetInt( "ChangeCount", pPlayer->GetTeamChangeCount() );
+ pKVData->SetUint64( "AccountIDPlayer", steamIDForPlayer.ConvertToUint64() );
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Records players touching currency packs - primarily MvM, but future modes will likely use
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerCollectedCurrency( CBasePlayer *pPlayer, int nAmount )
+{
+ Assert( pPlayer );
+
+ CTFPlayer *pTFPlayer = ToTFPlayer( pPlayer );
+ if ( pTFPlayer )
+ {
+ IncrementStat( pTFPlayer, TFSTAT_CURRENCY_COLLECTED, nAmount );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Records the item set a player is using and for how long (until class, map, server or loadout change)
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerLoadoutChanged( CTFPlayer *pPlayer, bool bForceReport )
+{
+ // Steam needs to be updated to take in the new table. So we disable the table first
+#if !defined(NO_STEAM)
+ if ( !pPlayer )
+ return;
+
+ if ( pPlayer->IsBot() )
+ return;
+
+ CSteamID steamIDForPlayer;
+ if ( !pPlayer->GetSteamID( &steamIDForPlayer ) )
+ return;
+
+ PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()];
+
+ // Not enough time reported, reset
+ int iSecondsUsed = (int)( gpGlobals->curtime - stats.loadoutStats.flStartTime );
+ bool bActuallyChanged = false;
+ bool bIsInit = false;
+
+ int iPrevClass = stats.loadoutStats.iClass;
+ bActuallyChanged |= pPlayer->GetPlayerClass()->GetClassIndex() != iPrevClass;
+
+ // if this is the first time through, class is invalid and we dont want to report anything
+
+ // Table updated, using v2
+ KeyValues* pKVData = new KeyValues( "TF2ServerPlayerLoadoutv2" );
+
+ int iSlotCount = LOADOUT_POSITION_MISC2 + 1;
+ for ( int iSlot = 0; iSlot < iSlotCount; ++iSlot )
+ {
+ int iDefIndex = stats.loadoutStats.iLoadoutItemDefIndices[ iSlot ];
+ bIsInit |= iDefIndex != INVALID_ITEM_DEF_INDEX;
+
+ pKVData->SetInt( CFmtStr("SlotDef%d", iSlot), iDefIndex );
+ pKVData->SetInt( CFmtStr("SlotQuality%d", iSlot), stats.loadoutStats.iLoadoutItemQualities[ iSlot ] );
+ pKVData->SetInt( CFmtStr("SlotStyle%d", iSlot), stats.loadoutStats.iLoadoutItemStyles[ iSlot ] );
+
+ // Check to see if the item actually changed
+ item_definition_index_t iItemDef = INVALID_ITEM_DEF_INDEX;
+ CEconItemView *pItem = pPlayer->GetLoadoutItem( pPlayer->GetPlayerClass()->GetClassIndex(), iSlot );
+ if ( pItem )
+ {
+ iItemDef = pItem->GetItemDefIndex();
+ }
+
+ // Check if there was actually a change
+ bActuallyChanged |= stats.loadoutStats.iLoadoutItemDefIndices[ iSlot ] != iItemDef;
+
+ // Set the new items
+ int iItemQuality = pItem ? pItem->GetItemQuality() : AE_UNDEFINED;
+ style_index_t iItemStyle = pItem ? pItem->GetStyle() : 0;
+ stats.loadoutStats.SetItemDef( iSlot, iItemDef, iItemQuality, iItemStyle );
+ }
+
+ pKVData->SetInt( "ID", ++m_iLoadoutChangesCount );
+ pKVData->SetInt( "SecondsEquipped", iSecondsUsed );
+ pKVData->SetInt( "Class", iPrevClass );
+ pKVData->SetInt( "AccountIDPlayer", (int)steamIDForPlayer.ConvertToUint64() ); // OGS does not actually support uints
+
+ if ( iPrevClass < TF_FIRST_NORMAL_CLASS || iPrevClass >= TF_LAST_NORMAL_CLASS )
+ {
+ bIsInit = false;
+ }
+
+ // pKVData->SetInt( "TimeSubmitted", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+ // Ignore forced respawn calls that trigger this but don't actually change your loadout
+ // do not report if time used is less then 5 minutes
+ if ( ( bActuallyChanged || bForceReport ) )
+ {
+ if ( stats.loadoutStats.flStartTime > 0 && iSecondsUsed > 300 && bIsInit )
+ {
+ // IsCompetitive
+ bool bIsCompetitive = TFGameRules() ? TFGameRules()->IsCompetitiveMode() : false;
+
+ // IsTrusted
+ const char *pszReg = GameCoordinator_GetRegistrationString();
+ bool bOfficial = pszReg && V_strstr( pszReg, "'Gordon'" ) && tf_mm_trusted.GetBool();
+
+ pKVData->SetInt( "IsTrustedServer", bOfficial );
+ pKVData->SetInt( "IsCompetitive", bIsCompetitive );
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+ }
+
+ stats.loadoutStats.Set( pPlayer->GetPlayerClass()->GetClassIndex() );
+ }
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerRevived( CTFPlayer *pPlayer )
+{
+ IncrementStat( pPlayer, TFSTAT_REVIVED, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerThrowableHit( CTFPlayer *pAttacker )
+{
+ IncrementStat( pAttacker, TFSTAT_THROWABLEHIT, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerThrowableKill( CTFPlayer *pAttacker )
+{
+ IncrementStat( pAttacker, TFSTAT_THROWABLEKILL, 1 );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Track only their highest - not cumulative
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PlayerEarnedKillStreak( CTFPlayer *pAttacker )
+{
+ if ( !pAttacker )
+ return;
+
+ PlayerStats_t &stats = m_aPlayerStats[pAttacker->entindex()];
+ int nCount = pAttacker->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_Kills );
+ int nMax = stats.statsCurrentRound.m_iStat[TFSTAT_KILLSTREAK_MAX];
+ if ( nCount > nMax )
+ {
+ stats.statsCurrentRound.m_iStat[TFSTAT_KILLSTREAK_MAX] = nCount;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Halloween!
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_HalloweenBossEvent( uint8 unBossType, uint16 unBossLevel, uint8 unEventType, uint8 unPlayersInvolved, float fElapsedTime )
+{
+ //if ( !GCClientSystem() )
+ // return;
+
+ //static uint8 unEventCounter = 0;
+
+ //GCSDK::CProtoBufMsg<CMsgHalloween_ServerBossEvent> msg( k_EMsgGC_Halloween_ServerBossEvent );
+
+ //msg.Body().set_event_counter( unEventCounter++ );
+ //msg.Body().set_timestamp( CRTime::RTime32TimeCur() );
+ //msg.Body().set_boss_type( unBossType );
+ //msg.Body().set_boss_level( unBossLevel );
+ //msg.Body().set_event_type( unEventType );
+ //msg.Body().set_players_involved( unPlayersInvolved );
+ //msg.Body().set_elapsed_time( fElapsedTime );
+
+ //GCClientSystem()->BSendMessage( msg );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Records player class activity during a round.
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_ClassChange( CTFPlayer* pPlayer, int oldClass, int newClass )
+{
+ if ( pPlayer->IsBot() )
+ return;
+
+ CSteamID steamIDForPlayer;
+ if ( !pPlayer->GetSteamID( &steamIDForPlayer ) )
+ return;
+
+ if ( oldClass == newClass )
+ return;
+
+// if ( oldClass == 0 || newClass == 0 )
+// return;
+
+#if !defined(NO_STEAM)
+ KeyValues* pKVData = new KeyValues( "TF2ServerClassChanges" );
+// pKVData->SetInt( "MapIndex", CBGSDriver.m_iNumLevels+1 );
+ pKVData->SetInt( "RoundIndex", m_iRoundsPlayed+1 );
+// pKVData->SetInt( "TimeSubmitted", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+
+ oldClass = clamp( oldClass, 0, TF_CLASS_COUNT-1 );
+ pKVData->SetString( "OldClass", g_aPlayerClassNames_NonLocalized[oldClass] );
+
+ newClass = clamp( newClass, 0, TF_CLASS_COUNT-1 );
+ pKVData->SetString( "NewClass", g_aPlayerClassNames_NonLocalized[newClass] );
+
+ pKVData->SetInt( "ChangeCount", pPlayer->GetClassChangeCount() );
+ pKVData->SetUint64( "AccountIDPlayer", steamIDForPlayer.ConvertToUint64() );
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Records player scoring activity during a round.
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_GameEvent( CTFPlayer* pPlayer, const char* pszEventID, int iPoints )
+{
+ if ( pPlayer && pPlayer->IsBot() )
+ return;
+
+#if !defined(NO_STEAM)
+ KeyValues* pKVData = new KeyValues( "TF2ServerGameEvents" );
+ pKVData->SetInt( "RoundIndex", m_iRoundsPlayed+1 );
+// pKVData->SetInt( "TimeSubmitted", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+ pKVData->SetInt( "ChangeCount", ++m_iEvents );
+ pKVData->SetString( "EventID", pszEventID );
+ if ( iPoints )
+ {
+ pKVData->SetInt( "Points", iPoints );
+ }
+ if ( pPlayer )
+ {
+ pKVData->SetString( "Team", ClampedArrayElement( g_aTeamNames, pPlayer->GetTeamNumber() ) );
+
+ pKVData->SetFloat( "LocationX", pPlayer->GetAbsOrigin().x );
+ pKVData->SetFloat( "LocationY", pPlayer->GetAbsOrigin().y );
+ pKVData->SetFloat( "LocationZ", pPlayer->GetAbsOrigin().z );
+
+ CSteamID steamIDForPlayer;
+ if ( pPlayer->GetSteamID( &steamIDForPlayer ) )
+ {
+ pKVData->SetUint64( "AccountIDPlayer", steamIDForPlayer.ConvertToUint64() );
+ }
+ else
+ {
+ pKVData->SetUint64( "AccountIDPlayer", 0 );
+ }
+ }
+ else
+ {
+ pKVData->SetString( "Team", g_aTeamNames[0] );
+ pKVData->SetUint64( "AccountIDPlayer", 0 );
+ }
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Records flag activity during a match.
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_FlagEvent( int iPlayer, int iCarrier, int iEventType )
+{
+ CTFPlayer* pPlayer = ToTFPlayer( UTIL_PlayerByIndex(iPlayer) );
+
+ int iPoints = 0;
+ const char* pszEventID = NULL;
+ switch ( iEventType )
+ {
+ case TF_FLAGEVENT_PICKUP:
+ pszEventID = "flag_pickup";
+ break;
+
+ case TF_FLAGEVENT_CAPTURE:
+ pszEventID = "flag_captured";
+ iPoints = 1;
+ break;
+
+ case TF_FLAGEVENT_DEFEND:
+ pszEventID = "flag_defended";
+ iPoints = 1;
+ break;
+
+ case TF_FLAGEVENT_DROPPED:
+ pszEventID = "flag_dropped";
+ break;
+
+ case TF_FLAGEVENT_RETURNED:
+ pszEventID = "flag_returned";
+ break;
+ }
+
+ SW_GameEvent( pPlayer, pszEventID, iPoints );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Records control point activity during a match.
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_CapEvent( int iPoint, int iPlayer, const char* pszEventID, int iPoints )
+{
+ CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex(iPlayer) );
+ if ( !pPlayer )
+ return;
+
+ SW_GameEvent( pPlayer, pszEventID, iPoints );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Uploads the hosts row, which has general information about the game server.
+//-----------------------------------------------------------------------------
+void CTFGameStats::SW_WriteHostsRow()
+{
+#if !defined(NO_STEAM)
+
+ // Gather info we'll be sending...
+ int maxClients = gpGlobals->maxClients;
+ bool isDedicated = engine->IsDedicatedServer();
+ bool isVACSecure = SteamGameServer_BSecure();
+ bool cheatsWereOn = TFGameRules() && TFGameRules()->HaveCheatsBeenEnabledDuringLevel();
+ bool isPassword = GetSteamWorksSGameStatsUploader().IsPassworded();
+
+ KeyValues* pKVData = new KeyValues( "TF2ServerHosts" );
+
+ // Server Browse Info
+ pKVData->SetInt( "ServerIP", GetSteamWorksSGameStatsUploader().GetServerIP() );
+ pKVData->SetString( "ServerName", GetSteamWorksSGameStatsUploader().GetHostName() );
+
+ // Server Config
+ pKVData->SetInt( "PlayerSlots", maxClients );
+ if ( isDedicated )
+ {
+ pKVData->SetInt( "IsDedicated", isDedicated );
+ }
+ if ( isPassword )
+ {
+ pKVData->SetInt( "IsPassworded", isPassword );
+ }
+ if ( isVACSecure )
+ {
+ pKVData->SetInt( "IsVACSecure", isVACSecure );
+ }
+ if ( cheatsWereOn )
+ {
+ pKVData->SetInt( "IsCheats", cheatsWereOn );
+ }
+ ConVarRef mp_timelimit( "mp_timelimit" );
+ if ( mp_timelimit.GetInt() )
+ {
+ pKVData->SetInt( "TimeLimit", mp_timelimit.GetInt() );
+ }
+ ConVarRef tf_flag_caps_per_round( "tf_flag_caps_per_round" );
+ if ( tf_flag_caps_per_round.GetInt() )
+ {
+ pKVData->SetInt( "FlagCapsPerRound", tf_flag_caps_per_round.GetInt() );
+ }
+ ConVarRef mp_maxrounds( "mp_maxrounds" );
+ if ( mp_maxrounds.GetInt() )
+ {
+ pKVData->SetInt( "MaxRounds", mp_maxrounds.GetInt() );
+ }
+ ConVarRef mp_winlimit( "mp_winlimit" );
+ if ( mp_winlimit.GetInt() )
+ {
+ pKVData->SetInt( "WinLimit", mp_winlimit.GetInt() );
+ }
+ ConVarRef mp_disable_respawn_times( "mp_disable_respawn_times" );
+ if ( mp_disable_respawn_times.GetInt() )
+ {
+ pKVData->SetInt( "DisableRespawnTimes", mp_disable_respawn_times.GetInt() );
+ }
+ ConVarRef mp_stalemate_meleeonly( "mp_stalemate_meleeonly" );
+ if ( mp_stalemate_meleeonly.GetInt() )
+ {
+ pKVData->SetInt( "StalemateMeleeOnly", mp_stalemate_meleeonly.GetInt() );
+ }
+ ConVarRef mp_forceautoteam( "mp_forceautoteam" );
+ if ( mp_forceautoteam.GetInt() )
+ {
+ pKVData->SetInt( "ForceAutoTeam", mp_forceautoteam.GetInt() );
+ }
+
+ // Server Activity Info
+ RTime32 starttime = GetSteamWorksSGameStatsUploader().GetStartTime();
+ if ( starttime )
+ {
+ pKVData->SetInt( "ServerStartTime", starttime );
+ }
+
+ RTime32 endtime = GetSteamWorksSGameStatsUploader().GetEndTime();
+ if ( endtime )
+ {
+ pKVData->SetInt( "ServerEndTime", endtime );
+ }
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+
+#endif
+}
+
+//-----------------------------------------------------------------------------
+#undef min
+#undef max
+struct PasstimeHistogramStats
+{
+ double min, max, mean, median, mode, stdev;
+ PasstimeHistogramStats() : min(0), max(0), mean(0), median(0), mode(0), stdev(0) {}
+};
+
+static int qsort_ascending_uint16( const void *a, const void *b )
+{
+ return *((uint16*)b) - *((uint16*)a);
+}
+
+template<int TMaxSamples>
+static PasstimeHistogramStats Passtime_SampleStats( const uint16 (&samples)[TMaxSamples], uint16 iSampleCount )
+{
+ PasstimeHistogramStats result;
+ if ( (iSampleCount <= 1) || (iSampleCount > TMaxSamples) )
+ {
+ return result;
+ }
+
+ // mode is useless, so don't bother
+
+ // sort for median
+ qsort( (void*) &samples[0], iSampleCount, sizeof(samples[0]), &qsort_ascending_uint16 );
+
+ //
+ // Sum, Min, Max
+ //
+ double sum = 0;
+ result.min = DBL_MAX;
+ result.max = DBL_MIN;
+ for ( uint32 i = 0; i < iSampleCount; ++i )
+ {
+ float s = samples[i];
+ sum += s;
+ result.min = MIN( s, result.min );
+ result.max = MAX( s, result.max );
+ }
+
+ //
+ // Mean
+ //
+ result.mean = (double)sum / (double)iSampleCount;
+
+ //
+ // Median
+ //
+ result.median = samples[ iSampleCount / 2 ]; // close enough
+
+ //
+ // Stdev
+ //
+ for ( uint32 i = 0; i < iSampleCount; ++i )
+ {
+ double s = samples[i];
+ result.stdev += (s - result.mean) * (s - result.mean);
+ }
+ result.stdev = sqrt( result.stdev / ((double)(iSampleCount - 1)) );
+
+ return result;
+}
+
+template<int TBinCount>
+static PasstimeHistogramStats Passtime_HistogramStats( const uint32 (&hist)[TBinCount], uint32 iHistSum, uint32 iSampleCount )
+{
+ PasstimeHistogramStats result;
+ if ( iSampleCount <= 1 )
+ {
+ return result;
+ }
+
+ //
+ // Mean
+ //
+ result.mean = (float)iHistSum / (float)iSampleCount;
+
+ //
+ // Min
+ //
+ for ( uint32 i = 0; i < 256; ++i )
+ {
+ if ( hist[i] != 0 )
+ {
+ result.min = i;
+ break;
+ }
+ }
+
+ //
+ // Max
+ //
+ for ( int32 i = 255; i >= 0; --i )
+ {
+ if ( hist[i] != 0 )
+ {
+ result.max = i;
+ break;
+ }
+ }
+
+ //
+ // Median
+ //
+ int iMedSample = iSampleCount / 2;
+ int iMedian;
+ for ( iMedian = 0; iMedian < 256; ++iMedian )
+ {
+ if ( hist[iMedian] != 0 )
+ break;
+ }
+ while( (iMedSample > 0) && (iMedian < 256) )
+ {
+ iMedSample -= hist[iMedian];
+ ++iMedian;
+ }
+ result.median = iMedian - 1;
+
+ //
+ // Mode, stdev
+ //
+ uint32 iLargestCount = 0;
+ result.mode = -1; // wat
+ for ( uint32 i = 0; i < 256; ++i )
+ {
+ uint32 iSampleCount = hist[i];
+ for ( uint32 j = 0; j < iSampleCount; ++j )
+ {
+ // this feels dumb
+ result.stdev += (i - result.mean) * (i - result.mean);
+ }
+
+ if ( iSampleCount > iLargestCount )
+ {
+ iLargestCount = iSampleCount;
+ result.mode = i;
+ }
+ }
+ result.stdev = sqrt( result.stdev / ((double)iSampleCount - 1) );
+
+ return result;
+}
+
+void CTFGameStats::SW_PasstimeRoundEnded()
+{
+#if !defined(NO_STEAM)
+ if ( !TFGameRules() || !g_pPasstimeLogic )
+ {
+ return;
+ }
+
+ // Flush data gathered so far...
+ GetSteamWorksSGameStatsUploader().FlushStats();
+
+ KeyValues *pKVData = new KeyValues( "TF2ServerPasstimeRoundEndedv2" );
+ pKVData->SetString( "MapID", m_currentMap.m_Header.m_szMapName ); // Reference table
+ pKVData->SetInt( "RoundIndex", m_iRoundsPlayed );
+
+ pKVData->SetInt( "TotalPassesStarted", m_passtimeStats.summary.nTotalPassesStarted );
+ pKVData->SetInt( "TotalPassesFailed", m_passtimeStats.summary.nTotalPassesFailed );
+ pKVData->SetInt( "TotalPassesShotDown", m_passtimeStats.summary.nTotalPassesShotDown );
+ pKVData->SetInt( "TotalPassesCompleted", m_passtimeStats.summary.nTotalPassesCompleted );
+ pKVData->SetInt( "TotalPassesCompletedNearGoal", m_passtimeStats.summary.nTotalPassesCompletedNearGoal );
+ pKVData->SetInt( "TotalPassesIntercepted", m_passtimeStats.summary.nTotalPassesIntercepted );
+ pKVData->SetInt( "TotalPassesInterceptedNearGoal", m_passtimeStats.summary.nTotalPassesInterceptedNearGoal );
+ pKVData->SetInt( "TotalPassRequests", m_passtimeStats.summary.nTotalPassRequests );
+ pKVData->SetInt( "TotalTosses", m_passtimeStats.summary.nTotalTosses );
+ pKVData->SetInt( "TotalTossesCompleted", m_passtimeStats.summary.nTotalTossesCompleted );
+ pKVData->SetInt( "TotalTossesIntercepted", m_passtimeStats.summary.nTotalTossesIntercepted );
+ pKVData->SetInt( "TotalTossesInterceptedNearGoal", m_passtimeStats.summary.nTotalTossesInterceptedNearGoal );
+ pKVData->SetInt( "TotalSteals", m_passtimeStats.summary.nTotalSteals );
+ pKVData->SetInt( "TotalStealsNearGoal", m_passtimeStats.summary.nTotalStealsNearGoal );
+ pKVData->SetInt( "TotalBallSpawnShots", m_passtimeStats.summary.nTotalBallSpawnShots );
+ pKVData->SetInt( "TotalScores", m_passtimeStats.summary.nTotalScores );
+ pKVData->SetInt( "TotalRecoveries", m_passtimeStats.summary.nTotalRecoveries );
+ pKVData->SetInt( "TotalCarrySec", m_passtimeStats.summary.nTotalCarrySec );
+ pKVData->SetInt( "TotalWinningTeamBallCarrySec", m_passtimeStats.summary.nTotalWinningTeamBallCarrySec );
+ pKVData->SetInt( "TotalLosingTeamBallCarrySec", m_passtimeStats.summary.nTotalLosingTeamBallCarrySec );
+ pKVData->SetInt( "TotalThrowCancels", m_passtimeStats.summary.nTotalThrowCancels );
+ pKVData->SetInt( "TotalSpeedBoosts", m_passtimeStats.summary.nTotalSpeedBoosts );
+ pKVData->SetInt( "TotalJumpPads", m_passtimeStats.summary.nTotalJumpPads );
+ pKVData->SetInt( "TotalCarrierSpeedBoosts", m_passtimeStats.summary.nTotalCarrierSpeedBoosts );
+ pKVData->SetInt( "TotalCarrierJumpPads", m_passtimeStats.summary.nTotalCarrierJumpPads );
+ pKVData->SetInt( "BallNeutralSec", m_passtimeStats.summary.nBallNeutralSec );
+ pKVData->SetInt( "GoalType", m_passtimeStats.summary.nGoalType );
+ pKVData->SetInt( "RoundEndReason", m_passtimeStats.summary.nRoundEndReason );
+ pKVData->SetInt( "RoundRemainingSec", m_passtimeStats.summary.nRoundRemainingSec );
+ pKVData->SetInt( "RoundMaxSec", m_passtimeStats.summary.nRoundMaxSec );
+ pKVData->SetInt( "RoundElapsedSec", m_passtimeStats.summary.nRoundMaxSec - m_passtimeStats.summary.nRoundRemainingSec );
+ pKVData->SetInt( "PlayersBlueMax", m_passtimeStats.summary.nPlayersBlueMax );
+ pKVData->SetInt( "PlayersRedMax", m_passtimeStats.summary.nPlayersRedMax );
+ pKVData->SetInt( "ScoreRed", m_passtimeStats.summary.nScoreRed );
+ pKVData->SetInt( "ScoreBlue", m_passtimeStats.summary.nScoreBlue );
+
+ pKVData->SetBool( "Stalemate", m_passtimeStats.summary.bStalemate );
+ pKVData->SetBool( "SuddenDeath", m_passtimeStats.summary.bSuddenDeath );
+ pKVData->SetBool( "MeleeOnlySuddenDeath", m_passtimeStats.summary.bMeleeOnlySuddenDeath );
+
+ auto ballFracStats = Passtime_HistogramStats( m_passtimeStats.summary.arrBallFracHist,
+ m_passtimeStats.summary.nBallFracHistSum, m_passtimeStats.summary.nBallFracSampleCount );
+ pKVData->SetInt( "BallFracHistMin", (int)round( ballFracStats.min ) );
+ pKVData->SetInt( "BallFracHistMax", (int)round( ballFracStats.max ) );
+ pKVData->SetInt( "BallFracHistMean", (int)round( ballFracStats.mean ) );
+ pKVData->SetInt( "BallFracHistMedian", (int)round( ballFracStats.median ) );
+ pKVData->SetInt( "BallFracHistMode", (int)round( ballFracStats.mode ) );
+ pKVData->SetInt( "BallFracHistStdev", (int)round( ballFracStats.stdev ) );
+ pKVData->SetInt( "BallFracHistSampleCount", (int)m_passtimeStats.summary.nBallFracSampleCount ); // for approx global average
+
+ auto passTravelDistStats = Passtime_SampleStats(
+ m_passtimeStats.summary.arrPassTravelDistSamples, m_passtimeStats.summary.nPassTravelDistSampleCount );
+ pKVData->SetInt( "PassTravelDistMin", (int)round( passTravelDistStats.min ) );
+ pKVData->SetInt( "PassTravelDistMax", (int)round( passTravelDistStats.max ) );
+ pKVData->SetInt( "PassTravelDistMean", (int)round( passTravelDistStats.mean ) );
+ pKVData->SetInt( "PassTravelDistMedian", (int)round( passTravelDistStats.median ) );
+ //pKVData->SetInt( "PassTravelDistMode", (int) round( passTravelDistStats.mode ) ); meaningless
+ pKVData->SetInt( "PassTravelDistStdev", (int)round( passTravelDistStats.stdev ) );
+ pKVData->SetInt( "PassTravelDistSampleCount", (int)m_passtimeStats.summary.nPassTravelDistSampleCount ); // for approx global average
+
+ // have to flatten class stats because stats system can't handle nested tables
+ {
+ char aClassKey[32] = { 0, };
+ for ( int nClass = TF_FIRST_NORMAL_CLASS; nClass <= TF_LAST_NORMAL_CLASS; ++nClass )
+ {
+ V_sprintf_safe( aClassKey, "TotalScores_%s", g_aRawPlayerClassNamesShort[nClass] );
+ pKVData->SetInt( aClassKey, m_passtimeStats.classes[nClass].nTotalScores );
+ V_sprintf_safe( aClassKey, "TotalCarrySec_%s", g_aRawPlayerClassNamesShort[nClass] );
+ pKVData->SetInt( aClassKey, m_passtimeStats.classes[nClass].nTotalCarrySec );
+ }
+ }
+
+ const char *pszReg = GameCoordinator_GetRegistrationString();
+ bool bOfficial = pszReg && V_strstr( pszReg, "'Gordon'" ) && tf_mm_trusted.GetBool();
+ pKVData->SetInt( "IsTrustedServer", bOfficial );
+
+ if ( tf_passtime_save_stats.GetBool() )
+ {
+ // do this before AddStatsForUpload because it might actually just delete pKVData
+
+ // i need to copy it because i need to add some keys and i don't want there to be any possibility
+ // of tainting the kv that's sent to the stats server
+ auto pKVCopy = pKVData->MakeCopy();
+ auto iNow = CRTime::RTime32TimeCur();
+ char filename[128];
+ V_sprintf_safe(filename, "passtime_stats_%u.txt", iNow);
+
+ // add keys to simulate what the stats server usually adds to the database automatically
+ pKVData->SetInt( "SessionID", 0 );
+ pKVData->SetInt( "TimeReported", iNow );
+
+ pKVData->SaveToFile( g_pFullFileSystem, filename );
+ pKVCopy->deleteThis();
+ }
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+
+#endif
+}
+
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PowerUpModeDeath( CTFPlayer *pKiller, CTFPlayer *pVictim )
+{
+#if !defined(NO_STEAM)
+
+ //START_TABLE( k_ESchemaCatalogOGS, TF2PowerUpModeKillsv2, TABLE_PROP_NORMAL )
+ // INT_FIELD( llSessionID, SessionID, uint64 ) // Reporting server
+ // INT_FIELD( nAccountID, AccountID, int32 ) // Player
+ // INT_FIELD( nID, ID, int32 ) // ID
+ // INT_FIELD( bIsTrustedServer, IsTrustedServer, bool )
+ // INT_FIELD( nKillerClass, KillerClass, int16 )
+ // INT_FIELD( nKillerRune, KillerRune, int16 )
+ // INT_FIELD( nKillerKillstreak, KillerKillstreak, int16 )
+
+ // INT_FIELD( nKillerPrimary, KillerPrimary, int32 )
+ // INT_FIELD( nKillerSecondary, KillerSecondary, int32 )
+ // INT_FIELD( nKillerMelee, KillerMelee, int32 )
+
+ // INT_FIELD( nVictimClass, VictimClass, int16 )
+ // INT_FIELD( nVictimRune, VictimRune, int16 )
+ // INT_FIELD( nVictimKillstreak, VictimKillstreak, int16 )
+ // INT_FIELD( RTime32UpdateTime, TimeSubmitted, RTime32 )
+ // PRIMARY_KEYS_CLUSTERED( 80, nAccountID, RTime32UpdateTime, llSessionID )
+ // WIPE_TABLE_BETWEEN_TESTS( k_EWipePolicyWipeForAllTests )
+ // PARTITION_INTERVAL( k_EPartitionIntervalDaily )
+ // OWNING_APPLICATION( 440 )
+ // END_TABLE
+
+ if ( !TFGameRules() || !TFGameRules()->IsPowerupMode() )
+ return;
+
+ if ( !pKiller || !pVictim )
+ return;
+
+ if ( pKiller->IsBot() || pVictim->IsBot() )
+ return;
+
+ CSteamID killerID;
+ pKiller->GetSteamID( &killerID );
+
+ if ( !killerID.IsValid() || !killerID.BIndividualAccount() )
+ return;
+
+ const char *pszReg = GameCoordinator_GetRegistrationString();
+ bool bOfficial = pszReg && V_strstr( pszReg, "'Gordon'" ) && tf_mm_trusted.GetBool();
+
+ KeyValues* pKVData = new KeyValues( "TF2PowerUpModeKillsv2" );
+
+ pKVData->SetInt( "AccountID", (int)killerID.GetAccountID() );
+ pKVData->SetInt( "ID", (int)m_iEvents++ );
+ pKVData->SetInt( "IsTrustedServer", bOfficial );
+ pKVData->SetInt( "KillerClass", pKiller->GetPlayerClass()->GetClassIndex() );
+ pKVData->SetInt( "KillerRune", GetConditionFromRuneType( pKiller->m_Shared.GetCarryingRuneType() ) );
+ pKVData->SetInt( "KillerKillstreak", pKiller->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_KillsAll ) );
+
+ CEconItemView *pItem = pKiller->GetLoadoutItem( pKiller->GetPlayerClass()->GetClassIndex(), LOADOUT_POSITION_PRIMARY );
+ item_definition_index_t iItemDef = pItem ? pItem->GetItemDefIndex() : 0;
+ pKVData->SetInt( "KillerPrimary", iItemDef );
+
+ pItem = pKiller->GetLoadoutItem( pKiller->GetPlayerClass()->GetClassIndex(), LOADOUT_POSITION_SECONDARY );
+ iItemDef = pItem ? pItem->GetItemDefIndex() : 0;
+ pKVData->SetInt( "KillerSecondary", iItemDef );
+
+ pItem = pKiller->GetLoadoutItem( pKiller->GetPlayerClass()->GetClassIndex(), LOADOUT_POSITION_MELEE );
+ iItemDef = pItem ? pItem->GetItemDefIndex() : 0;
+ pKVData->SetInt( "KillerMelee", iItemDef );
+
+ pKVData->SetInt( "VictimClass", pVictim->GetPlayerClass()->GetClassIndex() );
+ pKVData->SetInt( "VictimRune", GetConditionFromRuneType( pVictim->m_Shared.GetCarryingRuneType() ) );
+ pKVData->SetInt( "VictimKillstreak", pVictim->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_KillsAll ) );
+
+ //pKVData->SetInt( "TimeSubmitted", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+
+#endif // !NO_STEAM
+}
+
+//-----------------------------------------------------------------------------
+void CTFGameStats::Event_PowerUpRuneDuration( CTFPlayer *pPlayer, int iDuration, int nRune )
+{
+#if !defined(NO_STEAM)
+
+//-----------------------------------------------------------------------------
+// OGS: TF2 PowerUp Mode - Power Up duration
+//-----------------------------------------------------------------------------
+
+ //START_TABLE( k_ESchemaCatalogOGS, TF2PowerUpModeRuneDuration, TABLE_PROP_NORMAL )
+ // INT_FIELD( llSessionID, SessionID, uint64 ) // Reporting server
+ // INT_FIELD( nAccountID, AccountID, uint32 ) // Player
+ // INT_FIELD( nID, ID, int32 ) // ID
+ // INT_FIELD( bIsTrustedServer, IsTrustedServer, bool )
+ // INT_FIELD( nPlayerClass, PlayerClass, int16 )
+ // INT_FIELD( nPlayerRune, PlayerRune, int16 )
+ // INT_FIELD( nPlayerKillstreak, PlayerKillstreak, int16 )
+ // INT_FIELD( nRuneDuration, RuneDuration, int32 )
+ // INT_FIELD( nPlayerPrimary, PlayerPrimary, int32 )
+ // INT_FIELD( nPlayerSecondary, PlayerSecondary, int32 )
+ // INT_FIELD( nPlayerMelee, PlayerMelee, int32 )
+ // INT_FIELD( RTime32UpdateTime, TimeSubmitted, RTime32 )
+ // PRIMARY_KEYS_CLUSTERED( 80, nAccountID, nID, RTime32UpdateTime, llSessionID )
+ // WIPE_TABLE_BETWEEN_TESTS( k_EWipePolicyWipeForAllTests )
+ // PARTITION_INTERVAL( k_EPartitionIntervalDaily )
+ // OWNING_APPLICATION( 440 )
+ // END_TABLE
+
+ if ( !TFGameRules() || !TFGameRules()->IsPowerupMode() )
+ return;
+
+ if ( !pPlayer || pPlayer->IsBot() )
+ return;
+
+ CSteamID playerID;
+ pPlayer->GetSteamID( &playerID );
+
+ if ( !playerID.IsValid() || !playerID.BIndividualAccount() )
+ return;
+
+ const char *pszReg = GameCoordinator_GetRegistrationString();
+ bool bOfficial = pszReg && V_strstr( pszReg, "'Gordon'" ) && tf_mm_trusted.GetBool();
+
+ KeyValues* pKVData = new KeyValues( "TF2PowerUpModeRuneDuration" );
+
+ pKVData->SetInt( "AccountID", (int)playerID.GetAccountID() );
+ pKVData->SetInt( "ID", (int)m_iEvents++ );
+ pKVData->SetInt( "IsTrustedServer", bOfficial );
+ pKVData->SetInt( "PlayerClass", pPlayer->GetPlayerClass()->GetClassIndex() );
+ pKVData->SetInt( "PlayerRune", nRune );
+ pKVData->SetInt( "PlayerKillstreak", pPlayer->m_Shared.GetStreak( CTFPlayerShared::kTFStreak_KillsAll ) );
+ pKVData->SetInt( "RuneDuration", iDuration );
+
+ CEconItemView *pItem = pPlayer->GetLoadoutItem( pPlayer->GetPlayerClass()->GetClassIndex(), LOADOUT_POSITION_PRIMARY );
+ item_definition_index_t iItemDef = pItem ? pItem->GetItemDefIndex() : 0;
+ pKVData->SetInt( "PlayerPrimary", iItemDef );
+
+ pItem = pPlayer->GetLoadoutItem( pPlayer->GetPlayerClass()->GetClassIndex(), LOADOUT_POSITION_SECONDARY );
+ iItemDef = pItem ? pItem->GetItemDefIndex() : 0;
+ pKVData->SetInt( "PlayerSecondary", iItemDef );
+
+ pItem = pPlayer->GetLoadoutItem( pPlayer->GetPlayerClass()->GetClassIndex(), LOADOUT_POSITION_MELEE );
+ iItemDef = pItem ? pItem->GetItemDefIndex() : 0;
+ pKVData->SetInt( "PlayerMelee", iItemDef );
+
+ //pKVData->SetInt( "TimeSubmitted", GetSteamWorksSGameStatsUploader().GetTimeSinceEpoch() );
+
+ GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKVData );
+#endif // !NO_STEAM
+}