You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2042 lines
71 KiB
2042 lines
71 KiB
//-------------------------------------------------------------
|
|
// 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
|