|
|
//-------------------------------------------------------------
// File: cs_client_gamestats.cpp
// Desc: Manages client side stat storage, accumulation, and access
// Author: Peter Freese <[email protected]>
// Date: 2009/09/11
// Copyright: � 2009 Hidden Path Entertainment
//
// Keywords:
//-------------------------------------------------------------
#include "cbase.h"
#include "cs_client_gamestats.h"
#include "achievementmgr.h"
#include "usermessages.h"
#include "c_cs_player.h"
#include "achievements_cs.h"
#include "vgui/ILocalize.h"
#include "c_team.h"
#include "engineinterface.h"
#include "matchmaking/mm_helpers.h"
#include "cstrikeloadout.h"
#include "gametypes.h"
#include "cs_gamerules.h"
#include "matchmaking/iplayerrankingdata.h"
#include "inputsystem/iinputsystem.h"
#include "platforminputdevice.h"
#include "cs_player_rank_mgr.h"
#include "hltvreplaysystem.h"
#if defined (_X360)
#include "ixboxsystem.h"
#include "../common/xlast_csgo/csgo.spa.h"
#endif
#ifdef _PS3
#include "ps3/ps3_helpers.h"
#endif
// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"
// Added to facilitate data collection below. Add and remove to match current experimental column data
extern ConVar cl_crosshairstyle; extern ConVar cl_hud_color; extern ConVar cl_hud_healthammo_style; extern ConVar cl_hud_bomb_under_radar; extern ConVar cl_hud_playercount_pos; extern ConVar cl_hud_playercount_showcount; extern ConVar cl_radar_rotate;
CCSClientGameStats g_CSClientGameStats;
bool MsgFunc_PlayerStatsUpdate( const CCSUsrMsg_PlayerStatsUpdate &msg ) { return g_CSClientGameStats.MsgFunc_PlayerStatsUpdate(msg); }
#ifdef _X360
static CAsyncLeaderboardWriteThread g_AsyncLeaderboardWriteThread; #endif
struct MapName_LBStatID { char *szMapName; DWORD mapLeaderboardStat; };
// [jason] Map name -> Leaderboard property mapping. Should be kept in sync with maplist.txt
const MapName_LBStatID MapName_LBStatId_Table[] = { {"cs_italy", PROPERTY_CSS_LB_GP_TIME_MAP_ITALY }, {"cs_office", PROPERTY_CSS_LB_GP_TIME_MAP_OFFICE }, {"de_aztec", PROPERTY_CSS_LB_GP_TIME_MAP_AZTEC }, {"de_dust", PROPERTY_CSS_LB_GP_TIME_MAP_DUST }, {"de_dust2", PROPERTY_CSS_LB_GP_TIME_MAP_DUST2 }, {"de_inferno", PROPERTY_CSS_LB_GP_TIME_MAP_INFERNO }, {"de_nuke", PROPERTY_CSS_LB_GP_TIME_MAP_NUKE }, {"de_shorttrain", PROPERTY_CSS_LB_GP_TIME_MAP_SHORTTRAIN }, {"ar_baggage", PROPERTY_CSS_LB_GP_TIME_MAP_BAGGAGE }, {"ar_shoots", PROPERTY_CSS_LB_GP_TIME_MAP_SHOOTS }, {"de_bank", PROPERTY_CSS_LB_GP_TIME_MAP_BANK }, {"de_lake", PROPERTY_CSS_LB_GP_TIME_MAP_LAKE }, {"de_safehouse", PROPERTY_CSS_LB_GP_TIME_MAP_SAFEHOUSE }, {"de_sugarcane", PROPERTY_CSS_LB_GP_TIME_MAP_SUGARCANE }, {"de_stmarc", PROPERTY_CSS_LB_GP_TIME_MAP_STMARC }, {"de_train", PROPERTY_CSS_LB_GP_TIME_MAP_TRAIN }, {"training1", PROPERTY_CSS_LB_GP_TIME_MAP_TRAINING }, {"", DWORD(-1) }, };
const int kNumMapLeaderboardEntries = sizeof(MapName_LBStatId_Table)/sizeof(MapName_LBStatId_Table[0]);
struct LeaderboardMap_t { DWORD winsId; char* winsName; DWORD csId; char* csName; DWORD kdId; char* kdName; DWORD starsId; char* starsName; DWORD gpId; char* gpName; };
// Mapping of game mode/type to leaderboard id
LeaderboardMap_t g_LeaderboardIDMap[] = { { STATS_VIEW_WINS_ONLINE_CASUAL, "WINS_ONLINE_CASUAL", STATS_VIEW_CS_ONLINE_CASUAL, "CS_ONLINE_CASUAL", STATS_VIEW_KD_ONLINE_CASUAL, "KD_ONLINE_CASUAL", STATS_VIEW_STARS_ONLINE_CASUAL, "STARS_ONLINE_CASUAL", STATS_VIEW_GP_ONLINE_CASUAL, "GP_ONLINE_CASUAL" }, { STATS_VIEW_WINS_ONLINE_COMPETITIVE, "WINS_ONLINE_COMPETITIVE", STATS_VIEW_CS_ONLINE_COMPETITIVE, "CS_ONLINE_COMPETITIVE", STATS_VIEW_KD_ONLINE_COMPETITIVE, "KD_ONLINE_COMPETITIVE", STATS_VIEW_STARS_ONLINE_COMPETITIVE, "STARS_ONLINE_COMPETITIVE", STATS_VIEW_GP_ONLINE_COMPETITIVE, "GP_ONLINE_COMPETITIVE" }, { STATS_VIEW_WINS_ONLINE_GG_PROG, "WINS_ONLINE_GG_PROG", STATS_VIEW_CS_ONLINE_GG_PROG, "CS_ONLINE_GG_PROG", STATS_VIEW_KD_ONLINE_GG_PROG, "KD_ONLINE_GG_PROG", STATS_VIEW_STARS_ONLINE_GG_PROG, "STARS_ONLINE_GG_PROG", STATS_VIEW_GP_ONLINE_GG_PROG, "GP_ONLINE_GG_PROG" }, { STATS_VIEW_WINS_ONLINE_GG_BOMB, "WINS_ONLINE_GG_BOMB", STATS_VIEW_CS_ONLINE_GG_BOMB, "CS_ONLINE_GG_BOMB", STATS_VIEW_KD_ONLINE_GG_BOMB, "KD_ONLINE_GG_BOMB", STATS_VIEW_STARS_ONLINE_GG_BOMB, "STARS_ONLINE_GG_BOMB", STATS_VIEW_GP_ONLINE_GG_BOMB, "GP_ONLINE_GG_BOMB" }, };
const int kNumLeaderboardIDs = sizeof(g_LeaderboardIDMap)/sizeof(g_LeaderboardIDMap[0]);
//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CCSClientGameStats::CCSClientGameStats() { m_bSteamStatsDownload = false; }
//-----------------------------------------------------------------------------
// Purpose: called at init time after all systems are init'd. We have to
// do this in PostInit because the Steam app ID is not available earlier
//-----------------------------------------------------------------------------
void CCSClientGameStats::PostInit() { ACTIVE_SPLITSCREEN_PLAYER_GUARD( 0 );
// listen for events
ListenForGameEvent( "player_stats_updated" ); ListenForGameEvent( "user_data_downloaded" ); ListenForGameEvent( "round_end_upload_stats" ); ListenForGameEvent( "round_end" ); ListenForGameEvent( "cs_game_disconnected" ); ListenForGameEvent( "read_game_titledata" ); ListenForGameEvent( "write_game_titledata" ); ListenForGameEvent( "reset_game_titledata" ); ListenForGameEvent( "update_matchmaking_stats" ); ListenForGameEvent( "begin_new_match" ); ListenForGameEvent( "bomb_planted" ); ListenForGameEvent( "hostage_follows" ); // Client info messages
for ( int hh = 0; hh < MAX_SPLITSCREEN_PLAYERS; ++hh ) { ACTIVE_SPLITSCREEN_PLAYER_GUARD( hh ); m_UMCMsgPlayerStatsUpdate.Bind< CS_UM_PlayerStatsUpdate, CCSUsrMsg_PlayerStatsUpdate>( UtlMakeDelegate( ::MsgFunc_PlayerStatsUpdate )); }
#if !defined( _GAMECONSOLE )
m_RoundEndReason = Invalid_Round_End_Reason; m_bObjectiveAttempted = false; #endif
}
void CCSClientGameStats::LevelInitPostEntity() { #if !defined( _GAMECONSOLE )
// Need this for players who join mid-match to have a client session
if ( CSGameRules()->HasMatchStarted() ) { GetSteamWorksGameStatsClient().StartSession(); } #endif
}
//-----------------------------------------------------------------------------
// Purpose: called at level shutdown
//-----------------------------------------------------------------------------
void CCSClientGameStats::LevelShutdownPreEntity() { #if !defined( _GAMECONSOLE )
UploadRoundStats(); GetSteamWorksGameStatsClient().EndSession(); #else
// round stats are reset when we upload stats on PC, but we still need to reset on consoles as well so do it here
m_roundStats[0].Reset(); #endif
// This is a good opportunity to update our last match stats
UpdateLastMatchStats();
// upload user stats to Steam on every map change
UpdateSteamStats(); }
static inline int GetNumPlayers( C_Team *pTeam ) { return pTeam ? pTeam->Get_Number_Players() : 0; }
//-----------------------------------------------------------------------------
// Purpose: called when the stats have changed in-game
//-----------------------------------------------------------------------------
CEG_NOINLINE void CCSClientGameStats::FireGameEvent( IGameEvent *event ) { const char *pEventName = event->GetName(); if ( 0 == Q_strcmp( pEventName, "player_stats_updated" ) ) { UpdateSteamStats(); } else if ( 0 == Q_strcmp( pEventName, "user_data_downloaded" ) ) { RetrieveSteamStats(); } else if ( 0 == Q_strcmp( pEventName, "read_game_titledata" ) ) { SyncCSStatsToTitleData( event->GetInt( "controllerId" ), CSSTAT_READ_STAT ); SyncCSLoadoutsToTitleData( event->GetInt( "controllerId" ), CSSTAT_READ_STAT ); SyncCSMatchmakingDataToTitleData( event->GetInt( "controllerId" ), CSSTAT_READ_STAT ); SyncCSRankingDataToTitleData( event->GetInt( "controllerId" ), CSSTAT_READ_STAT ); } else if ( 0 == Q_strcmp( pEventName, "write_game_titledata" ) ) { SyncCSStatsToTitleData( event->GetInt( "controllerId" ), CSSTAT_WRITE_STAT ); SyncCSLoadoutsToTitleData( event->GetInt( "controllerId" ), CSSTAT_WRITE_STAT ); SyncCSMatchmakingDataToTitleData( event->GetInt( "controllerId" ), CSSTAT_WRITE_STAT ); SyncCSRankingDataToTitleData( event->GetInt( "controllerId" ), CSSTAT_WRITE_STAT ); } else if ( 0 == Q_strcmp( pEventName, "reset_game_titledata" ) ) { // $TODO(hpe) need to get controllerID and use it when stats handle splitscreen and loadouts handle splitscreen
ResetMatchmakingData( MMDATA_SCOPE_LIFETIME ); ResetMatchmakingData( MMDATA_SCOPE_ROUND );
// clear stats
int userSlot = XBX_GetSlotByUserId( event->GetInt( "controllerId" ) ); if ( userSlot < 0 || userSlot >= MAX_SPLITSCREEN_PLAYERS ) { AssertMsg( false, "CCSClientGameStats::FireGameEvent:: reset_game_titledata invalid userSlot\n" ); userSlot = STEAM_PLAYER_SLOT; } g_CSClientGameStats.ResetAllStats( userSlot );
} else if ( Q_strcmp( pEventName, "update_matchmaking_stats" ) == 0 ) { UpdateMatchmakingData(); } else if ( Q_strcmp( pEventName, "round_end_upload_stats" ) == 0 ) { // [jhail] Write leaderboard stats at pre-start, before our stats collection gets reset
WriteLeaderboardStats();
#if !defined( _GAMECONSOLE )
UploadRoundStats(); #else
// round stats are reset when we upload stats on PC, but we still need to reset on consoles as well so do it here
m_roundStats[0].Reset(); #endif
} else if ( Q_strcmp( pEventName, "round_end" ) == 0 ) {
#ifdef _PS3
g_pGcmSharedData->m_bDeFrag = 1; // Flag for a defrag at round end
#endif
m_RoundEndReason = event->GetInt( "reason", Invalid_Round_End_Reason ); int iCurrentPlayerCount = event->GetInt( "player_count", 0 ); #ifdef DBGFLAG_ASSERT
int nPlayerCountOnClient = GetNumPlayers( GetGlobalTeam( TEAM_CT ) ) + GetNumPlayers( GetGlobalTeam( TEAM_TERRORIST ) ); // don't collect stats at the wrong point in time if round_end is passed through during replay
if ( g_HltvReplaySystem.GetHltvReplayDelay() ) Assert( iCurrentPlayerCount <= nPlayerCountOnClient ); // the number of players at round end can shrink, but cannot grow comparing to the replayed state
else Assert( nPlayerCountOnClient == 0 || iCurrentPlayerCount == nPlayerCountOnClient ); // if we are not replaying, the number of player must be the same on server and client
#endif
m_matchMaxPlayerCount = Max( m_matchMaxPlayerCount, iCurrentPlayerCount ); } else if ( 0 == Q_strcmp( pEventName, "cs_game_disconnected" ) ) { #if !defined( _GAMECONSOLE )
UploadRoundStats(); #else
// round stats are reset when we upload stats on PC, but we still need to reset on consoles as well so do it here
m_roundStats[0].Reset(); #endif
} else if ( 0 == Q_strcmp( pEventName, "begin_new_match" ) ) { GetSteamWorksGameStatsClient().EndSession(); GetSteamWorksGameStatsClient().StartSession(); } else if ( 0 == Q_strcmp( pEventName, "bomb_planted" ) || 0 == Q_strcmp( pEventName, "hostage_follows" ) ) { //ignore events after round end (planting for cash after CT elimination, picking up a hostage after T elimination
if ( m_RoundEndReason == Invalid_Round_End_Reason || m_RoundEndReason == Game_Commencing ) { m_bObjectiveAttempted = true; } } }
void CCSClientGameStats::RetrieveSteamStats() { Assert( steamapicontext->SteamUserStats() ); if ( !steamapicontext->SteamUserStats() ) return;
// we shouldn't be downloading stats more than once
Assert(m_bSteamStatsDownload == false); if (m_bSteamStatsDownload) return;
int nStatFailCount = 0; for ( int i = 0; i < CSSTAT_MAX; ++i ) { if ( CSStatProperty_Table[i].szSteamName == NULL ) continue;
int iData; if ( steamapicontext->SteamUserStats()->GetStat( CSStatProperty_Table[i].szSteamName, &iData ) ) { m_lifetimeStats[STEAM_PLAYER_SLOT][i] = iData;
// Init our 'last upload' values to those we got from steam.
m_lifetimeStatsLastUpload[STEAM_PLAYER_SLOT][i] = iData; } else { ++nStatFailCount; } }
if ( nStatFailCount > 0 ) { Msg("RetrieveSteamStats: failed to get %i stats\n", nStatFailCount); return; }
IGameEvent * event = gameeventmanager->CreateEvent( "player_stats_updated" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); }
m_bSteamStatsDownload = true; }
//-----------------------------------------------------------------------------
// Purpose: Uploads stats for current Steam user to Steam
//-----------------------------------------------------------------------------
void CCSClientGameStats::UpdateSteamStats() { // only upload if Steam is running
if ( !steamapicontext->SteamUserStats() ) return;
// don't upload any stats if we haven't successfully download stats yet
if ( !m_bSteamStatsDownload ) { // this used to request stats periodically which is now handled in stats request heartbeat in PlayerLocal::Update
return; }
for ( int i = 0; i < CSSTAT_MAX; ++i ) { if ( CSStatProperty_Table[i].szSteamName == NULL ) continue;
if ( m_lifetimeStatsLastUpload[ STEAM_PLAYER_SLOT ][ i ] != m_lifetimeStats[ STEAM_PLAYER_SLOT ][ i ] ) { // set the stats locally in Steam client
steamapicontext->SteamUserStats()->SetStat( CSStatProperty_Table[ i ].szSteamName, m_lifetimeStats[ STEAM_PLAYER_SLOT ][ i ] ); m_lifetimeStatsLastUpload[ STEAM_PLAYER_SLOT ][ i ] = m_lifetimeStats[ STEAM_PLAYER_SLOT ][ i ]; }
}
// let the achievement manager know the stats have changed
g_AchievementMgrCS.SetDirty( true, STEAM_PLAYER_SLOT ); }
int CCSClientGameStats::GetStatCount() { return CSSTAT_MAX; }
PlayerStatData_t CCSClientGameStats::GetStatById( int id, int nUserSlot ) { Assert(id >= 0 && id < CSSTAT_MAX); if ( id >= 0 && id < CSSTAT_MAX) { PlayerStatData_t statData;
statData.iStatId = id; statData.iStatValue = m_lifetimeStats[nUserSlot][statData.iStatId];
// we can make this more efficient by caching the localized names
statData.pStatDisplayName = g_pVGuiLocalize->Find( CSStatProperty_Table[id].szLocalizationToken );
return statData; } else { PlayerStatData_t dummy; dummy.pStatDisplayName = NULL; dummy.iStatId = CSSTAT_UNDEFINED; dummy.iStatValue = 0; return dummy; } }
const StatsCollection_t& CCSClientGameStats::GetLifetimeStats( int nUserSlot ) { if ( nUserSlot < 0 || nUserSlot >= MAX_SPLITSCREEN_PLAYERS ) { AssertMsg( false, "CCSClientGameStats::GetLifetimeStats nUserSlot out of range; using 0\n" ); return m_lifetimeStats[STEAM_PLAYER_SLOT]; } return m_lifetimeStats[nUserSlot]; }
const StatsCollection_t& CCSClientGameStats::GetMatchStats( int nUserSlot ) { if ( nUserSlot < 0 || nUserSlot >= MAX_SPLITSCREEN_PLAYERS ) { AssertMsg( false, "CCSClientGameStats::GetMatchStats nUserSlot out of range; using 0\n" ); return m_matchStats[STEAM_PLAYER_SLOT]; } return m_matchStats[nUserSlot]; }
const StatsCollection_t& CCSClientGameStats::GetRoundStats( int nUserSlot ) { if ( nUserSlot < 0 || nUserSlot >= MAX_SPLITSCREEN_PLAYERS ) { AssertMsg( false, "CCSClientGameStats::GetRoundStats nUserSlot out of range; using 0\n" ); return m_roundStats[STEAM_PLAYER_SLOT]; } return m_roundStats[nUserSlot]; }
void CCSClientGameStats::UpdateStats( const StatsCollection_t &stats, int nUserSlot ) { C_CSPlayer *pPlayer = C_CSPlayer::GetLocalCSPlayer(); if ( !pPlayer ) return;
// don't count stats if cheats on, commentary mode, etc
if ( !g_AchievementMgrCS.CheckAchievementsEnabled() ) return;
// Update matchmaking related stats.
IncrementMatchmakingData( stats );
// Update the accumulated stats
// We don't aggregate stats in Offline Games with "Dumb" or No Bots
if ( CSGameRules() && CSGameRules()->IsAwardsProgressAllowedForBotDifficulty() ) { m_lifetimeStats[nUserSlot].Aggregate(stats); m_matchStats[nUserSlot].Aggregate(stats); m_roundStats[nUserSlot].Aggregate(stats); } // $TODO: hpe: sb: pass along the userSlot in the player_stats_updated message
IGameEvent * event = gameeventmanager->CreateEvent( "player_stats_updated" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); } }
void CCSClientGameStats::ResetAllStats( int nUSerSlot ) { ISteamUserStats* pSteamUserStats = steamapicontext->SteamUserStats(); if ( pSteamUserStats ) { pSteamUserStats->ResetAllStats(false); } else { // need to pass along user slot reset into the player_stats_updated message
int userSlot = nUSerSlot; if ( userSlot < 0 || userSlot >= MAX_SPLITSCREEN_PLAYERS ) { AssertMsg( false, "CCSClientGameStats::ResetAllStats invalid userSlot\n"); userSlot = STEAM_PLAYER_SLOT; } m_lifetimeStats[userSlot].Reset(); m_matchStats[userSlot].Reset();
m_roundStats[userSlot].Reset();
UpdateSteamStats();
IGameEvent * event = gameeventmanager->CreateEvent( "player_stats_updated" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); } } }
void CCSClientGameStats::ResetAllStatsAndAchievements( ) { ISteamUserStats* pSteamUserStats = steamapicontext->SteamUserStats(); if ( pSteamUserStats ) { pSteamUserStats->ResetAllStats(true); } }
void CRC32Helper_ProcessInt16( CRC32_t &crc, int16 n ) { int16 plat_n = LittleShort( n ); CRC32_ProcessBuffer( &crc, &plat_n, sizeof(plat_n) ); }
void CRC32Helper_ProcessInt32( CRC32_t &crc, int32 n ) { int32 plat_n = LittleDWord( n ); CRC32_ProcessBuffer( &crc, &plat_n, sizeof(plat_n) ); }
void CRC32Helper_ProcessUInt32( CRC32_t &crc, uint32 n ) { uint32 plat_n = LittleDWord( n ); CRC32_ProcessBuffer( &crc, &plat_n, sizeof(plat_n) ); }
bool CCSClientGameStats::MsgFunc_PlayerStatsUpdate( const CCSUsrMsg_PlayerStatsUpdate &msg ) { // Note: if any check fails while decoding this message, bail out and disregard this data to avoid
// potentially polluting player stats
StatsCollection_t deltaStats;
CRC32_t crc; CRC32_Init( &crc );
const uint32 key = 0x82DA9F4C; // this key should match the key in cs_gamestats.cpp
CRC32Helper_ProcessUInt32( crc, key );
const byte version = 0x03; CRC32_ProcessBuffer( &crc, &version, sizeof(version));
if (msg.version() != version) { Warning("PlayerStatsUpdate message: ignoring unsupported version\n"); return true; }
short iStatsToRead = msg.stats_size(); CRC32Helper_ProcessInt16( crc, iStatsToRead );
for ( int i = 0; i < iStatsToRead; ++i) { const CCSUsrMsg_PlayerStatsUpdate::Stat &stat = msg.stats(i);
short iStat = stat.idx(); CRC32Helper_ProcessInt16( crc, iStat );
if (iStat >= CSSTAT_MAX) { Warning("PlayerStatsUpdate: invalid statId encountered; ignoring stats update\n"); return true; } short delta = stat.delta(); deltaStats[iStat] = delta; CRC32Helper_ProcessInt16( crc, delta ); }
int userID = msg.user_id(); CRC32Helper_ProcessInt32( crc, userID ); CRC32_Final( &crc ); CRC32_t readCRC = msg.crc();
if ( readCRC != crc ) { Warning("PlayerStatsUpdate message from server is corrupt; ignoring\n"); return true; }
// do one additional pass for out of band values
for ( int iStat = CSSTAT_FIRST; iStat < CSSTAT_MAX; ++iStat ) { if (deltaStats[iStat] < 0 || deltaStats[iStat] >= 0x4000) { Warning("PlayerStatsUpdate message from server has out of band values; ignoring\n"); return true; } }
// everything looks okay at this point; add these stats for the player's round, match, and lifetime stats
int userSlot = STEAM_PLAYER_SLOT;
#if defined ( _X360 )
for ( int i = 0; i < MAX_SPLITSCREEN_PLAYERS; ++i ) { C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer(i); if ( pLocalPlayer && !pLocalPlayer->IsNPC() ) { if ( pLocalPlayer->GetUserID() == userID ) { userSlot = i; } } } #endif
UpdateStats(deltaStats, userSlot );
return true; }
void CCSClientGameStats::ResetMatchStats() { for ( int userSlot = 0; userSlot < MAX_SPLITSCREEN_PLAYERS; ++userSlot) { m_matchStats[userSlot].Reset(); } m_matchMaxPlayerCount = 0; }
void CCSClientGameStats::ResetRoundStats( void ) { for ( int userSlot = 0; userSlot < MAX_SPLITSCREEN_PLAYERS; ++userSlot) { m_roundStats[userSlot].Reset(); } }
// note, since we now reset the stats after we update them each time, this can be called multiple times without overwriting match stats
void CCSClientGameStats::UpdateLastMatchStats( void ) { for ( int userSlot = 0; userSlot < MAX_SPLITSCREEN_PLAYERS; ++userSlot ) { // only update that last match if we actually have valid data
if ( m_matchStats[userSlot][CSSTAT_ROUNDS_PLAYED] == 0 ) return;
// check to see if the player materially participate; they could have been spectating or joined just in time for the ending.
int s = 0; s += m_matchStats[userSlot][CSSTAT_ROUNDS_WON]; s += m_matchStats[userSlot][CSSTAT_KILLS]; s += m_matchStats[userSlot][CSSTAT_DEATHS]; s += m_matchStats[userSlot][CSSTAT_MVPS]; s += m_matchStats[userSlot][CSSTAT_DAMAGE]; s += m_matchStats[userSlot][CSSTAT_MONEY_SPENT];
if( s != 0 && CSGameRules() && CSGameRules()->IsAwardsProgressAllowedForBotDifficulty() ) { m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_CONTRIBUTION_SCORE] = m_matchStats[userSlot][CSSTAT_CONTRIBUTION_SCORE]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_GG_PROGRESSIVE_CONTRIBUTION_SCORE] = m_matchStats[userSlot][CSSTAT_GG_PROGRESSIVE_CONTRIBUTION_SCORE]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_T_ROUNDS_WON] = m_matchStats[userSlot][CSSTAT_T_ROUNDS_WON]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_CT_ROUNDS_WON] = m_matchStats[userSlot][CSSTAT_CT_ROUNDS_WON]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_ROUNDS_WON] = m_matchStats[userSlot][CSSTAT_ROUNDS_WON]; m_lifetimeStats[userSlot][CSTAT_LASTMATCH_ROUNDS_PLAYED] = m_matchStats[userSlot][CSSTAT_ROUNDS_PLAYED]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_KILLS] = m_matchStats[userSlot][CSSTAT_KILLS]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_DEATHS] = m_matchStats[userSlot][CSSTAT_DEATHS]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_MVPS] = m_matchStats[userSlot][CSSTAT_MVPS]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_DAMAGE] = m_matchStats[userSlot][CSSTAT_DAMAGE]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_MONEYSPENT] = m_matchStats[userSlot][CSSTAT_MONEY_SPENT]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_DOMINATIONS] = m_matchStats[userSlot][CSSTAT_DOMINATIONS]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_REVENGES] = m_matchStats[userSlot][CSSTAT_REVENGES]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_MAX_PLAYERS] = m_matchMaxPlayerCount; CalculateMatchFavoriteWeapons(); } } ResetMatchStats(); }
//-----------------------------------------------------------------------------
// Purpose: Calculate and store the match favorite weapon for each player as only deltaStats for that weapon are stored on Steam
//-----------------------------------------------------------------------------
void CCSClientGameStats::CalculateMatchFavoriteWeapons() { for ( int userSlot = 0; userSlot < MAX_SPLITSCREEN_PLAYERS; ++userSlot ) { int maxKills = 0, maxKillId = -1;
for( int j = CSSTAT_KILLS_DEAGLE; j <= CSSTAT_KILLS_M249; ++j ) { if ( m_matchStats[userSlot][j] > maxKills ) { maxKills = m_matchStats[userSlot][j]; maxKillId = j; } } if ( maxKillId == -1 ) { m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_ID] = WEAPON_NONE; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_SHOTS] = 0; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_HITS] = 0; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_KILLS] = 0; } else { int statTableID = -1; for (int j = 0; WeaponName_StatId_Table[j].killStatId != CSSTAT_UNDEFINED; ++j) { if ( WeaponName_StatId_Table[j].killStatId == maxKillId ) { statTableID = j; break; } } Assert( statTableID != -1 );
m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_ID] = WeaponName_StatId_Table[statTableID].weaponId; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_SHOTS] = m_matchStats[userSlot][WeaponName_StatId_Table[statTableID].shotStatId]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_HITS] = m_matchStats[userSlot][WeaponName_StatId_Table[statTableID].hitStatId]; m_lifetimeStats[userSlot][CSSTAT_LASTMATCH_FAVWEAPON_KILLS] = m_matchStats[userSlot][WeaponName_StatId_Table[statTableID].killStatId]; } } }
bool CCSClientGameStats::ValidateTitleBlockVersion( TitleDataFieldsDescription_t const *pFields, IPlayerLocal *pPlayerLocal, CSSyncStatValueDirection_t eOp, int titleBlockNo ) { #if defined ( _X360 )
if ( titleBlockNo < 1 || titleBlockNo > 3 ) return false; char versionIdentifier[32]; char convarIdenifier[32];
V_snprintf( versionIdentifier, sizeof(versionIdentifier), "TITLEDATA.BLOCK%d.VERSION", titleBlockNo ); V_snprintf( convarIdenifier, sizeof(convarIdenifier), "cl_titledataversionblock%d", titleBlockNo );
// check version number of the specified title block
TitleDataFieldsDescription_t const *versionField = TitleDataFieldsDescriptionFindByString( pFields, versionIdentifier ); if ( !versionField || versionField->m_eDataType != TitleDataFieldsDescription_t::DT_uint16 ) { Warning( "%s is expected to be defined as DT_uint16\n", versionIdentifier ); return false; }
ConVarRef cl_titledataversionblock( convarIdenifier ); if ( eOp == CSSTAT_READ_STAT ) { int versionNumber = TitleDataFieldsDescriptionGetValue<uint16>( versionField, pPlayerLocal ); if ( versionNumber != cl_titledataversionblock.GetInt() ) { Warning( "ValidateTitleBlockVersion unexpected version number for %s; got %d, expected %d\n", versionIdentifier, versionNumber, cl_titledataversionblock.GetInt() ); return false; } } else // we always set the version field
{ TitleDataFieldsDescriptionSetValue<uint16>( versionField, pPlayerLocal, cl_titledataversionblock.GetInt() ); }
return true; #else
return false; // no title data for non Xbox systems
#endif
}
//-----------------------------------------------------------------------------
// Purpose: Serialize lifetime stats to the user profile title data
//-----------------------------------------------------------------------------
bool CCSClientGameStats::SyncCSStatsToTitleData( int iController, CSSyncStatValueDirection_t eOp ) {
#if defined ( _X360 )
// we need to hook up a console version of m_bSteamStatsDownload
//// we shouldn't be downloading stats more than once
//Assert(m_bSteamStatsDownload == false);
//if (m_bSteamStatsDownload)
// return;
// get the local player
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( iController ); if ( !pPlayerLocal ) return false;
int userSlot = XBX_GetSlotByUserId( iController ); if ( userSlot < 0 || userSlot >= MAX_SPLITSCREEN_PLAYERS ) { userSlot = STEAM_PLAYER_SLOT; }
// we are writing values directly here since we know they are int32 and int16; we add checks to verify data files don't change the data types
// otherwise, we would need to use keyvalue or convar or write extra code we don't need to handle all data types
TitleDataFieldsDescription_t const *pFields = g_pMatchFramework->GetMatchTitle()->DescribeTitleDataStorage();
if ( !ValidateTitleBlockVersion( pFields, pPlayerLocal, eOp, 1 ) ) return false;
char statName[ 256 ]; for ( int i = 0, titleDataStat=0; i < CSSTAT_MAX; ++i ) { if ( CSStatProperty_Table[i].szSteamName == NULL ) continue;
Q_snprintf( statName, 255, "STATS.usr.stat%.3d", titleDataStat++ ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, statName ) ) { if ( pField->m_eDataType != TitleDataFieldsDescription_t::DT_uint32 ) { Warning( "%s is expected to be defined as DT_uint32\n", statName ); continue; }
if ( eOp == CSSTAT_READ_STAT ) m_lifetimeStats[userSlot][i] = TitleDataFieldsDescriptionGetValue<uint32>( pField, pPlayerLocal ); else TitleDataFieldsDescriptionSetValue<uint32>( pField, pPlayerLocal, m_lifetimeStats[userSlot][i] );
} else { Warning( "Could not find TitleDataField for %s\n", statName ); } }
IGameEvent * event = gameeventmanager->CreateEvent( "player_stats_updated" ); if ( event ) { gameeventmanager->FireEventClientSide( event ); }
//m_bSteamStatsDownload = true;
#endif
return true;
}
bool CCSClientGameStats::SyncCSLoadoutsToTitleData( int iController, CSSyncStatValueDirection_t eOp ) { // get the local player
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( iController ); if ( !pPlayerLocal ) return false;
#if defined ( _X360 )
// verify inc file matches hardcoded values
#define CFG( loadoutnum, equipmentnum ) \
int numLoadouts = loadoutnum; \ int numEquipmentSlots = equipmentnum; #include "xlast_csgo/inc_loadouts_usr.inc"
#undef CFG
if ( numLoadouts != cMaxLoadouts || numEquipmentSlots != cMaxEquipment ) { Warning( "CCSClientGameStats::SyncCSLoadoutsToTitleData mismatch between inc_loadouts_usr.inc and cMaxLoadouts/Equipment\n" ); return false; }
// verify version number
TitleDataFieldsDescription_t const *pFields = g_pMatchFramework->GetMatchTitle()->DescribeTitleDataStorage();
if ( !ValidateTitleBlockVersion( pFields, pPlayerLocal, eOp, 3 ) ) return false;
char loadoutName[30]; for (int teamcount = 0; teamcount<2; ++teamcount) { CCSLoadout *pLoadoutArray = NULL; char teamName[10]; if (teamcount) { pLoadoutArray = GetBuyMenuLoadoutData(TEAM_TERRORIST); Q_snprintf( teamName, 10, "T" ); } else { pLoadoutArray = GetBuyMenuLoadoutData(TEAM_CT); Q_snprintf( teamName, 10, "CT" ); }
for(int i=0; i<cMaxLoadouts; ++i) { CCSLoadout &pLoadout = pLoadoutArray[i];
// we can write bytes for the equipment info since we have less than 256 weapons
for (int j=0; j<cMaxEquipment; ++j) { Q_snprintf( loadoutName, 30, "%s.LOAD%.1d.EQUIP%.1d.ID", teamName, i, j ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, loadoutName ) ) { if ( eOp == CSSTAT_READ_STAT ) pLoadout.m_EquipmentArray[j].m_EquipmentID = (CSWeaponID)TitleDataFieldsDescriptionGetValue<uint8>( pField, pPlayerLocal ); else TitleDataFieldsDescriptionSetValue<uint8>( pField, pPlayerLocal, pLoadout.m_EquipmentArray[j].m_EquipmentID );
} Q_snprintf( loadoutName, 30, "%s.LOAD%.1d.EQUIP%.1d.QUANTITY", teamName, i, j ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, loadoutName ) ) { if ( eOp == CSSTAT_READ_STAT ) pLoadout.m_EquipmentArray[j].m_Quantity = TitleDataFieldsDescriptionGetValue<uint8>( pField, pPlayerLocal ); else TitleDataFieldsDescriptionSetValue<uint8>( pField, pPlayerLocal, pLoadout.m_EquipmentArray[j].m_Quantity );
} }
Q_snprintf( loadoutName, 30, "%s.LOAD%.1d.PRIMARY", teamName, i ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, loadoutName ) ) { if ( eOp == CSSTAT_READ_STAT ) pLoadout.m_primaryWeaponID = (CSWeaponID)TitleDataFieldsDescriptionGetValue<uint8>( pField, pPlayerLocal ); else TitleDataFieldsDescriptionSetValue<uint8>( pField, pPlayerLocal, pLoadout.m_primaryWeaponID );
} Q_snprintf( loadoutName, 30, "%s.LOAD%.1d.SECONDARY", teamName, i ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, loadoutName ) ) { if ( eOp == CSSTAT_READ_STAT ) pLoadout.m_secondaryWeaponID = (CSWeaponID)TitleDataFieldsDescriptionGetValue<uint8>( pField, pPlayerLocal ); else TitleDataFieldsDescriptionSetValue<uint8>( pField, pPlayerLocal, pLoadout.m_secondaryWeaponID );
}
Q_snprintf( loadoutName, 30, "%s.LOAD%.1d.FLAGS", teamName, i ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, loadoutName ) ) { if ( eOp == CSSTAT_READ_STAT ) pLoadout.m_flags = TitleDataFieldsDescriptionGetValue<uint8>( pField, pPlayerLocal ); else TitleDataFieldsDescriptionSetValue<uint8>( pField, pPlayerLocal, pLoadout.m_flags ); } } }
#endif
return true; }
#if defined( _X360 )
// Purpose: Helper function to write properties to the XUSER_PROPERTY stream for each leaderboard
static void WriteProperty( XUSER_PROPERTY *props, int index, DWORD propId, BYTE type, void* data ) { XUSER_PROPERTY &property = props[index]; property.dwPropertyId = propId; property.value.type = type;
switch ( type ) { default: Warning( "CS_CLIENT_GAMESTATS: WriteProperty error: unknown data type: %d!\n", type ); break;
case XUSER_DATA_TYPE_FLOAT: property.value.type = XUSER_DATA_TYPE_INT64; // Float isn't supported on Leaderboards: Convert to a 64-bit int and write it out scaled-up
property.value.i64Data = 10000000 * *((float*)data); break;
case XUSER_DATA_TYPE_INT64: property.value.i64Data = *((LONGLONG*)data); break;
case XUSER_DATA_TYPE_INT32: property.value.nData = *((LONG*)data); break; } }
#endif // #if defined( _X360 )
// Purpose: resets the server-side leaderboards for testing purposes
void CCSClientGameStats::ResetLeaderboardStats( void ) { #if defined ( _X360 )
#if !defined ( _CERT )
for ( int id=STATS_VIEW_WINS_ONLINE_CASUAL; id<=STATS_VIEW_GP_ONLINE_GG_BOMB; ++id ) XUserResetStatsViewAllUsers( id, NULL ); #endif // !_CERT
#endif // _X360
}
CEG_NOINLINE void CCSClientGameStats::WriteLeaderboardStats( void ) { return; // disabling client-writing leaderboards for now
#if !defined( _X360 )
for ( int userSlot = 0; userSlot < MAX_SPLITSCREEN_PLAYERS; ++userSlot ) { ACTIVE_SPLITSCREEN_PLAYER_GUARD( userSlot ); int userID = XBX_GetUserId( userSlot );
// Skip writing if we haven't completed a round
if ( m_roundStats[userSlot][CSSTAT_ROUNDS_PLAYED] == 0 ) continue;
IPlayerLocal *pProfile = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( userID );
if ( !pProfile ) { Warning( "CCSClientGameStats::WriteLeaderboardStats failed to find the PlayerLocal Profile for the Active User!\n" ); return; }
// Retrieve online status from our matchmaking framework.
bool bMultiplayerGame = false; bool bPublicGame = false;
// Check if this is already a public game
IMatchSession *pMatchSession = g_pMatchFramework ? g_pMatchFramework->GetMatchSession() : NULL; if ( pMatchSession ) { KeyValues *pSystemData = pMatchSession->GetSessionSystemData(); if ( pSystemData ) { KeyValues *kv = pMatchSession->GetSessionSettings(); if ( kv ) { char const *szOnline = kv->GetString( "system/network", NULL ); if ( szOnline && !V_stricmp( "LIVE", szOnline ) ) { bMultiplayerGame = true; }
char const *szAccess = kv->GetString( "system/access", NULL ); if ( szAccess && !V_stricmp( "public", szAccess ) ) { bPublicGame = true; }
char const *szQueue = kv->GetString( "game/mmqueue", NULL ); if ( szQueue && *szQueue ) { // Queue games are always public
bPublicGame = true; } } } }
// We don't write to leaderboards for Offline or Online Private games
if ( !bMultiplayerGame || !bPublicGame ) return;
// Online write to leaderboard if we played as CT/T during this round
C_CSPlayer *pPlayer = (C_CSPlayer*)C_BasePlayer::GetLocalPlayer( userSlot ); bool bIsCT = true;
if ( pPlayer && pPlayer->GetTeamNumber() == TEAM_CT ) { bIsCT = true; } else if ( pPlayer && pPlayer->GetTeamNumber() == TEAM_TERRORIST ) { bIsCT = false; } else { return; }
// Calculate which set of leaderboards in the g_LeaderboardIDMap we write to, based on the current game mode/type
int boardSetIndex = -1;
switch ( g_pGameTypes->GetCurrentGameType() ) { case CS_GameType_Classic: { if ( bMultiplayerGame && bPublicGame ) { switch ( g_pGameTypes->GetCurrentGameMode() ) { case CS_GameMode::Classic_Casual: boardSetIndex = 0; // ONLINE_CASUAL
break; case CS_GameMode::Classic_Competitive: boardSetIndex = 1; // ONLINE_COMPETITIVE
break; default: Warning( "Leaderboard Write Error: Unknown CurrentGameMode value: %d!\n", g_pGameTypes->GetCurrentGameMode() ); break; } } } break;
case CS_GameType_GunGame: { if ( bMultiplayerGame && bPublicGame ) { switch ( g_pGameTypes->GetCurrentGameMode() ) { case CS_GameMode::GunGame_Progressive: boardSetIndex = 2; // ONLINE_GG_PROG
break;
case CS_GameMode::GunGame_Bomb: boardSetIndex = 3; // ONLINE_GG_BOMB
break;
default: // Unsupported game type
Warning( "Leaderboard Write Error: Unknown CurrentGameMode value: %d!\n", g_pGameTypes->GetCurrentGameMode() ); break; } } } break; default: { Warning( "Leaderboard Write Error: Unknown CurrentGameType value: %d!\n", g_pGameTypes->GetCurrentGameType() ); } break; }
// Sanity check the board type we selected:
if ( boardSetIndex < 0 || boardSetIndex >= kNumLeaderboardIDs ) { Warning( "Leaderboard Write Error: Current Game type/mode does not have a valid Leaderboard associated with it!\n" ); Warning( " Game Setup: type = %d, mode = %d, isMultiplayer? %d, isPublic? %d [ expected boardSetIdx = %d ]\n", g_pGameTypes->GetCurrentGameType(), g_pGameTypes->GetCurrentGameMode(), bMultiplayerGame, bPublicGame, boardSetIndex ); return; }
// Construct keyvalues that set the values we want to write to the leaderboard.
KeyValues *pLeaderboardInfo = new KeyValues( "leaderboardinfo" ); KeyValues::AutoDelete autoDelete( pLeaderboardInfo );
CEG_PROTECT_MEMBER_FUNCTION( CCSClientGameStats_WriteLeaderboardStats );
//
// Write out: Contribution Score
char csBoardName[256] = {0}; // Write to the appropriate board based on input type for all online boards
InputDevice_t inputDevice = g_pInputSystem->GetCurrentInputDevice(); // If we somehow don't have a device set yet, assume we're using the default device for the platform
if ( inputDevice == INPUT_DEVICE_NONE ) { inputDevice = PlatformInputDevice::GetDefaultInputDeviceForPlatform(); }
const char* pDeviceName = PlatformInputDevice::GetInputDeviceNameInternal( inputDevice );
if ( pDeviceName == NULL ) { Warning( "Leaderboard Write Error: Invalid input device (InputType_t = %d)- cannot write to ELO leaderboard!\n", inputDevice ); } else { V_snprintf( csBoardName, ARRAYSIZE(csBoardName), "%s_%s", g_LeaderboardIDMap[boardSetIndex].csName, pDeviceName ); } KeyValues *pkv = NULL;
if ( csBoardName[0] != 0 ) { pkv = pLeaderboardInfo->FindKey( csBoardName, true ); if ( pkv ) { pkv->SetInt( "average_contribution", 0 ); pkv->SetInt( "mvp_awards", m_roundStats[userSlot][CSSTAT_MVPS] ); pkv->SetInt( "rounds_played", m_roundStats[userSlot][CSSTAT_ROUNDS_PLAYED] ); pkv->SetInt( "kills", m_roundStats[userSlot][CSSTAT_KILLS] ); pkv->SetInt( "deaths", m_roundStats[userSlot][CSSTAT_DEATHS] ); pkv->SetInt( "damage", m_roundStats[userSlot][CSSTAT_DAMAGE] ); pkv->SetInt( "total_contribution", m_roundStats[userSlot][CSSTAT_CONTRIBUTION_SCORE] ); } }
//
// Write out: Kill / Death Ratio
pkv = pLeaderboardInfo->FindKey( g_LeaderboardIDMap[boardSetIndex].kdName, true ); if ( pkv ) { pkv->SetInt( "kd_ratio", 0 ); pkv->SetInt( "kills", m_roundStats[userSlot][CSSTAT_KILLS] ); pkv->SetInt( "shots_fired", m_roundStats[userSlot][CSSTAT_SHOTS_FIRED] ); pkv->SetInt( "head_shots", m_roundStats[userSlot][CSSTAT_KILLS_HEADSHOT] ); pkv->SetInt( "deaths", m_roundStats[userSlot][CSSTAT_DEATHS] ); pkv->SetInt( "shots_hit", m_roundStats[userSlot][CSSTAT_SHOTS_HIT] ); pkv->SetInt( "rounds_played", m_roundStats[userSlot][CSSTAT_ROUNDS_PLAYED] ); }
//
// Write out: Wins
pkv = pLeaderboardInfo->FindKey( g_LeaderboardIDMap[boardSetIndex].winsName, true ); if ( pkv ) { int winsAsCT = bIsCT ? m_roundStats[userSlot][CSSTAT_CT_ROUNDS_WON] : 0; int winsAsT = !bIsCT ? m_roundStats[userSlot][CSSTAT_T_ROUNDS_WON] : 0; int totalWins = winsAsCT + winsAsT; int totalPlayed = m_roundStats[userSlot][CSSTAT_ROUNDS_PLAYED]; int totalLosses = clamp( totalPlayed - totalWins, 0, totalPlayed );
int lossesAsCT = bIsCT ? (totalPlayed - m_roundStats[userSlot][CSSTAT_CT_ROUNDS_WON]) : 0; int lossesAsT = !bIsCT ? (totalPlayed - m_roundStats[userSlot][CSSTAT_T_ROUNDS_WON]) : 0;
pkv->SetInt( "wins_ratio", 0 ); pkv->SetInt( "total_wins", totalWins ); pkv->SetInt( "total_losses", totalLosses ); pkv->SetInt( "win_as_ct", winsAsCT ); pkv->SetInt( "win_as_t", winsAsT ); pkv->SetInt( "loss_as_ct", lossesAsCT ); pkv->SetInt( "loss_as_t", lossesAsT ); }
//
// Write out: Stars
pkv = pLeaderboardInfo->FindKey( g_LeaderboardIDMap[boardSetIndex].starsName, true ); if ( pkv ) { // Number of detonations is the number of "completed objectives" if you're on the Terrorist team
int totalDetonations = 0; if ( !bIsCT ) { totalDetonations = m_roundStats[userSlot][CSSTAT_OBJECTIVES_COMPLETED]; }
pkv->SetInt( "numstars", m_roundStats[userSlot][CSSTAT_MVPS] ); pkv->SetInt( "bombs_planted", m_roundStats[userSlot][CSSTAT_NUM_BOMBS_PLANTED] ); pkv->SetInt( "bombs_detonated", totalDetonations ); pkv->SetInt( "bombs_defused", m_roundStats[userSlot][CSSTAT_NUM_BOMBS_DEFUSED] ); pkv->SetInt( "hostages_rescued", m_roundStats[userSlot][CSSTAT_NUM_HOSTAGES_RESCUED] ); }
//
// Write out: Games played
pkv = pLeaderboardInfo->FindKey( g_LeaderboardIDMap[boardSetIndex].gpName, true ); if ( pkv ) { // Run through all medals to determine how many have been unlocked
CUtlMap<int, CBaseAchievement *> &achievements = g_AchievementMgrCS.GetAchievements( userID ); DWORD nMedalCount = 0; for ( int i=achievements.FirstInorder(); i!=achievements.InvalidIndex(); i=achievements.NextInorder(i) ) { if ( achievements[i]->IsAchieved() ) ++nMedalCount; }
// $TODO: This credits the entire round to having played as CT or T - is there any info about actual playtime?
int playTimeTotal = m_roundStats[userSlot][CSSTAT_PLAYTIME]; int ctTime = bIsCT ? playTimeTotal : 0; int tTime = !bIsCT ? playTimeTotal : 0;
pkv->SetInt( "num_rounds", m_roundStats[userSlot][CSSTAT_ROUNDS_PLAYED] ); pkv->SetInt( "time_played", playTimeTotal ); pkv->SetInt( "time_played_ct", ctTime ); pkv->SetInt( "time_played_t", tTime ); pkv->SetInt( "total_medals", nMedalCount ); }
DevMsg( "Updating leaderboards with:\n" ); KeyValuesDumpAsDevMsg( pLeaderboardInfo, 1 );
pProfile->UpdateLeaderboardData( pLeaderboardInfo ); }
#endif // !_X360
}
#ifdef _X360
CAsyncLeaderboardWriteThread::CAsyncLeaderboardWriteThread() { m_hThread = NULL; m_hEvent = CreateEvent( NULL, TRUE, FALSE, NULL ); }
CAsyncLeaderboardWriteThread::~CAsyncLeaderboardWriteThread() { if ( m_hThread ) ReleaseThreadHandle( m_hThread );
if ( m_hEvent ) CloseHandle( m_hEvent ); }
CAsyncLeaderboardWriteThread::LeaderboardWriteData_t* CAsyncLeaderboardWriteThread::CreateLeaderboardWriteData( void ) { LeaderboardWriteData_t* pData = new LeaderboardWriteData_t; ZeroMemory( pData, sizeof(LeaderboardWriteData_t) ); return pData; }
void CAsyncLeaderboardWriteThread::QueueData( LeaderboardWriteData_t *pData ) { if ( !pData ) return;
AUTO_LOCK( m_mutex ); m_queue.AddToTail( pData );
if ( !m_hThread ) { m_hThread = CreateSimpleThread( CallbackThreadProc, this ); }
// Signal the event to let the thread know that some data is waiting
SetEvent( m_hEvent ); }
void CAsyncLeaderboardWriteThread::ThreadProc( void ) { for ( ; ; ) { // Wait until our event is signaled that says we have data waiting
if ( WaitForSingleObject( m_hEvent, INFINITE ) == WAIT_OBJECT_0 ) { // Reset our event
ResetEvent( m_hEvent );
while ( m_queue.Count() > 0 ) { // Grab an item from the queue
LeaderboardWriteData_t *pData = NULL; { AUTO_LOCK( m_mutex ); if ( m_queue.Count() ) { pData = m_queue[0]; m_queue.Remove( 0 ); } }
// [smessick] Check to see if the player is signed into LIVE
int userID = pData->userID; bool isSignedInToLIVE = ( XUserGetSigninState( userID ) == eXUserSigninState_SignedInToLive );
// [smessick] Don't attempt the write to the leaderboards if the player is not signed into Xbox LIVE.
//ReleaseAssert( pData != NULL );
if ( !isSignedInToLIVE ) { Warning( "[CAsyncLeaderboardWriteThread] Not signed into LIVE. Removing queued data.\n" ); delete pData; continue; }
bool writeSuccess = false;
if ( xboxsystem ) { int kills = pData->propertiesContribScore[3].value.nData; // PROPERTY_CSS_LB_CS_TOTAL_KILLS
int deaths = pData->propertiesContribScore[4].value.nData; // PROPERTY_CSS_LB_CS_TOTAL_DEATHS
int contribScore = pData->propertiesContribScore[7].value.nData; // PROPERTY_CSS_LB_CS_TOTAL_CONTRIB_SCORE
int roundsPlayed = pData->propertiesContribScore[2].value.nData; // PROPERTY_CSS_LB_CS_TOTAL_ROUNDS_PLAYED
int gamesWon = pData->propertiesWins[1].value.nData; // PROPERTY_CSS_LB_WINS_WINS
// Before we can write some values to leaderboard (contrib score/round, average k/d, etc) we must
// read from them so we can retrieve the existing data to ensure that our formulas
// that determine rank are based on the appropriate values.
int result = 0;
// Construct the stat specs for the data we're interested in
const int kNumSpecReads = 2; XUSER_STATS_SPEC statsSpec[kNumSpecReads]; ZeroMemory( statsSpec, kNumSpecReads * sizeof(statsSpec) ); statsSpec[0].dwViewId = pData->viewProperties[0].dwViewId; // Contrib score board
statsSpec[0].dwNumColumnIds = 4; statsSpec[0].rgwColumnIds[0] = STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_KILLS; statsSpec[0].rgwColumnIds[1] = STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_DEATHS; statsSpec[0].rgwColumnIds[2] = STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_CONTRIB_SCORE; statsSpec[0].rgwColumnIds[3] = STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_ROUNDS_PLAYED;
statsSpec[1].dwViewId = pData->viewProperties[2].dwViewId; // Wins board
statsSpec[1].dwNumColumnIds = 1; statsSpec[1].rgwColumnIds[0] = STATS_COLUMN_WINS_ONLINE_CASUAL_WINS_TOTAL;
XUSER_STATS_READ_RESULTS *pResultsBuffer = 0; result = xboxsystem->EnumerateStatsByXuid( pData->xuid, 1, kNumSpecReads, statsSpec, (void**)(&pResultsBuffer), false );
if ( result == ERROR_SUCCESS ) { // Make sure all queried views are included in our result:
if ( pResultsBuffer->dwNumViews == kNumSpecReads ) { // Get the data we're interested in: This will fail gracefully if this is our first leaderboard-write for the current user
// from the Contrib score board:
if ( pResultsBuffer->pViews[0].dwNumRows == 1 && pResultsBuffer->pViews[0].pRows[0].dwNumColumns == 4 ) { if ( pResultsBuffer->pViews[0].pRows[0].pColumns[0].wColumnId == STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_KILLS ) { kills += pResultsBuffer->pViews[0].pRows[0].pColumns[0].Value.nData; }
if ( pResultsBuffer->pViews[0].pRows[0].pColumns[1].wColumnId == STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_DEATHS ) { deaths += pResultsBuffer->pViews[0].pRows[0].pColumns[1].Value.nData; }
if ( pResultsBuffer->pViews[0].pRows[0].pColumns[2].wColumnId == STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_CONTRIB_SCORE ) { contribScore += pResultsBuffer->pViews[0].pRows[0].pColumns[2].Value.nData; }
if ( pResultsBuffer->pViews[0].pRows[0].pColumns[3].wColumnId == STATS_COLUMN_CS_ONLINE_CASUAL_TOTAL_ROUNDS_PLAYED ) { roundsPlayed += pResultsBuffer->pViews[0].pRows[0].pColumns[3].Value.nData; } } // from the Wins board:
if ( pResultsBuffer->pViews[1].dwNumRows == 1 && pResultsBuffer->pViews[1].pRows[0].dwNumColumns == 1 ) { if ( pResultsBuffer->pViews[1].pRows[0].pColumns[0].wColumnId == STATS_COLUMN_WINS_ONLINE_CASUAL_WINS_TOTAL ) { gamesWon += pResultsBuffer->pViews[1].pRows[0].pColumns[0].Value.nData; } } } }
delete [] pResultsBuffer;
// Calculate the player's new rank
float fAverageContribScore = 0.0f; float fGamesPlayedRatio = 0.0f; float fKillDeathRatio = 0.0f; float fWinRatio = 0.0f;
fGamesPlayedRatio = clamp( roundsPlayed, 0.0f, 20.0f ) / 20.0f;
if ( deaths > 0 ) { fKillDeathRatio = ( (float)kills / (float)deaths ) * fGamesPlayedRatio; // printf( "Calculating k/d ratio: kills=%d, deaths=%d, gameRatio=%f\n", kills, deaths, fGamesPlayedRatio );
} else { fKillDeathRatio = (float)kills * fGamesPlayedRatio; // printf( "Calculating k/d ratio with NO deaths: kills=%d, gameRatio=%f\n", kills, fGamesPlayedRatio );
}
if ( roundsPlayed > 0 ) { fWinRatio = ( (float)gamesWon / (float)roundsPlayed ) * fGamesPlayedRatio; fAverageContribScore = ( (float)contribScore / (float)roundsPlayed );
// printf( "Calculating avg contrib score: contribScore=%d, rounds=%d\n", contribScore, roundsPlayed );
// printf( "Calculating win ratio: wins=%d, rounds=%d, gameRatio=%f\n", gamesWon, roundsPlayed, fGamesPlayedRatio );
}
// Update our write data with the adjusted rank information
pData->propertiesContribScore[0].value.i64Data = fAverageContribScore * 10000000; // PROPERTY_CSS_LB_CS_AVERAGE_CONTRIB_SCORE (or PROPERTY_CSS_LB_CS_ELO_RATING, for an offline-mode board)
//printf( "**** Writing out average contrib score: %f as %lld\n", fAverageContribScore, pData->propertiesContribScore[0].value.i64Data );
pData->propertiesKillDeath[0].value.i64Data = fKillDeathRatio * 10000000; // PROPERTY_CSS_LB_KD_KD_FORMULA
// printf( "**** Writing out k/d ratio score: %f as %lld\n", fKillDeathRatio, pData->propertiesKillDeath[0].value.i64Data );
pData->propertiesWins[0].value.i64Data = fWinRatio * 10000000; // PROPERTY_CSS_LB_WINS_WIN_FORMULA
// printf( "**** Writing out win ratio score: %f as %lld\n", fWinRatio, pData->propertiesWins[0].value.i64Data );
// Create a fake session to write the data
DWORD userIndexes[XUSER_MAX_COUNT]; BOOL privateSlots[XUSER_MAX_COUNT] = { TRUE, TRUE, TRUE, TRUE }; XSESSION_INFO sessionInfo; ULONGLONG sessionNonce; HANDLE hSession = NULL; const int numValidUserIndexes = 1;
userIndexes[0] = userID;
XUserSetContext( userIndexes[0], X_CONTEXT_GAME_TYPE, X_CONTEXT_GAME_TYPE_STANDARD); DWORD dw = XSessionCreate(XSESSION_CREATE_USES_STATS, userIndexes[0], 0, numValidUserIndexes, &sessionNonce, &sessionInfo, NULL, &hSession); if ( dw == ERROR_SUCCESS ) { dw = XSessionJoinLocal(hSession, numValidUserIndexes, userIndexes, privateSlots, NULL); if ( dw == ERROR_SUCCESS ) { dw = XSessionStart(hSession, 0, NULL); if ( dw == ERROR_SUCCESS ) { // Perform the actual write to the XBox Live service
dw = xboxsystem->WriteStats( hSession, pData->xuid, NUM_VIEW_PROPERTIES, &pData->viewProperties, false ); if ( dw == ERROR_SUCCESS ) { writeSuccess = true; } XSessionEnd(hSession, NULL); } } XSessionDelete(hSession, NULL); } }
// [smessick] Log a warning if the write failed.
if ( !writeSuccess ) { Warning( "[CAsyncLeaderboardWriteThread] Failed to write leaderboard data. Ignoring data.\n" ); }
// Delete our allocated object
delete pData; } } } } #endif // _X360
bool CCSClientGameStats::SyncCSMatchmakingDataToTitleData( int iController, CSSyncStatValueDirection_t eOp ) { // Get the local player.
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( iController ); if ( !pPlayerLocal ) return false;
#if defined ( _X360 )
TitleDataFieldsDescription_t const *pFields = g_pMatchFramework->GetMatchTitle()->DescribeTitleDataStorage();
if ( !ValidateTitleBlockVersion( pFields, pPlayerLocal, eOp, 1 ) ) return false;
MatchmakingData *pMMData = pPlayerLocal->GetPlayerMatchmakingData(); if ( !pMMData ) { return false; }
#define MATCHMAKINGDATA_FIELD( mmDataField ) \
Q_snprintf( fieldName, 255, "MMDATA.usr.%s%d", #mmDataField, mmDataType ); \ if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, fieldName ) ) \ { \ if ( pField->m_eDataType != TitleDataFieldsDescription_t::DT_uint16 ) \ { \ Warning( "%s is expected to be defined as DT_uint16\n", fieldName ); \ continue; \ } \ \ if ( eOp == CSSTAT_READ_STAT ) \ pMMData->mmDataField[ mmDataType ][ MMDATA_SCOPE_LIFETIME ] = TitleDataFieldsDescriptionGetValue<uint16>( pField, pPlayerLocal ); \ else \ TitleDataFieldsDescriptionSetValue<uint16>( pField, pPlayerLocal, pMMData->mmDataField[ mmDataType ][ MMDATA_SCOPE_LIFETIME ] ); \ } \ else \ { \ Warning( "Could not find TitleDataField for %s%d\n", #mmDataField, mmDataType ); \ }
char fieldName[ 256 ] = { 0 }; for ( int mmDataType = 0; mmDataType < MMDATA_TYPE_COUNT; ++mmDataType ) { MATCHMAKINGDATA_FIELD( mContribution ); MATCHMAKINGDATA_FIELD( mMVPs ); MATCHMAKINGDATA_FIELD( mKills ); MATCHMAKINGDATA_FIELD( mDeaths ); MATCHMAKINGDATA_FIELD( mHeadShots ); MATCHMAKINGDATA_FIELD( mDamage ); MATCHMAKINGDATA_FIELD( mShotsFired ); MATCHMAKINGDATA_FIELD( mShotsHit ); MATCHMAKINGDATA_FIELD( mDominations ); MATCHMAKINGDATA_FIELD( mRoundsPlayed ); }
#undef MATCHMAKINGDATA_FIELD
#endif // _X360
return true; }
bool CCSClientGameStats::SyncCSRankingDataToTitleData( int iController, CSSyncStatValueDirection_t eOp ) { // Get the local player.
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( iController ); if ( !pPlayerLocal ) return false;
#if defined( _GAMECONSOLE )
TitleDataFieldsDescription_t const *pFields = g_pMatchFramework->GetMatchTitle()->DescribeTitleDataStorage();
#if defined( _X360 )
if ( !ValidateTitleBlockVersion( pFields, pPlayerLocal, eOp, 3 ) ) return false;
#endif
// Get Player's Local Ranking Data
IPlayerRankingDataStore *pRankingData = pPlayerLocal->GetPlayerRankingData(); Assert( pRankingData );
char fieldName[ 64 ] = { 0 };
// Iterate through the ELO data by history, game mode, controller, online mode
// Player Rankings by mode, controller, w/ optional history
for ( int m = 0; m < ELOTitleData::NUM_GAME_MODES_ELO_RANKED; m++ ) { int numControllers = PlatformInputDevice::GetInputDeviceCountforPlatform(); for ( int c = 1; c <= numControllers; c++ ) { V_snprintf( fieldName, sizeof(fieldName), TITLE_DATA_PREFIX "ELO.MODE%d.CTR%d", m, c );
if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, fieldName ) ) { InputDevice_t controller = PlatformInputDevice::GetInputDeviceTypefromPlatformOrdinal( c ); if ( pField->m_eDataType != TitleDataFieldsDescription_t::DT_ELO ) { ELOWarning( "ELO: %s is expected to be defined as DT_ELO\n", fieldName ); continue; }
if ( eOp == CSSTAT_READ_STAT ) { PlayerELORank_t ELORank = TitleDataFieldsDescriptionGetValue<PlayerELORank_t>( pField, pPlayerLocal ); ELOMsg( "ELO: TitleData ELO Read (%d, %d) = %d\n", m, (int) controller, ELORank ); pRankingData->InitELORank( m, controller, ELORank ); } else { PlayerELORank_t ELORank = pRankingData->ReadELORank( m, controller ); ELOMsg( "ELO: TitleDataELO Write (%d, %d ) = %d\n", m, (int) controller, ELORank ); TitleDataFieldsDescriptionSetValue<PlayerELORank_t>( pField, pPlayerLocal, ELORank ); } } else { Warning( "Could not find TitleDataField for %s\n", fieldName ); } }
// Load/save the elo bracket info for game modes.
CFmtStr bracketInfo( TITLE_DATA_PREFIX"ELO.MODE%d.BRACKETINFO", m ); if ( TitleDataFieldsDescription_t const *pField = TitleDataFieldsDescriptionFindByString( pFields, bracketInfo.Access() ) ) { if ( eOp == CSSTAT_READ_STAT ) { uint16 data = TitleDataFieldsDescriptionGetValue<uint16>( pField, pPlayerLocal ); PlayerELOBracketInfo_t tmp; V_memcpy( &tmp, &data, sizeof( uint16 ) ); g_PlayerRankManager.Console_SetEloBracket( (ELOGameType_t) m, tmp ); ELOMsg( "ELO: TitleData ELO Bracket Read (%d) = display: %d prev: %d count %d\n", m, tmp.m_DisplayBracket, tmp.m_PreviousBracket, tmp.m_NumGamesInBracket ); } else { uint16 data = 0; PlayerELOBracketInfo_t bracketInfo; if ( g_PlayerRankManager.Console_GetEloBracket( (ELOGameType_t) m, &bracketInfo ) >= 0 ) { V_memcpy( &data, &bracketInfo, sizeof( data ) ); ELOMsg( "ELO: TitleData ELO Bracket Write (%d) = display: %d prev: %d count %d\n", m, bracketInfo.m_DisplayBracket, bracketInfo.m_PreviousBracket, bracketInfo.m_NumGamesInBracket ); } else { ELOMsg( "ELO: TitleData ELO Bracket Write (%d) = No bracket info for game mode. Writing 0.", m ); } TitleDataFieldsDescriptionSetValue<uint16>( pField, pPlayerLocal, data ); } } else { Warning( "Could not find TitleDataField for %s\n", bracketInfo.Access() ); } }
#endif // _GAMECONSOLE
return true; }
// Increment the current round matchmaking data by the data in the given stats structure.
// This method may be called multiple times per round so we have to deal with rounds played
// differently. Each call to the method is the delta from the previous call.
void CCSClientGameStats::IncrementMatchmakingData( const StatsCollection_t &stats ) { #if defined ( _X360 )
ACTIVE_SPLITSCREEN_PLAYER_GUARD( GET_ACTIVE_SPLITSCREEN_SLOT() );
// Get the active local player.
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( XBX_GetActiveUserId() ); if ( !pPlayerLocal ) return;
// Get the current mode of play
if ( CSGameRules() ) { // Determine if we're playing gungame progress or not, because we use different matchmaking stats for that game type.
MatchmakingDataType mmDataType = CSGameRules()->IsPlayingGunGameProgressive() ? MMDATA_TYPE_GGPROGRESSIVE : MMDATA_TYPE_GENERAL;
// Get the matchmaking data for the player.
MatchmakingData *pMMData = pPlayerLocal->GetPlayerMatchmakingData();
// Increment each of the entries by the stats collection.
pMMData->mContribution[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_CONTRIBUTION_SCORE]; pMMData->mMVPs[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_MVPS]; pMMData->mKills[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_KILLS]; pMMData->mDeaths[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_DEATHS]; pMMData->mHeadShots[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_KILLS_HEADSHOT]; pMMData->mDamage[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_DAMAGE]; pMMData->mShotsFired[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_SHOTS_FIRED]; pMMData->mShotsHit[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_SHOTS_HIT]; pMMData->mDominations[mmDataType][MMDATA_SCOPE_ROUND] += stats[CSSTAT_DOMINATIONS]; } #endif
}
// Get the matchmaking data for current primary user and compute the new rolling average based
// on the data accumulated so far.
void CCSClientGameStats::UpdateMatchmakingData( void ) { #if defined ( _X360 )
ACTIVE_SPLITSCREEN_PLAYER_GUARD( GET_ACTIVE_SPLITSCREEN_SLOT() );
// Get the active local player.
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( XBX_GetActiveUserId() ); if ( !pPlayerLocal ) return;
// Get the current mode of play
if ( CSGameRules() ) { // Determine if we're playing gungame progress or not, because we use different matchmaking stats for that game type.
MatchmakingDataType mmDataType = CSGameRules()->IsPlayingGunGameProgressive() ? MMDATA_TYPE_GGPROGRESSIVE : MMDATA_TYPE_GENERAL;
// Update the player's rolling averages for their matchmaking data.
pPlayerLocal->UpdatePlayerMatchmakingData( mmDataType );
// Reset the per round matchmaking data.
pPlayerLocal->ResetPlayerMatchmakingData( MMDATA_SCOPE_ROUND ); } #endif // _X360
}
// Reset the matchmaking data for the current primary user for the given scope.
void CCSClientGameStats::ResetMatchmakingData( MatchmakingDataScope mmDataScope ) { #if defined ( _X360 )
ACTIVE_SPLITSCREEN_PLAYER_GUARD( GET_ACTIVE_SPLITSCREEN_SLOT() );
// Get the active local player.
IPlayerLocal *pPlayerLocal = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( XBX_GetActiveUserId() ); if ( !pPlayerLocal ) return;
pPlayerLocal->ResetPlayerMatchmakingData( mmDataScope ); #endif // _X360
}
// OGS data and functions
#if !defined( _GAMECONSOLE )
// WARNING: must be in sync with the CSClientCsgoGameEventType_t in .h file
char const *g_CSClientCsgoGameEventTypeNames[] = { "Undefined", "Spray", "ConnectionProblem", "ConnectionLoss", "ConnectionChoke", }; // WARNING: must be in sync with the CSClientCsgoGameEventType_t in .h file
void CCSClientGameStats::AddClientCSGOGameEvent( CSClientCsgoGameEventType_t eEvent, Vector const &pos, QAngle const &ang, uint64 ullData /* = 0ull */, char const *szMapName /* = NULL */, int16 nRound /* = CSCLIENTCSGOGAMEEVENTTYPE_AUTODETECT_INT16 */, int16 nRoundSecondsElapsed /* = CSCLIENTCSGOGAMEEVENTTYPE_AUTODETECT_INT16 */ ) { CsgoGameEvent_t &cge = m_arrClientCsgoGameEvents[ m_arrClientCsgoGameEvents.AddToTail() ]; cge.m_eEvent = eEvent; cge.m_pos = pos; cge.m_ang = ang; cge.m_ullData = ullData; cge.m_symMap = CUtlSymbol( ( szMapName && *szMapName ) ? szMapName : engine->GetLevelNameShort() ); cge.m_nRound = nRound; if ( nRound == CSCLIENTCSGOGAMEEVENTTYPE_AUTODETECT_INT16 ) { if ( !CSGameRules() || CSGameRules()->IsWarmupPeriod() ) { cge.m_nRound = 0; } else { cge.m_nRound = CSGameRules()->GetTotalRoundsPlayed(); if ( !CSGameRules()->IsRoundOver() ) ++ cge.m_nRound; } } cge.m_numRoundSeconds = nRoundSecondsElapsed; if ( nRoundSecondsElapsed == CSCLIENTCSGOGAMEEVENTTYPE_AUTODETECT_INT16 ) { cge.m_numRoundSeconds = gpGlobals->curtime - CSGameRules()->GetRoundStartTime(); } switch ( eEvent ) { case k_CSClientCsgoGameEventType_SprayApplication: cge.m_bRequireMoreReliableUpload = true; break; default: cge.m_bRequireMoreReliableUpload = false; break; } }
ConVar cl_debug_round_stat_submission( "cl_debug_round_stat_submission", "0", FCVAR_DEVELOPMENTONLY );
CCSClientGameStats::StatContainerList_t* CCSClientGameStats::s_StatLists = new CCSClientGameStats::StatContainerList_t();
void CCSClientGameStats::UploadRoundStats() { // Upload all client game events, and remove the ones that we don't need to reupload
FOR_EACH_VEC( m_arrClientCsgoGameEvents, i ) { CsgoGameEvent_t const &cge = m_arrClientCsgoGameEvents[i]; char const *szEvent = g_CSClientCsgoGameEventTypeNames[0]; Assert( cge.m_eEvent > 0 && cge.m_eEvent < Q_ARRAYSIZE( g_CSClientCsgoGameEventTypeNames ) ); if ( ( cge.m_eEvent > 0 ) && ( cge.m_eEvent < Q_ARRAYSIZE( g_CSClientCsgoGameEventTypeNames ) ) ) szEvent = g_CSClientCsgoGameEventTypeNames[cge.m_eEvent];
if ( GetSteamWorksGameStatsClient().AddCsgoGameEventStat( cge.m_symMap.String(), szEvent, cge.m_pos, cge.m_ang, cge.m_ullData, cge.m_nRound, cge.m_numRoundSeconds ) || !cge.m_bRequireMoreReliableUpload ) m_arrClientCsgoGameEvents.Remove( i -- ); }
C_CSPlayer *pPlayer = ToCSPlayer( C_BasePlayer::GetLocalPlayer() );
if ( cl_debug_round_stat_submission.GetBool() ) { Msg( "Attempting to submit round stats... "); }
// Need to have played more than 10 seconds. If you haven't, then that means it's a nop round when first joining a server and having it restart
// due to having players on it. Also need to ensure that rounds played is greater than 0 since we'll be subtracting 1 to make it 0 based.
bool bIsValidTimedMatch = ( m_roundStats[0][CSSTAT_PLAYTIME] > 10 && m_matchStats[0][CSSTAT_ROUNDS_PLAYED] > 0 ); bool bIsValidArmsRaceMatch = CSGameRules()->IsPlayingGunGameProgressive() && ( m_RoundEndReason == CTs_Win || m_RoundEndReason == Terrorists_Win );
if ( pPlayer && ( bIsValidTimedMatch || bIsValidArmsRaceMatch ) ) { SRoundData roundData( &m_roundStats[0] );
if ( cl_debug_round_stat_submission.GetBool() ) { Msg( "Client session ID %llu Server Session ID %llu \n", GetSteamWorksGameStatsClient().GetSessionID(), GetSteamWorksGameStatsClient().GetServerSessionID() ); } // Use servers count of rounds this match, not rounds played by this player.
//pRoundData->nRound = m_matchStats[0][CSSTAT_ROUNDS_PLAYED] - 1;
roundData.nRound = CSGameRules( )->GetTotalRoundsPlayed( );
if ( cl_debug_round_stat_submission.GetBool() ) { Msg( "Submitting session id %llu round %d\n", GetSteamWorksGameStatsClient().GetSessionID(), roundData.nRound ); }
static int sLastRoundSubmitted = -1; static uint64 sLastSessionIDSubmitted = 0;
if ( sLastRoundSubmitted >= 0 && sLastSessionIDSubmitted != 0 ) { // HACK: We've got so many primary key violations we're effecting OGS perf.
// We currently think there are community servers running mods/rule changes
// that are responsible for much of this so we can't fix all of it. This
// horrible hack will throw out problem submits.
if ( roundData.nRound <= sLastRoundSubmitted && sLastSessionIDSubmitted == GetSteamWorksGameStatsClient( ).GetSessionID( ) ) { Warning( "OGS PK VIOLATION: Dropping round data for round %d session %llu because we've already submitted it.\n", roundData.nRound, GetSteamWorksGameStatsClient().GetSessionID() ); m_roundStats[0].Reset(); return; } }
roundData.nReason = m_RoundEndReason;
// HACK: Adding to the 16th bit of pRoundData->nRoundTime
// This lets us keep track of whether a player has:
// attempted to rescue a hostage.
roundData.nRoundTime |= ((uint32)m_bObjectiveAttempted) << 16;
// EXPERIMENTAL COLUMN IN OGS
//
// This column is general-purpose, intended to collect interesting stats on a round-by-round granularity
// RoundData was selected because coarse player attributes (e.g., their current server's tick rate, certain convars)
// seem unlikely to change at a faster rate. In many cases these values will be repeated across rounds, so most
// SQL aggregations involving the column will be AVG.
//
// We expect that the specific attributes stored below will change, possibly frequently. In some cases the attributes will
// be stale. The primary value of this column is to exist as a rapid option for sampling new player stats that are unlikely
// to be relevant in the distant future.
//
// An obvious problem with this implementation is loss of history, so any changes to the experimental column should be
// listed here, with a timeline. List previously-recorded attributes in little endian order, with #bits
//
// Experiment 5:
//
// Primary Weapon Def Index;
// Primary Weapon Ammo Count at death;
// Secondary Weapon Def Index;
// Secondary Weapon Ammo Count at death;
//
// Master Music Volume
// Main Menu Volume
// Round Start Volume
// Round End Volume
// Map Objective Volume
// Ten Second Warning Volume
// Death Cam Volume
// // ConVarRef required
static ConVarRef snd_musicvolume( "snd_musicvolume" ); static ConVarRef snd_menumusic_volume( "snd_menumusic_volume" ); static ConVarRef snd_roundstart_volume( "snd_roundstart_volume" ); static ConVarRef snd_roundend_volume( "snd_roundend_volume" ); static ConVarRef snd_mapobjective_volume( "snd_mapobjective_volume" ); static ConVarRef snd_tensecondwarning_volume( "snd_tensecondwarning_volume" ); static ConVarRef snd_deathcamera_volume( "snd_deathcamera_volume" ); static ConVarRef voice_scale("voice_scale"); uint8 *pData = ( uint8* )&roundData.llExperimental;
// Ammo count at death OGS data
//
*( pData ) = ( uint8 )pPlayer->m_roundEndAmmoCount.nPrimaryWeaponDefIndex; *( ++pData ) = ( uint8 )pPlayer->m_roundEndAmmoCount.nPrimaryWeaponAmmoCount; *( ++pData ) = ( uint8 )pPlayer->m_roundEndAmmoCount.nSecondaryWeaponDefIndex; *( ++pData ) = ( uint8 )pPlayer->m_roundEndAmmoCount.nSecondaryWeaponAmmoCount; //
// end of ammo count at death OGS data
// This code allowed us to measure discrepency between client and server bullet hits.
// It became obsolete when we started using a separate seed for client and server
// to eliminate 'rage' hacks.
//
*( ++pData ) = ( uint8 )pPlayer->m_ui8ClientServerHitDifference;
// Experiment 6:
// Replay utilization
// EE_REPLAY_OFFERED = 1,
// EE_REPLAY_REQUESTED = 2,
// EE_REPLAY_STARTED = 4,
// EE_REPLAY_CANCELLED = 8,
// EE_REPLAY_AUTOMATIC = 16
*( ++pData ) = ( uint8 )g_HltvReplaySystem.GetExperimentalEvents();
// float tickrate = 1.0 / gpGlobals->interval_per_tick;
// *( ++pData ) = ( uint8 )tickrate;
// // Sound and Music
// uint8 nMusicVolumeAsPct = (uint8)( 100.0f * snd_musicvolume.GetFloat() );
// uint8 nMenuMusicVolumeAsPct = (uint8)( 100.0f * snd_menumusic_volume.GetFloat() );
// uint8 nRoundStartVolumeAsPct = (uint8)( 100.0f * snd_roundstart_volume.GetFloat() );
// uint8 nRoundEndVolumeAsPct = (uint8)( 100.0f * snd_roundend_volume.GetFloat() );
// uint8 nMapObjectiveVolumeAsPct = (uint8)( 100.0f * snd_mapobjective_volume.GetFloat() );
// uint8 nTenSecondWarningVolumeAsPct = (uint8)( 100.0f * snd_tensecondwarning_volume.GetFloat() );
// uint8 nDeathCameraVolumeAsPct = (uint8)( 100.0f * snd_deathcamera_volume.GetFloat() );
//
// // Pack Reasonable Volume Duplets to fit 64 bits - set each to 0-10 range, pack into left and right 4 bits.
// uint8 nMusicAndMenuMusicVolume = (uint8)( ( nMusicVolumeAsPct / 10 ) | ( nMenuMusicVolumeAsPct / 10 ) << 4 );
// uint8 nRoundStartAndEndVolume = (uint8)( ( nRoundStartVolumeAsPct / 10 ) | ( nRoundEndVolumeAsPct / 10 ) << 4 );
// uint8 nMapObjectiveAndWarning = (uint8)( ( nMapObjectiveVolumeAsPct / 10 ) | ( nTenSecondWarningVolumeAsPct / 10 ) << 4 );
//
// *( ++pData ) = (uint8)nMusicAndMenuMusicVolume;
// *( ++pData ) = (uint8)nRoundStartAndEndVolume;
// *( ++pData ) = (uint8)nMapObjectiveAndWarning;
// *( ++pData ) = (uint8)nDeathCameraVolumeAsPct;
// END EXPERIMENTAL
// Our current money + what we spent is what we started with at the beginning of round
roundData.nStartingMoney = pPlayer->m_iStartAccount & 0x0000FFFF; //NOTHER TEMP HACK: Store round start player net worth in the top 16 bits of starting money
roundData.nStartingMoney |= ((uint32)pPlayer->GetRoundStartEquipmentValue( )) << 16; roundData.nTeamID = pPlayer->m_iTeamNum;
if ( cl_debug_round_stat_submission.GetBool() ) Msg( "Setting team num %d", roundData.nTeamID );
if( CSGameRules()->IsPlayingGunGameProgressive() ) { roundData.nRoundScore = m_roundStats[ 0 ][ CSSTAT_GG_PROGRESSIVE_CONTRIBUTION_SCORE ]; } else { roundData.nRoundScore = m_roundStats[ 0 ][ CSSTAT_CONTRIBUTION_SCORE ]; }
// Send off all OGS stats at level shutdown
KeyValues *pKV = new KeyValues( "basedata" ); if ( !pKV ) return;
char szMapNameBuffer[MAX_PATH]; V_strcpy_safe( szMapNameBuffer, engine->GetLevelName() ); V_FixSlashes( szMapNameBuffer, '/' ); // use consistent slashes so we don't get double entries for different platforms
V_StripExtension(szMapNameBuffer, szMapNameBuffer, sizeof( szMapNameBuffer ) );
int nLen = V_strlen( "maps/" ); if ( StringHasPrefix( szMapNameBuffer, "maps/" ) && *( szMapNameBuffer + nLen ) ) { // skip maps dir
pKV->SetString( "MapID", szMapNameBuffer + nLen ); } else { pKV->SetString( "MapID", szMapNameBuffer ); }
SubmitStat( &roundData );
// Perform the actual submission
SubmitGameStats( pKV );
sLastRoundSubmitted = CSGameRules()->GetTotalRoundsPlayed(); sLastSessionIDSubmitted = GetSteamWorksGameStatsClient().GetSessionID();
pKV->deleteThis();
m_RoundEndReason = Invalid_Round_End_Reason; m_bObjectiveAttempted = false; } else if ( cl_debug_round_stat_submission.GetBool() ) { Msg( "Skipping -- Client thinks round time is %d and num matches is %d\n", m_roundStats[0][CSSTAT_PLAYTIME], m_matchStats[0][CSSTAT_ROUNDS_PLAYED] ); }
m_roundStats[0].Reset(); }
#endif
|