quake4-sdk/source/game/mp/stats/StatManager.cpp

1346 lines
41 KiB
C++
Raw Normal View History

2007-06-15 00:00:00 +00:00
//----------------------------------------------------------------
// StatManager.cpp
//
// Copyright 2002-2004 Raven Software
//----------------------------------------------------------------
#include "../../../idlib/precompiled.h"
#pragma hdrstop
#include "../../Game_local.h"
#include "StatManager.h"
// TTimo - *????????*
#include <new>
rvStatManager statManagerLocal;
rvStatManager* statManager = &statManagerLocal;
comboKillState_t rvStatManager::comboKillState[ MAX_CLIENTS ] = { CKS_NONE };
int rvStatManager::lastRailShot[ MAX_CLIENTS ] = { -2 };
int rvStatManager::lastRailShotHits[ MAX_CLIENTS ] = { 0 };
inGameAwardInfo_t inGameAwardInfo[ IGA_NUM_AWARDS ] = {
//IGA_INVALID
{
NULL
},
//IGA_CAPTURE
{
"capture"
},
//IGA_HUMILIATION
{
"humiliation"
},
//IGA_IMPRESSIVE
{
"impressive"
},
//IGA_EXCELLENT
{
"excellent"
},
//IGA_ASSIST
{
"assist"
},
//IGA_DEFENSE
{
"defense"
},
//IGA_COMBO_KILL
{
"combo_kill"
},
//IGA_RAMPAGE
{
"rampage"
},
//IGA_HOLY_SHIT
{
"holy_shit"
}
};
// RAVEN BEGIN
// rhummer: localized these strings.
endGameAwardInfo_t endGameAwardInfo[ EGA_NUM_AWARDS ] = {
//EGA_INVALID
{
NULL
},
//EGA_LEMMING
{
"#str_107260"
},
//EGA_RAIL_MASTER
{
"#str_107261"
},
//EGA_ROCKET_SAUCE
{
"#str_107262"
},
//EGA_BRAWLER
{
"#str_107263"
},
//EGA_SNIPER
{
"#str_107264"
},
//EGA_CRITICAL_FAILURE
{
"#str_107265"
},
//EGA_TEAM_PLAYER
{
"#str_107266"
},
//EGA_ACCURACY
{
"#str_107267"
},
//EGA_FRAGS
{
"#str_107268"
},
//EGA_PERFECT
{
"#str_107269"
}
};
// RAVEN END
void showStats_f( const idCmdArgs &args ) {
statManager->DebugPrint();
}
/*
===============================================================================
rvStatAllocator
Handles allocating stat events
===============================================================================
*/
void *rvStatAllocator::GetBlock( size_t blockSize, int* blockNumOut /* = NULL */ ) {
// if not enough bytes for this allocation, get a new block
if ( GetBytesLeftInBlock() < blockSize ) {
// recycle & reuse a block
currentBlock++;
placeInBlock = 0;
if( currentBlock >= MAX_BLOCKS ) {
currentBlock = 0;
if( statManager->GetStat( 0 ) && gameLocal.isMultiplayer ) {
// int delta = gameLocal.time - statManager->GetStat( 0 )->GetTimeStamp();
// gameLocal.Printf( "rvStatAllocator::GetBlock() - Tracked %g seconds of stats before recycle\n", delta / 1000.0f );
}
}
// gameLocal.Printf( "rvStatAllocator::GetBlock() - Using block %d\n", currentBlock );
// statManager->DebugPrint();
// int numFreed =
statManager->FreeEvents( currentBlock );
// statManager->DebugPrint();
// gameLocal.Printf( "rvStatAllocator::GetBlock() - stat manager freed %d events which used block %d\n", numFreed, currentBlock );
}
// if this fires our objects are too large or our alloc size is too small
assert( GetBytesLeftInBlock() >= blockSize );
byte *newBlock = blocks + ( BLOCK_SIZE * currentBlock ) + placeInBlock;
placeInBlock += blockSize;
totalAllocations++;
totalBytesUsed += blockSize;
if( blockNumOut ) {
// return the current block number if requested
(*blockNumOut) = currentBlock;
}
return newBlock;
}
void rvStatAllocator::Reset() {
currentBlock = 0;
placeInBlock = 0;
totalBytesUsed = 0;
totalAllocations = 0;
totalBytesAllocated = 0;
for ( int i = 0; i < ST_COUNT; i++ ) {
allocationsByType[ i ] = 0;
}
}
rvStatAllocator::rvStatAllocator() {
Reset();
}
void rvStatAllocator::Report()
{
// shouchard: for debugging and tuning only
common->Printf( "rvStatAllocator: dump of usage stats\n" );
common->Printf( "\t%d total bytes handed out in %d requests\n", GetTotalBytesUsed(), GetTotalAllocations() );
common->Printf( "\tbegin game: %3d; end game: %3d\n",
GetAllocationsByType( ST_BEGIN_GAME ),
GetAllocationsByType( ST_END_GAME ) );
common->Printf( "\tplayer hit: %3d; player kill: %3d\n",
GetAllocationsByType( ST_HIT ),
GetAllocationsByType( ST_KILL ) );
common->Printf( "\tplayer death: %3d;\n",
GetAllocationsByType( ST_DEATH ) );
common->Printf( "\tdamage dealt: %3d; damage taken: %3d\n",
GetAllocationsByType( ST_DAMAGE_DEALT ),
GetAllocationsByType( ST_DAMAGE_TAKEN ) );
common->Printf( "\tstat team: %3d\n",
GetAllocationsByType( ST_STAT_TEAM ) );
common->Printf( "\tflag capture: %3d;\n",
GetAllocationsByType( ST_CTF_FLAG_CAPTURE ) ),
common->Printf( "\tflag drop: %3d; flag return: %3d\n",
GetAllocationsByType( ST_CTF_FLAG_DROP ),
GetAllocationsByType( ST_CTF_FLAG_RETURN ) );
}
// object allocators
#if defined(_INLINEDEBUGMEMORY)
// Because we need inplace new.
#undef new
#define new new
#endif // _INLINEDEBUGMEMORY
rvStatBeginGame *rvStatAllocator::AllocStatBeginGame( int t, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatBeginGame ), blockNumOut );
assert( newBlock );
rvStatBeginGame *stat = new( newBlock ) rvStatBeginGame( t );
allocationsByType[ ST_BEGIN_GAME ]++;
return stat;
}
rvStatEndGame *rvStatAllocator::AllocStatEndGame( int t, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatEndGame ), blockNumOut );
assert( newBlock );
rvStatEndGame *stat = new( newBlock ) rvStatEndGame( t );
allocationsByType[ ST_END_GAME ]++;
return stat;
}
rvStatClientConnect *rvStatAllocator::AllocStatClientConnect( int t, int client, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatClientConnect ), blockNumOut );
assert( newBlock );
rvStatClientConnect *stat = new( newBlock ) rvStatClientConnect( t, client );
allocationsByType[ ST_CLIENT_CONNECT ]++;
return stat;
}
rvStatHit *rvStatAllocator::AllocStatHit( int t, int p, int v, int w, bool countForAccuracy, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatHit ), blockNumOut );
assert( newBlock );
rvStatHit *stat = new( newBlock ) rvStatHit( t, p, v, w, countForAccuracy );
allocationsByType[ ST_HIT ]++;
return stat;
}
rvStatKill *rvStatAllocator::AllocStatKill( int t, int p, int v, bool g, int mod, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatKill ), blockNumOut );
assert( newBlock );
rvStatKill *stat = new( newBlock ) rvStatKill( t, p, v, g, mod );
allocationsByType[ ST_KILL ]++;
return stat;
}
rvStatDeath *rvStatAllocator::AllocStatDeath( int t, int p, int mod, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatDeath ), blockNumOut );
assert( newBlock );
rvStatDeath *stat = new( newBlock ) rvStatDeath( t, p, mod );
allocationsByType[ ST_DEATH ]++;
return stat;
}
rvStatDamageDealt *rvStatAllocator::AllocStatDamageDealt( int t, int p, int w, int d, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatDamageDealt ), blockNumOut );
assert( newBlock );
rvStatDamageDealt *stat = new( newBlock ) rvStatDamageDealt( t, p, w, d );
allocationsByType[ ST_DAMAGE_DEALT ]++;
return stat;
}
rvStatDamageTaken *rvStatAllocator::AllocStatDamageTaken( int t, int p, int w, int d, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatDamageTaken ), blockNumOut );
assert( newBlock );
rvStatDamageTaken *stat = new( newBlock ) rvStatDamageTaken( t, p, w, d );
allocationsByType[ ST_DAMAGE_TAKEN ]++;
return stat;
}
rvStatFlagDrop *rvStatAllocator::AllocStatFlagDrop( int t, int p, int a, int tm, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatFlagDrop ), blockNumOut );
assert( newBlock );
rvStatFlagDrop *stat = new( newBlock ) rvStatFlagDrop( t, p, a, tm );
allocationsByType[ ST_CTF_FLAG_DROP ]++;
return stat;
}
rvStatFlagReturn *rvStatAllocator::AllocStatFlagReturn( int t, int p, int tm, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatFlagReturn ), blockNumOut );
assert( newBlock );
rvStatFlagReturn *stat = new( newBlock ) rvStatFlagReturn( t, p, tm );
allocationsByType[ ST_CTF_FLAG_CAPTURE ]++;
return stat;
}
rvStatFlagCapture *rvStatAllocator::AllocStatFlagCapture( int t, int p, int f, int tm, int* blockNumOut /* = NULL */ ) {
void *newBlock = GetBlock( sizeof( rvStatFlagCapture ), blockNumOut );
assert( newBlock );
rvStatFlagCapture *stat = new( newBlock ) rvStatFlagCapture( t, p, f, tm );
allocationsByType[ ST_CTF_FLAG_RETURN ]++;
return stat;
}
#if defined(_INLINEDEBUGMEMORY)
// Because we need inplace new.
#undef new
#define new ID_DEBUG_NEW
#endif // _INLINEDEBUGMEMORY
/*
===============================================================================
rvStatManager
Stores game statistic events in statQueue
===============================================================================
*/
// shouchard: stat manager start with 1 meg; we'll tune as we get better data
rvStatManager::rvStatManager() {
memset( localInGameAwards, 0, sizeof( int ) * (int)IGA_NUM_AWARDS );
inGameAwardHudTime = 0;
}
void rvStatManager::Init( void ) {
Shutdown();
statQueue.Clear();
awardQueue.Clear();
statQueue.SetGranularity( 1024 );
endGameSetup = false;
cmdSystem->AddCommand( "ShowInGameStats", showStats_f, CMD_FL_SYSTEM, "show in game stats." );
memset( localInGameAwards, 0, sizeof( int ) * (int)IGA_NUM_AWARDS );
inGameAwardHudTime = 0;
}
void rvStatManager::Shutdown( void ) {
statAllocator.Report();
statAllocator.Reset();
statQueue.Clear();
awardQueue.Clear();
for( int i = 0; i < MAX_CLIENTS; i++ ) {
playerStats[ i ] = rvPlayerStat();
}
for ( int q = 0; q < MAX_CLIENTS; ++q ) {
comboKillState[ q ] = CKS_NONE;
lastRailShot[ q ] = -2;
lastRailShotHits[ q ] = 0;
}
}
void rvStatManager::BeginGame( void ) {
int blockNum;
rvStatBeginGame* stat = statAllocator.AllocStatBeginGame( gameLocal.time, &blockNum );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(stat), blockNum ) );
endGameSetup = false;
#if ID_TRAFFICSTATS
startTime = gameLocal.time;
networkSystem->GetTrafficStats( startSent, startPacketsSent, startReceived, startPacketsReceived );
#endif
for ( int q = 0; q < MAX_CLIENTS; ++q ) {
comboKillState[ q ] = CKS_NONE;
lastRailShot[ q ] = -2;
lastRailShotHits[ q ] = 0;
}
}
void rvStatManager::EndGame( void ) {
int blockNum;
rvStatEndGame* stat = statAllocator.AllocStatEndGame( gameLocal.time, &blockNum );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(stat), blockNum ) );
CalculateEndGameStats();
awardQueue.Clear();
#if ID_TRAFFICSTATS
int sent, packetsSent, received, packetsReceived, time;
networkSystem->GetTrafficStats( sent, packetsSent, received, packetsReceived );
sent -= startSent;
packetsSent -= startPacketsSent;
received -= startReceived;
packetsReceived -= startPacketsReceived;
time = gameLocal.time - startTime;
common->Printf( "EndGame. bytes sent: %d packets sent: %d bytes received: %d packets received: %d\n", sent, packetsSent, received, packetsReceived );
// compute averages, including packet overhead
// adjust the UDP overhead, may depend on your TCP stack implementation ( 42 comes from ethereal analysis of the traffic )
sent += packetsSent * 42;
received += packetsReceived * 42;
float sentBps, recvBps;
sentBps = (float)( sent ) * 1000.0f / time;
recvBps = (float)( received ) * 1000.0f / time;
common->Printf( "avg sent %g B/s, received %g B/s\n", sentBps, recvBps );
#endif
}
void rvStatManager::ClientConnect( int clientNum ) {
// push a client connected event into the queue so we don't get confused with old
// events detailing the previous owner of this clientNum
int blockNum;
rvStatClientConnect* stat = statAllocator.AllocStatClientConnect( gameLocal.time, clientNum, &blockNum );
statQueue.Append( rvPair<rvStat*,int>( (rvStat*)stat, blockNum ) );
}
void rvStatManager::ClientDisconnect( int clientNum ) {
// re-init player stats
playerStats[ clientNum ] = rvPlayerStat();
}
void rvStatManager::Kill( const idPlayer* victim, const idEntity* killer, int methodOfDeath ) {
int deathBlock, killBlock;
rvStatDeath* statDeath = statAllocator.AllocStatDeath( gameLocal.time, victim->entityNumber, methodOfDeath, &deathBlock );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)statDeath, deathBlock ) );
if( killer && killer->IsType( idPlayer::GetClassType() ) ) {
rvStatKill* statKill = statAllocator.AllocStatKill( gameLocal.time, killer->entityNumber, victim->entityNumber, victim->IsGibbed(), methodOfDeath, &killBlock );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)statKill, killBlock ) );
} else if( !killer ) {
// basically a suicide
rvStatKill* statKill = statAllocator.AllocStatKill( gameLocal.time, victim->entityNumber, victim->entityNumber, victim->IsGibbed(), methodOfDeath, &killBlock );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)statKill, killBlock ) );
}
}
void rvStatManager::FlagCaptured( const idPlayer* player, int flagTeam ) {
int blockNum;
rvStatFlagCapture* stat = statAllocator.AllocStatFlagCapture( gameLocal.time, player->entityNumber, flagTeam, player->team, &blockNum );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(stat), blockNum ) );
}
void rvStatManager::WeaponFired( const idPlayer* player, int weapon, int num ) {
playerStats[ player->entityNumber ].weaponShots[ weapon ] += num;
lastRailShotHits[ player->entityNumber ] = 0;
comboKillState_t cks = comboKillState[ player->entityNumber ];
comboKillState[ player->entityNumber ] = CKS_NONE;
if ( player->GetWeaponIndex( "weapon_rocketlauncher" ) == weapon ) {
if ( cks == CKS_NONE ) {
comboKillState[ player->entityNumber ] = CKS_ROCKET_FIRED;
}
} else if ( player->GetWeaponIndex( "weapon_railgun" ) == weapon ) {
// apparently it processes hits before it does the fire....
if ( cks == CKS_RAIL_HIT ) {
comboKillState[ player->entityNumber ] = CKS_RAIL_FIRED;
}
}
}
void rvStatManager::WeaponHit( const idActor* attacker, const idEntity* victim, int weapon, bool countForAccuracy ) {
if( victim && attacker && ( victim == attacker || gameLocal.IsTeamGame( ) && victim->IsType( idPlayer::GetClassType( ) ) && attacker->IsType( idPlayer::GetClassType( ) )
&& static_cast<const idPlayer*>( victim )->team == static_cast<const idPlayer*>( attacker )->team ) ) {
return;
}
if( victim && victim != attacker ) {
if( attacker->IsType( idPlayer::GetClassType() ) ) {
// if attacker was a player, track hit and damage dealt
int hitBlock;
rvStatHit* statHit = statAllocator.AllocStatHit( gameLocal.time, attacker->entityNumber, victim->entityNumber, weapon, countForAccuracy, &hitBlock );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(statHit), hitBlock ) );
}
}
}
//asalmon modified to work for single player stats on Xenon
void rvStatManager::Damage( const idEntity* attacker, const idEntity* victim, int weapon, int damage ) {
if( victim && attacker && ( victim == attacker || gameLocal.IsTeamGame( ) && victim->IsType( idPlayer::GetClassType( ) ) && attacker->IsType( idPlayer::GetClassType( ) )
&& static_cast<const idPlayer*>( victim )->team == static_cast<const idPlayer*>( attacker )->team ) ) {
return;
}
if(attacker)
{
if( attacker->IsType( idPlayer::GetClassType() ) ) {
int damageBlock;
rvStatDamageDealt* statDamage = statAllocator.AllocStatDamageDealt( gameLocal.time, attacker->entityNumber, weapon, damage, &damageBlock );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(statDamage), damageBlock ) );
}
}
if(victim)
{
if( victim->IsType( idPlayer::GetClassType() ) ) {
// if victim was a player, track damage taken
int blockNum;
rvStatDamageTaken* stat = statAllocator.AllocStatDamageTaken( gameLocal.time, victim->entityNumber, weapon, damage, &blockNum );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(stat), blockNum ) );
}
}
}
void rvStatManager::FlagDropped( const idPlayer* player, const idEntity* attacker ) {
int blockNum;
rvStatFlagDrop* stat = statAllocator.AllocStatFlagDrop( gameLocal.time, player->entityNumber, attacker->entityNumber, player->team, &blockNum );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(stat), blockNum ) );
}
void rvStatManager::FlagReturned( const idPlayer* player ) {
int blockNum;
rvStatFlagReturn* stat = statAllocator.AllocStatFlagReturn( gameLocal.time, player->entityNumber, player->team, &blockNum );
statQueue.Append( rvPair<rvStat*, int>( (rvStat*)(stat), blockNum ) );
}
void rvStatManager::DebugPrint( void ) {
if( !gameLocal.isMultiplayer ) {
return;
}
//gameLocal.Printf( "Begin statistics debug dump:\n" );
//gameLocal.Printf( "Statistics queue:\n" );
for( int i = 0; i < Min( statQueue.Num(), 50 ); i++ ) {
gameLocal.Printf( "{%d, %d} ", statQueue[i].First()->GetType(), statQueue[i].First()->GetTimeStamp() );
}
gameLocal.Printf("\n");
//gameLocal.Printf( "In-game statistics\n\n" );
//for( int i = 0; i < inGameStats.Num(); i++ ) {
// gameLocal.Printf( "\t%d - %s:\n", i, statIndex->index[ i ].GetName().c_str() );
// gameLocal.Printf( "\t\tKills: %d\n", inGameStats[i].kills );
// gameLocal.Printf( "\t\tDeaths: %d\n", inGameStats[i].deaths );
// gameLocal.Printf( "\t\tFlag Caps: %d\n", inGameStats[i].flagCaptures );
// for( int j = 0; j < MAX_WEAPONS; j++ ) {
// gameLocal.Printf( "\t\tWeapon %d hits: %d\n\t\tWeapon %d shots: %d\n", j, inGameStats[i].weaponHits[j], j, inGameStats[i].weaponShots[j] );
// }
//}
}
int rvStatManager::FreeEvents( int blockNum ) {
int blockStart = -1;
int blockEnd = -1;
for( int i = 0; i < statQueue.Num(); i++ ) {
if( blockStart == -1 && statQueue[ i ].Second() == blockNum ) {
blockStart = i;
} else if( blockStart != -1 && statQueue[ i ].Second() != blockNum ) {
blockEnd = i;
break;
}
}
if( blockStart == -1 || blockEnd == -1 ) {
// rjohnson: commented out warning - I take it this is not a bad message?
// gameLocal.Warning( "rvStatManager::FreeEvents() - Could not find events with block num '%d'\n", blockNum );
return 0;
}
statQueue.RemoveRange( blockStart, blockEnd - 1 );
return (blockEnd - blockStart);
}
void rvStatManager::SendInGameAward( inGameAward_t award, int clientNum ) {
assert( gameLocal.isServer );
idBitMsg msg;
byte msgBuf[1024];
msg.Init( msgBuf, sizeof( msgBuf ) );
msg.WriteByte( GAME_RELIABLE_MESSAGE_INGAMEAWARD );
msg.WriteByte( award );
msg.WriteByte( clientNum );
networkSystem->ServerSendReliableMessage( -1, msg );
if( gameLocal.isListenServer ) {
msg.ReadByte();
ReceiveInGameAward( msg );
}
}
void rvStatManager::ReceiveInGameAward( const idBitMsg& msg ) {
assert( gameLocal.isClient || gameLocal.isListenServer );
int numAwards = 0;
inGameAward_t award = (inGameAward_t)msg.ReadByte();
int client = msg.ReadByte();
// display award on hud
idPlayer* player = gameLocal.GetLocalPlayer();
idPlayer* remote = gameLocal.GetClientByNum(client);
bool justSound = false;
if( client != gameLocal.localClientNum ) {
justSound = true;
}
if ( ( gameLocal.time - inGameAwardHudTime ) < 3000 || awardQueue.Num() > 0 ) {
if ( gameLocal.GetDemoFollowClient() == client || ( player && player->GetInstance() == remote->GetInstance() ) ) {
rvPair<int,bool> awardPair(award, justSound);
awardQueue.StackAdd(awardPair);
return;
}
}
if( client == gameLocal.localClientNum ) {
// don't count awards during warmup
if( !player || (gameLocal.mpGame.GetGameState()->GetMPGameState() != WARMUP &&
(gameLocal.gameType != GAME_TOURNEY || ((rvTourneyGameState*)gameLocal.mpGame.GetGameState())->GetArena( player->GetArena() ).GetState() != AS_WARMUP )) ) {
localInGameAwards[ award ]++;
numAwards = localInGameAwards[ award ];
} else {
numAwards = 1;
}
if( player && player->mphud ) {
player->mphud->HandleNamedEvent( "clearIGA" );
player->mphud->SetStateInt( "ig_awards", idMath::ClampInt( 0, 10, numAwards ) );
player->mphud->SetStateString( "ig_award", va( "gfx/mp/awards/%s", inGameAwardInfo[ award ].name ) );
if( numAwards < 10 ) {
player->mphud->SetStateString( "ig_award_num", "");
for( int i = 0; i < idMath::ClampInt( 0, 10, numAwards ); i++ ) {
player->mphud->SetStateInt( va( "ig_awards_%d", i + 1 ), 1 );
}
} else {
player->mphud->SetStateInt( "ig_award_num", numAwards );
player->mphud->SetStateInt( "ig_awards", 1 );
player->mphud->SetStateInt( va( "ig_awards_%d", 1 ), 1 );
}
//inGameAwardHudTime = gameLocal.time;
player->mphud->HandleNamedEvent( "giveIGA" );
player->mphud->StateChanged( gameLocal.time );
}
if ( player ) {
player->StartSound( va( "snd_award_%s", inGameAwardInfo[ award ].name ), SND_CHANNEL_ANY, 0, false, NULL );
}
}
else if ( player && remote && (player->GetInstance() == remote->GetInstance()) && (award == IGA_HOLY_SHIT || award == IGA_CAPTURE)) {
player->StartSound( va( "snd_award_%s", inGameAwardInfo[ award ].name ), SND_CHANNEL_ANY, 0, false, NULL );
}
inGameAwardHudTime = gameLocal.time;
idPlayer* awardee = gameLocal.GetClientByNum( client );
if ( awardee ) {
if ( player && player->GetInstance() == awardee->GetInstance() ) {
iconManager->AddIcon( client, va( "mtr_award_%s", inGameAwardInfo[ award ].name ) );
}
}
}
void rvStatManager::CheckAwardQueue() {
if(((gameLocal.time - inGameAwardHudTime) < 3000 || awardQueue.Num() == 0))
{
return;
}
idPlayer* player = gameLocal.GetLocalPlayer();
int award = awardQueue.StackTop().First();
bool justSound = awardQueue.StackTop().Second();
awardQueue.StackPop();
if ( player ) {
if(!justSound || award == IGA_HOLY_SHIT || award == IGA_CAPTURE)
{
player->StartSound( va( "snd_award_%s", inGameAwardInfo[ award ].name ), SND_CHANNEL_ANY, 0, false, NULL );
}
}
if( player && player->mphud && !justSound) {
player->mphud->HandleNamedEvent( "clearIGA" );
localInGameAwards[ award ]++;
player->mphud->SetStateInt( "ig_awards", idMath::ClampInt( 0, 10, localInGameAwards[ award ] ) );
player->mphud->SetStateString( "ig_award", va( "gfx/mp/awards/%s", inGameAwardInfo[ award ].name ) );
if(localInGameAwards[ award ] < 10) {
player->mphud->SetStateString( "ig_award_num", "");
for( int i = 0; i < idMath::ClampInt( 0, 10, localInGameAwards[ award ] ); i++ ) {
player->mphud->SetStateInt( va( "ig_awards_%d", i + 1 ), 1 );
}
}
else {
player->mphud->SetStateInt( "ig_award_num", localInGameAwards[ award ]);
player->mphud->SetStateInt( "ig_awards", 1 );
player->mphud->SetStateInt( va( "ig_awards_%d", 1 ), 1 );
}
inGameAwardHudTime = gameLocal.time;
player->mphud->HandleNamedEvent( "giveIGA" );
player->mphud->StateChanged( gameLocal.time );
}
}
void rvStatManager::GivePlayerCashForAward( idPlayer* player, inGameAward_t award )
{
if( !player )
return;
if( !gameLocal.isMultiplayer )
return;
if( !gameLocal.mpGame.IsBuyingAllowedInTheCurrentGameMode() )
return;
mpGameState_t mpGameState = gameLocal.mpGame.GetGameState()->GetMPGameState();
if( mpGameState != GAMEON && mpGameState != SUDDENDEATH )
return;
const char* awardCashValueName = NULL;
switch( award )
{
case IGA_CAPTURE: awardCashValueName = "playerCashAward_mpAward_capture"; break;
case IGA_HUMILIATION: awardCashValueName = "playerCashAward_mpAward_humiliation"; break;
case IGA_IMPRESSIVE: awardCashValueName = "playerCashAward_mpAward_impressive"; break;
case IGA_EXCELLENT: awardCashValueName = "playerCashAward_mpAward_excellent"; break;
case IGA_ASSIST: awardCashValueName = "playerCashAward_mpAward_assist"; break;
case IGA_DEFENSE: awardCashValueName = "playerCashAward_mpAward_defense"; break;
case IGA_COMBO_KILL: awardCashValueName = "playerCashAward_mpAward_combo_kill"; break;
case IGA_RAMPAGE: awardCashValueName = "playerCashAward_mpAward_rampage"; break;
case IGA_HOLY_SHIT: awardCashValueName = "playerCashAward_mpAward_holy_shit"; break;
}
if( awardCashValueName )
{
player->GiveCash( (float) gameLocal.mpGame.mpBuyingManager.GetIntValueForKey( awardCashValueName, 0 ) );
}
}
void rvStatManager::GiveInGameAward( inGameAward_t award, int clientNum ) {
idPlayer* player = (idPlayer*)gameLocal.entities[ clientNum ];
if( gameLocal.isMultiplayer ) {
// show in-game awards during warmup, but don't actually let players accumulate them
if( !player || (gameLocal.mpGame.GetGameState()->GetMPGameState() != WARMUP &&
(gameLocal.gameType != GAME_TOURNEY || ((rvTourneyGameState*)gameLocal.mpGame.GetGameState())->GetArena( player->GetArena() ).GetState() != AS_WARMUP )) ) {
playerStats[ clientNum ].inGameAwards[ award ]++;
GivePlayerCashForAward( player, award );
}
SendInGameAward( award, clientNum );
}
}
void rvStatManager::SetupStatWindow( idUserInterface* statHud ) {
statWindow.SetupStatWindow( statHud );
}
void rvStatManager::SelectStatWindow( int selectionIndex, int selectionTeam ) {
statWindow.SelectPlayer( statWindow.ClientNumFromSelection( selectionIndex, selectionTeam ) );
}
int rvStatManager::GetSelectedClientNum( int* selectionIndexOut, int* selectionTeamOut ) {
return statWindow.GetSelectedClientNum( selectionIndexOut, selectionTeamOut );
}
void rvStatManager::UpdateInGameHud( idUserInterface* statHud, bool visible ) {
idPlayer* player = NULL;
if( gameLocal.GetLocalPlayer() ) {
player = gameLocal.GetLocalPlayer();
player->GetHud()->SetStateInt( "stat_visible", visible? 1 : 0);
}
if( !visible ) {
statHud->SetStateInt( "stat_visible", 0 );
return;
} else {
statHud->SetStateInt( "stat_visible", 1 );
}
if( player ) {
statWindow.SetupStatWindow( statHud, player->spectating );
}
}
void rvStatManager::SendStat( int toClient, int statClient ) {
if( statClient < 0 || statClient >= MAX_CLIENTS ) {
gameLocal.Warning( "rvStatManager::SendStat() - Stats requested for invalid client num '%d'\n", statClient );
return;
}
idBitMsg outMsg;
byte msgBuf[ MAX_GAME_MESSAGE_SIZE ];
outMsg.Init( msgBuf, sizeof( msgBuf ) );
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_STAT );
outMsg.WriteByte( statClient );
playerStats[ statClient ].PackStats( outMsg );
networkSystem->ServerSendReliableMessage( toClient, outMsg );
}
void rvStatManager::ReceiveStat( const idBitMsg& msg ) {
//asalmon: added because this is used to restore single player stats from saves on Xbox 360
if(gameLocal.IsMultiplayer())
{
assert( gameLocal.isClient );
}
int client = msg.ReadByte();
playerStats[ client ].UnpackStats( msg );
playerStats[ client ].lastUpdateTime = gameLocal.time;
// display the updated stat
if(gameLocal.IsMultiplayer())
{
statWindow.SelectPlayer( client );
}
}
void rvStatManager::SendAllStats( int clientNum, bool full ) {
assert( gameLocal.isServer );
idBitMsg outMsg;
byte msgBuf[ MAX_GAME_MESSAGE_SIZE ];
outMsg.Init( msgBuf, sizeof( msgBuf ) );
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_ALL_STATS );
assert( MAX_CLIENTS <= 32 );
unsigned sentClients = 0;
for(int i=0; i < MAX_CLIENTS; i++)
{
if ( gameLocal.entities[ i ] ) {
sentClients |= 1 << i;
}
}
outMsg.WriteBits( sentClients, MAX_CLIENTS );
for(int i=0; i < MAX_CLIENTS; i++)
{
if ( sentClients & ( 1 << i ) ) {
playerStats[ i ].PackStats( outMsg );
}
}
networkSystem->ServerSendReliableMessage( clientNum, outMsg );
//common->Printf("SENT ALL STATS %i\n", Sys_Milliseconds());
}
void rvStatManager::ReceiveAllStats( const idBitMsg& msg ) {
assert( gameLocal.isClient );
assert( MAX_CLIENTS <= 32 );
unsigned sentClients = msg.ReadBits( MAX_CLIENTS );
for(int i=0; i < MAX_CLIENTS; i++)
{
if ( sentClients & ( 1 << i ) ) {
playerStats[ i ].UnpackStats( msg );
} else {
playerStats[ i ].Clear();
}
playerStats[ i ].lastUpdateTime = gameLocal.time;
}
if ( gameLocal.mpGame.GetGameState()->GetMPGameState() == GAMEREVIEW ) {
gameLocal.mpGame.ShowStatSummary();
}
}
void rvStatManager::ClearStats( void ) {
// clear connected stats
for( int i = 0; i < MAX_CLIENTS; i++ ) {
playerStats[ i ] = rvPlayerStat();
}
for ( int q = 0; q < MAX_CLIENTS; ++q ) {
lastRailShot[ q ] = -2;
lastRailShotHits[ q ] = 0;
}
#ifdef _XENON
lastFullUpdate = -50000;
#endif
}
void rvStatManager::CalculateEndGameStats( void ) {
int maxKills = idMath::INT_MIN;
int maxKillPlayer = -1;
int maxSuicides = idMath::INT_MIN;
int maxSuicidesPlayer = -1;
int maxGauntletKills = idMath::INT_MIN;
int maxGauntletKillsPlayer = -1;
float maxDamageKillsRatio = 0.0;
int maxDamageKillsRatioPlayer = -1;
//Dump the stats to a log file
idFile *log = NULL;
if(cvarSystem->GetCVarBool("com_logMPStats"))
{
log = fileSystem->OpenFileAppend("StatisticsLog.txt");
}
idStr toFile;
if ( !log ) {
// common->Warning("Statistics log will not be written\n");
}
else
{
struct tm *newtime;
time_t aclock;
time( &aclock );
newtime = localtime( &aclock );
toFile = va("Match on map %s played on %s\n", gameLocal.GetMapName(), asctime( newtime ));
log->Write(toFile.c_str(), toFile.Length());
}
for( int i = 0; i < MAX_CLIENTS; i++ ) {
if( !gameLocal.entities[ i ] ) {
continue;
}
if(log)
{
toFile = va("Statistics for %s\n", gameLocal.userInfo[ i ].GetString("ui_name"));
log->Write(toFile.c_str(), toFile.Length());
toFile = va("Kills: %i Deaths: %i Suicides: %i\n", playerStats[ i ].kills, playerStats[ i ].deaths, playerStats[ i ].suicides);
log->Write(toFile.c_str(), toFile.Length());
}
gameLocal.Printf( "Calculating stats for client %d (%s)\n", i, gameLocal.userInfo[ i ].GetString("ui_name") );
// overall accuracy award and sniper accuracy award
idPlayer* player = (idPlayer*)gameLocal.entities[ i ];
int railgunIndex = player->GetWeaponIndex( "weapon_railgun" );
int rocketIndex = player->GetWeaponIndex( "weapon_rocketlauncher" );
float accuracyAverage = 0.0f;
int numAccuracies = 0;
for( int j = 0; j < MAX_WEAPONS; j++ ) {
if( playerStats[ i ].weaponShots[ j ] == 0 ) {
if(log)
{
if(player->GetWeaponDef(j))
{
toFile = va("%s not used\n", common->GetLocalizedString(player->GetWeaponDef(j)->dict.GetString("inv_name")));
log->Write(toFile.c_str(), toFile.Length());
}
}
continue;
}
float weaponAccuracy = (float)playerStats[ i ].weaponHits[ j ] / (float)playerStats[ i ].weaponShots[ j ];
if(log)
{
if(player->GetWeaponDef(j))
{
toFile = va("%s: %i%%\n", common->GetLocalizedString(player->GetWeaponDef(j)->dict.GetString("inv_name")), (int)(((float)playerStats[ i ].weaponHits[ j ] / (float)playerStats[ i ].weaponShots[ j ]) * 100.0f));
log->Write(toFile.c_str(), toFile.Length());
}
}
if( j == railgunIndex ) {
// sniper award
if( weaponAccuracy >= 0.9f && playerStats[ i ].weaponShots[ railgunIndex ] >= 10 ) {
playerStats[ i ].endGameAwards.Append( EGA_SNIPER );
}
}
accuracyAverage += weaponAccuracy;
numAccuracies++;
}
if ( numAccuracies && ( accuracyAverage / (float)numAccuracies >= 0.5f ) ) {
playerStats[ i ].endGameAwards.Append( EGA_ACCURACY );
}
// rail master award
if ( playerStats[ i ].kills && ( (float)playerStats[ i ].weaponKills[ railgunIndex ] / (float)playerStats[ i ].kills >= 0.8f ) ) {
playerStats[ i ].endGameAwards.Append( EGA_RAIL_MASTER );
}
// rocket sauce award
if ( playerStats[ i ].kills && ( (float)playerStats[ i ].weaponKills[ rocketIndex ] / (float)playerStats[ i ].kills >= 0.8f ) ) {
playerStats[ i ].endGameAwards.Append( EGA_ROCKET_SAUCE );
}
// critical failure award
if( playerStats[ i ].kills == 0 ) {
playerStats[ i ].endGameAwards.Append( EGA_CRITICAL_FAILURE );
}
// frags award
//asalmon: Made the limit more reasonable for the shorter time limits and less players of Xenon
#ifdef _XENON
if( playerStats[ i ].kills >= 50 ) {
playerStats[ i ].endGameAwards.Append( EGA_FRAGS );
}
#else
if( playerStats[ i ].kills >= 100 ) {
playerStats[ i ].endGameAwards.Append( EGA_FRAGS );
}
#endif
if( playerStats[ i ].kills > maxKills ) {
maxKills = playerStats[ i ].kills;
maxKillPlayer = i;
}
if( playerStats[ i ].suicides > maxSuicides ) {
maxSuicides = playerStats[ i ].suicides;
maxSuicidesPlayer = i;
}
if( playerStats[ i ].weaponKills[ player->GetWeaponIndex( "weapon_gauntlet" ) ] > maxGauntletKills ) {
maxGauntletKills = playerStats[ i ].weaponKills[ player->GetWeaponIndex( "weapon_gauntlet" ) ];
maxGauntletKillsPlayer = i;
}
//asalmon: Calculate the damage ratio:
if(playerStats[i].kills > 0)
{
playerStats[i].damageRatio = playerStats[i].damageGiven / playerStats[i].kills;
}
else
{
playerStats[i].damageRatio = playerStats[i].damageGiven;
}
if( playerStats[ i ].damageRatio > maxDamageKillsRatio ) {
maxDamageKillsRatio = playerStats[ i ].damageRatio;
maxDamageKillsRatioPlayer = i;
}
if(log)
{
toFile = "\n";
log->Write(toFile.c_str(), toFile.Length());
}
//asalmon: hack to test certain achievement awards.
#ifdef _XENON
if(cvarSystem->GetCVarInteger("si_overrideFrags"))
{
common->Printf("Overriding Frags to: %i for player %i\n", cvarSystem->GetCVarInteger("si_overrideFrags"), i);
playerStats[i].kills = cvarSystem->GetCVarInteger("si_overrideFrags");
}
#endif
}
// Perfect award
if( maxKillPlayer >= 0 && playerStats[ maxKillPlayer ].deaths == 0 ) {
idPlayer* player = (idPlayer*)gameLocal.entities[ maxKillPlayer ];
if( !gameLocal.IsTeamGame() || ( gameLocal.IsTeamGame() && player && gameLocal.mpGame.TeamLeader() == player->team ) ) {
playerStats[ maxKillPlayer ].endGameAwards.Append( EGA_PERFECT );
}
}
// Lemming award
if( maxSuicidesPlayer >= 0 && maxSuicides >= 5 ) {
playerStats[ maxSuicidesPlayer ].endGameAwards.Append( EGA_LEMMING );
}
// Brawler award
if( maxGauntletKillsPlayer >= 0 && maxGauntletKills >= 3 ) {
playerStats[ maxGauntletKillsPlayer ].endGameAwards.Append( EGA_BRAWLER );
}
// Team player award
if( maxDamageKillsRatioPlayer >= 0 && maxDamageKillsRatio > 500 ) {
playerStats[ maxDamageKillsRatioPlayer ].endGameAwards.Append( EGA_TEAM_PLAYER );
}
if(log)
{
toFile = "\n";
log->Write(toFile.c_str(), toFile.Length());
log->Flush();
fileSystem->CloseFile(log);
}
}
void rvStatManager::GetAccuracyLeaders( int accuracyLeaders[ MAX_WEAPONS ] ) {
memset( accuracyLeaders, -1, sizeof( int ) * MAX_WEAPONS );
for( int i = 0; i < MAX_CLIENTS; i++ ) {
if( gameLocal.entities[ i ] == NULL ) {
continue;
}
rvPlayerStat* playerStats = GetPlayerStat( i );
for( int j = 0; j < MAX_WEAPONS; j++ ) {
if( playerStats->weaponShots[ j ] == 0 ) {
continue;
}
float playerAccuracy = (float)playerStats->weaponHits[ j ] / (float)playerStats->weaponShots[ j ];
float leaderAccuracy = -1.0f;
if( accuracyLeaders[ j ] != -1 ) {
rvPlayerStat* leaderStats = GetPlayerStat( accuracyLeaders[ j ] );
if( leaderStats->weaponShots[ j ] != 0 ) {
leaderAccuracy = (float)leaderStats->weaponHits[ j ] / (float)leaderStats->weaponShots[ j ];
}
}
if( playerAccuracy > leaderAccuracy ) {
accuracyLeaders[ j ] = i;
}
}
}
}
int rvStatManager::DamageGiven( int playerNum, int lowerBound, int upperBound ) {
if( playerNum < 0 || playerNum >= MAX_CLIENTS ) {
return 0;
}
int damage = 0;
for( int i = 0; i < statQueue.Num(); i++ ) {
if( statQueue[ i ].First()->GetType() == ST_DAMAGE_DEALT ) {
if( statQueue[ i ].First()->GetPlayerClientNum() == playerNum && (statQueue[ i ].First()->GetTimeStamp() > lowerBound && statQueue[ i ].First()->GetTimeStamp() < upperBound) ) {
damage += static_cast<rvStatDamageDealt*>(statQueue[ i ].First())->GetDamage();
}
}
}
return damage;
}
int rvStatManager::DamageTaken( int playerNum, int lowerBound, int upperBound ) {
if( playerNum < 0 || playerNum >= MAX_CLIENTS ) {
return 0;
}
int damage = 0;
for( int i = 0; i < statQueue.Num(); i++ ) {
if( statQueue[ i ].First()->GetType() == ST_DAMAGE_TAKEN ) {
if( statQueue[ i ].First()->GetPlayerClientNum() == playerNum && ( statQueue[ i ].First()->GetTimeStamp() > lowerBound && statQueue[ i ].First()->GetTimeStamp() < upperBound ) ) {
damage += static_cast<rvStatDamageTaken*>(statQueue[ i ].First())->GetDamage();
}
}
}
return damage;
}
rvStat* rvStatManager::GetLastClientStat( int clientNum, statType_t type, int time ) {
for( int i = (statQueue.Num() - 1); i >= 0; i-- ) {
if( statQueue[ i ].First()->GetTimeStamp() < time ) {
return NULL;
}
if( statQueue[ i ].First()->GetType() == type ) {
if( clientNum == -1 || statQueue[ i ].First()->GetPlayerClientNum() == clientNum ) {
return statQueue[ i ].First();
}
}
}
return NULL;
}
void rvStatManager::GetLastClientStats( int clientNum, statType_t type, int time, int num, rvStat** results ) {
int numFound = 0;
for( int i = (statQueue.Num() - 1); i >= 0; i-- ) {
if( statQueue[ i ].First()->GetTimeStamp() < time ) {
return;
}
if( statQueue[ i ].First()->GetType() == type ) {
if( clientNum == -1 || statQueue[ i ].First()->GetPlayerClientNum() == clientNum ) {
results[ numFound++ ] = statQueue[ i ].First();
if( numFound >= num ) {
return;
}
}
}
}
return;
}
rvStatTeam* rvStatManager::GetLastTeamStat( int team, statType_t type, int time ) {
assert( type > ST_STAT_TEAM );
for( int i = (statQueue.Num() - 1); i >= 0; i-- ) {
if( statQueue[ i ].First()->GetTimeStamp() < time ) {
return NULL;
}
if( statQueue[ i ].First()->GetType() == type ) {
if( ((rvStatTeam*)statQueue[ i ].First())->GetTeam() == team ) {
return (rvStatTeam*)statQueue[ i ].First();
}
}
}
return NULL;
}
void rvStatManager::SetupEndGameHud( idUserInterface* statHud ) {
if( gameLocal.IsFlagGameType() ) {
statHud->SetStateInt( "ctf_awards", 1 );
} else {
statHud->SetStateInt( "ctf_awards", 0 );
}
for( int i = 0; i < MAX_CLIENTS; i++ ) {
idPlayer* p = gameLocal.mpGame.GetRankedPlayer( i );
if( p && gameLocal.mpGame.IsInGame( p->entityNumber ) ) {
statHud->SetStateString( va( "player%d_name", i + 1 ), va( "%d. %s", i + 1, gameLocal.userInfo[ p->entityNumber ].GetString( "ui_name" ) ) );
statHud->SetStateString( va( "player%d_score", i + 1 ), va( "%d", gameLocal.mpGame.GetScore( i ) ) );
statHud->SetStateInt( va( "player%d_visible", i + 1 ), 1 );
statHud->SetStateInt( va( "player%d_team", i + 1 ), gameLocal.IsTeamGame() ? p->team : 0 );
} else {
statHud->SetStateInt( va( "player%d_visible", i + 1 ), 0 );
}
}
statHud->HandleNamedEvent( "Setup" );
}
rvPlayerStat* rvStatManager::GetPlayerStat( int clientNum ) {
return &playerStats[ clientNum ];
}
void rvStatManager::UpdateEndGameHud( idUserInterface* statHud, int clientNum ) {
/*if( !endGameSetup ) {
// no info yet
SetupEndGameHud( statHud );
endGameSetup = true;
}
rvPlayerStat* clientStat = &(playerStats[ clientNum ]);
statHud->HandleNamedEvent( "clear" );
statHud->SetStateString( "stat_name", gameLocal.userInfo[ clientNum ].GetString( "ui_name" ) );
// weapon accuracy
for( int i = 0; i < MAX_WEAPONS; i++ ) {
statHud->SetStateString( va( "stat_%d_pct", i ), va( "%d%%", (int)(clientStat->weaponAccuracy[ i ] * 100) ) );
}
// in-game awards
int igAwardCount[ IGA_NUM_AWARDS ];
memset( igAwardCount, 0, sizeof( int ) * IGA_NUM_AWARDS );
for( int i = 0; i < clientStat->inGameAwards.Num(); i++ ) {
igAwardCount[ clientStat->inGameAwards[ i ] ]++;
}
for( int i = 0; i < IGA_NUM_AWARDS; i++ ) {
statHud->SetStateString( inGameAwardInfo[ i ].name, va( "%d", igAwardCount[ i ] ) );
}
// end-game awards
for( int i = 0; i < clientStat->endGameAwards.Num(); i++ ) {
statHud->SetStateInt( va( "eg_award%d", i ), 1 );
statHud->SetStateString( va( "eg_award%d_text", i ), endGameAwardInfo[ clientStat->endGameAwards[ i ] ].name );
}
// kills
statHud->SetStateString( "stat_frags", va( "%d", clientStat->kills ) );
// deaths
statHud->SetStateString( "stat_deaths", va( "%d", clientStat->deaths ) );*/
}
/*
===============================================================================
rvStatSummary
Stores one player's summary information. Transmitted to clients for
intermission summary screen.
===============================================================================
*/
rvPlayerStat::rvPlayerStat() {
Clear();
}
void rvPlayerStat::Clear( void ) {
memset( weaponShots, 0, sizeof(int) * MAX_WEAPONS );
memset( weaponHits, 0, sizeof(int) * MAX_WEAPONS );
memset( weaponKills, 0, sizeof(int) * MAX_WEAPONS );
memset( inGameAwards, 0, sizeof( int ) * (int)IGA_NUM_AWARDS );
kills = deaths = suicides = lastUpdateTime = damageTaken = damageGiven = 0;
damageRatio = 0.0f;
}
void rvPlayerStat::PackStats( idBitMsg& msg ) {
for( int i = 0; i < MAX_WEAPONS; i++ ) {
msg.WriteShort( weaponShots[ i ] );
}
for( int i = 0; i < MAX_WEAPONS; i++ ) {
msg.WriteShort( weaponHits[ i ] );
}
for( int i = 0; i < IGA_NUM_AWARDS; i++ ) {
msg.WriteByte( inGameAwards[ i ] );
}
msg.WriteByte( endGameAwards.Num() );
for( int i = 0; i < endGameAwards.Num(); i++ ) {
msg.WriteByte( endGameAwards[ i ] );
}
msg.WriteBits( idMath::ClampInt( 0, MP_PLAYER_MAXDEATHS, deaths ), ASYNC_PLAYER_DEATH_BITS );
msg.WriteBits( idMath::ClampInt( 0, MP_PLAYER_MAXKILLS, kills ), ASYNC_PLAYER_KILL_BITS );
}
void rvPlayerStat::UnpackStats( const idBitMsg& msg ) {
for( int i = 0; i < MAX_WEAPONS; i++ ) {
weaponShots[ i ] = msg.ReadShort();
}
for( int i = 0; i < MAX_WEAPONS; i++ ) {
weaponHits[ i ] = msg.ReadShort();
}
for( int i = 0; i < IGA_NUM_AWARDS; i++ ) {
inGameAwards[ i ] = msg.ReadByte();
}
endGameAwards.SetNum( msg.ReadByte() );
for( int i = 0; i < endGameAwards.Num(); i++ ) {
endGameAwards[ i ] = (endGameAward_t)msg.ReadByte();
}
deaths = msg.ReadBits( ASYNC_PLAYER_DEATH_BITS );
kills = msg.ReadBits( ASYNC_PLAYER_KILL_BITS );
}