summaryrefslogtreecommitdiff
path: root/game/client/tf/tf_hud_statpanel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'game/client/tf/tf_hud_statpanel.cpp')
-rw-r--r--game/client/tf/tf_hud_statpanel.cpp1188
1 files changed, 1188 insertions, 0 deletions
diff --git a/game/client/tf/tf_hud_statpanel.cpp b/game/client/tf/tf_hud_statpanel.cpp
new file mode 100644
index 0000000..452bef3
--- /dev/null
+++ b/game/client/tf/tf_hud_statpanel.cpp
@@ -0,0 +1,1188 @@
+
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+// $NoKeywords: $
+//=============================================================================//
+
+#include "cbase.h"
+#ifdef WIN32
+#include "winerror.h"
+#endif
+#include "tf_hud_statpanel.h"
+#include "tf_hud_winpanel.h"
+#include <vgui/IVGui.h>
+#include "vgui_controls/AnimationController.h"
+#include "iclientmode.h"
+#include "c_tf_playerresource.h"
+#include <vgui_controls/Label.h>
+#include <vgui/ILocalize.h>
+#include <vgui/ISurface.h>
+#include "tf/c_tf_player.h"
+#include "tf/c_tf_team.h"
+#include "tf/tf_steamstats.h"
+#include "filesystem.h"
+#include "dmxloader/dmxloader.h"
+#include "fmtstr.h"
+#include "tf_statsummary.h"
+#include "usermessages.h"
+#include "hud_macros.h"
+#include "ixboxsystem.h"
+#include "achievementmgr.h"
+#include "tf_hud_freezepanel.h"
+#include "tf_gamerules.h"
+#include "tf_mapinfo.h"
+#include <vgui_controls/MessageBox.h>
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+DECLARE_HUDELEMENT_DEPTH( CTFStatPanel, 1 );
+DECLARE_HUD_MESSAGE( CTFStatPanel, PlayerStatsUpdate );
+DECLARE_HUD_MESSAGE( CTFStatPanel, MapStatsUpdate );
+
+BEGIN_DMXELEMENT_UNPACK( RoundStats_t )
+ DMXELEMENT_UNPACK_FIELD( "iNumShotsHit", "0", int, m_iStat[TFSTAT_SHOTS_HIT] )
+ DMXELEMENT_UNPACK_FIELD( "iNumShotsFired", "0", int, m_iStat[TFSTAT_SHOTS_FIRED] )
+ DMXELEMENT_UNPACK_FIELD( "iNumberOfKills", "0", int, m_iStat[TFSTAT_KILLS] )
+ DMXELEMENT_UNPACK_FIELD( "iNumDeaths", "0", int, m_iStat[TFSTAT_DEATHS] )
+ DMXELEMENT_UNPACK_FIELD( "iDamageDealt", "0", int, m_iStat[TFSTAT_DAMAGE] )
+ DMXELEMENT_UNPACK_FIELD( "iPlayTime", "0", int, m_iStat[TFSTAT_PLAYTIME] )
+ DMXELEMENT_UNPACK_FIELD( "iPointCaptures", "0", int, m_iStat[TFSTAT_CAPTURES] )
+ DMXELEMENT_UNPACK_FIELD( "iPointDefenses", "0", int, m_iStat[TFSTAT_DEFENSES] )
+ DMXELEMENT_UNPACK_FIELD( "iDominations", "0", int, m_iStat[TFSTAT_DOMINATIONS] )
+ DMXELEMENT_UNPACK_FIELD( "iRevenge", "0", int, m_iStat[TFSTAT_REVENGE] )
+ DMXELEMENT_UNPACK_FIELD( "iPointsScored", "0", int, m_iStat[TFSTAT_POINTSSCORED] )
+ DMXELEMENT_UNPACK_FIELD( "iBuildingsDestroyed", "0", int, m_iStat[TFSTAT_BUILDINGSDESTROYED] )
+ DMXELEMENT_UNPACK_FIELD( "iHeadshots", "0", int, m_iStat[TFSTAT_HEADSHOTS] )
+ DMXELEMENT_UNPACK_FIELD( "iHealthPointsHealed", "0", int, m_iStat[TFSTAT_HEALING] )
+ DMXELEMENT_UNPACK_FIELD( "iNumInvulnerable", "0", int, m_iStat[TFSTAT_INVULNS] )
+ DMXELEMENT_UNPACK_FIELD( "iKillAssists", "0", int, m_iStat[TFSTAT_KILLASSISTS] )
+ DMXELEMENT_UNPACK_FIELD( "iBackstabs", "0", int, m_iStat[TFSTAT_BACKSTABS] )
+ DMXELEMENT_UNPACK_FIELD( "iHealthPointsLeached", "0", int, m_iStat[TFSTAT_HEALTHLEACHED] )
+ DMXELEMENT_UNPACK_FIELD( "iBuildingsBuilt", "0", int, m_iStat[TFSTAT_BUILDINGSBUILT] )
+ DMXELEMENT_UNPACK_FIELD( "iSentryKills", "0", int, m_iStat[TFSTAT_MAXSENTRYKILLS] )
+ DMXELEMENT_UNPACK_FIELD( "iNumTeleports", "0", int, m_iStat[TFSTAT_TELEPORTS] )
+ DMXELEMENT_UNPACK_FIELD( "iFireDamage", "0", int, m_iStat[TFSTAT_FIREDAMAGE] )
+ DMXELEMENT_UNPACK_FIELD( "iBonusPoints", "0", int, m_iStat[TFSTAT_BONUS_POINTS] )
+ DMXELEMENT_UNPACK_FIELD( "iBlastDamage", "0", int, m_iStat[TFSTAT_BLASTDAMAGE] )
+END_DMXELEMENT_UNPACK( RoundStats_t, s_RoundStatsUnpack )
+
+BEGIN_DMXELEMENT_UNPACK( RoundMapStats_t )
+ DMXELEMENT_UNPACK_FIELD( "iPlayTime", "0", int, m_iStat[TFMAPSTAT_PLAYTIME] )
+END_DMXELEMENT_UNPACK( RoundMapStats_t, s_RoundMapStatsUnpack )
+
+BEGIN_DMXELEMENT_UNPACK( ClassStats_t )
+ DMXELEMENT_UNPACK_FIELD( "iPlayerClass", "0", int, iPlayerClass )
+ DMXELEMENT_UNPACK_FIELD( "iNumberOfRounds", "0", int, iNumberOfRounds )
+ // RoundStats_t accumulated;
+ // RoundStats_t max;
+ // RoundStats_t currentRound;
+ // RoundStats_t accumulatedMVM;
+ // RoundStats_t maxMVM;
+END_DMXELEMENT_UNPACK( ClassStats_t, s_ClassStatsUnpack )
+
+BEGIN_DMXELEMENT_UNPACK( MapStats_t )
+ DMXELEMENT_UNPACK_FIELD( "iMapID", "0", map_identifier_t, iMapID )
+ DMXELEMENT_UNPACK_FIELD( "iNumberOfRounds", "0", int, iNumberOfRounds )
+ // RoundStats_t accumulated;
+ // RoundStats_t currentRound;
+END_DMXELEMENT_UNPACK( MapStats_t, s_MapStatsUnpack )
+
+// priority order of stats to display record for; earlier position in list is highest
+TFStatType_t g_statPriority[] = { TFSTAT_HEADSHOTS, TFSTAT_BACKSTABS, TFSTAT_MAXSENTRYKILLS, TFSTAT_HEALING, TFSTAT_KILLS, TFSTAT_KILLASSISTS,
+ TFSTAT_DAMAGE, TFSTAT_DOMINATIONS, TFSTAT_INVULNS, TFSTAT_BUILDINGSDESTROYED, TFSTAT_CAPTURES, TFSTAT_DEFENSES, TFSTAT_REVENGE, TFSTAT_TELEPORTS, TFSTAT_BUILDINGSBUILT,
+ TFSTAT_HEALTHLEACHED, TFSTAT_POINTSSCORED, TFSTAT_PLAYTIME, TFSTAT_BONUS_POINTS };
+// stat types that we don't display records for, kept in this list just so we can assert all stats appear in one list or the other
+TFStatType_t g_statUnused[] = { TFSTAT_DEATHS, TFSTAT_UNDEFINED, TFSTAT_SHOTS_FIRED, TFSTAT_SHOTS_HIT, TFSTAT_FIREDAMAGE, TFSTAT_BLASTDAMAGE,
+ TFSTAT_DAMAGETAKEN, TFSTAT_HEALTHKITS, TFSTAT_AMMOKITS, TFSTAT_CLASSCHANGES, TFSTAT_CRITS, TFSTAT_SUICIDES, TFSTAT_CURRENCY_COLLECTED, TFSTAT_DAMAGE_ASSIST, TFSTAT_HEALING_ASSIST,
+ TFSTAT_DAMAGE_BOSS, TFSTAT_DAMAGE_BLOCKED, TFSTAT_DAMAGE_RANGED, TFSTAT_DAMAGE_RANGED_CRIT_RANDOM, TFSTAT_DAMAGE_RANGED_CRIT_BOOSTED, TFSTAT_REVIVED, TFSTAT_THROWABLEHIT, TFSTAT_THROWABLEKILL, TFSTAT_KILLS_RUNECARRIER, TFSTAT_FLAGRETURNS,
+ TFSTAT_KILLSTREAK_MAX };
+
+// priority order of stats to display record for; earlier position in list is highest
+TFMapStatType_t g_mapStatPriority[] = { TFMAPSTAT_PLAYTIME };
+// stat types that we don't display records for, kept in this list just so we can assert all stats appear in one list or the other
+TFMapStatType_t g_mapStatUnused[] = { TFMAPSTAT_UNDEFINED };
+
+// localization keys for stat panel text, must be in same order as TFStatType_t
+const char *g_szLocalizedRecordText[] =
+{
+ "",
+ "[shots hit]",
+ "[shots fired]",
+ "#StatPanel_Kills",
+ "[deaths]",
+ "#StatPanel_DamageDealt",
+ "#StatPanel_Captures",
+ "#StatPanel_Defenses",
+ "#StatPanel_Dominations",
+ "#StatPanel_Revenge",
+ "#StatPanel_PointsScored",
+ "#StatPanel_BuildingsDestroyed",
+ "#StatPanel_Headshots",
+ "#StatPanel_PlayTime",
+ "#StatPanel_Healing",
+ "#StatPanel_Invulnerable",
+ "#StatPanel_KillAssists",
+ "#StatPanel_Backstabs",
+ "#StatPanel_HealthLeached",
+ "#StatPanel_BuildingsBuilt",
+ "#StatPanel_SentryKills",
+ "#StatPanel_Teleports",
+ "#StatPanel_BonusPoints"
+};
+
+const char *g_szLocalizedMVMRecordText[] =
+{
+ "",
+ "[shots hit]",
+ "[shots fired]",
+ "#StatPanel_MVM_Kills",
+ "[deaths]",
+ "#StatPanel_MVM_DamageDealt",
+ "#StatPanel_MVM_Captures",
+ "#StatPanel_MVM_Defenses",
+ "#StatPanel_MVM_Dominations",
+ "#StatPanel_MVM_Revenge",
+ "#StatPanel_MVM_PointsScored",
+ "#StatPanel_MVM_BuildingsDestroyed",
+ "#StatPanel_MVM_Headshots",
+ "#StatPanel_MVM_PlayTime",
+ "#StatPanel_MVM_Healing",
+ "#StatPanel_MVM_Invulnerable",
+ "#StatPanel_MVM_KillAssists",
+ "#StatPanel_MVM_Backstabs",
+ "#StatPanel_MVM_HealthLeached",
+ "#StatPanel_MVM_BuildingsBuilt",
+ "#StatPanel_MVM_SentryKills",
+ "#StatPanel_MVM_Teleports",
+ "#StatPanel_MVM_BonusPoints"
+};
+
+
+static CTFStatPanel *statPanel = NULL;
+extern CAchievementMgr g_AchievementMgrTF;
+
+//-----------------------------------------------------------------------------
+// Purpose: Returns the static stats panel
+//-----------------------------------------------------------------------------
+CTFStatPanel *GetStatPanel()
+{
+ return statPanel;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Constructor
+//-----------------------------------------------------------------------------
+CTFStatPanel::CTFStatPanel( const char *pElementName )
+: EditablePanel( NULL, "StatPanel" ), CHudElement( pElementName )
+{
+ // Assert that all defined stats are in our prioritized list or explicitly unused
+ Assert( ARRAYSIZE( g_statPriority ) + ARRAYSIZE( g_statUnused ) == TFSTAT_TOTAL );
+ Assert( ARRAYSIZE( g_mapStatPriority ) + ARRAYSIZE( g_mapStatUnused ) == TFMAPSTAT_TOTAL );
+
+ ResetDisplayedStat();
+ m_bStatsChanged = false;
+ m_bLocalFileTrusted = false;
+ m_flTimeLastSpawn = 0;
+ vgui::Panel *pParent = g_pClientMode->GetViewport();
+ SetParent( pParent );
+ m_bShouldBeVisible = false;
+ SetScheme( "ClientScheme" );
+ statPanel = this;
+
+ m_pClassImage = new CTFClassImage( this, "StatPanelClassImage" );
+
+ // Read stats from disk. (Definitive stat store for X360; for PC, whatever we get from Steam is authoritative.)
+ ReadStats();
+
+ RegisterForRenderGroup( "mid" );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Destructor
+//-----------------------------------------------------------------------------
+CTFStatPanel::~CTFStatPanel()
+{
+ if ( statPanel == this )
+ statPanel = NULL;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: called when level is shutting down
+//-----------------------------------------------------------------------------
+void CTFStatPanel::LevelShutdown()
+{
+ // write out stats if they've changed
+ WriteStats();
+ UpdateStatSummaryPanel();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::ApplySettings( KeyValues *inResourceData )
+{
+ BaseClass::ApplySettings( inResourceData );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::Reset()
+{
+ if ( gpGlobals->curtime > m_flTimeHide )
+ {
+ Hide();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Resets which stat is being displayed
+//-----------------------------------------------------------------------------
+void CTFStatPanel::ResetDisplayedStat()
+{
+ m_iCurStatValue = 0;
+ m_iCurStatTeam = TEAM_UNASSIGNED;
+ m_statType = TFSTAT_UNDEFINED;
+ m_recordBreakType = RECORDBREAK_NONE;
+ m_iCurStatClass = TF_CLASS_UNDEFINED;
+ m_bDisplayAfterSpawn = false;
+ m_flTimeHide = 0;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::Init()
+{
+ // listen for events
+ HOOK_HUD_MESSAGE( CTFStatPanel, PlayerStatsUpdate );
+ HOOK_HUD_MESSAGE( CTFStatPanel, MapStatsUpdate );
+ ListenForGameEvent( "player_spawn" );
+
+ Hide();
+
+ CHudElement::Init();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::UpdateStats( int iClass, const RoundStats_t &stats, bool bAlive )
+{
+ C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer();
+ if ( !pPlayer )
+ return;
+
+ // don't count stats if cheats on, commentary mode, etc
+ if ( !g_AchievementMgrTF.CheckAchievementsEnabled() )
+ return;
+
+ ClassStats_t &classStats = GetClassStats( iClass );
+
+ bool bMVM = TFGameRules() && TFGameRules()->IsMannVsMachineMode();
+
+ if ( bMVM )
+ {
+ classStats.AccumulateMVMRound( stats );
+ }
+ else
+ {
+ classStats.AccumulateRound( stats );
+ }
+
+ // sentry kills is a max value rather than a count, meaningless to accumulate
+ classStats.accumulated.m_iStat[TFSTAT_MAXSENTRYKILLS] = 0;
+ classStats.accumulatedMVM.m_iStat[TFSTAT_MAXSENTRYKILLS] = 0;
+
+ ResetDisplayedStat();
+ m_iCurStatClass = iClass;
+
+ // run through all stats we keep records for, update the max value, and if a record is set,
+ // remember the highest priority record
+ for ( int i= ARRAYSIZE( g_statPriority )-1; i >= 0; i-- )
+ {
+ TFStatType_t statType = g_statPriority[i];
+ if ( statType == TFSTAT_BONUS_POINTS )
+ continue;
+
+ int iCur = stats.m_iStat[statType];
+ int iMax = ( bMVM ? classStats.maxMVM.m_iStat[statType] : classStats.max.m_iStat[statType] );
+ if ( iCur > iMax )
+ {
+ // Record was set, remember what stat set a record.
+ if ( bMVM )
+ {
+ classStats.maxMVM.m_iStat[statType] = iCur;
+ }
+ else
+ {
+ classStats.max.m_iStat[statType] = iCur;
+ }
+
+ m_iCurStatValue = iCur;
+ m_statType = statType;
+ m_recordBreakType = RECORDBREAK_BEST;
+ }
+ else if ( ( iCur > 0 ) && ( m_recordBreakType <= RECORDBREAK_TIE ) && ( iCur == iMax ) )
+ {
+ // if we haven't broken a record and we tied this one, display it
+ m_iCurStatValue = iCur;
+ m_statType = statType;
+ m_recordBreakType = RECORDBREAK_TIE;
+ }
+ else if ( ( iCur > 0 ) && ( m_recordBreakType <= RECORDBREAK_CLOSE ) && ( iCur >= (int) ( (float) iMax * 0.8f ) ) )
+ {
+ // if we haven't broken a record or tied a record but we came close to this one, display it
+ m_iCurStatValue = iCur;
+ m_statType = statType;
+ m_recordBreakType = RECORDBREAK_CLOSE;
+ }
+ }
+
+ m_bStatsChanged = true;
+
+ if ( m_statType > TFSTAT_UNDEFINED )
+ {
+ m_iCurStatTeam = pPlayer->GetTeamNumber();
+ if ( !bAlive || ( gpGlobals->curtime - m_flTimeLastSpawn < 3.0 ) )
+ {
+ // show the panel now if dead or very recently spawned
+ vgui::ivgui()->AddTickSignal( GetVPanel(), 1000 );
+ extern ConVar hud_freezecamhide;
+ if ( !hud_freezecamhide.GetBool() && !IsTakingAFreezecamScreenshot() )
+ {
+ ShowStatPanel( m_iCurStatClass, m_iCurStatTeam, m_iCurStatValue, m_statType, m_recordBreakType, bAlive );
+ m_flTimeHide = gpGlobals->curtime + 20.0f;
+ }
+ }
+ else
+ {
+ // otherwise wait until next spawn to show panel
+ m_bDisplayAfterSpawn = true;
+ }
+ }
+
+ IGameEvent * event = gameeventmanager->CreateEvent( "player_stats_updated" );
+ if ( event )
+ {
+ event->SetBool( "forceupload", false );
+ gameeventmanager->FireEventClientSide( event );
+ }
+
+ UpdateStatSummaryPanel();
+}
+
+void CTFStatPanel::UpdateMapStats( map_identifier_t iMapID, const RoundMapStats_t &stats )
+{
+ C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer();
+ if ( !pPlayer )
+ return;
+
+ // It's ok to count map stats while cheating and such
+ //if ( !g_AchievementMgrTF.CheckAchievementsEnabled() )
+ // return;
+
+ MapStats_t &mapStats = GetMapStats( iMapID );
+
+ mapStats.AccumulateRound( stats );
+
+ ResetDisplayedStat();
+ m_iCurStatClass = iMapID;
+
+ m_bStatsChanged = true;
+
+ IGameEvent * event = gameeventmanager->CreateEvent( "player_stats_updated" );
+ if ( event )
+ {
+ event->SetBool( "forceupload", false );
+ gameeventmanager->FireEventClientSide( event );
+ }
+
+ UpdateStatSummaryPanel();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::TestStatPanel( TFStatType_t statType, RecordBreakType_t recordType )
+{
+ C_TFPlayer *pPlayer = C_TFPlayer::GetLocalTFPlayer();
+ if ( !pPlayer )
+ {
+ Msg( "Please load a map first.\n" );
+ return;
+ }
+
+ bool bMVM = TFGameRules() && TFGameRules()->IsMannVsMachineMode();
+
+ m_iCurStatClass = pPlayer->GetPlayerClass()->GetClassIndex();
+ ClassStats_t &classStats = GetClassStats( m_iCurStatClass );
+ m_iCurStatValue = bMVM ? classStats.maxMVM.m_iStat[statType] : classStats.max.m_iStat[statType];
+ m_iCurStatTeam = pPlayer->GetTeamNumber();
+
+ ShowStatPanel( m_iCurStatClass, m_iCurStatTeam, m_iCurStatValue, statType, recordType, false );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Writes stat file. Used as primary storage for X360. For PC,
+// Steam is authoritative but we write stat file for debugging (although
+// we never read it).
+//-----------------------------------------------------------------------------
+void CTFStatPanel::WriteStats( void )
+{
+ if ( !m_bStatsChanged )
+ return;
+
+ MEM_ALLOC_CREDIT();
+
+ DECLARE_DMX_CONTEXT();
+ CDmxElement *pPlayerStats = CreateDmxElement( "PlayerStats" );
+ CDmxElementModifyScope modify( pPlayerStats );
+
+ // get Steam ID. If not logged into Steam, use 0
+ int iSteamID = 0;
+ if ( steamapicontext->SteamUser() )
+ {
+ CSteamID steamID = steamapicontext->SteamUser()->GetSteamID();
+ iSteamID = steamID.GetAccountID();
+ }
+ // Calc CRC of all data to make the local data file somewhat tamper-resistant
+ int iCRC = CalcCRC( iSteamID );
+
+ pPlayerStats->SetValue( "iVersion", static_cast<int>( PLAYERSTATS_FILE_VERSION ) );
+ pPlayerStats->SetValue( "SteamID", iSteamID );
+ pPlayerStats->SetValue( "iTimestamp", iCRC ); // store the CRC with a non-obvious name
+
+ CDmxAttribute *pClassStatsList = pPlayerStats->AddAttribute( "aClassStats" );
+ CUtlVector< CDmxElement* >& classStats = pClassStatsList->GetArrayForEdit<CDmxElement*>();
+
+ modify.Release();
+
+ for( int i = 0; i < m_aClassStats.Count(); i++ )
+ {
+ const ClassStats_t &stat = m_aClassStats[ i ];
+
+ // strip out any garbage class data
+ if ( ( stat.iPlayerClass > (TF_LAST_NORMAL_CLASS-1) ) || ( stat.iPlayerClass < TF_FIRST_NORMAL_CLASS ) )
+ continue;
+
+ CDmxElement *pClass = CreateDmxElement( "ClassStats_t" );
+ classStats.AddToTail( pClass );
+
+ CDmxElementModifyScope modifyClass( pClass );
+
+ pClass->SetValue( "comment: classname", g_aPlayerClassNames_NonLocalized[ stat.iPlayerClass ] );
+ pClass->AddAttributesFromStructure( &stat, s_ClassStatsUnpack );
+
+ CDmxElement *pAccumulated = CreateDmxElement( "RoundStats_t" );
+ pAccumulated->AddAttributesFromStructure( &stat.accumulated, s_RoundStatsUnpack );
+ pClass->SetValue( "accumulated", pAccumulated );
+
+ CDmxElement *pMax = CreateDmxElement( "RoundStats_t" );
+ pMax->AddAttributesFromStructure( &stat.max, s_RoundStatsUnpack );
+ pClass->SetValue( "max", pMax );
+
+ CDmxElement *pAccumulatedMVM = CreateDmxElement( "RoundStats_t" );
+ pAccumulatedMVM->AddAttributesFromStructure( &stat.accumulatedMVM, s_RoundStatsUnpack );
+ pClass->SetValue( "accumulatedmvm", pAccumulatedMVM );
+
+ CDmxElement *pMaxMVM = CreateDmxElement( "RoundStats_t" );
+ pMaxMVM->AddAttributesFromStructure( &stat.maxMVM, s_RoundStatsUnpack );
+ pClass->SetValue( "maxmvm", pMaxMVM );
+ }
+
+ CDmxAttribute *pMapStatsList = pPlayerStats->AddAttribute( "aMapStats" );
+ CUtlVector< CDmxElement* >& mapStats = pMapStatsList->GetArrayForEdit<CDmxElement*>();
+
+ for( int i = 0; i < m_aMapStats.Count(); i++ )
+ {
+ const MapStats_t &stat = m_aMapStats[ i ];
+
+ // strip out any garbage map data
+ if ( !IsValidMapID( stat.iMapID ) )
+ continue;
+
+ CDmxElement *pMap = CreateDmxElement( "MapStats_t" );
+ mapStats.AddToTail( pMap );
+
+ CDmxElementModifyScope modifyClass( pMap );
+
+ pMap->SetValue( "comment: mapname", GetMapNameFromID( stat.iMapID ) );
+ pMap->AddAttributesFromStructure( &stat, s_MapStatsUnpack );
+
+ CDmxElement *pAccumulated = CreateDmxElement( "RoundMapStats_t" );
+ pAccumulated->AddAttributesFromStructure( &stat.accumulated, s_RoundMapStatsUnpack );
+ pMap->SetValue( "accumulated", pAccumulated );
+ }
+
+ if ( IsX360() )
+ {
+#ifdef _X360
+ if ( XBX_GetStorageDeviceId() == XBX_INVALID_STORAGE_ID || XBX_GetStorageDeviceId() == XBX_STORAGE_DECLINED )
+ return;
+#endif
+ }
+
+ char szFilename[_MAX_PATH];
+
+ if ( IsX360() )
+ Q_snprintf( szFilename, sizeof( szFilename ), "cfg:/tf2_playerstats.dmx" );
+ else
+ Q_snprintf( szFilename, sizeof( szFilename ), "tf2_playerstats.dmx" );
+
+ {
+ MEM_ALLOC_CREDIT();
+ CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
+ if ( SerializeDMX( buf, pPlayerStats, szFilename ) )
+ {
+ filesystem->WriteFile( szFilename, "MOD", buf );
+ }
+ }
+
+ CleanupDMX( pPlayerStats );
+
+ if ( IsX360() )
+ {
+ xboxsystem->FinishContainerWrites();
+ }
+
+ m_bStatsChanged = false;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFStatPanel::ReadStats( void )
+{
+ CDmxElement *pPlayerStats;
+
+ DECLARE_DMX_CONTEXT();
+
+ if ( IsX360() )
+ {
+#ifdef _X360
+ if ( XBX_GetStorageDeviceId() == XBX_INVALID_STORAGE_ID || XBX_GetStorageDeviceId() == XBX_STORAGE_DECLINED )
+ return false;
+#endif
+ }
+
+ char szFilename[_MAX_PATH];
+
+ if ( IsX360() )
+ {
+ Q_snprintf( szFilename, sizeof( szFilename ), "cfg:/tf2_playerstats.dmx" );
+ }
+ else
+ {
+ Q_snprintf( szFilename, sizeof( szFilename ), "tf2_playerstats.dmx" );
+ }
+
+ MEM_ALLOC_CREDIT();
+
+ bool bOk = UnserializeDMX( szFilename, "MOD", true, &pPlayerStats );
+
+ if ( !bOk )
+ return false;
+
+ int iVersion = pPlayerStats->GetValue< int >( "iVersion" );
+ if ( iVersion > PLAYERSTATS_FILE_VERSION )
+ {
+ // file is beyond our comprehension
+ return false;
+ }
+
+ int iSteamID = pPlayerStats->GetValue<int>( "SteamID" );
+ int iCRCFile = pPlayerStats->GetValue<int>( "iTimestamp" );
+
+ const CUtlVector< CDmxElement* > &aClassStatsList = pPlayerStats->GetArray< CDmxElement * >( "aClassStats" );
+ int iCount = aClassStatsList.Count();
+ m_aClassStats.SetCount( iCount );
+ for( int i = 0; i < m_aClassStats.Count(); i++ )
+ {
+ CDmxElement *pClass = aClassStatsList[ i ];
+ ClassStats_t &stat = m_aClassStats[ i ];
+
+ pClass->UnpackIntoStructure( &stat, sizeof( stat ), s_ClassStatsUnpack );
+
+ CDmxElement *pAccumulated = pClass->GetValue< CDmxElement * >( "accumulated" );
+ if ( pAccumulated )
+ {
+ pAccumulated->UnpackIntoStructure( &stat.accumulated, sizeof( stat.accumulated ), s_RoundStatsUnpack );
+ }
+
+ CDmxElement *pMax = pClass->GetValue< CDmxElement * >( "max" );
+ if ( pMax )
+ {
+ pMax->UnpackIntoStructure( &stat.max, sizeof( stat.max ), s_RoundStatsUnpack );
+ }
+
+ CDmxElement *pAccumulatedMVM = pClass->GetValue< CDmxElement * >( "accumulatedMVM" );
+ if ( pAccumulatedMVM )
+ {
+ pAccumulatedMVM->UnpackIntoStructure( &stat.accumulatedMVM, sizeof( stat.accumulatedMVM ), s_RoundStatsUnpack );
+ }
+
+ CDmxElement *pMaxMVM = pClass->GetValue< CDmxElement * >( "maxMVM" );
+ if ( pMaxMVM )
+ {
+ pMaxMVM->UnpackIntoStructure( &stat.maxMVM, sizeof( stat.maxMVM ), s_RoundStatsUnpack );
+ }
+ }
+
+ const CUtlVector< CDmxElement* > &aMapStatsList = pPlayerStats->GetArray< CDmxElement * >( "aMapStats" );
+ iCount = aMapStatsList.Count();
+ m_aMapStats.SetCount( iCount );
+ for( int i = 0; i < aMapStatsList.Count(); i++ )
+ {
+ CDmxElement *pMap = aMapStatsList[ i ];
+ MapStats_t &stat = m_aMapStats[ i ];
+
+ pMap->UnpackIntoStructure( &stat, sizeof( stat ), s_MapStatsUnpack );
+
+ CDmxElement *pAccumulated = pMap->GetValue< CDmxElement * >( "accumulated" );
+ if ( pAccumulated )
+ {
+ pAccumulated->UnpackIntoStructure( &stat.accumulated, sizeof( stat.accumulated ), s_RoundMapStatsUnpack );
+ }
+ }
+
+ CleanupDMX( pPlayerStats );
+
+ UpdateStatSummaryPanel();
+
+ // check file CRC and steam ID to see if we think this file has not been tampered with
+ int iCRC = CalcCRC( iSteamID );
+ // does file CRC match CRC generated from file data, and is there a Steam ID in the file
+ if ( ( iCRC == iCRCFile ) && ( iSteamID > 0 ) && steamapicontext->SteamUser() )
+ {
+ // does the file Steam ID match current Steam ID (so you can't hand around files)
+ CSteamID steamID = steamapicontext->SteamUser()->GetSteamID();
+ if ( steamID.GetAccountID() == (uint32) iSteamID )
+ {
+ m_bLocalFileTrusted = true;
+ }
+ }
+
+ m_bStatsChanged = false;
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Calcs CRC of all stat data
+//-----------------------------------------------------------------------------
+int CTFStatPanel::CalcCRC( int iSteamID )
+{
+ CRC32_t crc;
+ CRC32_Init( &crc );
+
+ // make a CRC of stat data
+ CRC32_ProcessBuffer( &crc, &iSteamID, sizeof( iSteamID ) );
+
+ for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass <= TF_LAST_NORMAL_CLASS; iClass++ )
+ {
+ // add each class' data to the CRC
+ ClassStats_t &classStats = GetClassStats( iClass );
+ CRC32_ProcessBuffer( &crc, &classStats, sizeof( classStats ) );
+ // since the class data structure is highly guessable from the file, add one other thing to make the CRC hard to hack w/o code disassembly
+ int iObfuscate = iClass * iClass;
+ CRC32_ProcessBuffer( &crc, &iObfuscate, sizeof( iObfuscate ) );
+ }
+
+ CRC32_Final( &crc );
+
+ return (int) ( crc & 0x7FFFFFFF );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::ShowStatPanel( int iClass, int iTeam, int iCurStatValue, TFStatType_t statType, RecordBreakType_t recordBreakType,
+ bool bAlive )
+{
+ bool bMVM = TFGameRules() && TFGameRules()->IsMannVsMachineMode();
+
+ // If this is MvM mode and we're looking at the round end, dont show the stats
+ // panel because the PVEWinPanel will be up
+ if( bMVM && TFGameRules()->State_Get() == GR_STATE_TEAM_WIN )
+ {
+ return;
+ }
+
+ ClassStats_t &classStats = GetClassStats( iClass );
+ vgui::Label *pLabel = dynamic_cast<Label *>( FindChildByName( "summaryLabel" ) );
+ if ( !pLabel )
+ return;
+
+ const char *pRecordTextSuffix[RECORDBREAK_MAX] = { "", "close", "tie", "best" };
+
+ const char *pLocalizedTitle = bAlive ? "#StatPanel_Title_Alive" : "#StatPanel_Title_Dead";
+ SetDialogVariable( "title", g_pVGuiLocalize->Find( pLocalizedTitle ) );
+ SetDialogVariable( "stattextlarge", "" );
+ SetDialogVariable( "stattextsmall", "" );
+ if ( recordBreakType == RECORDBREAK_CLOSE )
+ {
+ // if we are displaying that the player got close to a record, show current & best values
+ char szCur[32],szBest[32];
+ wchar_t wzCur[32],wzBest[32];
+ GetStatValueAsString( iCurStatValue, statType, szCur, ARRAYSIZE( szCur ) );
+ GetStatValueAsString( bMVM ? classStats.maxMVM.m_iStat[statType] : classStats.max.m_iStat[statType], statType, szBest, ARRAYSIZE( szBest ) );
+ g_pVGuiLocalize->ConvertANSIToUnicode( szCur, wzCur, sizeof( wzCur ) );
+ g_pVGuiLocalize->ConvertANSIToUnicode( szBest, wzBest, sizeof( wzBest ) );
+ wchar_t *wzFormat = g_pVGuiLocalize->Find( "#StatPanel_Format_Close" );
+ wchar_t wzText[256];
+ g_pVGuiLocalize->ConstructString_safe( wzText, wzFormat, 2, wzCur, wzBest );
+ SetDialogVariable( "stattextsmall", wzText );
+ }
+ else
+ {
+ // player broke or tied a record, just show current value
+ char szValue[32];
+ GetStatValueAsString( iCurStatValue, statType, szValue, ARRAYSIZE( szValue ) );
+ SetDialogVariable( "stattextlarge", szValue );
+ }
+
+ SetDialogVariable( "statdesc", g_pVGuiLocalize->Find( CFmtStr( "%s_%s", bMVM ? g_szLocalizedMVMRecordText[statType] : g_szLocalizedRecordText[statType],
+ pRecordTextSuffix[recordBreakType] ) ) );
+
+ // Set the class name. We can't use a dialog variable because it's a string that's already
+ // been set using a dialog variable, and apparently we don't support nested dialog variables.
+ wchar_t szOriginalSummary[ 256 ];
+ wchar_t szSummary[ 256 ];
+
+ // This is the field that "statdesc" completed for us
+ pLabel->GetText( szOriginalSummary, sizeof( szOriginalSummary ) );
+ const wchar_t *pszPlayerClass = L"undefined";
+
+ if ( ( iClass >= TF_FIRST_NORMAL_CLASS ) && ( iClass <= TF_LAST_NORMAL_CLASS ) )
+ {
+ pszPlayerClass = g_pVGuiLocalize->Find( g_aPlayerClassNames[ iClass ] );
+ }
+
+ g_pVGuiLocalize->ConstructString_safe( szSummary, szOriginalSummary, 1, pszPlayerClass );
+
+ pLabel->SetText( szSummary );
+
+ if ( m_pClassImage )
+ {
+ m_pClassImage->SetClass( iTeam, iClass, 0 );
+ }
+
+ Show();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::FireGameEvent( IGameEvent * event )
+{
+ const char *pEventName = event->GetName();
+
+ if ( Q_strcmp( "player_spawn", pEventName ) == 0 )
+ {
+ int iUserID = event->GetInt( "userid" );
+ if ( !C_TFPlayer::GetLocalTFPlayer() || ( C_TFPlayer::GetLocalTFPlayer()->GetUserID() != iUserID ) )
+ return;
+
+ if ( m_bDisplayAfterSpawn )
+ {
+ // if we have a panel to display after spawn, show it now
+ vgui::ivgui()->AddTickSignal( GetVPanel(), 1000 );
+ ShowStatPanel( m_iCurStatClass, m_iCurStatTeam, m_iCurStatValue, m_statType, m_recordBreakType, true );
+ m_flTimeHide = gpGlobals->curtime + 12.0f;
+ m_bDisplayAfterSpawn = false;
+ }
+ else
+ {
+ // hide panel if we're currently showing it
+ Hide();
+ }
+ m_flTimeLastSpawn = gpGlobals->curtime;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::ApplySchemeSettings( vgui::IScheme *pScheme )
+{
+ BaseClass::ApplySchemeSettings( pScheme );
+
+ LoadControlSettings( "resource/UI/StatPanel_Base.res" );
+
+ vgui::Panel *pStatBox = FindChildByName("StatBox");
+ if ( pStatBox )
+ {
+ // Dirty hack: Make the statbox update now, and then change its bgColor.
+ // When it then gets ApplySchemeSetting called shortly after this, it doesn't
+ // reapply the scheme because its dirty-scheme flag has been removed.
+ pStatBox->ApplySchemeSettings( pScheme );
+ pStatBox->SetBgColor( GetSchemeColor("TransparentLightBlack", pScheme) );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::OnTick()
+{
+ // see if it's time to hide the panel
+ if ( m_flTimeHide > 0 && gpGlobals->curtime > m_flTimeHide )
+ {
+ Hide();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::Show()
+{
+ m_bShouldBeVisible = true;
+
+ HideLowerPriorityHudElementsInGroup( "mid" );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::Hide()
+{
+ m_bShouldBeVisible = false;
+ if ( m_flTimeHide > 0 )
+ {
+ m_flTimeHide = 0;
+ vgui::ivgui()->RemoveTickSignal( GetVPanel() );
+ }
+
+ UnhideLowerPriorityHudElementsInGroup( "mid" );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+bool CTFStatPanel::ShouldDraw( void )
+{
+ if ( !m_bShouldBeVisible )
+ return false;
+
+ if ( IsTakingAFreezecamScreenshot() )
+ return false;
+
+ if ( TFGameRules() && TFGameRules()->IsInArenaMode() == true )
+ {
+ m_flTimeHide = gpGlobals->curtime;
+ return false;
+ }
+
+ return CHudElement::ShouldDraw();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::ClearStatsInMemory( void )
+{
+ m_aClassStats.RemoveAll();
+ m_aMapStats.RemoveAll();
+ m_bStatsChanged = true;
+ ResetDisplayedStat();
+ UpdateStatSummaryPanel();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFStatPanel::ResetStats( void )
+{
+ // Only allow this action if the player is connected to steam and the achievement manager exists.
+ // Otherwise, our stat panel stats, stat file, and local steam context stats will be out of sync because UploadStats will fail.
+ CAchievementMgr *pAchievementMgr = dynamic_cast<CAchievementMgr *>( engine->GetAchievementMgr() );
+ if ( !steamapicontext->SteamUserStats() || !pAchievementMgr )
+ {
+ MessageBox *msg = new MessageBox( "#TF_SteamRequired", "#TF_SteamRequiredResetStats" );
+ if ( msg != NULL )
+ {
+ msg->AddActionSignalTarget(this);
+ msg->MoveToFront();
+ msg->DoModal();
+ }
+ return;
+ }
+
+ // The following operations are order dependent:
+
+ // Nukes the panel's version of the stats. Note that after this is called the local steam stats are out of sync.
+ ClearStatsInMemory();
+
+ // WriteStats will write out our stat file (not really used on the PC, authoritative on the XBox 360).
+ WriteStats();
+
+ // At this point local and remote steam stats are out of sync.
+
+ // Nuke the players stats on steam to seal the deal.
+ steamapicontext->SteamUserStats()->ResetAllStats( false );
+
+ // UploadStats will pull the players' class stats from CTFStatPanel and stomp the steam stats in memory.
+ g_TFSteamStats.UploadStats();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: returns class stat struct for specified class
+//-----------------------------------------------------------------------------
+ClassStats_t &CTFStatPanel::GetClassStats( int iClass )
+{
+ Assert( statPanel );
+ Assert( iClass >= TF_FIRST_NORMAL_CLASS );
+ Assert( iClass <= TF_LAST_NORMAL_CLASS );
+ int i;
+ for( i = 0; i < statPanel->m_aClassStats.Count(); i++ )
+ {
+ if ( statPanel->m_aClassStats[i].iPlayerClass == iClass )
+ return statPanel->m_aClassStats[i];
+ }
+
+ ClassStats_t stats;
+ stats.iPlayerClass = iClass;
+ statPanel->m_aClassStats.AddToTail( stats );
+ return statPanel->m_aClassStats[statPanel->m_aClassStats.Count()-1];
+}
+
+MapStats_t &CTFStatPanel::GetMapStats( map_identifier_t iMapID )
+{
+ Assert( statPanel );
+ Assert( IsValidMapID( iMapID ) );
+ int i;
+ for( i = 0; i < statPanel->m_aMapStats.Count(); i++ )
+ {
+ if ( statPanel->m_aMapStats[i].iMapID == iMapID )
+ return statPanel->m_aMapStats[i];
+ }
+
+ MapStats_t stats;
+ stats.iMapID = iMapID;
+ statPanel->m_aMapStats.AddToTail( stats );
+ return statPanel->m_aMapStats[statPanel->m_aMapStats.Count()-1];
+}
+
+bool CTFStatPanel::IsValidMapID( map_identifier_t iMapID )
+{
+ for ( int i = 0; i < GetItemSchema()->GetMapCount(); i++ )
+ {
+ const MapDef_t* pMap = GetItemSchema()->GetMasterMapDefByIndex( i );
+ if ( iMapID == pMap->GetStatsIdentifier() )
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+const char* CTFStatPanel::GetMapNameFromID( map_identifier_t iMapID )
+{
+ for ( int i = 0; i < GetItemSchema()->GetMapCount(); i++ )
+ {
+ const MapDef_t* pMap = GetItemSchema()->GetMasterMapDefByIndex( i );
+ if ( iMapID == pMap->GetStatsIdentifier() )
+ {
+ return pMap->pszMapName;
+ }
+ }
+
+ return "";
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Updates the stat summary panel w/current stats
+//-----------------------------------------------------------------------------
+void CTFStatPanel::UpdateStatSummaryPanel()
+{
+ UpdateStatSummaryPanels( m_aClassStats );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Return the total time this player has played the game, in hours.
+//-----------------------------------------------------------------------------
+float CTFStatPanel::GetTotalHoursPlayed( void )
+{
+ float totalTimePlayed = 0;
+
+ for ( int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++ )
+ {
+ totalTimePlayed += GetClassStats( iClass ).accumulated.m_iStat[ TFSTAT_PLAYTIME ] + GetClassStats( iClass ).accumulatedMVM.m_iStat[ TFSTAT_PLAYTIME ];
+ }
+
+ return totalTimePlayed / ( 60.0f * 60.0f );
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Renders stat value as string
+//-----------------------------------------------------------------------------
+void CTFStatPanel::GetStatValueAsString( int iValue, TFStatType_t statType, char *value, int valuelen )
+{
+ if ( TFSTAT_PLAYTIME == statType )
+ {
+ // Format time as a time string
+ Q_strncpy( value, FormatSeconds( iValue ), valuelen );
+ }
+ else
+ {
+ // all other stats are just displayed as #'s
+ Q_snprintf( value, valuelen, "%d", iValue );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Called when we get a stat update for the local player
+//-----------------------------------------------------------------------------
+void CTFStatPanel::MsgFunc_PlayerStatsUpdate( bf_read &msg )
+{
+ RoundStats_t stats;
+
+ // get the fixed-size information
+ int iClass = msg.ReadByte();
+ bool bAlive = msg.ReadByte();
+ int iSendBits = msg.ReadLong();
+
+ Assert( iClass >= TF_FIRST_NORMAL_CLASS && iClass <= TF_LAST_NORMAL_CLASS );
+ if ( iClass < TF_FIRST_NORMAL_CLASS || iClass > TF_LAST_NORMAL_CLASS )
+ return;
+
+ // the bitfield indicates which stats are contained in the message. Set the stats appropriately.
+ int iStat = TFSTAT_FIRST;
+ while ( iSendBits > 0 && iStat <= TFSTAT_LAST )
+ {
+ if ( iSendBits & 1 )
+ {
+ stats.m_iStat[iStat] = msg.ReadLong();
+ }
+ iSendBits >>= 1;
+ iStat++;
+ }
+
+ // sanity check: the message should contain exactly the # of bytes we expect based on the bit field
+ Assert( !msg.IsOverflowed() );
+ Assert( 0 == msg.GetNumBytesLeft() );
+ // if byte count isn't correct, bail out and don't use this data, rather than risk polluting player stats with garbage
+ if ( msg.IsOverflowed() || ( 0 != msg.GetNumBytesLeft() ) )
+ return;
+
+ UpdateStats( iClass, stats, bAlive );
+}
+
+void CTFStatPanel::MsgFunc_MapStatsUpdate( bf_read &msg )
+{
+ RoundMapStats_t stats;
+
+ // get the fixed-size information
+ map_identifier_t iMapID = msg.ReadUBitLong( 32 );
+ int iSendBits = msg.ReadLong();
+
+ if ( !IsValidMapID( iMapID ) )
+ return;
+
+ // the bitfield indicates which stats are contained in the message. Set the stats appropriately.
+ int iStat = TFMAPSTAT_FIRST;
+ while ( iSendBits > 0 && iStat <= TFMAPSTAT_LAST )
+ {
+ if ( iSendBits & 1 )
+ {
+ stats.m_iStat[iStat] = msg.ReadLong();
+ }
+ iSendBits >>= 1;
+ iStat++;
+ }
+
+ // sanity check: the message should contain exactly the # of bytes we expect based on the bit field
+ Assert( !msg.IsOverflowed() );
+ Assert( 0 == msg.GetNumBytesLeft() );
+ // if byte count isn't correct, bail out and don't use this data, rather than risk polluting player stats with garbage
+ if ( msg.IsOverflowed() || ( 0 != msg.GetNumBytesLeft() ) )
+ return;
+
+ UpdateMapStats( iMapID, stats );
+}
+
+/**********************************************************************************/
+
+void TestStatPanel( const CCommand &args )
+{
+ int iPanelType;
+
+ if( args.ArgC() < 2 )
+ {
+ ConMsg( "Usage: teststatpanel < panel type > < optional record type >\n" );
+ ConMsg( "Usable panel types are %d to %d. Record types are %d to %d\n", TFSTAT_FIRST, TFSTAT_LAST, RECORDBREAK_NONE+1, RECORDBREAK_MAX-1 );
+ return;
+ }
+
+ if ( statPanel )
+ {
+ iPanelType = atoi( args.Arg( 1 ) );
+ int iRecordType = RECORDBREAK_BEST;
+
+ if ( args.ArgC() >= 3 )
+ {
+ iRecordType = atoi( args.Arg( 2 ) );
+ }
+
+ if ( ( iPanelType <= TFSTAT_UNDEFINED ) || ( iPanelType >= TFSTAT_TOTAL ) || (iRecordType <= RECORDBREAK_NONE) || (iRecordType >= RECORDBREAK_MAX) )
+ {
+ ConMsg( "Usage: teststatpanel < panel type > < optional record type >\n" );
+ ConMsg( "Usable panel types are %d to %d. Record types are %d to %d\n", TFSTAT_FIRST, TFSTAT_LAST, RECORDBREAK_NONE+1, RECORDBREAK_MAX-1 );
+ return;
+ }
+
+ statPanel->TestStatPanel( (TFStatType_t) iPanelType, (RecordBreakType_t)iRecordType );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void HideStatPanel()
+{
+ if ( statPanel )
+ {
+ statPanel->Hide();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void ResetPlayerStats()
+{
+ if ( statPanel )
+ {
+ statPanel->ResetStats();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void RefreshPlayerStats()
+{
+ if ( statPanel )
+ {
+ if ( !statPanel->ReadStats() )
+ {
+ // Read failed, need to clear everything
+ statPanel->ClearStatsInMemory();
+ }
+ }
+}
+
+ConCommand teststatpanel( "teststatpanel", TestStatPanel, "", FCVAR_DEVELOPMENTONLY );
+ConCommand hidestatpanel( "hidestatpanel", HideStatPanel, "", FCVAR_DEVELOPMENTONLY );
+ConCommand refreshplayerstats( "refreshplayerstats", RefreshPlayerStats, "", FCVAR_DEVELOPMENTONLY );
+
+ConCommand resetplayerstats( "resetplayerstats", ResetPlayerStats, "", FCVAR_CLIENTCMD_CAN_EXECUTE );
+