//========= 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 #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 #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(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(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 ); } } }