diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/cstrike/cs_gamestats.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/cstrike/cs_gamestats.cpp')
| -rw-r--r-- | game/server/cstrike/cs_gamestats.cpp | 1721 |
1 files changed, 1721 insertions, 0 deletions
diff --git a/game/server/cstrike/cs_gamestats.cpp b/game/server/cstrike/cs_gamestats.cpp new file mode 100644 index 0000000..afb02a0 --- /dev/null +++ b/game/server/cstrike/cs_gamestats.cpp @@ -0,0 +1,1721 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: CS game stats +// +// $NoKeywords: $ +//=============================================================================// + +// Some tricky business here - we don't want to include the precompiled header for the statreader +// and trying to #ifdef it out does funky things like ignoring the #endif. Define our header file +// separately and include it based on the switch + +#include "cbase.h" + +#include <tier0/platform.h> +#include "cs_gamerules.h" +#include "cs_gamestats.h" +#include "weapon_csbase.h" +#include "props.h" +#include "cs_achievement_constants.h" +#include "../../shared/cstrike/weapon_c4.h" + +#include <time.h> +#include "filesystem.h" +#include "bot_util.h" +#include "cdll_int.h" +#include "steamworks_gamestats.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +float g_flGameStatsUpdateTime = 0.0f; +short g_iTerroristVictories[CS_NUM_LEVELS]; +short g_iCounterTVictories[CS_NUM_LEVELS]; +short g_iWeaponPurchases[WEAPON_MAX]; + +short g_iAutoBuyPurchases = 0; +short g_iReBuyPurchases = 0; +short g_iAutoBuyM4A1Purchases = 0; +short g_iAutoBuyAK47Purchases = 0; +short g_iAutoBuyFamasPurchases = 0; +short g_iAutoBuyGalilPurchases = 0; +short g_iAutoBuyVestHelmPurchases = 0; +short g_iAutoBuyVestPurchases = 0; + +struct PropModelStats_t +{ + const char* szPropModelName; + CSStatType_t statType; +} PropModelStatsTableInit[] = +{ + { "models/props/cs_office/computer_caseb.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/computer_monitor.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/phone.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/projector.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/TV_plasma.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/computer_keyboard.mdl", CSSTAT_PROPSBROKEN_OFFICEELECTRONICS }, + { "models/props/cs_office/radio.mdl", CSSTAT_PROPSBROKEN_OFFICERADIO }, + { "models/props/cs_office/trash_can.mdl", CSSTAT_PROPSBROKEN_OFFICEJUNK }, + { "models/props/cs_office/file_box.mdl", CSSTAT_PROPSBROKEN_OFFICEJUNK }, + { "models/props_junk/watermelon01.mdl", CSSTAT_PROPSBROKEN_ITALY_MELON }, + // models/props/de_inferno/claypot01.mdl + // models/props/de_inferno/claypot02.mdl + // models/props/de_dust/grainbasket01c.mdl + // models/props_junk/wood_crate001a.mdl + // models/props/cs_office/file_box_p1.mdl +}; + + +struct ServerStats_t +{ + int achievementId; + int statId; + int roundRequirement; + int matchRequirement; + const char* mapFilter; + + bool IsMet(int roundStat, int matchStat) + { + return roundStat >= roundRequirement && matchStat >= matchRequirement; + } +} ServerStatBasedAchievements[] = +{ + { CSBreakWindows, CSSTAT_NUM_BROKEN_WINDOWS, AchievementConsts::BreakWindowsInOfficeRound_Windows, 0, "cs_office" }, + { CSBreakProps, CSSTAT_PROPSBROKEN_ALL, AchievementConsts::BreakPropsInRound_Props, 0, NULL }, + { CSUnstoppableForce, CSSTAT_KILLS, AchievementConsts::UnstoppableForce_Kills, 0, NULL }, + { CSHeadshotsInRound, CSSTAT_KILLS_HEADSHOT, AchievementConsts::HeadshotsInRound_Kills, 0, NULL }, + { CSDominationOverkillsMatch, CSSTAT_DOMINATION_OVERKILLS, 0, 10, NULL }, +}; + +//============================================================================= +// HPE_BEGIN: +// [Forrest] Allow nemesis/revenge to be turned off for a server +//============================================================================= +static void SvNoNemesisChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) +{ + ConVarRef var( pConVar ); + if ( var.IsValid() && var.GetBool() ) + { + // Clear all nemesis relationships. + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + CCSPlayer *pTemp = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + if ( pTemp ) + { + pTemp->RemoveNemesisRelationships(); + } + } + } +} + +ConVar sv_nonemesis( "sv_nonemesis", "0", 0, "Disable nemesis and revenge.", SvNoNemesisChangeCallback ); +//============================================================================= +// HPE_END +//============================================================================= + +int GetCSLevelIndex( const char *pLevelName ) +{ + for ( int i = 0; MapName_StatId_Table[i].statWinsId != CSSTAT_UNDEFINED; i ++ ) + { + if ( Q_strcmp( pLevelName, MapName_StatId_Table[i].szMapName ) == 0 ) + return i; + } + + return -1; +} + +ConVar sv_debugroundstats( "sv_debugroundstats", "0", 0, "A temporary variable that will print extra information about stats upload which may be useful in debugging any problems." ); + +//============================================================================= +// HPE_BEGIN: +// [menglish] Addition of CCS_GameStats class +//============================================================================= + +CCSGameStats CCS_GameStats; +CCSGameStats::StatContainerList_t* CCSGameStats::s_StatLists = new CCSGameStats::StatContainerList_t(); + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : - +//----------------------------------------------------------------------------- +CCSGameStats::CCSGameStats() +{ + gamestats = this; + Clear(); + m_fDisseminationTimerLow = m_fDisseminationTimerHigh = 0.0f; + + // create table for mapping prop models to stats + for ( int i = 0; i < ARRAYSIZE(PropModelStatsTableInit); ++i) + { + m_PropStatTable.Insert(PropModelStatsTableInit[i].szPropModelName, PropModelStatsTableInit[i].statType); + } + + m_numberOfRoundsForDirectAverages = 0; + m_numberOfTerroristEntriesForDirectAverages = 0; + m_numberOfCounterTerroristEntriesForDirectAverages = 0; + +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +// Input : - +//----------------------------------------------------------------------------- +CCSGameStats::~CCSGameStats() +{ + Clear(); +} + +//----------------------------------------------------------------------------- +// Purpose: Clear out game stats +// Input : - +//----------------------------------------------------------------------------- +void CCSGameStats::Clear( void ) +{ + m_PlayerSnipedPosition.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CCSGameStats::Init( void ) +{ + ListenForGameEvent( "round_start" ); + ListenForGameEvent( "round_end" ); + ListenForGameEvent( "break_prop" ); + ListenForGameEvent( "player_decal"); + ListenForGameEvent( "hegrenade_detonate"); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::PostInit( void ) +{ + +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::LevelShutdownPreClearSteamAPIContext( void ) +{ + // If we have any unsent round stats we better send them now or we'll lose them + UploadRoundStats(); + + GetSteamWorksSGameStatsUploader().EndSession(); +} + +extern double g_rowCommitTime; +extern double g_rowWriteTime; + +void CCSGameStats::UploadRoundStats( void ) +{ + CFastTimer totalTimer, purchasesAndDeathsStatTimer, weaponsStatBuildTimer, weaponsStatSubmitTimer, submitTimer, cleanupTimer; + + // Prevent uploading empty data + if ( !m_bInRound ) + return; + + if ( sv_noroundstats.GetBool() ) + { + if ( sv_debugroundstats.GetBool() ) + { + Msg( "sv_noroundstats is enabled. Not uploading round stats.\n" ); + } + + m_MarketPurchases.PurgeAndDeleteElements(); + m_WeaponData.PurgeAndDeleteElements(); + m_DeathData.PurgeAndDeleteElements(); + return; + } + + m_bInRound = false; + + KeyValues *pKV = new KeyValues( "basedata" ); + if ( !pKV ) + return; + + totalTimer.Start(); + purchasesAndDeathsStatTimer.Start(); + + const char *pzMapName = gpGlobals->mapname.ToCStr(); + pKV->SetString( "MapID", pzMapName ); + + for ( int k=0 ; k < m_MarketPurchases.Count() ; ++k ) + SubmitStat( m_MarketPurchases[ k ] ); + + for ( int k=0 ; k < m_DeathData.Count() ; ++k ) + SubmitStat( m_DeathData[ k ] ); + + purchasesAndDeathsStatTimer.End(); + weaponsStatBuildTimer.Start(); + + // Now add the weapon stats that HPE collected + for (int iWeapon = 0; iWeapon < WEAPON_MAX; ++iWeapon) + { + CCSWeaponInfo* pInfo = GetWeaponInfo( (CSWeaponID)iWeapon ); + if ( !pInfo ) + continue; + + const char* pWeaponName = pInfo->szClassName; + if ( !pWeaponName || !pWeaponName[0] || ( m_weaponStats[iWeapon][0].shots == 0 && m_weaponStats[iWeapon][1].shots == 0 ) ) + continue; + + + m_WeaponData.AddToTail( new SCSSWeaponData( pWeaponName, m_weaponStats[iWeapon][0] ) ); + + // Now add the bot data if we're collecting detailled data + if ( GetSteamWorksSGameStatsUploader().IsCollectingDetails() ) + { + char pWeaponNameModified[64]; + V_snprintf( pWeaponNameModified, ARRAYSIZE(pWeaponNameModified), "%s_bot", pWeaponName ); + m_WeaponData.AddToTail( new SCSSWeaponData( pWeaponNameModified, m_weaponStats[iWeapon][1] ) ); + } + } + + weaponsStatBuildTimer.End(); + weaponsStatSubmitTimer.Start(); + + for ( int k=0 ; k < m_WeaponData.Count() ; ++k ) + SubmitStat( m_WeaponData[ k ] ); + + weaponsStatSubmitTimer.End(); + + // Perform the actual submission + submitTimer.Start(); + SubmitGameStats( pKV ); + submitTimer.End(); + + int iPurchases, iDeathData, iWeaponData; + int listCount = s_StatLists->Count(); + iPurchases = m_MarketPurchases.Count(); + iDeathData = m_DeathData.Count(); + iWeaponData = m_WeaponData.Count(); + + // Clear out the per round stats + cleanupTimer.Start(); + m_MarketPurchases.Purge(); + m_WeaponData.Purge(); + m_DeathData.Purge(); + pKV->deleteThis(); + cleanupTimer.End(); + + totalTimer.End(); + + if ( sv_debugroundstats.GetBool() ) + { + Msg( "**** ROUND STAT DEBUG ****\n" ); + Msg( "UploadRoundStats completed. %.3f msec. Breakdown:\n a: %.3f msec\n b: %.3f msec\n c: %.3f msec\n d: %.3f msec\n e: %.3f msec\n objects: %d %d %d %d\n commit: %.3fms\n write: %.3fms.\n\n", + totalTimer.GetDuration().GetMillisecondsF(), + purchasesAndDeathsStatTimer.GetDuration().GetMillisecondsF(), + weaponsStatBuildTimer.GetDuration().GetMillisecondsF(), + weaponsStatSubmitTimer.GetDuration().GetMillisecondsF(), + submitTimer.GetDuration().GetMillisecondsF(), + cleanupTimer.GetDuration().GetMillisecondsF(), + iPurchases, iDeathData, iWeaponData, listCount, + g_rowCommitTime, g_rowWriteTime); + } +} + +#ifdef _DEBUG +CON_COMMAND ( teststats, "Test command" ) +{ + CFastTimer totalTimer; + double uploadTime = 0.0f; + g_rowCommitTime = 0.0f; + g_rowWriteTime = 0.0f; + + + for( int i = 0; i < 1000; i++ ) + { + KeyValues *pKV = new KeyValues( "basedata" ); + if ( !pKV ) + return; + + pKV->SetName( "foobartest" ); + pKV->SetUint64( "test1", 1234 ); + pKV->SetUint64( "test2", 1234 ); + pKV->SetUint64( "test3", 1234 ); + pKV->SetUint64( "test4", 1234 ); + pKV->SetString( "test5", "TEST1234567890TEST1234567890TEST!"); + /*pKV->SetString( "test6", "TEST1234567890TEST1234567890TEST!"); + pKV->SetString( "test7", "TEST1234567890TEST1234567890TEST!"); + pKV->SetString( "test8", "TEST1234567890TEST1234567890TEST!"); + pKV->SetString( "test9", "TEST1234567890TEST1234567890TEST!");*/ + + totalTimer.Start(); + GetSteamWorksSGameStatsUploader().AddStatsForUpload( pKV, args.ArgC() == 1 ); + totalTimer.End(); + + uploadTime += totalTimer.GetDuration().GetMillisecondsF(); + } + + Msg( "teststats took %.3f msec commit: %.3fms write: %.3fms.\n", uploadTime, g_rowCommitTime, g_rowWriteTime ); +} +#endif + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::SubmitGameStats( KeyValues *pKV ) +{ + int listCount = s_StatLists->Count(); + for( int i=0; i < listCount; ++i ) + { + // Create a master key value that has stats everybody should share (map name, session ID, etc) + (*s_StatLists)[i]->SendData(pKV); + (*s_StatLists)[i]->Clear(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_ShotFired( CBasePlayer *pPlayer, CBaseCombatWeapon* pWeapon ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + CWeaponCSBase* pCSWeapon = dynamic_cast< CWeaponCSBase * >(pWeapon); + + //============================================================================= + // HPE_BEGIN: + // [dwenger] adding tracking for weapon used fun fact + //============================================================================= + + if ( pCSPlayer ) + { + // [dwenger] Update the player's tracking of which weapon type they fired + pCSPlayer->PlayerUsedFirearm( pWeapon ); + + //============================================================================= + // HPE_END + //============================================================================= + + IncrementStat( pCSPlayer, CSSTAT_SHOTS_FIRED, 1 ); + // Increment the individual weapon + if( pCSWeapon ) + { + CSWeaponID weaponId = pCSWeapon->GetWeaponID(); + for (int i = 0; WeaponName_StatId_Table[i].shotStatId != CSSTAT_UNDEFINED; ++i) + { + if ( WeaponName_StatId_Table[i].weaponId == weaponId ) + { + IncrementStat( pCSPlayer, WeaponName_StatId_Table[i].shotStatId, 1 ); + break; + } + } + + int iType = pCSPlayer->IsBot(); + ++m_weaponStats[weaponId][iType].shots; + } + } +} + +void CCSGameStats::Event_ShotHit( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + IncrementStat( pCSPlayer, CSSTAT_SHOTS_HIT, 1 ); + + CBaseEntity *pInflictor = info.GetInflictor(); + + if ( pInflictor ) + { + if ( pInflictor == pPlayer ) + { + if ( pPlayer->GetActiveWeapon() ) + { + CWeaponCSBase* pCSWeapon = dynamic_cast< CWeaponCSBase * >(pPlayer->GetActiveWeapon()); + if (pCSWeapon) + { + CSWeaponID weaponId = pCSWeapon->GetWeaponID(); + for (int i = 0; WeaponName_StatId_Table[i].shotStatId != CSSTAT_UNDEFINED; ++i) + { + if ( WeaponName_StatId_Table[i].weaponId == weaponId ) + { + IncrementStat( pCSPlayer, WeaponName_StatId_Table[i].hitStatId, 1 ); + break; + } + } + } + } + } + } +} +void CCSGameStats::Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + IncrementStat( pCSPlayer, CSSTAT_DEATHS, 1 ); +} + +void CCSGameStats::Event_PlayerSprayedDecal( CCSPlayer* pPlayer ) +{ + IncrementStat( pPlayer, CSSTAT_DECAL_SPRAYS, 1 ); +} + +void CCSGameStats::Event_PlayerKilled_PreWeaponDrop( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + CCSPlayer *pAttacker = ToCSPlayer( info.GetAttacker() ); + bool victimZoomed = ( pCSPlayer->GetFOV() != pCSPlayer->GetDefaultFOV() ); + + if (victimZoomed) + { + IncrementStat(pAttacker, CSSTAT_KILLS_AGAINST_ZOOMED_SNIPER, 1); + } + + + //Check for knife fight + if (pAttacker && pCSPlayer && pAttacker == info.GetInflictor() && pAttacker->GetTeamNumber() != pCSPlayer->GetTeamNumber()) + { + CWeaponCSBase* attackerWeapon = pAttacker->GetActiveCSWeapon(); + CWeaponCSBase* victimWeapon = pCSPlayer->GetActiveCSWeapon(); + + if (attackerWeapon && victimWeapon) + { + CSWeaponID attackerWeaponID = attackerWeapon->GetWeaponID(); + CSWeaponID victimWeaponID = victimWeapon->GetWeaponID(); + + if (attackerWeaponID == WEAPON_KNIFE && victimWeaponID == WEAPON_KNIFE) + { + IncrementStat(pAttacker, CSSTAT_KILLS_KNIFE_FIGHT, 1); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_BombPlanted( CCSPlayer* pPlayer) +{ + IncrementStat( pPlayer, CSSTAT_NUM_BOMBS_PLANTED, 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_BombDefused( CCSPlayer* pPlayer) +{ + + IncrementStat( pPlayer, CSSTAT_NUM_BOMBS_DEFUSED, 1 ); + IncrementStat( pPlayer, CSSTAT_OBJECTIVES_COMPLETED, 1 ); + if( pPlayer && pPlayer->HasDefuser() ) + { + IncrementStat( pPlayer, CSSTAT_BOMBS_DEFUSED_WITHKIT, 1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Increment terrorist team stat +//----------------------------------------------------------------------------- +void CCSGameStats::Event_BombExploded( CCSPlayer* pPlayer ) +{ + IncrementStat( pPlayer, CSSTAT_OBJECTIVES_COMPLETED, 1 ); +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_HostageRescued( CCSPlayer* pPlayer) +{ + IncrementStat( pPlayer, CSSTAT_NUM_HOSTAGES_RESCUED, 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Increment counter-terrorist team stat +//----------------------------------------------------------------------------- +void CCSGameStats::Event_AllHostagesRescued() +{ + IncrementTeamStat( TEAM_CT, CSSTAT_OBJECTIVES_COMPLETED, 1 ); +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_WindowShattered( CBasePlayer *pPlayer) +{ + Assert( pPlayer ); + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + + IncrementStat( pCSPlayer, CSSTAT_NUM_BROKEN_WINDOWS, 1 ); +} + + +void CCSGameStats::Event_BreakProp( CCSPlayer* pPlayer, CBreakableProp *pProp ) +{ + if (!pPlayer) + return; + + DevMsg("Player %s broke a %s (%i)\n", pPlayer->GetPlayerName(), pProp->GetModelName().ToCStr(), pProp->entindex()); + + int iIndex = m_PropStatTable.Find(pProp->GetModelName().ToCStr()); + if (m_PropStatTable.IsValidIndex(iIndex)) + { + IncrementStat(pPlayer, m_PropStatTable[iIndex], 1); + } + IncrementStat(pPlayer, CSSTAT_PROPSBROKEN_ALL, 1); +} + + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::UpdatePlayerRoundStats(int winner) +{ + int mapIndex = GetCSLevelIndex(gpGlobals->mapname.ToCStr()); + CSStatType_t mapStatWinIndex = CSSTAT_UNDEFINED, mapStatRoundIndex = CSSTAT_UNDEFINED; + + if ( mapIndex != -1 ) + { + mapStatWinIndex = MapName_StatId_Table[mapIndex].statWinsId; + mapStatRoundIndex = MapName_StatId_Table[mapIndex].statRoundsId; + } + + // increment the team specific stats + IncrementTeamStat( winner, CSSTAT_ROUNDS_WON, 1 ); + if ( mapStatWinIndex != CSSTAT_UNDEFINED ) + { + IncrementTeamStat( winner, mapStatWinIndex, 1 ); + } + if ( CSGameRules()->IsPistolRound() ) + { + IncrementTeamStat( winner, CSSTAT_PISTOLROUNDS_WON, 1 ); + } + IncrementTeamStat( TEAM_TERRORIST, CSSTAT_ROUNDS_PLAYED, 1 ); + IncrementTeamStat( TEAM_CT, CSSTAT_ROUNDS_PLAYED, 1 ); + IncrementTeamStat( TEAM_TERRORIST, mapStatRoundIndex, 1 ); + IncrementTeamStat( TEAM_CT, mapStatRoundIndex, 1 ); + + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected() ) + { + if ( winner == TEAM_CT ) + { + IncrementStat( pPlayer, CSSTAT_CT_ROUNDS_WON, 1, true ); + } + else if ( winner == TEAM_TERRORIST ) + { + IncrementStat( pPlayer, CSSTAT_T_ROUNDS_WON, 1, true ); + } + + if ( winner == TEAM_CT || winner == TEAM_TERRORIST ) + { + // Increment the win stats if this player is on the winning team + if ( pPlayer->GetTeamNumber() == winner ) + { + IncrementStat( pPlayer, CSSTAT_ROUNDS_WON, 1, true ); + + if ( CSGameRules()->IsPistolRound() ) + { + IncrementStat( pPlayer, CSSTAT_PISTOLROUNDS_WON, 1, true ); + } + + if ( mapStatWinIndex != CSSTAT_UNDEFINED ) + { + IncrementStat( pPlayer, mapStatWinIndex, 1, true ); + } + } + + IncrementStat( pPlayer, CSSTAT_ROUNDS_PLAYED, 1 ); + if ( mapStatWinIndex != CSSTAT_UNDEFINED ) + { + IncrementStat( pPlayer, mapStatRoundIndex, 1, true ); + } + + // set the play time for the round + IncrementStat( pPlayer, CSSTAT_PLAYTIME, (int)CSGameRules()->GetRoundElapsedTime(), true ); + } + } + } + + // send a stats update to all players + for ( int iPlayerIndex = 1; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected()) + { + SendStatsToPlayer(pPlayer, CSSTAT_PRIORITY_ENDROUND); + } + } +} + +void CCSGameStats::SendRollingStatsAveragesToAllPlayers() +{ + //The most stats we can send at once is the max message size minus the header, divided by the total size of each stat. + const int maxStatsPerMessage = (MAX_USER_MSG_DATA - sizeof(CSStatType_t)) / (3 * sizeof(float)); + + const int numMessagesNeeded = ((CSSTAT_MAX - CSSTAT_FIRST) / maxStatsPerMessage) + 1; + + for (int batchIndex = 0 ; batchIndex < numMessagesNeeded ; ++batchIndex) + { + int firstStatInThisBatch = (batchIndex * maxStatsPerMessage) + CSSTAT_FIRST; + int lastMessageInThisBatch = firstStatInThisBatch + maxStatsPerMessage - 1; + + CRecipientFilter filter; + filter.AddAllPlayers(); + UserMessageBegin( filter, "MatchStatsUpdate" ); + + WRITE_SHORT(firstStatInThisBatch); + + for ( int iStat = firstStatInThisBatch; iStat < CSSTAT_MAX && iStat <= lastMessageInThisBatch; ++iStat ) + { + WRITE_FLOAT(m_rollingTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_rollingCTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_rollingPlayerStatAverages.m_fStat[iStat]); + } + + MessageEnd(); + } +} + + +void CCSGameStats::SendDirectStatsAveragesToAllPlayers() +{ + //The most stats we can send at once is the max message size minus the header, divided by the total size of each stat. + const int maxStatsPerMessage = (MAX_USER_MSG_DATA - sizeof(CSStatType_t)) / (3 * sizeof(float)); + + const int numMessagesNeeded = ((CSSTAT_MAX - CSSTAT_FIRST) / maxStatsPerMessage) + 1; + + for (int batchIndex = 0 ; batchIndex < numMessagesNeeded ; ++batchIndex) + { + int firstStatInThisBatch = (batchIndex * maxStatsPerMessage) + CSSTAT_FIRST; + int lastMessageInThisBatch = firstStatInThisBatch + maxStatsPerMessage - 1; + + CRecipientFilter filter; + filter.AddAllPlayers(); + UserMessageBegin( filter, "MatchStatsUpdate" ); + + WRITE_SHORT(firstStatInThisBatch); + + for ( int iStat = firstStatInThisBatch; iStat < CSSTAT_MAX && iStat <= lastMessageInThisBatch; ++iStat ) + { + WRITE_FLOAT(m_directTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_directCTStatAverages.m_fStat[iStat]); + WRITE_FLOAT(m_directPlayerStatAverages.m_fStat[iStat]); + } + + MessageEnd(); + } +} + +void CCSGameStats::ComputeRollingStatAverages() +{ + int numPlayers = 0; + int numCTs = 0; + int numTs = 0; + + RoundStatsRollingAverage_t currentRoundTStatsAverage; + RoundStatsRollingAverage_t currentRoundCTStatsAverage; + RoundStatsRollingAverage_t currentRoundPlayerStatsAverage; + + currentRoundTStatsAverage.Reset(); + currentRoundCTStatsAverage.Reset(); + currentRoundPlayerStatsAverage.Reset(); + + //for ( int iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) + { + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected()) + { + StatsCollection_t &roundStats = m_aPlayerStats[pPlayer->entindex()].statsCurrentRound; + + int teamNumber = pPlayer->GetTeamNumber(); + if (teamNumber == TEAM_CT) + { + numCTs++; + numPlayers++; + currentRoundCTStatsAverage += roundStats; + currentRoundPlayerStatsAverage += roundStats; + } + else if (teamNumber == TEAM_TERRORIST) + { + numTs++; + numPlayers++; + currentRoundTStatsAverage += roundStats; + currentRoundPlayerStatsAverage += roundStats; + } + } + } + + if (numTs > 0) + { + currentRoundTStatsAverage /= numTs; + } + + if (numCTs > 0) + { + currentRoundCTStatsAverage /= numCTs; + } + + if (numPlayers > 0) + { + currentRoundPlayerStatsAverage /= numPlayers; + } + + m_rollingTStatAverages.RollDataSetIntoAverage(currentRoundTStatsAverage); + m_rollingCTStatAverages.RollDataSetIntoAverage(currentRoundCTStatsAverage); + m_rollingPlayerStatAverages.RollDataSetIntoAverage(currentRoundPlayerStatsAverage); + } +} + + +void CCSGameStats::ComputeDirectStatAverages() +{ + m_numberOfRoundsForDirectAverages++; + + m_directCTStatAverages.Reset(); + m_directTStatAverages.Reset(); + m_directPlayerStatAverages.Reset(); + + + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->IsConnected()) + { + StatsCollection_t &matchStats = m_aPlayerStats[pPlayer->entindex()].statsCurrentMatch; + + int teamNumber = pPlayer->GetTeamNumber(); + if (teamNumber == TEAM_CT) + { + m_numberOfCounterTerroristEntriesForDirectAverages++; + m_directCTStatAverages += matchStats; + m_directPlayerStatAverages += matchStats; + } + else if (teamNumber == TEAM_TERRORIST) + { + m_numberOfTerroristEntriesForDirectAverages++; + m_directTStatAverages += matchStats; + m_directPlayerStatAverages += matchStats; + } + } + } + + if (m_numberOfTerroristEntriesForDirectAverages > 0) + { + m_directTStatAverages /= m_numberOfTerroristEntriesForDirectAverages; + m_directTStatAverages *= m_numberOfRoundsForDirectAverages; + } + + if (m_numberOfCounterTerroristEntriesForDirectAverages > 0) + { + m_directCTStatAverages /= m_numberOfCounterTerroristEntriesForDirectAverages; + m_directCTStatAverages *= m_numberOfRoundsForDirectAverages; + } + + int numPlayers = m_numberOfCounterTerroristEntriesForDirectAverages + m_numberOfTerroristEntriesForDirectAverages; + + if (numPlayers > 0) + { + m_directPlayerStatAverages /= numPlayers; + m_directPlayerStatAverages *= m_numberOfRoundsForDirectAverages; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Log accumulated weapon usage and performance data +//----------------------------------------------------------------------------- +void CCSGameStats::DumpMatchWeaponMetrics() +{ + //// generate a filename + //time_t t = time( NULL ); + //struct tm *now = localtime( &t ); + //if ( !now ) + // return; + + //int year = now->tm_year + 1900; + //int month = now->tm_mon + 1; + //int day = now->tm_mday; + //int hour = now->tm_hour; + //int minute = now->tm_min; + //int second = now->tm_sec; + + //char filename[ 128 ]; + //Q_snprintf( filename, sizeof(filename), "wm_%4d%02d%02d_%02d%02d%02d_%s.csv", + // year, month, day, hour, minute, second, gpGlobals->mapname.ToCStr()); + + //FileHandle_t hLogFile = filesystem->Open( filename, "wt" ); + + //if ( hLogFile == FILESYSTEM_INVALID_HANDLE ) + // return; + + //filesystem->FPrintf(hLogFile, "%s\n", "WeaponId, Mode, Cost, Bullets, CycleTime, TotalShots, TotalHits, TotalDamage, TotalKills"); + + //for (int iWeapon = 0; iWeapon < WEAPON_MAX; ++iWeapon) + //{ + // CCSWeaponInfo* pInfo = GetWeaponInfo( (CSWeaponID)iWeapon ); + // if ( !pInfo ) + // continue; + + // const char* pWeaponName = pInfo->szClassName; + // if ( !pWeaponName ) + // continue; + + // if ( V_strncmp(pWeaponName, "weapon_", 7) == 0 ) + // pWeaponName += 7; + + // for ( int iMode = 0; iMode < WeaponMode_MAX; ++iMode) + // { + // filesystem->FPrintf(hLogFile, "%s, %d, %d, %d, %f, %d, %d, %d, %d\n", + // pWeaponName, + // iMode, + // pInfo->GetWeaponPrice(), + // pInfo->m_iBullets, + // pInfo->m_flCycleTime, + // m_weaponStats[iWeapon][iMode].shots, + // m_weaponStats[iWeapon][iMode].hits, + // m_weaponStats[iWeapon][iMode].damage, + // m_weaponStats[iWeapon][iMode].kills); + // } + //} + + //filesystem->FPrintf(hLogFile, "\n"); + //filesystem->FPrintf(hLogFile, "weapon_accuracy_model, %d\n", weapon_accuracy_model.GetInt()); + //filesystem->FPrintf(hLogFile, "bot_difficulty, %d\n", cv_bot_difficulty.GetInt()); + + //g_pFullFileSystem->Close(hLogFile); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerConnected( CBasePlayer *pPlayer ) +{ + ResetPlayerStats( ToCSPlayer( pPlayer ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerDisconnected( CBasePlayer *pPlayer ) +{ + CCSPlayer *pCSPlayer = ToCSPlayer( pPlayer ); + if ( !pCSPlayer ) + return; + + ResetPlayerStats( pCSPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CCSGameStats::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; + + CBaseEntity *pInflictor = info.GetInflictor(); + + CCSPlayer *pPlayerAttacker = ToCSPlayer( pAttacker ); + CCSPlayer *pPlayerVictim = ToCSPlayer( pVictim ); + + // keep track of how many times every player kills every other player + TrackKillStats( pPlayerAttacker, pPlayerVictim ); + + // Skip rest of stat reporting for friendly fire + if ( pPlayerAttacker->GetTeam() == pVictim->GetTeam() ) + return; + + CSWeaponID weaponId = WEAPON_NONE; + + if ( pInflictor ) + { + if ( pInflictor == pAttacker ) + { + if ( pAttacker->GetActiveWeapon() ) + { + CBaseCombatWeapon* weapon = pAttacker->GetActiveWeapon(); + if (weapon) + { + CWeaponCSBase* pCSWeapon = static_cast<CWeaponCSBase*>(weapon); + + weaponId = pCSWeapon->GetWeaponID(); + + CCSWeaponInfo *info = GetWeaponInfo(weaponId); + + if (info && info->m_iTeam != TEAM_UNASSIGNED && pAttacker->GetTeamNumber() != info->m_iTeam ) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_ENEMY_WEAPON, 1); + } + } + } + } + else + { + //In the case of grenades, the inflictor is the spawned grenade entity. + if ( V_strcmp(pInflictor->m_iClassname.ToCStr(), "hegrenade_projectile") == 0 ) + weaponId = WEAPON_HEGRENADE; + } + } + + // update weapon stats + ++m_weaponStats[weaponId][pPlayerAttacker->IsBot()].kills; + + for (int i = 0; WeaponName_StatId_Table[i].killStatId != CSSTAT_UNDEFINED; ++i) + { + if ( WeaponName_StatId_Table[i].weaponId == weaponId ) + { + IncrementStat( pPlayerAttacker, WeaponName_StatId_Table[i].killStatId, 1 ); + break; + } + } + + if (pPlayerVictim && pPlayerVictim->IsBlind()) + { + IncrementStat( pPlayerAttacker, CSSTAT_KILLS_ENEMY_BLINDED, 1 ); + } + + if (pPlayerVictim && pPlayerAttacker && pPlayerAttacker->IsBlindForAchievement()) + { + IncrementStat( pPlayerAttacker, CSSTAT_KILLS_WHILE_BLINDED, 1 ); + } + + // [sbodenbender] check for deaths near planted bomb for funfact + if (pPlayerVictim && pPlayerAttacker && pPlayerAttacker->GetTeamNumber() == TEAM_TERRORIST && CSGameRules()->m_bBombPlanted) + { + float bombCheckDistSq = AchievementConsts::KillEnemyNearBomb_MaxDistance * AchievementConsts::KillEnemyNearBomb_MaxDistance; + for ( int i=0; i < g_PlantedC4s.Count(); i++ ) + { + CPlantedC4 *pC4 = g_PlantedC4s[i]; + + if ( pC4->IsBombActive() ) + { + Vector bombPos = pC4->GetAbsOrigin(); + Vector victimToBomb = pPlayerVictim->GetAbsOrigin() - bombPos; + Vector attackerToBomb = pPlayerAttacker->GetAbsOrigin() - bombPos; + if (victimToBomb.LengthSqr() < bombCheckDistSq || attackerToBomb.LengthSqr() < bombCheckDistSq) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_WHILE_DEFENDING_BOMB, 1); + break; // you only get credit for one kill even if you happen to be by more than one bomb + } + } + } + } + + //Increment stat if this is a headshot. + if (info.GetDamageType() & DMG_HEADSHOT) + { + IncrementStat( pPlayerAttacker, CSSTAT_KILLS_HEADSHOT, 1 ); + } + + IncrementStat( pPlayerAttacker, CSSTAT_KILLS, 1 ); + + // we don't have a simple way (yet) to check if the victim actually just achieved The Unstoppable Force, so we + // award this achievement simply if they've met the requirements and would have received it. + PlayerStats_t &victimStats = m_aPlayerStats[pVictim->entindex()]; + if (victimStats.statsCurrentRound[CSSTAT_KILLS] >= AchievementConsts::UnstoppableForce_Kills) + { + pPlayerAttacker->AwardAchievement(CSImmovableObject); + } + + CCSGameRules::TeamPlayerCounts playerCounts[TEAM_MAXCOUNT]; + + CSGameRules()->GetPlayerCounts(playerCounts); + int iAttackerTeamNumber = pPlayerAttacker->GetTeamNumber() ; + if (playerCounts[iAttackerTeamNumber].totalAlivePlayers == 1 && playerCounts[iAttackerTeamNumber].killedPlayers >= 2) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_WHILE_LAST_PLAYER_ALIVE, 1); + } + + //if they were damaged by more than one person that must mean that someone else did damage before the killer finished them off. + if (pPlayerVictim->GetNumEnemyDamagers() > 1) + { + IncrementStat(pPlayerAttacker, CSSTAT_KILLS_ENEMY_WOUNDED, 1); + } + + // Let's check for the "Happy Camper" achievement where we snipe two players while standing in the same spot. + if ( pPlayerAttacker && !pPlayerAttacker->IsBot() && weaponId != WEAPON_NONE ) + { + // Were we using a sniper rifle? + bool bUsingSniper = ( weaponId == WEAPON_AWP || + weaponId == WEAPON_SCOUT || + weaponId == WEAPON_SG550 || + weaponId == WEAPON_G3SG1 ); + + // If we're zoomed in + if ( bUsingSniper && pPlayerAttacker->GetFOV() != pPlayerAttacker->GetDefaultFOV() ) + { + // Get our position + Vector position = pPlayerAttacker->GetAbsOrigin(); + int userid = pPlayerAttacker->GetUserID(); + + bool bFoundPlayerEntry = false; + FOR_EACH_LL( m_PlayerSnipedPosition, iElement ) + { + sHappyCamperSnipePosition *pSnipePosition = &m_PlayerSnipedPosition[iElement]; + + // We've found this player's entry. Next, check to see if they meet the achievement criteria + if ( userid == pSnipePosition->m_iUserID ) + { + bFoundPlayerEntry = true; + Vector posDif = position - pSnipePosition->m_vPos; + + // Give a small epsilon to account for floating point anomalies + if ( posDif.IsLengthLessThan( .01f) ) + { + pPlayerAttacker->AwardAchievement( CSSnipeTwoFromSameSpot ); + } + + // Update their position + pSnipePosition->m_vPos = position; + break; + } + } + + // No entry so add one + if ( !bFoundPlayerEntry ) + { + m_PlayerSnipedPosition.AddToTail( sHappyCamperSnipePosition( userid, position ) ); + } + } + } +} + + +void CCSGameStats::CalculateOverkill(CCSPlayer* pAttacker, CCSPlayer* pVictim) +{ + //Count domination overkills - Do this before determining domination + if (pAttacker->GetTeam() != pVictim->GetTeam()) + { + if (pAttacker->IsPlayerDominated(pVictim->entindex())) + { + IncrementStat( pAttacker, CSSTAT_DOMINATION_OVERKILLS, 1 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Steamworks Gamestats death tracking +//----------------------------------------------------------------------------- +void CCSGameStats::PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info ) +{ + if ( !pVictim ) + return; + + m_DeathData.AddToTail( new SCSSDeathData( pVictim, info ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Stats event for giving damage to player +//----------------------------------------------------------------------------- +void CCSGameStats::Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info ) +{ + CCSPlayer *pAttacker = ToCSPlayer( info.GetAttacker() ); + if ( pAttacker && pAttacker->GetTeam() != pBasePlayer->GetTeam() ) + { + CSWeaponMode weaponMode = Primary_Mode; + IncrementStat( pAttacker, CSSTAT_DAMAGE, info.GetDamage() ); + + if (pAttacker->m_bNightVisionOn) + { + IncrementStat( pAttacker, CSSTAT_NIGHTVISION_DAMAGE, info.GetDamage() ); + } + + const char *pWeaponName = NULL; + + if ( info.GetInflictor() == info.GetAttacker() ) + { + if ( pAttacker->GetActiveWeapon() ) + { + CWeaponCSBase* pCSWeapon = dynamic_cast< CWeaponCSBase * >(pAttacker->GetActiveWeapon()); + if (pCSWeapon) + { + pWeaponName = pCSWeapon->GetClassname(); + weaponMode = pCSWeapon->m_weaponMode; + } + } + } + // Need to determine the weapon name + else + { + pWeaponName = info.GetInflictor()->GetClassname(); + } + + // Unknown weapon?!? + if ( !pWeaponName ) + return; + + // Now update the damage this weapon has done + CSWeaponID weaponId = AliasToWeaponID( GetTranslatedWeaponAlias( pWeaponName ) ); + for (int i = 0; WeaponName_StatId_Table[i].shotStatId != CSSTAT_UNDEFINED; ++i) + { + if ( weaponId == WeaponName_StatId_Table[i].weaponId ) + { + int weaponId = WeaponName_StatId_Table[i].weaponId; + int iType = pAttacker->IsBot(); + ++m_weaponStats[weaponId][iType].hits; + m_weaponStats[weaponId][iType].damage += info.GetDamage(); + break; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Stats event for giving money to player +//----------------------------------------------------------------------------- +void CCSGameStats::Event_MoneyEarned( CCSPlayer* pPlayer, int moneyEarned) +{ + if ( pPlayer && moneyEarned > 0) + { + IncrementStat(pPlayer, CSSTAT_MONEY_EARNED, moneyEarned); + } +} + +void CCSGameStats::Event_MoneySpent( CCSPlayer* pPlayer, int moneySpent, const char *pItemName ) +{ + if ( pPlayer && moneySpent > 0) + { + IncrementStat(pPlayer, CSSTAT_MONEY_SPENT, moneySpent); + if ( pItemName && !pPlayer->IsBot() ) + { + CSteamID steamIDForBuyer; + pPlayer->GetSteamID( &steamIDForBuyer ); + m_MarketPurchases.AddToTail( new SMarketPurchases( steamIDForBuyer.ConvertToUint64(), moneySpent, pItemName ) ); + } + } +} + +void CCSGameStats::Event_PlayerDonatedWeapon (CCSPlayer* pPlayer) +{ + if (pPlayer) + { + IncrementStat(pPlayer, CSSTAT_WEAPONS_DONATED, 1); + } +} + +void CCSGameStats::Event_MVPEarned( CCSPlayer* pPlayer ) +{ + if (pPlayer) + { + IncrementStat(pPlayer, CSSTAT_MVPS, 1); + } +} + +void CCSGameStats::Event_KnifeUse( CCSPlayer* pPlayer, bool bStab, int iDamage ) +{ + if (pPlayer) + { + int weaponId = WEAPON_KNIFE; + int iType = pPlayer->IsBot(); + + IncrementStat( pPlayer, CSSTAT_SHOTS_KNIFE, 1 ); + ++m_weaponStats[weaponId][iType].shots; + if ( iDamage ) + { + IncrementStat( pPlayer, CSSTAT_HITS_KNIFE, 1 ); + ++m_weaponStats[weaponId][iType].hits; + + IncrementStat( pPlayer, CSSTAT_DAMAGE_KNIFE, iDamage ); + m_weaponStats[weaponId][iType].damage += iDamage; + } + } +} +//----------------------------------------------------------------------------- +// Purpose: Event handler +//----------------------------------------------------------------------------- +void CCSGameStats::FireGameEvent( IGameEvent *event ) +{ + const char *pEventName = event->GetName(); + + if ( V_strcmp(pEventName, "round_start") == 0 ) + { + m_PlayerSnipedPosition.Purge(); + m_bInRound = true; + } + else if ( V_strcmp(pEventName, "round_end") == 0 ) + { + const int reason = event->GetInt( "reason" ); + + if( reason == Game_Commencing ) + { + ResetPlayerClassMatchStats(); + } + else + { + UpdatePlayerRoundStats(event->GetInt("winner")); + ComputeDirectStatAverages(); + SendDirectStatsAveragesToAllPlayers(); + + UploadRoundStats(); + ResetWeaponStats(); + } + return; + } + else if ( V_strcmp(pEventName, "break_prop") == 0 ) + { + int userid = event->GetInt("userid", 0); + int entindex = event->GetInt("entindex", 0); + CBreakableProp* pProp = static_cast<CBreakableProp*>(CBaseEntity::Instance(entindex)); + Event_BreakProp(ToCSPlayer(UTIL_PlayerByUserId(userid)), pProp); + } + else if ( V_strcmp(pEventName, "player_decal") == 0 ) + { + int userid = event->GetInt("userid", 0); + Event_PlayerSprayedDecal(ToCSPlayer(UTIL_PlayerByUserId(userid))); + } + else if ( V_strcmp(pEventName, "hegrenade_detonate") == 0 ) + { + int userid = event->GetInt("userid", 0); + IncrementStat( ToCSPlayer(UTIL_PlayerByUserId(userid)), CSSTAT_SHOTS_HEGRENADE, 1 ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Return stats for the given player +//----------------------------------------------------------------------------- +const PlayerStats_t& CCSGameStats::FindPlayerStats( CBasePlayer *pPlayer ) const +{ + return m_aPlayerStats[pPlayer->entindex()]; +} + +//----------------------------------------------------------------------------- +// Purpose: Return stats for the given team +//----------------------------------------------------------------------------- +const StatsCollection_t& CCSGameStats::GetTeamStats( int iTeamIndex ) const +{ + int arrayIndex = iTeamIndex - FIRST_GAME_TEAM; + Assert( arrayIndex >= 0 && arrayIndex < TEAM_MAXCOUNT - FIRST_GAME_TEAM ); + return m_aTeamStats[arrayIndex]; +} + +//----------------------------------------------------------------------------- +// Purpose: Resets the stats for each team +//----------------------------------------------------------------------------- +void CCSGameStats::ResetAllTeamStats() +{ + for ( int i = 0; i < ARRAYSIZE(m_aTeamStats); ++i ) + { + m_aTeamStats[i].Reset(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Resets all stats (including round, match, accumulated and rolling averages +//----------------------------------------------------------------------------- +void CCSGameStats::ResetAllStats() +{ + for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ ) + { + m_aPlayerStats[i].statsDelta.Reset(); + m_aPlayerStats[i].statsCurrentRound.Reset(); + m_aPlayerStats[i].statsCurrentMatch.Reset(); + m_rollingCTStatAverages.Reset(); + m_rollingTStatAverages.Reset(); + m_rollingPlayerStatAverages.Reset(); + m_numberOfRoundsForDirectAverages = 0; + m_numberOfTerroristEntriesForDirectAverages = 0; + m_numberOfCounterTerroristEntriesForDirectAverages = 0; + } +} + + +void CCSGameStats::ResetWeaponStats() +{ + V_memset(m_weaponStats, 0, sizeof(m_weaponStats)); +} + +void CCSGameStats::IncrementTeamStat( int iTeamIndex, int iStatIndex, int iAmount ) +{ + int arrayIndex = iTeamIndex - TEAM_TERRORIST; + Assert( iStatIndex >= 0 && iStatIndex < CSSTAT_MAX ); + if( arrayIndex >= 0 && arrayIndex < TEAM_MAXCOUNT - TEAM_TERRORIST ) + { + m_aTeamStats[arrayIndex][iStatIndex] += iAmount; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Resets all stats for this player +//----------------------------------------------------------------------------- +void CCSGameStats::ResetPlayerStats( CBasePlayer* 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 CCSGameStats::ResetKillHistory( CBasePlayer* pPlayer ) +{ + int iPlayerIndex = pPlayer->entindex(); + + PlayerStats_t& statsPlayer = m_aPlayerStats[iPlayerIndex]; + + // 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++ ) + { + //reset their record of us. + PlayerStats_t &statsOther = m_aPlayerStats[i]; + statsOther.statsKills.iNumKilled[iPlayerIndex] = 0; + statsOther.statsKills.iNumKilledBy[iPlayerIndex] = 0; + statsOther.statsKills.iNumKilledByUnanswered[iPlayerIndex] = 0; + + //reset our record of them + statsPlayer.statsKills.iNumKilled[i] = 0; + statsPlayer.statsKills.iNumKilledBy[i] = 0; + statsPlayer.statsKills.iNumKilledByUnanswered[i] = 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Resets per-round stats for all players +//----------------------------------------------------------------------------- +void CCSGameStats::ResetRoundStats() +{ + for ( int i = 0; i < ARRAYSIZE( m_aPlayerStats ); i++ ) + { + m_aPlayerStats[i].statsCurrentRound.Reset(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Increments specified stat for specified player by specified amount +//----------------------------------------------------------------------------- +void CCSGameStats::IncrementStat( CCSPlayer* pPlayer, CSStatType_t statId, int iDelta, bool bPlayerOnly /* = false */ ) +{ + if ( pPlayer ) + { + PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()]; + stats.statsDelta[statId] += iDelta; + stats.statsCurrentRound[statId] += iDelta; + stats.statsCurrentMatch[statId] += iDelta; + + // increment team stat + int teamIndex = pPlayer->GetTeamNumber() - FIRST_GAME_TEAM; + if ( !bPlayerOnly && teamIndex >= 0 && teamIndex < ARRAYSIZE(m_aTeamStats) ) + { + m_aTeamStats[teamIndex][statId] += iDelta; + } + + for (int i = 0; i < ARRAYSIZE(ServerStatBasedAchievements); ++i) + { + if (ServerStatBasedAchievements[i].statId == statId) + { + // skip this if there is a map filter and it doesn't match + if (ServerStatBasedAchievements[i].mapFilter != NULL && V_strcmp(gpGlobals->mapname.ToCStr(), ServerStatBasedAchievements[i].mapFilter) != 0) + continue; + + bool bWasMet = ServerStatBasedAchievements[i].IsMet(stats.statsCurrentRound[statId] - iDelta, stats.statsCurrentMatch[statId] - iDelta); + bool bIsMet = ServerStatBasedAchievements[i].IsMet(stats.statsCurrentRound[statId], stats.statsCurrentMatch[statId]); + if (!bWasMet && bIsMet) + { + pPlayer->AwardAchievement(ServerStatBasedAchievements[i].achievementId); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the specified stat for specified player to the specified amount +//----------------------------------------------------------------------------- +void CCSGameStats::SetStat( CCSPlayer *pPlayer, CSStatType_t statId, int iValue ) +{ + if (pPlayer) + { + int oldRoundValue, oldMatchValue; + PlayerStats_t &stats = m_aPlayerStats[pPlayer->entindex()]; + + oldRoundValue = stats.statsCurrentRound[statId]; + oldMatchValue = stats.statsCurrentMatch[statId]; + + stats.statsDelta[statId] = iValue; + stats.statsCurrentRound[statId] = iValue; + stats.statsCurrentMatch[statId] = iValue; + + for (int i = 0; i < ARRAYSIZE(ServerStatBasedAchievements); ++i) + { + if (ServerStatBasedAchievements[i].statId == statId) + { + // skip this if there is a map filter and it doesn't match + if (ServerStatBasedAchievements[i].mapFilter != NULL && V_strcmp(gpGlobals->mapname.ToCStr(), ServerStatBasedAchievements[i].mapFilter) != 0) + continue; + + bool bWasMet = ServerStatBasedAchievements[i].IsMet(oldRoundValue, oldMatchValue); + bool bIsMet = ServerStatBasedAchievements[i].IsMet(stats.statsCurrentRound[statId], stats.statsCurrentMatch[statId]); + if (!bWasMet && bIsMet) + { + pPlayer->AwardAchievement(ServerStatBasedAchievements[i].achievementId); + } + } + } + } +} + +void CCSGameStats::SendStatsToPlayer( CCSPlayer * pPlayer, int iMinStatPriority ) +{ + ASSERT(CSSTAT_MAX < 255); // if we add more than 255 stats, we'll need to update this protocol + if ( pPlayer && pPlayer->IsConnected()) + { + StatsCollection_t &deltaStats = m_aPlayerStats[pPlayer->entindex()].statsDelta; + + // check to see if we have any stats to actually send + byte iStatsToSend = 0; + for ( int iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) + { + ASSERT(CSStatProperty_Table[iStat].statId == iStat); + if ( CSStatProperty_Table[iStat].statId != iStat ) + { + Warning( "CSStatProperty_Table[iStat].statId != iStat, (%d)", CSStatProperty_Table[iStat].statId ); + } + int iPriority = CSStatProperty_Table[iStat].flags & CSSTAT_PRIORITY_MASK; + if (deltaStats[iStat] != 0 && iPriority >= iMinStatPriority) + { + ++iStatsToSend; + } + } + + // nothing changed - bail out + if ( !iStatsToSend ) + return; + + CSingleUserRecipientFilter filter( pPlayer ); + filter.MakeReliable(); + UserMessageBegin( filter, "PlayerStatsUpdate" ); + + CRC32_t crc; + CRC32_Init( &crc ); + + // begin the CRC with a trivially hidden key value to discourage packet modification + const uint32 key = 0x82DA9F4C; // this key should match the key in cs_client_gamestats.cpp + CRC32_ProcessBuffer( &crc, &key, sizeof(key)); + + // if we make any change to the ordering of the stats or this message format, update this value + const byte version = 0x01; + CRC32_ProcessBuffer( &crc, &version, sizeof(version)); + WRITE_BYTE(version); + + CRC32_ProcessBuffer( &crc, &iStatsToSend, sizeof(iStatsToSend)); + WRITE_BYTE(iStatsToSend); + + for ( byte iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) + { + int iPriority = CSStatProperty_Table[iStat].flags & CSSTAT_PRIORITY_MASK; + if (deltaStats[iStat] != 0 && iPriority >= iMinStatPriority) + { + CRC32_ProcessBuffer( &crc, &iStat, sizeof(iStat)); + WRITE_BYTE(iStat); + Assert(deltaStats[iStat] <= 0x7FFF && deltaStats[iStat] > 0); // make sure we aren't truncating bits + short delta = deltaStats[iStat]; + CRC32_ProcessBuffer( &crc, &delta, sizeof(delta)); + WRITE_SHORT( deltaStats[iStat]); + deltaStats[iStat] = 0; + --iStatsToSend; + } + } + + Assert(iStatsToSend == 0); + + CRC32_Final( &crc ); + WRITE_LONG(crc); + + MessageEnd(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Sends intermittent stats updates for stats that need to be updated during a round and/or life +//----------------------------------------------------------------------------- +void CCSGameStats::PreClientUpdate() +{ + int iMinStatPriority = -1; + m_fDisseminationTimerHigh += gpGlobals->frametime; + m_fDisseminationTimerLow += gpGlobals->frametime; + + if ( m_fDisseminationTimerHigh > cDisseminationTimeHigh) + { + iMinStatPriority = CSSTAT_PRIORITY_HIGH; + m_fDisseminationTimerHigh = 0.0f; + + if ( m_fDisseminationTimerLow > cDisseminationTimeLow) + { + iMinStatPriority = CSSTAT_PRIORITY_LOW; + m_fDisseminationTimerLow = 0.0f; + } + } + else + return; + + //The proper time has elapsed, now send the update to every player + for ( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex(iPlayerIndex) ); + SendStatsToPlayer(pPlayer, iMinStatPriority); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Updates the stats of who has killed whom +//----------------------------------------------------------------------------- +void CCSGameStats::TrackKillStats( CCSPlayer *pAttacker, CCSPlayer *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; +} + + +//----------------------------------------------------------------------------- +// Purpose: Determines if attacker and victim have gotten domination or revenge +//----------------------------------------------------------------------------- +void CCSGameStats::CalcDominationAndRevenge( CCSPlayer *pAttacker, CCSPlayer *pVictim, int *piDeathFlags ) +{ + //============================================================================= + // HPE_BEGIN: + // [Forrest] Allow nemesis/revenge to be turned off for a server + //============================================================================= + if ( sv_nonemesis.GetBool() ) + { + return; + } + //============================================================================= + // HPE_END + //============================================================================= + + //If there is no attacker, there is no domination or revenge + if( !pAttacker || !pVictim ) + { + return; + } + if (pAttacker->GetTeam() == pVictim->GetTeam()) + { + return; + } + int iPlayerIndexVictim = pVictim->entindex(); + PlayerStats_t &statsVictim = m_aPlayerStats[iPlayerIndexVictim]; + // calculate # of unanswered kills between killer & victim + // This is plus 1 as this function gets called before the stat is updated. That is done so that the domination + // and revenge will be calculated prior to the death message being sent to the clients + int attackerEntityIndex = pAttacker->entindex(); + int iKillsUnanswered = statsVictim.statsKills.iNumKilledByUnanswered[attackerEntityIndex] + 1; + + if ( CS_KILLS_FOR_DOMINATION == iKillsUnanswered ) + { + // this is the Nth unanswered kill between killer and victim, killer is now dominating victim + *piDeathFlags |= ( CS_DEATH_DOMINATION ); + } + else if ( pVictim->IsPlayerDominated( pAttacker->entindex() ) ) + { + // the killer killed someone who was dominating him, gains revenge + *piDeathFlags |= ( CS_DEATH_REVENGE ); + } + + //Check the overkill on 1 player achievement + if (iKillsUnanswered == CS_KILLS_FOR_DOMINATION + AchievementConsts::ExtendedDomination_AdditionalKills) + { + pAttacker->AwardAchievement(CSExtendedDomination); + } + + if (iKillsUnanswered == CS_KILLS_FOR_DOMINATION) + { + //this is the Nth unanswered kill between killer and victim, killer is now dominating victim + //set victim to be dominated by killer + pAttacker->SetPlayerDominated( pVictim, true ); + + //Check concurrent dominations achievement + int numConcurrentDominations = 0; + for ( int i = 1 ; i <= gpGlobals->maxClients ; i++ ) + { + CCSPlayer *pPlayer= ToCSPlayer( UTIL_PlayerByIndex( i ) ); + if (pPlayer && pAttacker->IsPlayerDominated(pPlayer->entindex())) + { + numConcurrentDominations++; + } + } + if (numConcurrentDominations >= AchievementConsts::ConcurrentDominations_MinDominations) + { + pAttacker->AwardAchievement(CSConcurrentDominations); + } + + + // record stats + Event_PlayerDominatedOther( pAttacker, pVictim ); + } + else if ( pVictim->IsPlayerDominated( pAttacker->entindex() ) ) + { + // the killer killed someone who was dominating him, gains revenge + // set victim to no longer be dominating the killer + + pVictim->SetPlayerDominated( pAttacker, false ); + // record stats + Event_PlayerRevenge( pAttacker ); + } +} + +void CCSGameStats::Event_PlayerDominatedOther( CCSPlayer *pAttacker, CCSPlayer* pVictim ) +{ + IncrementStat( pAttacker, CSSTAT_DOMINATIONS, 1 ); +} + +void CCSGameStats::Event_PlayerRevenge( CCSPlayer *pAttacker ) +{ + IncrementStat( pAttacker, CSSTAT_REVENGES, 1 ); +} + +void CCSGameStats::Event_PlayerAvengedTeammate( CCSPlayer* pAttacker, CCSPlayer* pAvengedPlayer ) +{ + if (pAttacker && pAvengedPlayer) + { + IGameEvent *event = gameeventmanager->CreateEvent( "player_avenged_teammate" ); + + if ( event ) + { + event->SetInt( "avenger_id", pAttacker->GetUserID() ); + event->SetInt( "avenged_player_id", pAvengedPlayer->GetUserID() ); + gameeventmanager->FireEvent( event ); + } + } +} + +void CCSGameStats::Event_LevelInit() +{ + ResetAllTeamStats(); + ResetWeaponStats(); + CBaseGameStats::Event_LevelInit(); + GetSteamWorksSGameStatsUploader().StartSession(); +} + +void CCSGameStats::Event_LevelShutdown( float fElapsed ) +{ + DumpMatchWeaponMetrics(); + CBaseGameStats::Event_LevelShutdown(fElapsed); +} + +// Reset any per match info that resides in the player class +void CCSGameStats::ResetPlayerClassMatchStats() +{ + for ( int i = 1; i <= MAX_PLAYERS; i++ ) + { + CCSPlayer *pPlayer = ToCSPlayer( UTIL_PlayerByIndex( i ) ); + + if ( pPlayer ) + { + pPlayer->SetNumMVPs( 0 ); + } + } +} |