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.
509 lines
16 KiB
509 lines
16 KiB
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose: The CS game stats header
|
|
//
|
|
// $NoKeywords: $
|
|
//=============================================================================//
|
|
|
|
#ifndef CS_GAMESTATS_H
|
|
#define CS_GAMESTATS_H
|
|
#ifdef _WIN32
|
|
#pragma once
|
|
#endif
|
|
|
|
#include "cs_blackmarket.h"
|
|
#include "GameStats.h"
|
|
#include "cs_gamestats_shared.h"
|
|
#include "GameEventListener.h"
|
|
#include "weapon_csbase.h"
|
|
#include "steamworks_gamestats_server.h"
|
|
|
|
// forward declares
|
|
class CBreakableProp;
|
|
|
|
const float cDisseminationTimeHigh = 0.25f; // Time interval for high priority stats sent to the player
|
|
const float cDisseminationTimeLow = 2.5f; // Time interval for medium priority stats sent to the player
|
|
|
|
#define BULLET_SUB_GROUP_MASK 0xF0000000
|
|
#define BULLET_ID_GROUP_MASK 0x000FFFFF
|
|
#define BULLET_RECOIL_MASK 0x0FF00000
|
|
#define RECOIL_BIT_SHIFT(val) (val<<20)
|
|
#define SUB_BULLET_BIT_SHIFT(val) (val << 28 )
|
|
|
|
//Helper enum table and conversion function to simplify bomb data recording. Update whenever new bomb-related events become relevant
|
|
enum CSBombEventName
|
|
{
|
|
BOMB_EVENT_NAME_NONE = 0, // 0 is an unknown event
|
|
|
|
BOMB_EVENT_NAME_FIRST,
|
|
BOMB_EVENT_NAME_PLANTED = BOMB_EVENT_NAME_FIRST,
|
|
BOMB_EVENT_NAME_DEFUSED,
|
|
|
|
BOMB_EVENT_NAME_MAX = BOMB_EVENT_NAME_DEFUSED, // number of BOMB_EVENT_NAMES
|
|
};
|
|
CSBombEventName BombEventNameFromString( const char* pEventName );
|
|
|
|
int GetCSLevelIndex( const char *pLevelName );
|
|
|
|
#if !defined( NO_STEAM )
|
|
uint32 GetPlayerID( CCSPlayer *pPlayer );
|
|
#endif
|
|
|
|
typedef struct
|
|
{
|
|
char szGameName[8];
|
|
byte iVersion;
|
|
char szMapName[32];
|
|
char ipAddr[4];
|
|
short port;
|
|
int serverid;
|
|
} gamestats_header_t;
|
|
|
|
typedef struct
|
|
{
|
|
gamestats_header_t header;
|
|
short iMinutesPlayed;
|
|
|
|
short iTerroristVictories[CS_NUM_LEVELS];
|
|
short iCounterTVictories[CS_NUM_LEVELS];
|
|
short iBlackMarketPurchases[WEAPON_MAX];
|
|
|
|
short iAutoBuyPurchases;
|
|
short iReBuyPurchases;
|
|
short iAutoBuyM4A1Purchases;
|
|
short iAutoBuyAK47Purchases;
|
|
short iAutoBuyFamasPurchases;
|
|
short iAutoBuyGalilPurchases;
|
|
short iAutoBuyGalilARPurchases;
|
|
short iAutoBuyVestHelmPurchases;
|
|
short iAutoBuyVestPurchases;
|
|
|
|
} cs_gamestats_t;
|
|
|
|
|
|
|
|
struct WeaponStats
|
|
{
|
|
int shots;
|
|
int hits;
|
|
int kills;
|
|
int damage;
|
|
};
|
|
|
|
static byte PackVelocityComponent( float f )
|
|
{
|
|
// Gets velocity components and sticks fits them into a byte by scaling them
|
|
// and clamping them
|
|
f = f / 2.0f;
|
|
if ( f > 127 ) f = 127;
|
|
if ( f < -128 ) f = -128;
|
|
return (byte) f;
|
|
}
|
|
|
|
static byte PackMovementComponent( bool bDucking, bool bInAir)
|
|
{
|
|
return (((byte)bDucking) << 1) | (byte)bInAir;
|
|
}
|
|
|
|
static uint32 PackMovementStatInternal( Vector vVelocity, bool bDucking, bool bInAir )
|
|
{
|
|
return ( (uint32)PackVelocityComponent( vVelocity.x ) << 24 ) |
|
|
( (uint32)PackVelocityComponent( vVelocity.y ) << 16 ) |
|
|
( (uint32)PackVelocityComponent( vVelocity.z ) << 8 ) |
|
|
( (uint32)PackMovementComponent( bDucking, bInAir ) );
|
|
}
|
|
|
|
static uint32 PackPlayerMovementStat( CCSPlayer *pPlayer )
|
|
{
|
|
Vector vAttackerVelocity;
|
|
pPlayer->GetVelocity( &vAttackerVelocity, NULL );
|
|
bool bInAir = FBitSet( pPlayer->GetFlags(), FL_ONGROUND ) ? false : true;
|
|
bool bDucking = FBitSet( pPlayer->GetFlags(), FL_DUCKING ) ? true : false;
|
|
return PackMovementStatInternal( vAttackerVelocity, bDucking, bInAir );
|
|
}
|
|
|
|
// Pack and convert Target Aim Angle component into 32bits
|
|
// OGS
|
|
static uint8 PackAimAngleComponent( float f )
|
|
{
|
|
// Pack component of QAngle into Byte. Angle is -180 -> 180, so there's no space for the full range.
|
|
|
|
// Make sure we're not getting weird values, rather have dumb data than broken game
|
|
if ( f > 180 ) f = 180;
|
|
if ( f < -180 ) f = -180;
|
|
|
|
// The greatest granularity comes from packing angle into ( 2 * Pi )/255 units.
|
|
// Effectively that's just a shift and stretch of the range: ( ( f + 180 )/ 360 ) * 255;
|
|
return (uint8) ( ( ( f + 180 ) / 360 ) * 255 );
|
|
}
|
|
|
|
static uint32 PackAimAngleStat( CCSPlayer *pPlayer )
|
|
{
|
|
QAngle vPlayerAimAngle = pPlayer->GetFinalAimAngle();
|
|
return ( (uint32)PackAimAngleComponent( (float)vPlayerAimAngle.z ) << 16 |
|
|
(uint32)PackAimAngleComponent( (float)vPlayerAimAngle.y ) << 8 |
|
|
(uint32)PackAimAngleComponent( (float)vPlayerAimAngle.x )
|
|
);
|
|
}
|
|
|
|
|
|
//=============================================================================
|
|
//
|
|
// OGS Gamestats
|
|
//
|
|
#if !defined( _GAMECONSOLE ) && !defined( NO_STEAM )
|
|
struct SWeaponShotData : public BaseStatData
|
|
{
|
|
SWeaponShotData( CCSPlayer *pPlayer, CWeaponCSBase* pWeapon, uint8 subBullet, uint8 round, uint8 iRecoilIndex )
|
|
{
|
|
Clear();
|
|
|
|
if ( pWeapon )
|
|
{
|
|
m_ui8WeaponID = (uint8)pWeapon->GetEconItemView()->GetItemIndex();
|
|
}
|
|
|
|
if ( pPlayer )
|
|
{
|
|
m_iUserID = GetPlayerID( pPlayer );
|
|
m_uiBulletID = pPlayer->GetBulletGroup();
|
|
m_vAttackerPos = pPlayer->GetAbsOrigin();
|
|
m_uAttackerMovement = PackPlayerMovementStat( pPlayer );
|
|
}
|
|
|
|
m_uiSubBulletID = subBullet;
|
|
m_RoundID = round;
|
|
m_uiRecoilIndex = iRecoilIndex;
|
|
}
|
|
|
|
void Clear()
|
|
{
|
|
m_iUserID = 0;
|
|
m_WeaponID = WEAPON_NONE;
|
|
m_ui8WeaponID = 0;
|
|
m_uiBulletID = 0;
|
|
m_uiSubBulletID = 0;
|
|
m_vAttackerPos.Init();
|
|
m_uAttackerMovement = 0;
|
|
m_RoundID = 0;
|
|
m_uiRecoilIndex = 0;
|
|
}
|
|
|
|
int m_iUserID;
|
|
CSWeaponID m_WeaponID;
|
|
uint8 m_ui8WeaponID;
|
|
uint32 m_uiBulletID;
|
|
uint8 m_uiSubBulletID;
|
|
uint8 m_uiRecoilIndex;
|
|
Vector m_vAttackerPos;
|
|
uint32 m_uAttackerMovement;
|
|
uint8 m_RoundID;
|
|
};
|
|
|
|
struct SWeaponHitData : public BaseStatData
|
|
{
|
|
SWeaponHitData( CCSPlayer *pCSTarget, const CTakeDamageInfo &info, uint8 subBullet, uint8 round, uint8 iRecoilIndex );
|
|
|
|
SWeaponHitData()
|
|
{
|
|
Clear();
|
|
}
|
|
|
|
// When any grenade explodes-- this is separate from any damage it may deal, which are also recorded as hits.
|
|
// Can fail! check the return!
|
|
bool InitAsGrenadeDetonation( class CBaseCSGrenadeProjectile *pGrenade, uint32 unBulletGroup );
|
|
|
|
// When a bomb is planted or defused, we want to collect data from each alive player, reporting their locations
|
|
bool InitAsBombEvent( CCSPlayer *pCSPlayer, class CPlantedC4 *pPlantedC4, uint32 unBulletGroup, uint8 nBombsite, CSBombEventName nBombEventName );
|
|
|
|
void Clear()
|
|
{
|
|
m_uiBulletID = 0;
|
|
m_uiSubBulletID = 0;
|
|
m_vAttackerPos.Init();
|
|
m_vTargetPos.Init();
|
|
m_uiDamage = 0;
|
|
m_HitRegion = 0;
|
|
m_RoundID = 0;
|
|
m_ui8WeaponID = 0;
|
|
m_ui64TargertID = 0;
|
|
m_ui64AttackerID = 0;
|
|
m_ui8Health = 0;
|
|
m_uAttackerMovement = 0;
|
|
m_uiRecoilIndex = 0;
|
|
}
|
|
|
|
void CompactBulletID()
|
|
{
|
|
m_uiBulletID = (m_uiBulletID & BULLET_ID_GROUP_MASK) | (SUB_BULLET_BIT_SHIFT(m_uiSubBulletID) & BULLET_SUB_GROUP_MASK) | (RECOIL_BIT_SHIFT(MIN(m_uiRecoilIndex, 255)) & BULLET_RECOIL_MASK);
|
|
}
|
|
|
|
// Data that gets sent to OGS
|
|
uint32 m_uiBulletID;
|
|
uint8 m_uiSubBulletID;
|
|
uint8 m_uiRecoilIndex;
|
|
Vector m_vAttackerPos;
|
|
Vector m_vTargetPos;
|
|
uint16 m_uiDamage;
|
|
uint8 m_HitRegion;
|
|
uint8 m_RoundID;
|
|
|
|
uint8 m_ui8Health;
|
|
uint8 m_ui8WeaponID;
|
|
uint32 m_ui64AttackerID;
|
|
uint32 m_ui64TargertID;
|
|
uint32 m_uAttackerMovement;
|
|
|
|
BEGIN_STAT_TABLE( "CSGOWeaponHitData" )
|
|
REGISTER_STAT_NAMED( m_ui8WeaponID, "WeaponID" )
|
|
REGISTER_STAT_NAMED( m_uiBulletID, "BulletID" )
|
|
REGISTER_STAT_NAMED( m_ui64AttackerID, "AttackerID" )
|
|
REGISTER_STAT_NAMED( (int)m_vAttackerPos.x, "AttackerX" )
|
|
REGISTER_STAT_NAMED( (int)m_vAttackerPos.y, "AttackerY" )
|
|
REGISTER_STAT_NAMED( (int)m_vAttackerPos.z, "AttackerZ" )
|
|
REGISTER_STAT_NAMED( (int)m_uAttackerMovement, "AttackerMovement" )
|
|
REGISTER_STAT_NAMED( m_ui64TargertID, "TargetID" )
|
|
REGISTER_STAT_NAMED( (int)m_vTargetPos.x, "TargetX" )
|
|
REGISTER_STAT_NAMED( (int)m_vTargetPos.y, "TargetY" )
|
|
REGISTER_STAT_NAMED( (int)m_vTargetPos.z, "TargetZ" )
|
|
REGISTER_STAT_NAMED( m_ui8Health, "Health" )
|
|
REGISTER_STAT_NAMED( m_uiDamage, "Damage" )
|
|
REGISTER_STAT_NAMED( m_HitRegion, "HitRegion" )
|
|
REGISTER_STAT_NAMED( m_RoundID, "Round" )
|
|
END_STAT_TABLE()
|
|
|
|
};
|
|
|
|
struct SWeaponMissData : public BaseStatData
|
|
{
|
|
SWeaponMissData( SWeaponShotData *data )
|
|
{
|
|
Clear();
|
|
|
|
if ( data )
|
|
{
|
|
m_ui64AttackerID = data->m_iUserID;
|
|
m_ui8WeaponID = data->m_ui8WeaponID;//data->m_WeaponID;
|
|
m_uiBulletID = data->m_uiBulletID;
|
|
m_uiRecoilIndex = data->m_uiRecoilIndex;
|
|
m_uiSubBulletID = data->m_uiSubBulletID;
|
|
m_vAttackerPos = data->m_vAttackerPos;
|
|
m_uAttackerMovement = data->m_uAttackerMovement;
|
|
m_RoundID = data->m_RoundID;
|
|
|
|
TimeSubmitted = data->TimeSubmitted;
|
|
}
|
|
}
|
|
|
|
void Clear()
|
|
{
|
|
m_ui8WeaponID = 0;
|
|
m_uiBulletID = 0;
|
|
m_uiSubBulletID = 0;
|
|
m_uiRecoilIndex = 0;
|
|
m_ui64AttackerID = 0;
|
|
m_vAttackerPos.Init();
|
|
m_uAttackerMovement = 0;
|
|
m_RoundID = 0;
|
|
}
|
|
|
|
void CompactBulletID()
|
|
{
|
|
m_uiBulletID = (m_uiBulletID & BULLET_ID_GROUP_MASK) | (SUB_BULLET_BIT_SHIFT(m_uiSubBulletID) & BULLET_SUB_GROUP_MASK) | (RECOIL_BIT_SHIFT(MIN(m_uiRecoilIndex, 255)) & BULLET_RECOIL_MASK);
|
|
}
|
|
|
|
uint8 m_ui8WeaponID;
|
|
uint32 m_uiBulletID;
|
|
uint8 m_uiSubBulletID;
|
|
uint8 m_uiRecoilIndex;
|
|
uint32 m_ui64AttackerID;
|
|
Vector m_vAttackerPos;
|
|
uint32 m_uAttackerMovement;
|
|
uint8 m_RoundID;
|
|
|
|
|
|
BEGIN_STAT_TABLE( "CSGOWeaponMissData" )
|
|
REGISTER_STAT_NAMED( m_ui8WeaponID, "WeaponID" )
|
|
REGISTER_STAT_NAMED( m_uiBulletID, "BulletID" )
|
|
REGISTER_STAT_NAMED( m_ui64AttackerID, "AttackerID" )
|
|
REGISTER_STAT_NAMED( (int)m_vAttackerPos.x, "AttackerX" )
|
|
REGISTER_STAT_NAMED( (int)m_vAttackerPos.y, "AttackerY" )
|
|
REGISTER_STAT_NAMED( (int)m_vAttackerPos.z, "AttackerZ" )
|
|
REGISTER_STAT_NAMED( (int)m_uAttackerMovement, "AttackerMovement" )
|
|
REGISTER_STAT_NAMED( m_RoundID, "Round" )
|
|
END_STAT_TABLE()
|
|
|
|
};
|
|
|
|
struct SMarketPurchases : public BaseStatData
|
|
{
|
|
SMarketPurchases( uint64 ulPlayerID, int iPrice, const char *pName, int round ) : ItemCost(iPrice)
|
|
{
|
|
m_nPlayerID = ulPlayerID;
|
|
|
|
if ( pName )
|
|
{
|
|
//Can we find a valid Item Definition?
|
|
if ( GetItemSchema()->GetItemDefinitionByName( pName ) )
|
|
{
|
|
ItemID = (uint)GetItemSchema()->GetItemDefinitionByName( pName )->GetDefinitionIndex();
|
|
}
|
|
else
|
|
{
|
|
//We don't know about you, you're probably equipment (armor, defuse kit)
|
|
//Use CSWeaponID instead (works for all equipment)
|
|
ItemID = WeaponIdFromString( pName ); //Returns WEAPON_NONE on failure to find string pName
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ItemID = WEAPON_NONE;
|
|
}
|
|
|
|
// Can't buy 'none'. Investigate why we can't get a weapon ID from the given string.
|
|
Assert( ItemID != WEAPON_NONE );
|
|
|
|
m_iPurchaseCnt = 1;
|
|
m_niRound = round;
|
|
}
|
|
uint32 m_nPlayerID;
|
|
int ItemCost;
|
|
uint ItemID;
|
|
char m_iPurchaseCnt;
|
|
int m_niRound;
|
|
|
|
BEGIN_STAT_TABLE( "CSGOMarketPurchase" )
|
|
REGISTER_STAT_NAMED( m_nPlayerID, "AccountID" )
|
|
REGISTER_STAT( ItemCost )
|
|
REGISTER_STAT_NAMED( m_iPurchaseCnt, "PurchaseCount" )
|
|
REGISTER_STAT_NAMED( ItemID, "WeaponID" )
|
|
REGISTER_STAT_NAMED( m_niRound, "Round" )
|
|
END_STAT_TABLE()
|
|
};
|
|
|
|
typedef CUtlVector< SWeaponHitData* > CSGOWeaponHitData;
|
|
typedef CUtlVector< SWeaponMissData* > CSGOWeaponMissData;
|
|
typedef CUtlVector< SWeaponShotData* > CSGOWeaponShotsData;
|
|
typedef CUtlVector< SMarketPurchases* > CSGOMarketPurchaseData;
|
|
|
|
#endif // OGS Data
|
|
|
|
//=============================================================================
|
|
//
|
|
// CS Game Stats Class
|
|
//
|
|
class CCSGameStats : public CBaseGameStats, public CGameEventListener, public CAutoGameSystemPerFrame
|
|
#if !defined( _GAMECONSOLE )
|
|
, public IGameStatTracker
|
|
#endif
|
|
{
|
|
public:
|
|
|
|
// Constructor/Destructor.
|
|
CCSGameStats( void );
|
|
~CCSGameStats( void );
|
|
|
|
virtual void Clear( void );
|
|
virtual bool Init();
|
|
virtual void PreClientUpdate();
|
|
|
|
// Overridden events
|
|
virtual void Event_LevelInit( void );
|
|
virtual void Event_LevelShutdown( float flElapsed );
|
|
virtual void Event_ShotFired( CBasePlayer *pPlayer, CBaseCombatWeapon* pWeapon );
|
|
virtual void Event_ShotHit( CBasePlayer *pPlayer, const CTakeDamageInfo &info );
|
|
virtual void Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info );
|
|
virtual void Event_PlayerKilled_PreWeaponDrop( CBasePlayer *pPlayer, const CTakeDamageInfo &info );
|
|
virtual void Event_PlayerConnected( CBasePlayer *pPlayer );
|
|
virtual void Event_PlayerDisconnected( CBasePlayer *pPlayer );
|
|
virtual void Event_WindowShattered( CBasePlayer *pPlayer );
|
|
virtual void Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info );
|
|
virtual void Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info );
|
|
|
|
// CSS specific events
|
|
void Event_BombPlanted( CCSPlayer *pPlayer );
|
|
void Event_BombDefused( CCSPlayer *pPlayer );
|
|
void Event_BombExploded( CCSPlayer *pPlayer );
|
|
void Event_MoneyEarned( CCSPlayer *pPlayer, int moneyEarned );
|
|
void Event_MoneySpent( CCSPlayer* pPlayer, int moneySpent, const char *pItemName );
|
|
void Event_HostageRescued( CCSPlayer *pPlayer );
|
|
void Event_PlayerSprayedDecal( CCSPlayer*pPlayer );
|
|
void Event_AllHostagesRescued();
|
|
void Event_BreakProp( CCSPlayer *pPlayer, CBreakableProp *pProp );
|
|
void Event_PlayerDonatedWeapon (CCSPlayer* pPlayer);
|
|
void Event_PlayerDominatedOther( CCSPlayer* pAttacker, CCSPlayer* pVictim);
|
|
void Event_PlayerRevenge( CCSPlayer* pAttacker );
|
|
void Event_PlayerAvengedTeammate( CCSPlayer* pAttacker, CCSPlayer* pAvengedPlayer );
|
|
void Event_MVPEarned( CCSPlayer* pPlayer );
|
|
void Event_KnifeUse( CCSPlayer* pPlayer, bool bStab, int iDamage );
|
|
|
|
void RecordWeaponHit( SWeaponHitData* pHitData );
|
|
|
|
// Steamworks Gamestats
|
|
#if !defined( _GAMECONSOLE )
|
|
void UploadRoundStats( void );
|
|
virtual void SubmitGameStats( KeyValues *pKV );
|
|
virtual StatContainerList_t* GetStatContainerList( void );
|
|
bool AnyOGSDataToSubmit( void );
|
|
#endif
|
|
|
|
virtual void FireGameEvent( IGameEvent *event );
|
|
|
|
void UpdatePlayerRoundStats(int winner);
|
|
void DumpMatchWeaponMetrics();
|
|
|
|
const PlayerStats_t& FindPlayerStats( CBasePlayer *pPlayer ) const;
|
|
void ResetPlayerStats( CBasePlayer *pPlayer );
|
|
void ResetKillHistory( CBasePlayer *pPlayer );
|
|
void ResetRoundStats();
|
|
void ResetPlayerClassMatchStats();
|
|
void ClearOGSRoundStats();
|
|
|
|
const StatsCollection_t& GetTeamStats( int iTeamIndex ) const;
|
|
void ResetAllTeamStats();
|
|
void ResetAllStats();
|
|
void ResetWeaponStats();
|
|
void IncrementTeamStat( int iTeamIndex, int iStatIndex, int iAmount );
|
|
void CalcDominationAndRevenge( CCSPlayer *pAttacker, CCSPlayer *pVictim, int *piDeathFlags );
|
|
void CalculateOverkill(CCSPlayer* pAttacker, CCSPlayer* pVictim);
|
|
void PlayerKilled( CBasePlayer *pVictim, const CTakeDamageInfo &info );
|
|
|
|
void IncrementStat( CCSPlayer* pPlayer, CSStatType_t statId, int iValue, bool bPlayerOnly = false, bool bIncludeBotController = false );
|
|
|
|
void SendStatsToPlayer( CCSPlayer * pPlayer, int iMinStatPriority );
|
|
void CreateNewGameStatsSession( void );
|
|
|
|
protected:
|
|
void SetStat( CCSPlayer *pPlayer, CSStatType_t statId, int iValue );
|
|
void TrackKillStats( CCSPlayer *pAttacker, CCSPlayer *pVictim );
|
|
|
|
private:
|
|
PlayerStats_t m_aPlayerStats[MAX_PLAYERS+1]; // List of stats for each player for current life - reset after each death
|
|
StatsCollection_t m_aTeamStats[TEAM_MAXCOUNT - FIRST_GAME_TEAM];
|
|
|
|
float m_fDisseminationTimerLow; // how long since last medium priority stat update
|
|
float m_fDisseminationTimerHigh; // how long since last high priority stat update
|
|
|
|
int m_numberOfRoundsForDirectAverages;
|
|
int m_numberOfTerroristEntriesForDirectAverages;
|
|
int m_numberOfCounterTerroristEntriesForDirectAverages;
|
|
|
|
CUtlDict< CSStatType_t, short > m_PropStatTable;
|
|
|
|
WeaponStats m_weaponStats[WEAPON_MAX][WeaponMode_MAX];
|
|
|
|
// Steamworks Gamestats
|
|
#if !defined( _GAMECONSOLE ) && !defined( NO_STEAM )
|
|
CSGOWeaponHitData m_WeaponHitData;
|
|
CSGOWeaponMissData m_WeaponMissData;
|
|
CSGOWeaponShotsData m_WeaponShotData;
|
|
CSGOMarketPurchaseData m_MarketPurchases;
|
|
|
|
// A static list of all the stat containers, one for each data structure being tracked
|
|
static StatContainerList_t * s_StatLists;
|
|
#endif
|
|
};
|
|
|
|
extern CCSGameStats CCS_GameStats;
|
|
|
|
#endif // CS_GAMESTATS_H
|