3515 lines
98 KiB
C++
3515 lines
98 KiB
C++
#include "../idlib/precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "Game_local.h"
|
|
// RAVEN BEGIN
|
|
// bdube: client entities
|
|
#include "client/ClientEffect.h"
|
|
// shouchard: ban list support
|
|
#define BANLIST_FILENAME "banlist.txt"
|
|
// RAVEN END
|
|
|
|
/*
|
|
===============================================================================
|
|
|
|
Client running game code:
|
|
- entity events don't work and should not be issued
|
|
- entities should never be spawned outside idGameLocal::ClientReadSnapshot
|
|
|
|
===============================================================================
|
|
*/
|
|
|
|
// adds tags to the network protocol to detect when things go bad ( internal consistency )
|
|
// NOTE: this changes the network protocol
|
|
#ifndef ASYNC_WRITE_TAGS
|
|
#define ASYNC_WRITE_TAGS 0
|
|
#endif
|
|
|
|
idCVar net_clientShowSnapshot( "net_clientShowSnapshot", "0", CVAR_GAME | CVAR_INTEGER, "", 0, 3, idCmdSystem::ArgCompletion_Integer<0,3> );
|
|
idCVar net_clientShowSnapshotRadius( "net_clientShowSnapshotRadius", "128", CVAR_GAME | CVAR_FLOAT, "" );
|
|
idCVar net_clientMaxPrediction( "net_clientMaxPrediction", "1000", CVAR_SYSTEM | CVAR_INTEGER | CVAR_NOCHEAT, "maximum number of milliseconds a client can predict ahead of server." );
|
|
idCVar net_clientLagOMeter( "net_clientLagOMeter", "0", CVAR_GAME | CVAR_BOOL | CVAR_NOCHEAT | PC_CVAR_ARCHIVE, "draw prediction graph" );
|
|
|
|
// RAVEN BEGIN
|
|
// ddynerman: performance profiling
|
|
int net_entsInSnapshot;
|
|
int net_snapshotSize;
|
|
|
|
extern const int ASYNC_PLAYER_FRAG_BITS;
|
|
// RAVEN END
|
|
|
|
/*
|
|
================
|
|
idGameLocal::InitAsyncNetwork
|
|
================
|
|
*/
|
|
void idGameLocal::InitAsyncNetwork( void ) {
|
|
memset( clientEntityStates, 0, sizeof( clientEntityStates ) );
|
|
memset( clientPVS, 0, sizeof( clientPVS ) );
|
|
memset( clientSnapshots, 0, sizeof( clientSnapshots ) );
|
|
|
|
eventQueue.Init();
|
|
|
|
// NOTE: now that we removed spawning by typeNum, we could stick to a >0 entityDefBits
|
|
// not making the change at this point because of proto69 back compat stuff
|
|
entityDefBits = -( idMath::BitsForInteger( declManager->GetNumDecls( DECL_ENTITYDEF ) ) + 1 );
|
|
localClientNum = 0; // on a listen server SetLocalUser will set this right
|
|
realClientTime = 0;
|
|
isNewFrame = true;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ShutdownAsyncNetwork
|
|
================
|
|
*/
|
|
void idGameLocal::ShutdownAsyncNetwork( void ) {
|
|
entityStateAllocator.Shutdown();
|
|
snapshotAllocator.Shutdown();
|
|
eventQueue.Shutdown();
|
|
memset( clientEntityStates, 0, sizeof( clientEntityStates ) );
|
|
memset( clientPVS, 0, sizeof( clientPVS ) );
|
|
memset( clientSnapshots, 0, sizeof( clientSnapshots ) );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::InitLocalClient
|
|
================
|
|
*/
|
|
void idGameLocal::InitLocalClient( int clientNum ) {
|
|
isServer = false;
|
|
isClient = true;
|
|
localClientNum = clientNum;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerAllowClient
|
|
clientId is the ID of the connecting client - can later be mapped to a clientNum by calling networkSystem->ServerGetClientNum( clientId )
|
|
================
|
|
*/
|
|
allowReply_t idGameLocal::ServerAllowClient( int clientId, int numClients, const char *IP, const char *guid, const char *password, const char *privatePassword, char reason[ MAX_STRING_CHARS ] ) {
|
|
reason[0] = '\0';
|
|
|
|
// RAVEN BEGIN
|
|
// shouchard: ban support
|
|
if ( IsGuidBanned( guid ) ) {
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107239" );
|
|
return ALLOW_NO;
|
|
}
|
|
// RAVEN END
|
|
|
|
if ( serverInfo.GetInt( "si_pure" ) && !mpGame.IsPureReady() ) {
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107139" );
|
|
return ALLOW_NOTYET;
|
|
}
|
|
|
|
if ( !serverInfo.GetInt( "si_maxPlayers" ) ) {
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107140" );
|
|
return ALLOW_NOTYET;
|
|
}
|
|
|
|
// completely full
|
|
if ( numClients >= serverInfo.GetInt( "si_maxPlayers" ) ) {
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107141" );
|
|
return ALLOW_NOTYET;
|
|
}
|
|
|
|
// check private clients
|
|
if( serverInfo.GetInt( "si_privatePlayers" ) > 0 ) {
|
|
// just in case somehow we have a stale private clientId that matches a new client
|
|
mpGame.RemovePrivatePlayer( clientId );
|
|
|
|
const char *privatePass = cvarSystem->GetCVarString( "g_privatePassword" );
|
|
if( privatePass[ 0 ] == '\0' ) {
|
|
common->Warning( "idGameLocal::ServerAllowClient() - si_privatePlayers > 0 with no g_privatePassword" );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_NOW, "say si_privatePlayers is set but g_privatePassword is empty" );
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107142" );
|
|
return ALLOW_NOTYET;
|
|
}
|
|
|
|
|
|
int numPrivateClients = cvarSystem->GetCVarInteger( "si_numPrivatePlayers" );
|
|
|
|
// private clients that take up public slots are considered public clients
|
|
numPrivateClients = idMath::ClampInt( 0, serverInfo.GetInt( "si_privatePlayers" ), numPrivateClients );
|
|
|
|
if ( !idStr::Cmp( privatePass, privatePassword ) ) {
|
|
// once this client spawns in, they'll be marked private
|
|
mpGame.AddPrivatePlayer( clientId );
|
|
} else if( (numClients - numPrivateClients) >= (serverInfo.GetInt( "si_maxPlayers" ) - serverInfo.GetInt( "si_privatePlayers" )) ) {
|
|
// if the number of public clients is greater than or equal to the number of public slots, require a private slot
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107141" );
|
|
return ALLOW_NOTYET;
|
|
}
|
|
}
|
|
|
|
|
|
if ( !cvarSystem->GetCVarBool( "si_usePass" ) ) {
|
|
return ALLOW_YES;
|
|
}
|
|
|
|
const char *pass = cvarSystem->GetCVarString( "g_password" );
|
|
if ( pass[ 0 ] == '\0' ) {
|
|
common->Warning( "si_usePass is set but g_password is empty" );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_NOW, "say si_usePass is set but g_password is empty" );
|
|
// avoids silent misconfigured state
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107142" );
|
|
return ALLOW_NOTYET;
|
|
}
|
|
|
|
if ( !idStr::Cmp( pass, password ) ) {
|
|
return ALLOW_YES;
|
|
}
|
|
|
|
idStr::snPrintf( reason, MAX_STRING_CHARS, "#str_107143" );
|
|
Printf( "Rejecting client %s from IP %s: invalid password\n", guid, IP );
|
|
return ALLOW_BADPASS;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerClientConnect
|
|
================
|
|
*/
|
|
void idGameLocal::ServerClientConnect( int clientNum, const char *guid ) {
|
|
// make sure no parasite entity is left
|
|
if ( entities[ clientNum ] ) {
|
|
common->DPrintf( "ServerClientConnect: remove old player entity\n" );
|
|
delete entities[ clientNum ];
|
|
}
|
|
unreliableMessages[ clientNum ].Init( 0 );
|
|
userInfo[ clientNum ].Clear();
|
|
mpGame.ServerClientConnect( clientNum );
|
|
Printf( "client %d connected.\n", clientNum );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerClientBegin
|
|
================
|
|
*/
|
|
void idGameLocal::ServerClientBegin( int clientNum ) {
|
|
idBitMsg outMsg;
|
|
byte msgBuf[MAX_GAME_MESSAGE_SIZE];
|
|
|
|
// spawn the player
|
|
SpawnPlayer( clientNum );
|
|
if ( clientNum == localClientNum ) {
|
|
mpGame.EnterGame( clientNum );
|
|
}
|
|
|
|
// ddynerman: connect time
|
|
((idPlayer*)entities[ clientNum ])->SetConnectTime( time );
|
|
|
|
// send message to spawn the player at the clients
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.BeginWriting();
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_SPAWN_PLAYER );
|
|
outMsg.WriteByte( clientNum );
|
|
outMsg.WriteLong( spawnIds[ clientNum ] );
|
|
networkSystem->ServerSendReliableMessage( -1, outMsg );
|
|
|
|
if( gameType != GAME_TOURNEY ) {
|
|
((idPlayer*)entities[ clientNum ])->JoinInstance( 0 );
|
|
} else {
|
|
// instance 0 might be empty in Tourney
|
|
((idPlayer*)entities[ clientNum ])->JoinInstance( ((rvTourneyGameState*)gameLocal.mpGame.GetGameState())->GetNextActiveArena( 0 ) );
|
|
}
|
|
//RAVEN BEGIN
|
|
//asalmon: This client has finish loading and will be spawned mark them as ready.
|
|
#ifdef _XENON
|
|
Live()->ClientReady(clientNum);
|
|
#endif
|
|
//RAVEN END
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerClientDisconnect
|
|
clientNum == MAX_CLIENTS for cleanup of server demo recording data
|
|
================
|
|
*/
|
|
void idGameLocal::ServerClientDisconnect( int clientNum ) {
|
|
int i;
|
|
idBitMsg outMsg;
|
|
byte msgBuf[MAX_GAME_MESSAGE_SIZE];
|
|
|
|
if ( clientNum < MAX_CLIENTS ) {
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.BeginWriting();
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_DELETE_ENT );
|
|
outMsg.WriteBits( ( spawnIds[ clientNum ] << GENTITYNUM_BITS ) | clientNum, 32 ); // see GetSpawnId
|
|
networkSystem->ServerSendReliableMessage( -1, outMsg );
|
|
}
|
|
|
|
// free snapshots stored for this client
|
|
FreeSnapshotsOlderThanSequence( clientNum, 0x7FFFFFFF );
|
|
|
|
// free entity states stored for this client
|
|
for ( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
if ( clientEntityStates[ clientNum ][ i ] ) {
|
|
entityStateAllocator.Free( clientEntityStates[ clientNum ][ i ] );
|
|
clientEntityStates[ clientNum ][ i ] = NULL;
|
|
}
|
|
}
|
|
|
|
// clear the client PVS
|
|
memset( clientPVS[ clientNum ], 0, sizeof( clientPVS[ clientNum ] ) );
|
|
|
|
if ( clientNum == MAX_CLIENTS ) {
|
|
return;
|
|
}
|
|
|
|
// only drop MP clients if we're in multiplayer and the server isn't going down
|
|
if ( gameLocal.isMultiplayer && !(gameLocal.isListenServer && clientNum == gameLocal.localClientNum ) ) {
|
|
// idMultiplayerGame::DisconnectClient will do the delete in MP
|
|
mpGame.DisconnectClient( clientNum );
|
|
} else {
|
|
// delete the player entity
|
|
delete entities[ clientNum ];
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerWriteInitialReliableMessages
|
|
|
|
Send reliable messages to initialize the client game up to a certain initial state.
|
|
================
|
|
*/
|
|
void idGameLocal::ServerWriteInitialReliableMessages( int clientNum ) {
|
|
int i;
|
|
idBitMsg outMsg;
|
|
byte msgBuf[MAX_GAME_MESSAGE_SIZE];
|
|
|
|
// spawn players
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( entities[i] == NULL || i == clientNum ) {
|
|
continue;
|
|
}
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.BeginWriting( );
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_SPAWN_PLAYER );
|
|
outMsg.WriteByte( i );
|
|
outMsg.WriteLong( spawnIds[ i ] );
|
|
networkSystem->ServerSendReliableMessage( clientNum, outMsg );
|
|
}
|
|
|
|
// update portals for opened doors
|
|
int numPortals = gameRenderWorld->NumPortals();
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.BeginWriting();
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_PORTALSTATES );
|
|
outMsg.WriteLong( numPortals );
|
|
for ( i = 0; i < numPortals; i++ ) {
|
|
outMsg.WriteBits( gameRenderWorld->GetPortalState( (qhandle_t) (i+1) ) , NUM_RENDER_PORTAL_BITS );
|
|
}
|
|
networkSystem->ServerSendReliableMessage( clientNum, outMsg );
|
|
|
|
mpGame.ServerWriteInitialReliableMessages( clientNum );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::FreeSnapshotsOlderThanSequence
|
|
================
|
|
*/
|
|
void idGameLocal::FreeSnapshotsOlderThanSequence( int clientNum, int sequence ) {
|
|
snapshot_t *snapshot, *lastSnapshot, *nextSnapshot;
|
|
entityState_t *state;
|
|
|
|
for ( lastSnapshot = NULL, snapshot = clientSnapshots[clientNum]; snapshot; snapshot = nextSnapshot ) {
|
|
nextSnapshot = snapshot->next;
|
|
if ( snapshot->sequence < sequence ) {
|
|
for ( state = snapshot->firstEntityState; state; state = snapshot->firstEntityState ) {
|
|
snapshot->firstEntityState = snapshot->firstEntityState->next;
|
|
entityStateAllocator.Free( state );
|
|
}
|
|
if ( lastSnapshot ) {
|
|
lastSnapshot->next = snapshot->next;
|
|
} else {
|
|
clientSnapshots[clientNum] = snapshot->next;
|
|
}
|
|
snapshotAllocator.Free( snapshot );
|
|
} else {
|
|
lastSnapshot = snapshot;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ApplySnapshot
|
|
================
|
|
*/
|
|
bool idGameLocal::ApplySnapshot( int clientNum, int sequence ) {
|
|
snapshot_t *snapshot, *lastSnapshot, *nextSnapshot;
|
|
entityState_t *state;
|
|
|
|
FreeSnapshotsOlderThanSequence( clientNum, sequence );
|
|
|
|
for ( lastSnapshot = NULL, snapshot = clientSnapshots[clientNum]; snapshot; snapshot = nextSnapshot ) {
|
|
nextSnapshot = snapshot->next;
|
|
if ( snapshot->sequence == sequence ) {
|
|
for ( state = snapshot->firstEntityState; state; state = state->next ) {
|
|
if ( clientEntityStates[clientNum][state->entityNumber] ) {
|
|
entityStateAllocator.Free( clientEntityStates[clientNum][state->entityNumber] );
|
|
}
|
|
clientEntityStates[clientNum][state->entityNumber] = state;
|
|
}
|
|
// ~512 bytes
|
|
memcpy( clientPVS[clientNum], snapshot->pvs, sizeof( snapshot->pvs ) );
|
|
if ( lastSnapshot ) {
|
|
lastSnapshot->next = nextSnapshot;
|
|
} else {
|
|
clientSnapshots[clientNum] = nextSnapshot;
|
|
}
|
|
snapshotAllocator.Free( snapshot );
|
|
return true;
|
|
} else {
|
|
lastSnapshot = snapshot;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::WriteGameStateToSnapshot
|
|
================
|
|
*/
|
|
void idGameLocal::WriteGameStateToSnapshot( idBitMsgDelta &msg ) const {
|
|
int i;
|
|
|
|
for( i = 0; i < MAX_GLOBAL_SHADER_PARMS; i++ ) {
|
|
msg.WriteFloat( globalShaderParms[i] );
|
|
}
|
|
|
|
mpGame.WriteToSnapshot( msg );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ReadGameStateFromSnapshot
|
|
================
|
|
*/
|
|
void idGameLocal::ReadGameStateFromSnapshot( const idBitMsgDelta &msg ) {
|
|
int i;
|
|
|
|
for( i = 0; i < MAX_GLOBAL_SHADER_PARMS; i++ ) {
|
|
globalShaderParms[i] = msg.ReadFloat();
|
|
}
|
|
|
|
mpGame.ReadFromSnapshot( msg );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerWriteSnapshot
|
|
Write a snapshot of the current game state for the given client.
|
|
================
|
|
*/
|
|
// RAVEN BEGIN
|
|
// jnewquist: Use dword array to match pvs array so we don't have endianness problems.
|
|
void idGameLocal::ServerWriteSnapshot( int clientNum, int sequence, idBitMsg &msg, dword *clientInPVS, int numPVSClients, int lastSnapshotFrame ) {
|
|
// RAVEN END
|
|
int i, msgSize, msgWriteBit;
|
|
idPlayer *player, *spectated = NULL;
|
|
idEntity *ent;
|
|
pvsHandle_t pvsHandle;
|
|
idBitMsgDelta deltaMsg;
|
|
snapshot_t *snapshot;
|
|
entityState_t *base, *newBase;
|
|
int numSourceAreas, sourceAreas[ idEntity::MAX_PVS_AREAS ];
|
|
|
|
player = static_cast<idPlayer *>( entities[ clientNum ] );
|
|
if ( !player ) {
|
|
return;
|
|
}
|
|
if ( player->spectating && player->spectator != clientNum && entities[ player->spectator ] ) {
|
|
spectated = static_cast< idPlayer * >( entities[ player->spectator ] );
|
|
} else {
|
|
spectated = player;
|
|
}
|
|
|
|
// free too old snapshots
|
|
// ( that's a security, normal acking from server keeps a smaller backlog of snaps )
|
|
FreeSnapshotsOlderThanSequence( clientNum, sequence - 64 );
|
|
|
|
// allocate new snapshot
|
|
snapshot = snapshotAllocator.Alloc();
|
|
snapshot->sequence = sequence;
|
|
snapshot->firstEntityState = NULL;
|
|
snapshot->next = clientSnapshots[clientNum];
|
|
clientSnapshots[clientNum] = snapshot;
|
|
memset( snapshot->pvs, 0, sizeof( snapshot->pvs ) );
|
|
|
|
// get PVS for this player
|
|
// don't use PVSAreas for networking - PVSAreas depends on animations (and md5 bounds), which are not synchronized
|
|
numSourceAreas = gameRenderWorld->BoundsInAreas( spectated->GetPlayerPhysics()->GetAbsBounds(), sourceAreas, idEntity::MAX_PVS_AREAS );
|
|
pvsHandle = gameLocal.pvs.SetupCurrentPVS( sourceAreas, numSourceAreas, PVS_NORMAL );
|
|
|
|
#if ASYNC_WRITE_TAGS
|
|
idRandom tagRandom;
|
|
tagRandom.SetSeed( random.RandomInt() );
|
|
msg.WriteLong( tagRandom.GetSeed() );
|
|
#endif
|
|
|
|
// write unreliable messages
|
|
unreliableMessages[ clientNum ].FlushTo( msg );
|
|
|
|
#if ASYNC_WRITE_TAGS
|
|
msg.WriteLong( tagRandom.RandomInt() );
|
|
#endif
|
|
|
|
// create the snapshot
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
|
|
// if the entity is not in the player PVS
|
|
if ( !ent->PhysicsTeamInPVS( pvsHandle ) && ent->entityNumber != clientNum ) {
|
|
continue;
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// ddynerman: don't transmit entities not in your clip world
|
|
if ( ent->GetInstance() != player->GetInstance() ) {
|
|
continue;
|
|
}
|
|
// RAVEN END
|
|
|
|
// if the entity is a map entity, mark it in PVS
|
|
if ( isMapEntity[ ent->entityNumber ] ) {
|
|
snapshot->pvs[ ent->entityNumber >> 5 ] |= 1 << ( ent->entityNumber & 31 );
|
|
}
|
|
|
|
// if that entity is not marked for network synchronization
|
|
if ( !ent->fl.networkSync ) {
|
|
continue;
|
|
}
|
|
|
|
// add the entity to the snapshot PVS
|
|
snapshot->pvs[ ent->entityNumber >> 5 ] |= 1 << ( ent->entityNumber & 31 );
|
|
|
|
// save the write state to which we can revert when the entity didn't change at all
|
|
msg.SaveWriteState( msgSize, msgWriteBit );
|
|
|
|
// write the entity to the snapshot
|
|
msg.WriteBits( ent->entityNumber, GENTITYNUM_BITS );
|
|
|
|
base = clientEntityStates[clientNum][ent->entityNumber];
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = ent->entityNumber;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
|
|
deltaMsg.InitWriting( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
deltaMsg.WriteBits( spawnIds[ ent->entityNumber ], 32 - GENTITYNUM_BITS );
|
|
assert( ent->entityDefNumber > 0 );
|
|
deltaMsg.WriteBits( ent->entityDefNumber, entityDefBits );
|
|
|
|
// write the class specific data to the snapshot
|
|
ent->WriteToSnapshot( deltaMsg );
|
|
|
|
if ( !deltaMsg.HasChanged() ) {
|
|
msg.RestoreWriteState( msgSize, msgWriteBit );
|
|
entityStateAllocator.Free( newBase );
|
|
} else {
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
|
|
#if ASYNC_WRITE_TAGS
|
|
msg.WriteLong( tagRandom.RandomInt() );
|
|
#endif
|
|
}
|
|
}
|
|
|
|
msg.WriteBits( ENTITYNUM_NONE, GENTITYNUM_BITS );
|
|
|
|
// write the PVS to the snapshot
|
|
#if ASYNC_WRITE_PVS
|
|
for ( i = 0; i < idEntity::MAX_PVS_AREAS; i++ ) {
|
|
if ( i < numSourceAreas ) {
|
|
msg.WriteLong( sourceAreas[ i ] );
|
|
} else {
|
|
msg.WriteLong( 0 );
|
|
}
|
|
}
|
|
gameLocal.pvs.WritePVS( pvsHandle, msg );
|
|
#endif
|
|
for ( i = 0; i < ENTITY_PVS_SIZE; i++ ) {
|
|
msg.WriteDeltaLong( clientPVS[clientNum][i], snapshot->pvs[i] );
|
|
}
|
|
|
|
// free the PVS
|
|
pvs.FreeCurrentPVS( pvsHandle );
|
|
|
|
// write the game and player state to the snapshot
|
|
base = clientEntityStates[clientNum][ENTITYNUM_NONE]; // ENTITYNUM_NONE is used for the game and player state
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = ENTITYNUM_NONE;
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
deltaMsg.InitWriting( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
if ( player->spectating && player->spectator != player->entityNumber && entities[ player->spectator ] ) {
|
|
assert( entities[ player->spectator ]->IsType( idPlayer::GetClassType() ) );
|
|
deltaMsg.WriteBits( player->spectator, idMath::BitsForInteger( MAX_CLIENTS ) );
|
|
static_cast< idPlayer * >( gameLocal.entities[ player->spectator ] )->WritePlayerStateToSnapshot( deltaMsg );
|
|
} else {
|
|
deltaMsg.WriteBits( player->entityNumber, idMath::BitsForInteger( MAX_CLIENTS ) );
|
|
player->WritePlayerStateToSnapshot( deltaMsg );
|
|
}
|
|
WriteGameStateToSnapshot( deltaMsg );
|
|
|
|
// copy the client PVS string
|
|
// RAVEN BEGIN
|
|
// JSinger: Changed to call optimized memcpy
|
|
// jnewquist: Use dword array to match pvs array so we don't have endianness problems.
|
|
const int numDwords = ( numPVSClients + 31 ) >> 5;
|
|
for ( i = 0; i < numDwords; i++ ) {
|
|
clientInPVS[i] = snapshot->pvs[i];
|
|
}
|
|
// RAVEN END
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ServerWriteServerDemoSnapshot
|
|
===============
|
|
*/
|
|
void idGameLocal::ServerWriteServerDemoSnapshot( int sequence, idBitMsg &msg, int lastSnapshotFrame ) {
|
|
snapshot_t *snapshot;
|
|
int i, msgSize, msgWriteBit;
|
|
idEntity *ent;
|
|
entityState_t *base, *newBase;
|
|
idBitMsgDelta deltaMsg;
|
|
|
|
bool ret = ServerApplySnapshot( MAX_CLIENTS, sequence - 1 );
|
|
ret = ret; // silence warning
|
|
assert( ret || sequence == 1 ); // past the first snapshot of the server demo stream, there's always exactly one to clear
|
|
|
|
snapshot = snapshotAllocator.Alloc();
|
|
snapshot->sequence = sequence;
|
|
snapshot->firstEntityState = NULL;
|
|
snapshot->next = clientSnapshots[ MAX_CLIENTS ];
|
|
clientSnapshots[ MAX_CLIENTS ] = snapshot;
|
|
|
|
unreliableMessages[ MAX_CLIENTS ].FlushTo( msg );
|
|
|
|
for ( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
|
|
if ( !ent->fl.networkSync ) {
|
|
continue;
|
|
}
|
|
|
|
// only record instance 0 ( tourney games )
|
|
if ( ent->GetInstance() != 0 ) {
|
|
continue;
|
|
}
|
|
|
|
msg.SaveWriteState( msgSize, msgWriteBit );
|
|
|
|
msg.WriteBits( ent->entityNumber, GENTITYNUM_BITS );
|
|
|
|
base = clientEntityStates[ MAX_CLIENTS ][ ent->entityNumber ];
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = ent->entityNumber;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
|
|
deltaMsg.InitWriting( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
deltaMsg.WriteBits( spawnIds[ ent->entityNumber ], 32 - GENTITYNUM_BITS );
|
|
assert( ent->entityDefNumber > 0 );
|
|
deltaMsg.WriteBits( ent->entityDefNumber, entityDefBits );
|
|
|
|
ent->WriteToSnapshot( deltaMsg );
|
|
|
|
if ( !deltaMsg.HasChanged() ) {
|
|
msg.RestoreWriteState( msgSize, msgWriteBit );
|
|
entityStateAllocator.Free( newBase );
|
|
} else {
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
}
|
|
}
|
|
msg.WriteBits( ENTITYNUM_NONE, GENTITYNUM_BITS );
|
|
|
|
// write player states and game states
|
|
base = clientEntityStates[MAX_CLIENTS][ENTITYNUM_NONE]; // ENTITYNUM_NONE is used for the game and player state
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = ENTITYNUM_NONE;
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
deltaMsg.InitWriting( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
// all the players
|
|
for ( i = 0; i < numClients; i++ ) {
|
|
if ( entities[i] ) {
|
|
assert( entities[i]->IsType( idPlayer::GetClassType() ) );
|
|
idPlayer *p = static_cast< idPlayer * >( entities[i] );
|
|
p->WritePlayerStateToSnapshot( deltaMsg );
|
|
}
|
|
}
|
|
// and the game state
|
|
WriteGameStateToSnapshot( deltaMsg );
|
|
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerApplySnapshot
|
|
================
|
|
*/
|
|
bool idGameLocal::ServerApplySnapshot( int clientNum, int sequence ) {
|
|
return ApplySnapshot( clientNum, sequence );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::NetworkEventWarning
|
|
================
|
|
*/
|
|
void idGameLocal::NetworkEventWarning( const entityNetEvent_t *event, const char *fmt, ... ) {
|
|
char buf[1024];
|
|
int length = 0;
|
|
va_list argptr;
|
|
|
|
int entityNum = event->spawnId & ( ( 1 << GENTITYNUM_BITS ) - 1 );
|
|
int id = event->spawnId >> GENTITYNUM_BITS;
|
|
|
|
length += idStr::snPrintf( buf+length, sizeof(buf)-1-length, "event %d for entity %d %d: ", event->event, entityNum, id );
|
|
va_start( argptr, fmt );
|
|
length = idStr::vsnPrintf( buf+length, sizeof(buf)-1-length, fmt, argptr );
|
|
va_end( argptr );
|
|
idStr::Append( buf, sizeof(buf), "\n" );
|
|
|
|
common->DWarning( buf );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerProcessEntityNetworkEventQueue
|
|
================
|
|
*/
|
|
void idGameLocal::ServerProcessEntityNetworkEventQueue( void ) {
|
|
idEntity *ent;
|
|
entityNetEvent_t *event;
|
|
idBitMsg eventMsg;
|
|
|
|
while ( eventQueue.Start() ) {
|
|
event = eventQueue.Start();
|
|
|
|
if ( event->time > time ) {
|
|
break;
|
|
}
|
|
|
|
idEntityPtr< idEntity > entPtr;
|
|
|
|
if( !entPtr.SetSpawnId( event->spawnId ) ) {
|
|
NetworkEventWarning( event, "Entity does not exist any longer, or has not been spawned yet." );
|
|
} else {
|
|
ent = entPtr.GetEntity();
|
|
assert( ent );
|
|
|
|
eventMsg.Init( event->paramsBuf, sizeof( event->paramsBuf ) );
|
|
eventMsg.SetSize( event->paramsSize );
|
|
eventMsg.BeginReading();
|
|
if ( !ent->ServerReceiveEvent( event->event, event->time, eventMsg ) ) {
|
|
NetworkEventWarning( event, "unknown event" );
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
entityNetEvent_t* freedEvent = eventQueue.Dequeue();
|
|
assert( freedEvent == event );
|
|
#else
|
|
eventQueue.Dequeue();
|
|
#endif
|
|
eventQueue.Free( event );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerSendChatMessage
|
|
================
|
|
*/
|
|
void idGameLocal::ServerSendChatMessage( int to, const char *name, const char *text, const char *parm ) {
|
|
idBitMsg outMsg;
|
|
byte msgBuf[ MAX_GAME_MESSAGE_SIZE ];
|
|
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.BeginWriting();
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_CHAT );
|
|
outMsg.WriteString( name );
|
|
outMsg.WriteString( text );
|
|
outMsg.WriteString( parm );
|
|
networkSystem->ServerSendReliableMessage( to, outMsg );
|
|
|
|
if ( to == -1 || to == localClientNum ) {
|
|
idStr temp = va( "%s%s", common->GetLocalizedString( text ), parm );
|
|
mpGame.AddChatLine( "%s^0: %s", name, temp.c_str() );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerProcessReliableMessage
|
|
================
|
|
*/
|
|
void idGameLocal::ServerProcessReliableMessage( int clientNum, const idBitMsg &msg ) {
|
|
int id;
|
|
|
|
id = msg.ReadByte();
|
|
switch( id ) {
|
|
case GAME_RELIABLE_MESSAGE_CHAT:
|
|
case GAME_RELIABLE_MESSAGE_TCHAT: {
|
|
char name[128];
|
|
char text[128];
|
|
char parm[128];
|
|
|
|
msg.ReadString( name, sizeof( name ) );
|
|
msg.ReadString( text, sizeof( text ) );
|
|
// This parameter is ignored - it is only used when going to client from server
|
|
msg.ReadString( parm, sizeof( parm ) );
|
|
|
|
mpGame.ProcessChatMessage( clientNum, id == GAME_RELIABLE_MESSAGE_TCHAT, name, text, NULL );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_VCHAT: {
|
|
int index = msg.ReadLong();
|
|
bool team = msg.ReadBits( 1 ) != 0;
|
|
mpGame.ProcessVoiceChat( clientNum, team, index );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_KILL: {
|
|
mpGame.WantKilled( clientNum );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_DROPWEAPON: {
|
|
mpGame.DropWeapon( clientNum );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_CALLVOTE: {
|
|
mpGame.ServerCallVote( clientNum, msg );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_CASTVOTE: {
|
|
bool vote = ( msg.ReadByte() != 0 );
|
|
mpGame.CastVote( clientNum, vote );
|
|
break;
|
|
}
|
|
// RAVEN BEGIN
|
|
// shouchard: multivalue votes
|
|
case GAME_RELIABLE_MESSAGE_CALLPACKEDVOTE: {
|
|
mpGame.ServerCallPackedVote( clientNum, msg );
|
|
break;
|
|
}
|
|
// RAVEN END
|
|
#if 0
|
|
// uncomment this if you want to track when players are in a menu
|
|
case GAME_RELIABLE_MESSAGE_MENU: {
|
|
bool menuUp = ( msg.ReadBits( 1 ) != 0 );
|
|
break;
|
|
}
|
|
#endif
|
|
case GAME_RELIABLE_MESSAGE_EVENT: {
|
|
entityNetEvent_t *event;
|
|
|
|
// allocate new event
|
|
event = eventQueue.Alloc();
|
|
eventQueue.Enqueue( event, idEventQueue::OUTOFORDER_DROP );
|
|
|
|
event->spawnId = msg.ReadBits( 32 );
|
|
event->event = msg.ReadByte();
|
|
event->time = msg.ReadLong();
|
|
|
|
event->paramsSize = msg.ReadBits( idMath::BitsForInteger( MAX_EVENT_PARAM_SIZE ) );
|
|
if ( event->paramsSize ) {
|
|
if ( event->paramsSize > MAX_EVENT_PARAM_SIZE ) {
|
|
NetworkEventWarning( event, "invalid param size" );
|
|
return;
|
|
}
|
|
msg.ReadByteAlign();
|
|
msg.ReadData( event->paramsBuf, event->paramsSize );
|
|
}
|
|
break;
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// jscott: voice comms
|
|
case GAME_RELIABLE_MESSAGE_VOICEDATA_CLIENT:
|
|
case GAME_RELIABLE_MESSAGE_VOICEDATA_CLIENT_ECHO:
|
|
case GAME_RELIABLE_MESSAGE_VOICEDATA_CLIENT_TEST:
|
|
case GAME_RELIABLE_MESSAGE_VOICEDATA_CLIENT_ECHO_TEST: {
|
|
mpGame.ReceiveAndForwardVoiceData( clientNum, msg, id - GAME_RELIABLE_MESSAGE_VOICEDATA_CLIENT );
|
|
break;
|
|
}
|
|
// ddynerman: stats
|
|
case GAME_RELIABLE_MESSAGE_STAT: {
|
|
int client = msg.ReadByte();
|
|
statManager->SendStat( clientNum, client );
|
|
break;
|
|
}
|
|
// shouchard: voice chat
|
|
case GAME_RELIABLE_MESSAGE_VOICECHAT_MUTING: {
|
|
int clientDest = msg.ReadByte();
|
|
bool mute = ( 0 != msg.ReadByte() );
|
|
mpGame.ServerHandleVoiceMuting( clientNum, clientDest, mute );
|
|
break;
|
|
}
|
|
// shouchard: server admin
|
|
case GAME_RELIABLE_MESSAGE_SERVER_ADMIN: {
|
|
int commandType = msg.ReadByte();
|
|
int clientNum = msg.ReadByte();
|
|
if ( SERVER_ADMIN_REMOVE_BAN == commandType ) {
|
|
mpGame.HandleServerAdminRemoveBan( "" );
|
|
} else if ( SERVER_ADMIN_KICK == commandType ) {
|
|
mpGame.HandleServerAdminKickPlayer( clientNum );
|
|
} else if ( SERVER_ADMIN_FORCE_SWITCH == commandType ) {
|
|
mpGame.HandleServerAdminForceTeamSwitch( clientNum );
|
|
} else {
|
|
Warning( "Server admin packet with bad type %d", commandType );
|
|
}
|
|
break;
|
|
}
|
|
// mekberg: get ban list for server
|
|
case GAME_RELIABLE_MESSAGE_GETADMINBANLIST: {
|
|
ServerSendBanList( clientNum );
|
|
break;
|
|
}
|
|
// RAVEN END
|
|
|
|
default: {
|
|
Warning( "Unknown client->server reliable message: %d", id );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientShowSnapshot
|
|
================
|
|
*/
|
|
void idGameLocal::ClientShowSnapshot( int clientNum ) const {
|
|
int baseBits;
|
|
idEntity *ent;
|
|
idPlayer *player;
|
|
idMat3 viewAxis;
|
|
idBounds viewBounds;
|
|
entityState_t *base;
|
|
|
|
if ( !net_clientShowSnapshot.GetInteger() ) {
|
|
return;
|
|
}
|
|
|
|
player = static_cast<idPlayer *>( entities[clientNum] );
|
|
if ( !player ) {
|
|
return;
|
|
}
|
|
|
|
viewAxis = player->viewAngles.ToMat3();
|
|
viewBounds = player->GetPhysics()->GetAbsBounds().Expand( net_clientShowSnapshotRadius.GetFloat() );
|
|
|
|
for( ent = snapshotEntities.Next(); ent != NULL; ent = ent->snapshotNode.Next() ) {
|
|
|
|
if ( net_clientShowSnapshot.GetInteger() == 1 && ent->snapshotBits == 0 ) {
|
|
continue;
|
|
}
|
|
|
|
const idBounds &entBounds = ent->GetPhysics()->GetAbsBounds();
|
|
|
|
if ( !entBounds.IntersectsBounds( viewBounds ) ) {
|
|
continue;
|
|
}
|
|
|
|
base = clientEntityStates[clientNum][ent->entityNumber];
|
|
if ( base ) {
|
|
baseBits = base->state.GetNumBitsWritten();
|
|
} else {
|
|
baseBits = 0;
|
|
}
|
|
|
|
if ( net_clientShowSnapshot.GetInteger() == 2 && baseBits == 0 ) {
|
|
continue;
|
|
}
|
|
|
|
gameRenderWorld->DebugBounds( colorGreen, entBounds );
|
|
gameRenderWorld->DrawText( va( "%d: %s (%d,%d bytes of %d,%d)\n", ent->entityNumber,
|
|
ent->name.c_str(), ent->snapshotBits >> 3, ent->snapshotBits & 7, baseBits >> 3, baseBits & 7 ),
|
|
entBounds.GetCenter(), 0.1f, colorWhite, viewAxis, 1 );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::UpdateLagometer
|
|
================
|
|
*/
|
|
void idGameLocal::UpdateLagometer( int aheadOfServer, int dupeUsercmds ) {
|
|
int i, j, ahead;
|
|
for ( i = 0; i < LAGO_HEIGHT; i++ ) {
|
|
memmove( (byte *)lagometer + LAGO_WIDTH * 4 * i, (byte *)lagometer + LAGO_WIDTH * 4 * i + 4, ( LAGO_WIDTH - 1 ) * 4 );
|
|
}
|
|
j = LAGO_WIDTH - 1;
|
|
for ( i = 0; i < LAGO_HEIGHT; i++ ) {
|
|
lagometer[i][j][0] = lagometer[i][j][1] = lagometer[i][j][2] = lagometer[i][j][3] = 0;
|
|
}
|
|
ahead = idMath::Rint( (float)aheadOfServer / 16.0f );
|
|
if ( ahead >= 0 ) {
|
|
for ( i = 2 * Max( 0, 5 - ahead ); i < 2 * 5; i++ ) {
|
|
lagometer[i][j][1] = 255;
|
|
lagometer[i][j][3] = 255;
|
|
}
|
|
} else {
|
|
for ( i = 2 * 5; i < 2 * ( 5 + Min( 10, -ahead ) ); i++ ) {
|
|
lagometer[i][j][0] = 255;
|
|
lagometer[i][j][1] = 255;
|
|
lagometer[i][j][3] = 255;
|
|
}
|
|
}
|
|
for ( i = LAGO_HEIGHT - 2 * Min( 6, dupeUsercmds ); i < LAGO_HEIGHT; i++ ) {
|
|
lagometer[i][j][0] = 255;
|
|
lagometer[i][j][1] = 255;
|
|
lagometer[i][j][3] = 255;
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientReadSnapshot
|
|
================
|
|
*/
|
|
void idGameLocal::ClientReadSnapshot( int clientNum, int snapshotSequence, const int gameFrame, const int gameTime, const int dupeUsercmds, const int aheadOfServer, const idBitMsg &msg ) {
|
|
int i, entityDefNumber, numBitsRead;
|
|
idEntity *ent;
|
|
idPlayer *player, *spectated;
|
|
pvsHandle_t pvsHandle;
|
|
idDict args;
|
|
idBitMsgDelta deltaMsg;
|
|
snapshot_t *snapshot;
|
|
entityState_t *base, *newBase;
|
|
int spawnId;
|
|
int numSourceAreas, sourceAreas[ idEntity::MAX_PVS_AREAS ];
|
|
|
|
int proto69TypeNum = 0;
|
|
bool proto69 = ( gameLocal.GetCurrentDemoProtocol() == 69 );
|
|
|
|
const idDeclEntityDef *decl;
|
|
|
|
if ( net_clientLagOMeter.GetBool() && renderSystem && !IsServerDemo() ) {
|
|
UpdateLagometer( aheadOfServer, dupeUsercmds );
|
|
if ( !renderSystem->UploadImage( LAGO_IMAGE, (byte *)lagometer, LAGO_IMG_WIDTH, LAGO_IMG_HEIGHT ) ) {
|
|
common->Printf( "lagometer: UploadImage failed. turning off net_clientLagOMeter\n" );
|
|
net_clientLagOMeter.SetBool( false );
|
|
}
|
|
}
|
|
|
|
InitLocalClient( clientNum );
|
|
|
|
gameRenderWorld->DebugClear( time );
|
|
|
|
// update the game time
|
|
framenum = gameFrame;
|
|
time = gameTime;
|
|
// RAVEN BEGIN
|
|
// bdube: use GetMSec access rather than USERCMD_TIME
|
|
previousTime = time - GetMSec();
|
|
// RAVEN END
|
|
|
|
// so that StartSound/StopSound doesn't risk skipping
|
|
isNewFrame = true;
|
|
|
|
// clear the snapshot entity list
|
|
snapshotEntities.Clear();
|
|
|
|
// allocate new snapshot
|
|
snapshot = snapshotAllocator.Alloc();
|
|
snapshot->sequence = snapshotSequence;
|
|
snapshot->firstEntityState = NULL;
|
|
snapshot->next = clientSnapshots[clientNum];
|
|
clientSnapshots[clientNum] = snapshot;
|
|
|
|
#if ASYNC_WRITE_TAGS
|
|
idRandom tagRandom;
|
|
tagRandom.SetSeed( msg.ReadLong() );
|
|
#endif
|
|
|
|
ClientReadUnreliableMessages( msg );
|
|
|
|
#if ASYNC_WRITE_TAGS
|
|
if ( msg.ReadLong() != tagRandom.RandomInt() ) {
|
|
Error( "error after read unreliable" );
|
|
}
|
|
#endif
|
|
|
|
// read all entities from the snapshot
|
|
for ( i = msg.ReadBits( GENTITYNUM_BITS ); i != ENTITYNUM_NONE; i = msg.ReadBits( GENTITYNUM_BITS ) ) {
|
|
base = clientEntityStates[clientNum][i];
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = i;
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
|
|
numBitsRead = msg.GetNumBitsRead();
|
|
|
|
deltaMsg.InitReading( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
spawnId = deltaMsg.ReadBits( 32 - GENTITYNUM_BITS );
|
|
if ( proto69 ) {
|
|
proto69TypeNum = deltaMsg.ReadBits( idClass::GetTypeNumBits() );
|
|
}
|
|
entityDefNumber = deltaMsg.ReadBits( entityDefBits );
|
|
|
|
ent = entities[i];
|
|
|
|
// if there is no entity or an entity of the wrong type
|
|
if ( !ent || ent->entityDefNumber != entityDefNumber || spawnId != spawnIds[ i ] ) {
|
|
|
|
if ( i < MAX_CLIENTS && ent ) {
|
|
// SPAWN_PLAYER should be taking care of spawning the entity with the right spawnId
|
|
common->Warning( "ClientReadSnapshot: recycling client entity %d\n", i );
|
|
}
|
|
|
|
delete ent;
|
|
|
|
spawnCount = spawnId;
|
|
|
|
args.Clear();
|
|
args.SetInt( "spawn_entnum", i );
|
|
args.Set( "name", va( "entity%d", i ) );
|
|
|
|
// assume any items spawned from a server-snapshot are in our instance
|
|
if ( gameLocal.GetLocalPlayer() ) {
|
|
args.SetInt( "instance", gameLocal.GetLocalPlayer()->GetInstance() );
|
|
}
|
|
|
|
if ( entityDefNumber >= 0 ) {
|
|
if ( entityDefNumber >= declManager->GetNumDecls( DECL_ENTITYDEF ) ) {
|
|
Error( "server has %d entityDefs instead of %d", entityDefNumber, declManager->GetNumDecls( DECL_ENTITYDEF ) );
|
|
}
|
|
decl = static_cast< const idDeclEntityDef * >( declManager->DeclByIndex( DECL_ENTITYDEF, entityDefNumber, false ) );
|
|
assert( decl && decl->GetType() == DECL_ENTITYDEF );
|
|
args.Set( "classname", decl->GetName() );
|
|
if ( !SpawnEntityDef( args, &ent ) || !entities[i] ) {
|
|
Error( "Failed to spawn entity with classname '%s' of type '%s'", decl->GetName(), decl->dict.GetString( "spawnclass" ) );
|
|
}
|
|
} else {
|
|
// we no longer support spawning entities by type num only. we would only hit this when playing 1.2 demos for backward compatibility
|
|
assert( proto69 );
|
|
switch ( proto69TypeNum ) {
|
|
case 183:
|
|
ent = SpawnEntityType( rvViewWeapon::GetClassType(), &args, true );
|
|
break;
|
|
case 182:
|
|
ent = SpawnEntityType( idAnimatedEntity::GetClassType(), &args, true );
|
|
ent->fl.networkSync = true;
|
|
break;
|
|
default:
|
|
Error( "Unexpected protocol 69 typenum (%d) for spawning entity by type", proto69TypeNum );
|
|
}
|
|
if ( !entities[i] ) {
|
|
Error( "Failed to spawn entity by typenum %d ( protocol 69 backwards compat )", proto69TypeNum );
|
|
}
|
|
}
|
|
if ( i < MAX_CLIENTS && i >= numClients ) {
|
|
numClients = i + 1;
|
|
}
|
|
}
|
|
|
|
// add the entity to the snapshot list
|
|
ent->snapshotNode.AddToEnd( snapshotEntities );
|
|
ent->snapshotSequence = snapshotSequence;
|
|
|
|
// RAVEN BEGIN
|
|
// bdube: stale network entities
|
|
// Ensure the clipmodel is relinked when transitioning from state
|
|
if ( ent->fl.networkStale ) {
|
|
ent->GetPhysics()->LinkClip();
|
|
}
|
|
// RAVEN END
|
|
|
|
// read the class specific data from the snapshot
|
|
ent->ReadFromSnapshot( deltaMsg );
|
|
|
|
// once we read new snapshot data, unstale the ent
|
|
if( ent->fl.networkStale ) {
|
|
ent->ClientUnstale();
|
|
ent->fl.networkStale = false;
|
|
}
|
|
ent->snapshotBits = msg.GetNumBitsRead() - numBitsRead;
|
|
|
|
#if ASYNC_WRITE_TAGS
|
|
if ( msg.ReadLong() != tagRandom.RandomInt() ) {
|
|
//cmdSystem->BufferCommandText( CMD_EXEC_NOW, "writeGameState" );
|
|
assert( entityDefNumber >= 0 );
|
|
assert( entityDefNumber < declManager->GetNumDecls( DECL_ENTITYDEF ) );
|
|
const char * classname = declManager->DeclByIndex( DECL_ENTITYDEF, entityDefNumber, false )->GetName();
|
|
Error( "write to and read from snapshot out of sync for classname '%s'\n", classname );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
player = static_cast<idPlayer *>( entities[clientNum] );
|
|
if ( !player ) {
|
|
return;
|
|
}
|
|
|
|
if ( player->spectating && player->spectator != clientNum && entities[ player->spectator ] ) {
|
|
spectated = static_cast< idPlayer * >( entities[ player->spectator ] );
|
|
} else {
|
|
spectated = player;
|
|
}
|
|
|
|
// get PVS for this player
|
|
// don't use PVSAreas for networking - PVSAreas depends on animations (and md5 bounds), which are not synchronized
|
|
numSourceAreas = gameRenderWorld->BoundsInAreas( spectated->GetPlayerPhysics()->GetAbsBounds(), sourceAreas, idEntity::MAX_PVS_AREAS );
|
|
pvsHandle = gameLocal.pvs.SetupCurrentPVS( sourceAreas, numSourceAreas, PVS_NORMAL );
|
|
|
|
// read the PVS from the snapshot
|
|
#if ASYNC_WRITE_PVS
|
|
int serverPVS[idEntity::MAX_PVS_AREAS];
|
|
i = numSourceAreas;
|
|
while ( i < idEntity::MAX_PVS_AREAS ) {
|
|
sourceAreas[ i++ ] = 0;
|
|
}
|
|
for ( i = 0; i < idEntity::MAX_PVS_AREAS; i++ ) {
|
|
serverPVS[ i ] = msg.ReadLong();
|
|
}
|
|
if ( memcmp( sourceAreas, serverPVS, idEntity::MAX_PVS_AREAS * sizeof( int ) ) ) {
|
|
common->Warning( "client PVS areas != server PVS areas, sequence 0x%x", snapshotSequence );
|
|
for ( i = 0; i < idEntity::MAX_PVS_AREAS; i++ ) {
|
|
common->DPrintf( "%3d ", sourceAreas[ i ] );
|
|
}
|
|
common->DPrintf( "\n" );
|
|
for ( i = 0; i < idEntity::MAX_PVS_AREAS; i++ ) {
|
|
common->DPrintf( "%3d ", serverPVS[ i ] );
|
|
}
|
|
common->DPrintf( "\n" );
|
|
}
|
|
gameLocal.pvs.ReadPVS( pvsHandle, msg );
|
|
#endif
|
|
for ( i = 0; i < ENTITY_PVS_SIZE; i++ ) {
|
|
snapshot->pvs[i] = msg.ReadDeltaLong( clientPVS[clientNum][i] );
|
|
}
|
|
// RAVEN BEGIN
|
|
// ddynerman: performance profiling
|
|
net_entsInSnapshot += snapshotEntities.Num();
|
|
net_snapshotSize += msg.GetSize();
|
|
// RAVEN END
|
|
// add entities in the PVS that haven't changed since the last applied snapshot
|
|
idEntity *nextSpawnedEnt;
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = nextSpawnedEnt ) {
|
|
nextSpawnedEnt = ent->spawnNode.Next();
|
|
|
|
// if the entity is already in the snapshot
|
|
if ( ent->snapshotSequence == snapshotSequence ) {
|
|
continue;
|
|
}
|
|
|
|
// if the entity is not in the snapshot PVS
|
|
if ( !( snapshot->pvs[ent->entityNumber >> 5] & ( 1 << ( ent->entityNumber & 31 ) ) ) ) {
|
|
|
|
if ( !ent->fl.networkSync ) {
|
|
// don't do stale / unstale on entities that are not marked network sync
|
|
continue;
|
|
}
|
|
|
|
if ( ent->PhysicsTeamInPVS( pvsHandle ) ) {
|
|
if ( ent->entityNumber >= MAX_CLIENTS && isMapEntity[ ent->entityNumber ] ) {
|
|
// server says it's not in PVS, client says it's in PVS
|
|
// if that happens on map entities, most likely something is wrong
|
|
// I can see that moving pieces along several PVS could be a legit situation though
|
|
// this is a band aid, which means something is not done right elsewhere
|
|
if ( net_warnStale.GetInteger() > 1 || ( net_warnStale.GetInteger() == 1 && !ent->fl.networkStale ) ) {
|
|
common->Warning( "client thinks map entity 0x%x (%s) is stale, sequence 0x%x", ent->entityNumber, ent->name.c_str(), snapshotSequence );
|
|
}
|
|
}
|
|
// RAVEN BEGIN
|
|
// bdube: hide while not in snapshot
|
|
if ( !ent->fl.networkStale ) {
|
|
if ( ent->ClientStale() ) {
|
|
delete ent;
|
|
ent = NULL;
|
|
} else {
|
|
ent->fl.networkStale = true;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if ( !ent->fl.networkStale ) {
|
|
if ( ent->ClientStale() ) {
|
|
delete ent;
|
|
ent = NULL;
|
|
} else {
|
|
ent->fl.networkStale = true;
|
|
}
|
|
}
|
|
}
|
|
// RAVEN END
|
|
|
|
continue;
|
|
}
|
|
|
|
// add the entity to the snapshot list
|
|
ent->snapshotNode.AddToEnd( snapshotEntities );
|
|
ent->snapshotSequence = snapshotSequence;
|
|
ent->snapshotBits = 0;
|
|
|
|
// RAVEN BEGIN
|
|
// bdube: hide while not in snapshot
|
|
// Ensure the clipmodel is relinked when transitioning from state
|
|
if ( ent->fl.networkStale ) {
|
|
ent->GetPhysics()->LinkClip();
|
|
}
|
|
// RAVEN END
|
|
|
|
base = clientEntityStates[clientNum][ent->entityNumber];
|
|
if ( !base ) {
|
|
// entity has probably fl.networkSync set to false
|
|
// non netsynced map entities go in and out of PVS, and may need stale/unstale calls
|
|
if ( ent->fl.networkStale ) {
|
|
ent->ClientUnstale();
|
|
ent->fl.networkStale = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ( !ent->fl.networkSync ) {
|
|
// this is not supposed to happen
|
|
// it did however, when restarting a map with a different inhibit of entities caused entity numbers to be laid differently
|
|
// an idLight would occupy the entity number of an idItem for instance, and although it's not network-synced ( static level light ),
|
|
// the presence of a base would cause the system to think that it is and corrupt things
|
|
// we changed the map population so the entity numbers are kept the same no matter how things are inhibited
|
|
// this code is left as a fall-through fixup / sanity type of thing
|
|
// if this still happens, it's likely "client thinks map entity is stale" is happening as well, and we're still at risk of corruption
|
|
Warning( "ClientReadSnapshot: entity %d of type %s is not networkSync and has a snapshot base", ent->entityNumber, ent->GetType()->classname );
|
|
entityStateAllocator.Free( clientEntityStates[clientNum][ent->entityNumber] );
|
|
clientEntityStates[clientNum][ent->entityNumber] = NULL;
|
|
continue;
|
|
}
|
|
|
|
base->state.BeginReading();
|
|
|
|
deltaMsg.InitReading( &base->state, NULL, (const idBitMsg *)NULL );
|
|
spawnId = deltaMsg.ReadBits( 32 - GENTITYNUM_BITS );
|
|
if ( proto69 ) {
|
|
deltaMsg.ReadBits( idClass::GetTypeNumBits() );
|
|
}
|
|
entityDefNumber = deltaMsg.ReadBits( entityDefBits );
|
|
|
|
// read the class specific data from the base state
|
|
ent->ReadFromSnapshot( deltaMsg );
|
|
|
|
// after snapshot read, notify client of unstale
|
|
if ( ent->fl.networkStale ) {
|
|
ent->ClientUnstale();
|
|
ent->fl.networkStale = false;
|
|
}
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// ddynerman: add the ambient lights to the snapshot entities
|
|
for( int i = 0; i < ambientLights.Num(); i++ ) {
|
|
ambientLights[ i ]->snapshotNode.AddToEnd( snapshotEntities );
|
|
ambientLights[ i ]->fl.networkStale = false;
|
|
}
|
|
// RAVEN END
|
|
|
|
// free the PVS
|
|
pvs.FreeCurrentPVS( pvsHandle );
|
|
|
|
// read the game and player state from the snapshot
|
|
base = clientEntityStates[clientNum][ENTITYNUM_NONE]; // ENTITYNUM_NONE is used for the game and player state
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = ENTITYNUM_NONE;
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
deltaMsg.InitReading( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
int targetPlayer = deltaMsg.ReadBits( idMath::BitsForInteger( MAX_CLIENTS ) );
|
|
if ( entities[ targetPlayer ] ) {
|
|
static_cast< idPlayer* >( entities[ targetPlayer ] )->ReadPlayerStateFromSnapshot( deltaMsg );
|
|
} else {
|
|
player->ReadPlayerStateFromSnapshot( deltaMsg );
|
|
}
|
|
|
|
ReadGameStateFromSnapshot( deltaMsg );
|
|
|
|
// visualize the snapshot
|
|
ClientShowSnapshot( clientNum );
|
|
|
|
// process entity events
|
|
ClientProcessEntityNetworkEventQueue();
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ClientReadServerDemoSnapshot
|
|
|
|
server demos use a slightly different snapshot format
|
|
mostly, we don't need to transmit any PVS visibility information, as we transmit the whole entity activity
|
|
plus, we read that data to the virtual 'seeing it all' MAX_CLIENTS client
|
|
|
|
===============
|
|
*/
|
|
void idGameLocal::ClientReadServerDemoSnapshot( int sequence, const int gameFrame, const int gameTime, const idBitMsg &msg ) {
|
|
int i;
|
|
snapshot_t *snapshot;
|
|
entityState_t *base, *newBase;
|
|
idBitMsgDelta deltaMsg;
|
|
int numBitsRead, spawnId, entityDefNumber;
|
|
idEntity *ent;
|
|
idDict args;
|
|
const idDeclEntityDef *decl;
|
|
|
|
int proto69TypeNum = 0;
|
|
bool proto69 = ( gameLocal.GetCurrentDemoProtocol() == 69 );
|
|
|
|
bool ret = ClientApplySnapshot( MAX_CLIENTS, sequence - 1 );
|
|
ret = ret; // silence warning
|
|
assert( ret || sequence == 1 ); // past the first snapshot of the server demo stream, there's always exactly one to clear
|
|
|
|
gameRenderWorld->DebugClear( time );
|
|
|
|
framenum = gameFrame;
|
|
time = gameTime;
|
|
previousTime = time - GetMSec();
|
|
|
|
isNewFrame = true;
|
|
|
|
snapshotEntities.Clear();
|
|
|
|
snapshot = snapshotAllocator.Alloc();
|
|
snapshot->sequence = sequence;
|
|
snapshot->firstEntityState = NULL;
|
|
snapshot->next = clientSnapshots[ MAX_CLIENTS ];
|
|
clientSnapshots[ MAX_CLIENTS ] = snapshot;
|
|
|
|
ClientReadUnreliableMessages( msg );
|
|
|
|
for ( i = msg.ReadBits( GENTITYNUM_BITS ); i != ENTITYNUM_NONE; i = msg.ReadBits( GENTITYNUM_BITS ) ) {
|
|
|
|
base = clientEntityStates[ MAX_CLIENTS ][i];
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = i;
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
|
|
numBitsRead = msg.GetNumBitsRead();
|
|
|
|
deltaMsg.InitReading( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
spawnId = deltaMsg.ReadBits( 32 - GENTITYNUM_BITS );
|
|
if ( proto69 ) {
|
|
proto69TypeNum = deltaMsg.ReadBits( idClass::GetTypeNumBits() );
|
|
}
|
|
entityDefNumber = deltaMsg.ReadBits( entityDefBits );
|
|
|
|
ent = entities[i];
|
|
|
|
// if there is no entity or an entity of the wrong type
|
|
if ( !ent || ent->entityDefNumber != entityDefNumber || spawnId != spawnIds[ i ] ) {
|
|
|
|
if ( i < MAX_CLIENTS && ent ) {
|
|
// SpawnPlayer should be taking care of spawning the entity with the right spawnId
|
|
common->Warning( "ClientReadServerDemoSnapshot: recycling client entity %d\n", i );
|
|
}
|
|
|
|
delete ent;
|
|
|
|
spawnCount = spawnId;
|
|
|
|
args.Clear();
|
|
args.SetInt( "spawn_entnum", i );
|
|
args.Set( "name", va( "entity%d", i ) );
|
|
|
|
// assume any items spawned from a server-snapshot are in our instance
|
|
// FIXME: hu ho gonna have to rework that for server demos
|
|
if ( gameLocal.GetLocalPlayer() ) {
|
|
args.SetInt( "instance", gameLocal.GetLocalPlayer()->GetInstance() );
|
|
}
|
|
|
|
if ( entityDefNumber >= 0 ) {
|
|
if ( entityDefNumber >= declManager->GetNumDecls( DECL_ENTITYDEF ) ) {
|
|
Error( "server has %d entityDefs instead of %d", entityDefNumber, declManager->GetNumDecls( DECL_ENTITYDEF ) );
|
|
}
|
|
decl = static_cast< const idDeclEntityDef * >( declManager->DeclByIndex( DECL_ENTITYDEF, entityDefNumber, false ) );
|
|
assert( decl && decl->GetType() == DECL_ENTITYDEF );
|
|
args.Set( "classname", decl->GetName() );
|
|
if ( !SpawnEntityDef( args, &ent ) || !entities[i] ) {
|
|
Error( "Failed to spawn entity with classname '%s' of type '%s'", decl->GetName(), decl->dict.GetString( "spawnclass" ) );
|
|
}
|
|
} else {
|
|
// we no longer support spawning entities by type num only. we would only hit this when playing 1.2 demos for backward compatibility
|
|
assert( proto69 );
|
|
switch ( proto69TypeNum ) {
|
|
case 183:
|
|
ent = SpawnEntityType( rvViewWeapon::GetClassType(), &args, true );
|
|
break;
|
|
case 182:
|
|
ent = SpawnEntityType( idAnimatedEntity::GetClassType(), &args, true );
|
|
ent->fl.networkSync = true;
|
|
break;
|
|
default:
|
|
Error( "Unexpected protocol 69 typenum (%d) for spawning entity by type", proto69TypeNum );
|
|
}
|
|
if ( !entities[i] ) {
|
|
Error( "Failed to spawn entity by typenum %d ( protocol 69 backwards compat )", proto69TypeNum );
|
|
}
|
|
}
|
|
if ( i < MAX_CLIENTS && i >= numClients ) {
|
|
numClients = i + 1;
|
|
}
|
|
}
|
|
|
|
// add the entity to the snapshot list
|
|
ent->snapshotNode.AddToEnd( snapshotEntities );
|
|
ent->snapshotSequence = sequence;
|
|
|
|
// RAVEN BEGIN
|
|
// bdube: stale network entities
|
|
// Ensure the clipmodel is relinked when transitioning from state
|
|
if ( ent->fl.networkStale ) {
|
|
ent->GetPhysics()->LinkClip();
|
|
}
|
|
// RAVEN END
|
|
|
|
// read the class specific data from the snapshot
|
|
ent->ReadFromSnapshot( deltaMsg );
|
|
|
|
// once we read new snapshot data, unstale the ent
|
|
if( ent->fl.networkStale ) {
|
|
ent->ClientUnstale();
|
|
ent->fl.networkStale = false;
|
|
}
|
|
ent->snapshotBits = msg.GetNumBitsRead() - numBitsRead;
|
|
|
|
}
|
|
|
|
// add entities that haven't changed since the last applied snapshot
|
|
idEntity *nextSpawnedEnt;
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = nextSpawnedEnt ) {
|
|
nextSpawnedEnt = ent->spawnNode.Next();
|
|
|
|
// if the entity is already in the snapshot
|
|
if ( ent->snapshotSequence == sequence ) {
|
|
continue;
|
|
}
|
|
|
|
// add the entity to the snapshot list
|
|
ent->snapshotNode.AddToEnd( snapshotEntities );
|
|
ent->snapshotSequence = sequence;
|
|
ent->snapshotBits = 0;
|
|
|
|
// Ensure the clipmodel is relinked when transitioning from stale
|
|
if ( ent->fl.networkStale ) {
|
|
ent->GetPhysics()->LinkClip();
|
|
}
|
|
|
|
base = clientEntityStates[ MAX_CLIENTS ][ ent->entityNumber ];
|
|
if ( !base ) {
|
|
// entity has probably fl.networkSync set to false
|
|
// non netsynced map entities go in and out of PVS, and may need stale/unstale calls
|
|
if ( ent->fl.networkStale ) {
|
|
ent->ClientUnstale();
|
|
ent->fl.networkStale = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
base->state.BeginReading();
|
|
|
|
deltaMsg.InitReading( &base->state, NULL, (const idBitMsg *)NULL );
|
|
spawnId = deltaMsg.ReadBits( 32 - GENTITYNUM_BITS );
|
|
if ( proto69 ) {
|
|
deltaMsg.ReadBits( idClass::GetTypeNumBits() );
|
|
}
|
|
entityDefNumber = deltaMsg.ReadBits( entityDefBits );
|
|
|
|
// if the entity is not the right type
|
|
if ( ent->entityDefNumber != entityDefNumber ) {
|
|
// should never happen
|
|
common->DWarning( "entity '%s' is not the right type ( 0x%x, expected 0x%x )", ent->GetName(), ent->entityDefNumber, entityDefNumber );
|
|
continue;
|
|
}
|
|
|
|
// read the class specific data from the base state
|
|
ent->ReadFromSnapshot( deltaMsg );
|
|
|
|
// after snapshot read, notify client of unstale
|
|
if( ent->fl.networkStale ) {
|
|
// FIXME: does this happen ( in a server demo replay? )
|
|
assert( false );
|
|
ent->ClientUnstale();
|
|
ent->fl.networkStale = false;
|
|
}
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// ddynerman: add the ambient lights to the snapshot entities
|
|
for( i = 0; i < ambientLights.Num(); i++ ) {
|
|
ambientLights[ i ]->snapshotNode.AddToEnd( snapshotEntities );
|
|
ambientLights[ i ]->fl.networkStale = false;
|
|
}
|
|
// RAVEN END
|
|
|
|
// visualize the snapshot
|
|
// FIXME
|
|
// ClientShowSnapshot( MAX_CLIENTS );
|
|
|
|
// read the game and player states
|
|
base = clientEntityStates[MAX_CLIENTS][ENTITYNUM_NONE]; // ENTITYNUM_NONE is used for the game and player state
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
}
|
|
newBase = entityStateAllocator.Alloc();
|
|
newBase->entityNumber = ENTITYNUM_NONE;
|
|
newBase->next = snapshot->firstEntityState;
|
|
snapshot->firstEntityState = newBase;
|
|
newBase->state.Init( newBase->stateBuf, sizeof( newBase->stateBuf ) );
|
|
newBase->state.BeginWriting();
|
|
deltaMsg.InitReading( base ? &base->state : NULL, &newBase->state, &msg );
|
|
|
|
// all the players
|
|
for ( i = 0; i < numClients; i++ ) {
|
|
if ( entities[i] ) {
|
|
assert( entities[i]->IsType( idPlayer::GetClassType() ) );
|
|
idPlayer *p = static_cast< idPlayer * >( entities[i] );
|
|
p->ReadPlayerStateFromSnapshot( deltaMsg );
|
|
}
|
|
}
|
|
// the game state
|
|
ReadGameStateFromSnapshot( deltaMsg );
|
|
|
|
// process entity events
|
|
ClientProcessEntityNetworkEventQueue();
|
|
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientApplySnapshot
|
|
================
|
|
*/
|
|
bool idGameLocal::ClientApplySnapshot( int clientNum, int sequence ) {
|
|
return ApplySnapshot( clientNum, sequence );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientProcessEntityNetworkEventQueue
|
|
================
|
|
*/
|
|
void idGameLocal::ClientProcessEntityNetworkEventQueue( void ) {
|
|
idEntity *ent;
|
|
entityNetEvent_t *event;
|
|
idBitMsg eventMsg;
|
|
|
|
while( eventQueue.Start() ) {
|
|
event = eventQueue.Start();
|
|
|
|
// only process forward, in order
|
|
if ( event->time > time ) {
|
|
break;
|
|
}
|
|
|
|
idEntityPtr< idEntity > entPtr;
|
|
|
|
if( !entPtr.SetSpawnId( event->spawnId ) ) {
|
|
if( !gameLocal.entities[ event->spawnId & ( ( 1 << GENTITYNUM_BITS ) - 1 ) ] ) {
|
|
// if new entity exists in this position, silently ignore
|
|
NetworkEventWarning( event, "Entity does not exist any longer, or has not been spawned yet." );
|
|
}
|
|
} else {
|
|
ent = entPtr.GetEntity();
|
|
assert( ent );
|
|
|
|
eventMsg.Init( event->paramsBuf, sizeof( event->paramsBuf ) );
|
|
eventMsg.SetSize( event->paramsSize );
|
|
eventMsg.BeginReading();
|
|
if ( !ent->ClientReceiveEvent( event->event, event->time, eventMsg ) ) {
|
|
NetworkEventWarning( event, "unknown event" );
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
entityNetEvent_t* freedEvent = eventQueue.Dequeue();
|
|
assert( freedEvent == event );
|
|
#else
|
|
eventQueue.Dequeue();
|
|
#endif
|
|
eventQueue.Free( event );
|
|
}
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// bdube: client side hitscan
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientHitScan
|
|
================
|
|
*/
|
|
void idGameLocal::ClientHitScan( const idBitMsg &msg ) {
|
|
int hitscanDefIndex;
|
|
idVec3 muzzleOrigin;
|
|
idVec3 dir;
|
|
idVec3 fxOrigin;
|
|
const idDeclEntityDef *decl;
|
|
int num_hitscans;
|
|
int i;
|
|
idEntity *owner;
|
|
|
|
assert( isClient );
|
|
|
|
hitscanDefIndex = msg.ReadLong();
|
|
decl = static_cast< const idDeclEntityDef *>( declManager->DeclByIndex( DECL_ENTITYDEF, hitscanDefIndex ) );
|
|
if ( !decl ) {
|
|
common->Warning( "idGameLocal::ClientHitScan: entity def index %d not found\n", hitscanDefIndex );
|
|
return;
|
|
}
|
|
num_hitscans = decl->dict.GetInt( "hitscans", "1" );
|
|
|
|
owner = entities[ msg.ReadBits( idMath::BitsForInteger( MAX_CLIENTS ) ) ];
|
|
|
|
muzzleOrigin[0] = msg.ReadFloat();
|
|
muzzleOrigin[1] = msg.ReadFloat();
|
|
muzzleOrigin[2] = msg.ReadFloat();
|
|
fxOrigin[0] = msg.ReadFloat();
|
|
fxOrigin[1] = msg.ReadFloat();
|
|
fxOrigin[2] = msg.ReadFloat();
|
|
|
|
// one direction sent per hitscan
|
|
for( i = 0; i < num_hitscans; i++ ) {
|
|
dir = msg.ReadDir( 24 );
|
|
gameLocal.HitScan( decl->dict, muzzleOrigin, dir, fxOrigin, owner );
|
|
}
|
|
}
|
|
|
|
// RAVEN END
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientProcessReliableMessage
|
|
================
|
|
*/
|
|
void idGameLocal::ClientProcessReliableMessage( int clientNum, const idBitMsg &msg ) {
|
|
int id;
|
|
idDict backupSI;
|
|
|
|
InitLocalClient( clientNum );
|
|
|
|
if ( serverDemo ) {
|
|
assert( demoState == DEMO_PLAYING );
|
|
int record_type = msg.ReadByte();
|
|
assert( record_type < DEMO_RECORD_COUNT );
|
|
// if you need to do some special filtering:
|
|
switch ( record_type ) {
|
|
case DEMO_RECORD_CLIENTNUM: {
|
|
msg.ReadByte();
|
|
/*
|
|
int client = msg.ReadByte();
|
|
if ( client != -1 ) {
|
|
// reliable was targetted
|
|
if ( followPlayer != client ) {
|
|
// we're free flying or following someone else
|
|
return;
|
|
}
|
|
}
|
|
*/
|
|
break;
|
|
}
|
|
case DEMO_RECORD_EXCLUDE: {
|
|
int exclude = msg.ReadByte();
|
|
exclude = exclude; // silence warning
|
|
assert( exclude != -1 );
|
|
/*
|
|
if ( exclude == followPlayer ) {
|
|
return;
|
|
}
|
|
*/
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
id = msg.ReadByte();
|
|
|
|
switch( id ) {
|
|
case GAME_RELIABLE_MESSAGE_SPAWN_PLAYER: {
|
|
int client = msg.ReadByte();
|
|
int spawnId = msg.ReadLong();
|
|
if ( !entities[ client ] ) {
|
|
SpawnPlayer( client );
|
|
entities[ client ]->FreeModelDef();
|
|
}
|
|
// fix up the spawnId to match what the server says
|
|
// otherwise there is going to be a bogus delete/new of the client entity in the first ClientReadFromSnapshot
|
|
spawnIds[ client ] = spawnId;
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_DELETE_ENT: {
|
|
int spawnId = msg.ReadBits( 32 );
|
|
idEntityPtr< idEntity > entPtr;
|
|
if( !entPtr.SetSpawnId( spawnId ) ) {
|
|
break;
|
|
}
|
|
if( entPtr.GetEntity() && entPtr.GetEntity()->entityNumber < MAX_CLIENTS ) {
|
|
delete entPtr.GetEntity();
|
|
gameLocal.mpGame.UpdatePlayerRanks();
|
|
} else {
|
|
delete entPtr.GetEntity();
|
|
}
|
|
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_CHAT:
|
|
case GAME_RELIABLE_MESSAGE_TCHAT: { // (client should never get a TCHAT though)
|
|
char name[128];
|
|
char text[128];
|
|
char parm[128];
|
|
|
|
msg.ReadString( name, sizeof( name ) );
|
|
msg.ReadString( text, sizeof( text ) );
|
|
msg.ReadString( parm, sizeof( parm ) );
|
|
idStr temp = va( "%s%s", common->GetLocalizedString( text ), parm );
|
|
mpGame.AddChatLine( "%s^0: %s\n", name, temp.c_str() );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_DB: {
|
|
msg_evt_t msg_evt = (msg_evt_t)msg.ReadByte();
|
|
int parm1, parm2;
|
|
parm1 = msg.ReadByte( );
|
|
parm2 = msg.ReadByte( );
|
|
mpGame.PrintMessageEvent( -1, msg_evt, parm1, parm2 );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_EVENT: {
|
|
entityNetEvent_t *event;
|
|
|
|
// allocate new event
|
|
event = eventQueue.Alloc();
|
|
eventQueue.Enqueue( event, idEventQueue::OUTOFORDER_IGNORE );
|
|
|
|
event->spawnId = msg.ReadBits( 32 );
|
|
event->event = msg.ReadByte();
|
|
event->time = msg.ReadLong();
|
|
|
|
event->paramsSize = msg.ReadBits( idMath::BitsForInteger( MAX_EVENT_PARAM_SIZE ) );
|
|
if ( event->paramsSize ) {
|
|
if ( event->paramsSize > MAX_EVENT_PARAM_SIZE ) {
|
|
NetworkEventWarning( event, "invalid param size" );
|
|
return;
|
|
}
|
|
msg.ReadByteAlign();
|
|
msg.ReadData( event->paramsBuf, event->paramsSize );
|
|
}
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_SERVERINFO: {
|
|
idDict info;
|
|
msg.ReadDeltaDict( info, NULL );
|
|
SetServerInfo( info );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_RESTART: {
|
|
MapRestart();
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_STARTVOTE: {
|
|
char voteString[ MAX_STRING_CHARS ];
|
|
int clientNum = msg.ReadByte( );
|
|
msg.ReadString( voteString, sizeof( voteString ) );
|
|
mpGame.ClientStartVote( clientNum, voteString );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_PRINT: {
|
|
char str[ MAX_PRINT_LEN ] = { '\0' };
|
|
msg.ReadString( str, MAX_PRINT_LEN );
|
|
mpGame.PrintMessage( -1, str );
|
|
break;
|
|
}
|
|
// RAVEN BEGIN
|
|
// shouchard: multifield vote stuff
|
|
case GAME_RELIABLE_MESSAGE_STARTPACKEDVOTE: {
|
|
voteStruct_t voteData;
|
|
memset( &voteData, 0, sizeof( voteData ) );
|
|
int clientNum = msg.ReadByte();
|
|
voteData.m_fieldFlags = msg.ReadShort();
|
|
char mapName[256];
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_KICK ) ) {
|
|
voteData.m_kick = msg.ReadByte();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_MAP ) ) {
|
|
msg.ReadString( mapName, sizeof( mapName ) );
|
|
voteData.m_map = mapName;
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_GAMETYPE ) ) {
|
|
voteData.m_gameType = msg.ReadByte();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_TIMELIMIT ) ) {
|
|
voteData.m_timeLimit = msg.ReadByte();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_FRAGLIMIT ) ) {
|
|
voteData.m_fragLimit = msg.ReadShort();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_TOURNEYLIMIT ) ) {
|
|
voteData.m_tourneyLimit = msg.ReadShort();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_CAPTURELIMIT ) ) {
|
|
voteData.m_captureLimit = msg.ReadShort();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_BUYING ) ) {
|
|
voteData.m_buying = msg.ReadByte();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_TEAMBALANCE ) ) {
|
|
voteData.m_teamBalance = msg.ReadByte();
|
|
}
|
|
if ( 0 != ( voteData.m_fieldFlags & VOTEFLAG_CONTROLTIME ) ) {
|
|
voteData.m_controlTime = msg.ReadShort();
|
|
}
|
|
mpGame.ClientStartPackedVote( clientNum, voteData );
|
|
break;
|
|
}
|
|
// RAVEN END
|
|
case GAME_RELIABLE_MESSAGE_UPDATEVOTE: {
|
|
int result = msg.ReadByte( );
|
|
int yesCount = msg.ReadByte( );
|
|
int noCount = msg.ReadByte( );
|
|
// RAVEN BEGIN
|
|
// shouchard: multifield vote stuff
|
|
int multiVote = msg.ReadByte( );
|
|
voteStruct_t voteData;
|
|
char mapNameBuffer[256];
|
|
memset( &voteData, 0, sizeof( voteData ) );
|
|
if ( multiVote ) {
|
|
voteData.m_fieldFlags = msg.ReadShort();
|
|
voteData.m_kick = msg.ReadByte();
|
|
msg.ReadString( mapNameBuffer, sizeof( mapNameBuffer ) );
|
|
voteData.m_map = mapNameBuffer;
|
|
voteData.m_gameType = msg.ReadByte();
|
|
voteData.m_timeLimit = msg.ReadByte();
|
|
voteData.m_fragLimit = msg.ReadShort();
|
|
voteData.m_tourneyLimit = msg.ReadShort();
|
|
voteData.m_captureLimit = msg.ReadShort();
|
|
voteData.m_buying = msg.ReadByte();
|
|
voteData.m_teamBalance = msg.ReadByte();
|
|
if ( gameLocal.GetCurrentDemoProtocol() == 69 ) {
|
|
voteData.m_controlTime = 0;
|
|
} else {
|
|
voteData.m_controlTime = msg.ReadShort();
|
|
}
|
|
}
|
|
mpGame.ClientUpdateVote( (idMultiplayerGame::vote_result_t)result, yesCount, noCount, voteData );
|
|
// RAVEN END
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_PORTALSTATES: {
|
|
int numPortals = msg.ReadLong();
|
|
assert( numPortals == gameRenderWorld->NumPortals() );
|
|
for ( int i = 0; i < numPortals; i++ ) {
|
|
gameRenderWorld->SetPortalState( (qhandle_t) (i+1), msg.ReadBits( NUM_RENDER_PORTAL_BITS ) );
|
|
}
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_PORTAL: {
|
|
qhandle_t portal = msg.ReadLong();
|
|
int blockingBits = msg.ReadBits( NUM_RENDER_PORTAL_BITS );
|
|
assert( portal > 0 && portal <= gameRenderWorld->NumPortals() );
|
|
gameRenderWorld->SetPortalState( portal, blockingBits );
|
|
break;
|
|
}
|
|
case GAME_RELIABLE_MESSAGE_STARTSTATE: {
|
|
mpGame.ClientReadStartState( msg );
|
|
break;
|
|
}
|
|
// RAVEN BEGIN
|
|
// bdube:
|
|
case GAME_RELIABLE_MESSAGE_ITEMACQUIRESOUND:
|
|
mpGame.PlayGlobalItemAcquireSound( msg.ReadBits ( gameLocal.entityDefBits ) );
|
|
break;
|
|
|
|
// ddynerman: death messagse
|
|
case GAME_RELIABLE_MESSAGE_DEATH: {
|
|
int attackerEntityNumber = msg.ReadByte( );
|
|
int attackerScore = -1;
|
|
if( attackerEntityNumber >= 0 && attackerEntityNumber < MAX_CLIENTS ) {
|
|
attackerScore = msg.ReadBits( ASYNC_PLAYER_FRAG_BITS );
|
|
}
|
|
int victimEntityNumber = msg.ReadByte( );
|
|
int victimScore = -1;
|
|
if( victimEntityNumber >= 0 && victimEntityNumber < MAX_CLIENTS ) {
|
|
victimScore = msg.ReadBits( ASYNC_PLAYER_FRAG_BITS );
|
|
}
|
|
|
|
idPlayer* attacker = (attackerEntityNumber != 255 ? static_cast<idPlayer*>(gameLocal.entities[ attackerEntityNumber ]) : NULL);
|
|
idPlayer* victim = (victimEntityNumber != 255 ? static_cast<idPlayer*>(gameLocal.entities[ victimEntityNumber ]) : NULL);
|
|
int methodOfDeath = msg.ReadByte( );
|
|
|
|
mpGame.ReceiveDeathMessage( attacker, attackerScore, victim, victimScore, methodOfDeath );
|
|
break;
|
|
}
|
|
// ddynerman: game state
|
|
case GAME_RELIABLE_MESSAGE_GAMESTATE: {
|
|
mpGame.GetGameState()->ReceiveState( msg );
|
|
break;
|
|
}
|
|
// ddynerman: game stats
|
|
case GAME_RELIABLE_MESSAGE_STAT: {
|
|
statManager->ReceiveStat( msg );
|
|
break;
|
|
}
|
|
// asalmon: game stats for Xenon receive all client stats
|
|
case GAME_RELIABLE_MESSAGE_ALL_STATS: {
|
|
statManager->ReceiveAllStats( msg );
|
|
break;
|
|
}
|
|
// ddynerman: multiple instances
|
|
case GAME_RELIABLE_MESSAGE_SET_INSTANCE: {
|
|
mpGame.ClientSetInstance( msg );
|
|
break;
|
|
}
|
|
// ddynerman: awards
|
|
case GAME_RELIABLE_MESSAGE_INGAMEAWARD: {
|
|
statManager->ReceiveInGameAward( msg );
|
|
break;
|
|
}
|
|
|
|
// mekberg: get ban list for server
|
|
case GAME_RELIABLE_MESSAGE_GETADMINBANLIST: {
|
|
mpBanInfo_t banInfo;
|
|
char name[MAX_STRING_CHARS];
|
|
char guid[MAX_STRING_CHARS];
|
|
|
|
FlushBanList( );
|
|
while ( msg.ReadString( name, MAX_STRING_CHARS ) && msg.ReadString( guid, MAX_STRING_CHARS ) ) {
|
|
banInfo.name = name;
|
|
strncpy( banInfo.guid, guid, CLIENT_GUID_LENGTH );
|
|
banList.Append( banInfo );
|
|
}
|
|
|
|
break;
|
|
}
|
|
// RAVEN END
|
|
// RAVEN END
|
|
default: {
|
|
Error( "Unknown server->client reliable message: %d", id );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
/*
|
|
================
|
|
idGameLocal::ClientRun
|
|
Called once each client render frame (before any ClientPrediction frames have been run)
|
|
================
|
|
*/
|
|
void idGameLocal::ClientRun( void ) {
|
|
if( isMultiplayer ) {
|
|
mpGame.ClientRun();
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientEndFrame
|
|
Called once each client render frame (after all ClientPrediction frames have been run)
|
|
================
|
|
*/
|
|
void idGameLocal::ClientEndFrame( void ) {
|
|
if( isMultiplayer ) {
|
|
mpGame.ClientEndFrame();
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ProcessRconReturn
|
|
================
|
|
*/
|
|
void idGameLocal::ProcessRconReturn( bool success ) {
|
|
if( isMultiplayer ) {
|
|
mpGame.ProcessRconReturn( success );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ResetGuiRconStatus
|
|
================
|
|
*/
|
|
void idGameLocal::ResetRconGuiStatus( void ) {
|
|
if( isMultiplayer ) {
|
|
mpGame.ResetRconGuiStatus( );
|
|
}
|
|
}
|
|
|
|
// RAVEN END
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ClientPrediction
|
|
server demos: clientNum == MAX_CLIENTS
|
|
================
|
|
*/
|
|
gameReturn_t idGameLocal::ClientPrediction( int clientNum, const usercmd_t *clientCmds, bool lastPredictFrame, ClientStats_t *cs ) {
|
|
idEntity *ent;
|
|
idPlayer *player; // may be NULL when predicting for a server demo
|
|
gameReturn_t ret;
|
|
|
|
ret.sessionCommand[ 0 ] = '\0';
|
|
|
|
if ( clientNum == MAX_CLIENTS ) {
|
|
// clientCmds[ MAX_CLIENTS ] has the local interaction
|
|
// firing -> cycle follow players, jump -> free fly and cycle map spawns
|
|
|
|
int btn_mask;
|
|
|
|
player = NULL;
|
|
oldUsercmd = usercmd;
|
|
usercmd = clientCmds[ MAX_CLIENTS ];
|
|
btn_mask = usercmd.buttons ^ oldUsercmd.buttons;
|
|
|
|
if ( usercmd.buttons & btn_mask & BUTTON_ATTACK ) {
|
|
// find the next suitable player to follow
|
|
int delta = 0;
|
|
while ( true ) {
|
|
int i_next = GetNextClientNum( followPlayer );
|
|
if ( followPlayer < i_next ) {
|
|
delta += i_next - followPlayer;
|
|
} else {
|
|
delta += numClients - followPlayer + i_next;
|
|
}
|
|
if ( delta > numClients ) {
|
|
// tried them all, no fit
|
|
followPlayer = -1;
|
|
break;
|
|
}
|
|
followPlayer = i_next;
|
|
if ( !entities[ followPlayer ] ) {
|
|
continue;
|
|
}
|
|
idPlayer *p = static_cast< idPlayer * >( entities[ followPlayer ] );
|
|
if ( p->spectating ) {
|
|
continue;
|
|
}
|
|
// Tourney games, we only record instance 0, only cycle on instance 0 players
|
|
if ( p->GetInstance() != 0 ) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if ( usercmd.upmove & !oldUsercmd.upmove ) {
|
|
if ( followPlayer != -1 ) {
|
|
// set yourself up a bit above whoever you were following
|
|
freeView.SetFreeView( followPlayer );
|
|
} else {
|
|
// pick a random spawn spot to start flying from
|
|
freeView.PickRandomSpawn();
|
|
}
|
|
followPlayer = -1;
|
|
}
|
|
|
|
player = NULL;
|
|
if ( followPlayer >= 0 ) {
|
|
player = static_cast< idPlayer* >( entities[ followPlayer ] );
|
|
if ( !player ) {
|
|
// that player we were following was removed from the game
|
|
freeView.PickRandomSpawn();
|
|
} else if ( player->spectating ) {
|
|
// our followed player went spectator, go free fly
|
|
freeView.SetFreeView( followPlayer );
|
|
player = NULL;
|
|
followPlayer = -1;
|
|
}
|
|
}
|
|
|
|
if ( !player && !freeView.Initialized() ) {
|
|
freeView.PickRandomSpawn();
|
|
}
|
|
|
|
} else {
|
|
player = static_cast<idPlayer *>( entities[clientNum] );
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// bdube: added advanced debug support
|
|
if ( g_showDebugHud.GetInteger() && net_entsInSnapshot && net_snapshotSize) {
|
|
gameDebug.SetInt( "snap_ents", net_entsInSnapshot );
|
|
gameDebug.SetInt( "snap_size", net_snapshotSize );
|
|
|
|
net_entsInSnapshot = 0;
|
|
net_snapshotSize = 0;
|
|
}
|
|
|
|
if ( clientNum == localClientNum ) {
|
|
gameDebug.BeginFrame( );
|
|
gameLog->BeginFrame( time );
|
|
}
|
|
|
|
isLastPredictFrame = lastPredictFrame;
|
|
// RAVEN END
|
|
|
|
// check for local client lag
|
|
if ( player ) {
|
|
if ( networkSystem->ClientGetTimeSinceLastPacket() >= net_clientMaxPrediction.GetInteger() ) {
|
|
player->isLagged = true;
|
|
} else {
|
|
player->isLagged = false;
|
|
}
|
|
}
|
|
|
|
InitLocalClient( clientNum );
|
|
|
|
// update the game time
|
|
framenum++;
|
|
previousTime = time;
|
|
// RAVEN BEGIN
|
|
// bdube: use GetMSec access rather than USERCMD_TIME
|
|
time += GetMSec();
|
|
// RAVEN END
|
|
|
|
// update the real client time and the new frame flag
|
|
if ( time > realClientTime ) {
|
|
realClientTime = time;
|
|
isNewFrame = true;
|
|
} else {
|
|
isNewFrame = false;
|
|
}
|
|
|
|
if ( cs ) {
|
|
cs->isLastPredictFrame = isLastPredictFrame;
|
|
cs->isLagged = player ? player->isLagged : false;
|
|
cs->isNewFrame = isNewFrame;
|
|
}
|
|
|
|
// set the user commands for this frame
|
|
// RAVEN BEGIN
|
|
usercmds = clientCmds;
|
|
// RAVEN END
|
|
|
|
if ( clientNum == MAX_CLIENTS && !player ) {
|
|
freeView.Fly( usercmd );
|
|
}
|
|
|
|
// TMP
|
|
bool verbose = cvarSystem->GetCVarBool( "verbose_predict" );
|
|
|
|
// run prediction on all entities from the last snapshot
|
|
for ( ent = snapshotEntities.Next(); ent != NULL; ent = ent->snapshotNode.Next() ) {
|
|
#if 0
|
|
ent->thinkFlags |= TH_PHYSICS;
|
|
ent->ClientPredictionThink();
|
|
#else
|
|
// don't force TH_PHYSICS on, only call ClientPredictionThink if thinkFlags != 0
|
|
// it's better to synchronize TH_PHYSICS on specific entities when needed ( movers may be trouble )
|
|
// thinkMask is a temp thing see if there are problems with only checking for TH_PHYSICS
|
|
if ( ent->thinkFlags != 0 ) {
|
|
if ( verbose ) {
|
|
common->Printf( "%d: %s %d\n", ent->entityNumber, ent->GetType()->classname, ent->thinkFlags );
|
|
}
|
|
ent->ClientPredictionThink();
|
|
} else {
|
|
if ( verbose ) {
|
|
common->Printf( "skip %d: %s %d\n", ent->entityNumber, ent->GetType()->classname, ent->thinkFlags );
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// bdube: client entities
|
|
// run client entities
|
|
if ( isNewFrame ) {
|
|
// rjohnson: only run the entire logic when it is a new frame
|
|
rvClientEntity* cent;
|
|
for ( cent = clientSpawnedEntities.Next(); cent != NULL; cent = cent->spawnNode.Next() ) {
|
|
cent->Think();
|
|
}
|
|
}
|
|
// RAVEN END
|
|
|
|
// service any pending events
|
|
idEvent::ServiceEvents();
|
|
|
|
// show any debug info for this frame
|
|
if ( isNewFrame ) {
|
|
RunDebugInfo();
|
|
D_DrawDebugLines();
|
|
}
|
|
|
|
if ( sessionCommand.Length() ) {
|
|
strncpy( ret.sessionCommand, sessionCommand, sizeof( ret.sessionCommand ) );
|
|
sessionCommand = "";
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// ddynerman: client logging/debugging
|
|
if ( clientNum == localClientNum ) {
|
|
gameDebug.EndFrame();
|
|
gameLog->EndFrame();
|
|
}
|
|
// RAVEN END
|
|
|
|
g_simpleItems.ClearModified();
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::Tokenize
|
|
===============
|
|
*/
|
|
void idGameLocal::Tokenize( idStrList &out, const char *in ) {
|
|
char buf[ MAX_STRING_CHARS ];
|
|
char *token, *next;
|
|
|
|
idStr::Copynz( buf, in, MAX_STRING_CHARS );
|
|
token = buf;
|
|
next = strchr( token, ';' );
|
|
while ( token ) {
|
|
if ( next ) {
|
|
*next = '\0';
|
|
}
|
|
idStr::ToLower( token );
|
|
out.Append( token );
|
|
if ( next ) {
|
|
token = next + 1;
|
|
next = strchr( token, ';' );
|
|
} else {
|
|
token = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::DownloadRequest
|
|
===============
|
|
*/
|
|
bool idGameLocal::DownloadRequest( const char *IP, const char *guid, const char *paks, char urls[ MAX_STRING_CHARS ] ) {
|
|
if ( !cvarSystem->GetCVarInteger( "net_serverDownload" ) ) {
|
|
return false;
|
|
}
|
|
if ( cvarSystem->GetCVarInteger( "net_serverDownload" ) == 1 ) {
|
|
// 1: single URL redirect
|
|
if ( !strlen( cvarSystem->GetCVarString( "si_serverURL" ) ) ) {
|
|
common->Warning( "si_serverURL not set" );
|
|
return false;
|
|
}
|
|
idStr::snPrintf( urls, MAX_STRING_CHARS, "1;%s", cvarSystem->GetCVarString( "si_serverURL" ) );
|
|
return true;
|
|
} else {
|
|
// 2: table of pak URLs
|
|
// first token is the game pak if requested, empty if not requested by the client
|
|
// there may be empty tokens for paks the server couldn't pinpoint - the order matters
|
|
idStr reply = "2;";
|
|
idStrList dlTable, pakList;
|
|
bool matchAll = false;
|
|
int i, j;
|
|
|
|
if ( !idStr::Icmp( cvarSystem->GetCVarString( "net_serverDlTable" ), "*" ) ) {
|
|
matchAll = true;
|
|
} else {
|
|
Tokenize( dlTable, cvarSystem->GetCVarString( "net_serverDlTable" ) );
|
|
}
|
|
Tokenize( pakList, paks );
|
|
|
|
for ( i = 0; i < pakList.Num(); i++ ) {
|
|
if ( i > 0 ) {
|
|
reply += ";";
|
|
}
|
|
if ( pakList[ i ][ 0 ] == '\0' ) {
|
|
if ( i == 0 ) {
|
|
// pak 0 will always miss when client doesn't ask for game bin
|
|
common->DPrintf( "no game pak request\n" );
|
|
} else {
|
|
common->DPrintf( "no pak %d\n", i );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ( matchAll ) {
|
|
idStr url = cvarSystem->GetCVarString( "net_serverDlBaseURL" );
|
|
url.AppendPath( pakList[i] );
|
|
reply += url;
|
|
common->Printf( "download for %s: %s\n", IP, url.c_str() );
|
|
} else {
|
|
for ( j = 0; j < dlTable.Num(); j++ ) {
|
|
if ( !pakList[ i ].Icmp( dlTable[ j ] ) ) {
|
|
break;
|
|
}
|
|
}
|
|
if ( j == dlTable.Num() ) {
|
|
common->Printf( "download for %s: pak not matched: %s\n", IP, pakList[ i ].c_str() );
|
|
} else {
|
|
idStr url = cvarSystem->GetCVarString( "net_serverDlBaseURL" );
|
|
url.AppendPath( dlTable[ j ] );
|
|
reply += url;
|
|
common->Printf( "download for %s: %s\n", IP, url.c_str() );
|
|
}
|
|
}
|
|
}
|
|
|
|
idStr::Copynz( urls, reply, MAX_STRING_CHARS );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::HTTPRequest
|
|
===============
|
|
*/
|
|
bool idGameLocal::HTTPRequest( const char *IP, const char *file, bool isGamePak ) {
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::Alloc
|
|
===============
|
|
*/
|
|
entityNetEvent_t* idEventQueue::Alloc() {
|
|
entityNetEvent_t* event = eventAllocator.Alloc();
|
|
event->prev = NULL;
|
|
event->next = NULL;
|
|
return event;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::Free
|
|
===============
|
|
*/
|
|
void idEventQueue::Free( entityNetEvent_t *event ) {
|
|
// should only be called on an unlinked event!
|
|
assert( !event->next && !event->prev );
|
|
eventAllocator.Free( event );
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::Shutdown
|
|
===============
|
|
*/
|
|
void idEventQueue::Shutdown() {
|
|
eventAllocator.Shutdown();
|
|
this->Init();
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::Init
|
|
===============
|
|
*/
|
|
void idEventQueue::Init( void ) {
|
|
start = NULL;
|
|
end = NULL;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::Dequeue
|
|
===============
|
|
*/
|
|
entityNetEvent_t* idEventQueue::Dequeue( void ) {
|
|
entityNetEvent_t* event = start;
|
|
if ( !event ) {
|
|
return NULL;
|
|
}
|
|
|
|
start = start->next;
|
|
|
|
if ( !start ) {
|
|
end = NULL;
|
|
} else {
|
|
start->prev = NULL;
|
|
}
|
|
|
|
event->next = NULL;
|
|
event->prev = NULL;
|
|
|
|
return event;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::RemoveLast
|
|
===============
|
|
*/
|
|
entityNetEvent_t* idEventQueue::RemoveLast( void ) {
|
|
entityNetEvent_t *event = end;
|
|
if ( !event ) {
|
|
return NULL;
|
|
}
|
|
|
|
end = event->prev;
|
|
|
|
if ( !end ) {
|
|
start = NULL;
|
|
} else {
|
|
end->next = NULL;
|
|
}
|
|
|
|
event->next = NULL;
|
|
event->prev = NULL;
|
|
|
|
return event;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idEventQueue::Enqueue
|
|
===============
|
|
*/
|
|
void idEventQueue::Enqueue( entityNetEvent_t *event, outOfOrderBehaviour_t behaviour ) {
|
|
if ( behaviour == OUTOFORDER_DROP ) {
|
|
// go backwards through the queue and determine if there are
|
|
// any out-of-order events
|
|
while ( end && end->time > event->time ) {
|
|
entityNetEvent_t *outOfOrder = RemoveLast();
|
|
common->DPrintf( "WARNING: new event with id %d ( time %d ) caused removal of event with id %d ( time %d ), game time = %d.\n", event->event, event->time, outOfOrder->event, outOfOrder->time, gameLocal.time );
|
|
Free( outOfOrder );
|
|
}
|
|
} else if ( behaviour == OUTOFORDER_SORT && end ) {
|
|
// NOT TESTED -- sorting out of order packets hasn't been
|
|
// tested yet... wasn't strictly necessary for
|
|
// the patch fix.
|
|
entityNetEvent_t *cur = end;
|
|
// iterate until we find a time < the new event's
|
|
while ( cur && cur->time > event->time ) {
|
|
cur = cur->prev;
|
|
}
|
|
if ( !cur ) {
|
|
// add to start
|
|
event->next = start;
|
|
event->prev = NULL;
|
|
start = event;
|
|
} else {
|
|
// insert
|
|
event->prev = cur;
|
|
event->next = cur->next;
|
|
cur->next = event;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// add the new event
|
|
event->next = NULL;
|
|
event->prev = NULL;
|
|
|
|
if ( end ) {
|
|
end->next = event;
|
|
event->prev = end;
|
|
} else {
|
|
start = event;
|
|
}
|
|
end = event;
|
|
}
|
|
|
|
// RAVEN BEGIN
|
|
// shouchard: ban list stuff here
|
|
|
|
/*
|
|
================
|
|
idGameLocal::LoadBanList
|
|
================
|
|
*/
|
|
void idGameLocal::LoadBanList() {
|
|
|
|
// open file
|
|
idStr token;
|
|
idFile *banFile = fileSystem->OpenFileRead( BANLIST_FILENAME );
|
|
mpBanInfo_t banInfo;
|
|
if ( NULL == banFile ) {
|
|
common->DPrintf( "idGameLocal::LoadBanList: unable to open ban list file!\n" ); // fixme: need something better here
|
|
return;
|
|
}
|
|
|
|
// parse file (read three consecutive strings per banInfo (real complex ;) ) )
|
|
while ( banFile->ReadString( token ) > 0 ) {
|
|
// name
|
|
banInfo.name = token;
|
|
|
|
// guid
|
|
if ( banFile->ReadString( token ) > 0 && token.Length() >= 11 ) {
|
|
idStr::Copynz( banInfo.guid, token.c_str(), CLIENT_GUID_LENGTH );
|
|
|
|
banList.Append( banInfo );
|
|
continue;
|
|
}
|
|
|
|
gameLocal.Warning( "idGameLocal::LoadBanList: Potential curruption of banlist file (%s).", BANLIST_FILENAME );
|
|
}
|
|
fileSystem->CloseFile( banFile );
|
|
|
|
banListLoaded = true;
|
|
banListChanged = false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SaveBanList
|
|
================
|
|
*/
|
|
void idGameLocal::SaveBanList() {
|
|
if ( !banListChanged ) {
|
|
return;
|
|
}
|
|
|
|
// open file
|
|
idFile *banFile = fileSystem->OpenFileWrite( BANLIST_FILENAME );
|
|
if ( NULL == banFile ) {
|
|
common->DPrintf( "idGameLocal::SaveBanList: unable to open ban list file!\n" ); // fixme: need something better here
|
|
return;
|
|
}
|
|
|
|
for ( int i = 0; i < banList.Num(); i++ ) {
|
|
const mpBanInfo_t& banInfo = banList[ i ];
|
|
char temp[ 16 ] = { 0, };
|
|
banFile->WriteString( va( "%s", banInfo.name.c_str() ) );
|
|
idStr::Copynz( temp, banInfo.guid, CLIENT_GUID_LENGTH );
|
|
banFile->WriteString( temp );
|
|
// idStr::Copynz( temp, (const char*)banInfo.ip, 15 );
|
|
// banFile->WriteString( "255.255.255.255" );
|
|
}
|
|
fileSystem->CloseFile( banFile );
|
|
banListChanged = false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::FlushBanList
|
|
================
|
|
*/
|
|
void idGameLocal::FlushBanList() {
|
|
banList.Clear();
|
|
banListLoaded = false;
|
|
banListChanged = false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::IsPlayerBanned
|
|
================
|
|
*/
|
|
bool idGameLocal::IsPlayerBanned( const char *name ) {
|
|
assert( name );
|
|
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
// check vs. each line in the list, if we found one return true
|
|
for ( int i = 0; i < banList.Num(); i++ ) {
|
|
if ( 0 == idStr::Icmp( name, banList[ i ].name ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::IsGuidBanned
|
|
================
|
|
*/
|
|
bool idGameLocal::IsGuidBanned( const char *guid ) {
|
|
assert( guid );
|
|
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
// check vs. each line in the list, if we found one return true
|
|
for ( int i = 0; i < banList.Num(); i++ ) {
|
|
if ( 0 == idStr::Icmp( guid, banList[ i ].guid ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::AddGuidToBanList
|
|
================
|
|
*/
|
|
void idGameLocal::AddGuidToBanList( const char *guid ) {
|
|
assert( guid );
|
|
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
mpBanInfo_t banInfo;
|
|
char name[ 512 ]; // TODO: clean this up
|
|
gameLocal.GetPlayerName( gameLocal.GetClientNumByGuid( guid ), name );
|
|
banInfo.name = name;
|
|
idStr::Copynz( banInfo.guid, guid, CLIENT_GUID_LENGTH );
|
|
// SIMDProcessor->Memset( banInfo.ip, 0xFF, 15 );
|
|
banList.Append( banInfo );
|
|
banListChanged = true;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RemoveGuidFromBanList
|
|
================
|
|
*/
|
|
void idGameLocal::RemoveGuidFromBanList( const char *guid ) {
|
|
assert( guid );
|
|
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
// check vs. each line in the list, if we find a match remove it.
|
|
for ( int i = 0; i < banList.Num(); i++ ) {
|
|
if ( 0 == idStr::Icmp( guid, banList[ i ].guid ) ) {
|
|
banList.RemoveIndex( i );
|
|
banListChanged = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RegisterClientGuid
|
|
================
|
|
*/
|
|
void idGameLocal::RegisterClientGuid( int clientNum, const char *guid ) {
|
|
assert( clientNum >= 0 && clientNum < MAX_CLIENTS );
|
|
assert( guid );
|
|
memset( clientGuids[ clientNum ], 0, CLIENT_GUID_LENGTH ); // just in case
|
|
idStr::Copynz( clientGuids[ clientNum ], guid, CLIENT_GUID_LENGTH );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetBanListCount
|
|
================
|
|
*/
|
|
int idGameLocal::GetBanListCount() {
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
return banList.Num();
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetBanListEntry
|
|
================
|
|
*/
|
|
const mpBanInfo_t* idGameLocal::GetBanListEntry( int entry ) {
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
if ( entry < 0 || entry >= banList.Num() ) {
|
|
return NULL;
|
|
}
|
|
|
|
return &banList[ entry ];
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetGuidByClientNum
|
|
================
|
|
*/
|
|
const char *idGameLocal::GetGuidByClientNum( int clientNum ) {
|
|
assert( clientNum >= 0 && clientNum < numClients );
|
|
|
|
return clientGuids[ clientNum ];
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetClientNumByGuid
|
|
================
|
|
*/
|
|
int idGameLocal::GetClientNumByGuid( const char * guid ) {
|
|
assert( guid );
|
|
|
|
for ( int i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( !idStr::Icmp( networkSystem->GetClientGUID( i ), guid ) ) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// mekberg: send ban list to client
|
|
/*
|
|
================
|
|
idGameLocal::ServerSendBanList
|
|
================
|
|
*/
|
|
void idGameLocal::ServerSendBanList( int clientNum ) {
|
|
idBitMsg outMsg;
|
|
byte msgBuf[ MAX_GAME_MESSAGE_SIZE ];
|
|
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_GETADMINBANLIST ) ;
|
|
|
|
if ( !banListLoaded ) {
|
|
LoadBanList();
|
|
}
|
|
|
|
int i;
|
|
int c = banList.Num();
|
|
for ( i = 0; i < c; i++ ) {
|
|
outMsg.WriteString( banList[ i ].name.c_str() );
|
|
outMsg.WriteString( banList[ i ].guid, CLIENT_GUID_LENGTH );
|
|
}
|
|
|
|
networkSystem->ServerSendReliableMessage( clientNum, outMsg );
|
|
}
|
|
|
|
// mekberg: so we can populate ban list outside of multiplayer game
|
|
/*
|
|
===================
|
|
idGameLocal::PopulateBanList
|
|
===================
|
|
*/
|
|
void idGameLocal::PopulateBanList( idUserInterface* hud ) {
|
|
if ( !hud ) {
|
|
return;
|
|
}
|
|
|
|
int bans = GetBanListCount();
|
|
for ( int i = 0; i < bans; i++ ) {
|
|
const mpBanInfo_t * banInfo = GetBanListEntry( i );
|
|
hud->SetStateString( va( "sa_banList_item_%d", i ), va( "%d: %s\t%s", i+1, banInfo->name.c_str(), banInfo->guid ) );
|
|
}
|
|
hud->DeleteStateVar( va( "sa_banList_item_%d", bans ) );
|
|
hud->SetStateString( "sa_banList_sel_0", "-1" );
|
|
// used to trigger a redraw, was slow, and doesn't seem to do anything so took it out. fixes #13675
|
|
hud->StateChanged( gameLocal.time, false );
|
|
}
|
|
// RAVEN END
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerSendInstanceReliableMessageExcluding
|
|
Works like networkSystem->ServerSendReliableMessageExcluding, but only sends to entities in the owner's instance
|
|
================
|
|
*/
|
|
void idGameLocal::ServerSendInstanceReliableMessageExcluding( const idEntity* owner, int excludeClient, const idBitMsg& msg ) {
|
|
int i;
|
|
assert( isServer );
|
|
|
|
if ( owner == NULL ) {
|
|
networkSystem->ServerSendReliableMessageExcluding( excludeClient, msg );
|
|
return;
|
|
}
|
|
|
|
for( i = 0; i < numClients; i++ ) {
|
|
if ( i == excludeClient ) {
|
|
continue;
|
|
}
|
|
|
|
if( entities[ i ] == NULL ) {
|
|
continue;
|
|
}
|
|
|
|
if( entities[ i ]->GetInstance() != owner->GetInstance() ) {
|
|
continue;
|
|
}
|
|
|
|
networkSystem->ServerSendReliableMessage( i, msg );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ServerSendInstanceReliableMessage
|
|
Works like networkSystem->ServerSendReliableMessage, but only sends to entities in the owner's instance
|
|
================
|
|
*/
|
|
void idGameLocal::ServerSendInstanceReliableMessage( const idEntity* owner, int clientNum, const idBitMsg& msg ) {
|
|
int i;
|
|
assert( isServer );
|
|
|
|
if( owner == NULL ) {
|
|
networkSystem->ServerSendReliableMessage( clientNum, msg );
|
|
return;
|
|
}
|
|
|
|
if( clientNum == -1 ) {
|
|
for( i = 0; i < numClients; i++ ) {
|
|
if( entities[ i ] == NULL ) {
|
|
continue;
|
|
}
|
|
|
|
if( entities[ i ]->GetInstance() != owner->GetInstance() ) {
|
|
continue;
|
|
}
|
|
|
|
networkSystem->ServerSendReliableMessage( i, msg );
|
|
}
|
|
} else {
|
|
if( entities[ clientNum ] && entities[ clientNum ]->GetInstance() == owner->GetInstance() ) {
|
|
networkSystem->ServerSendReliableMessage( clientNum, msg );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::SendUnreliableMessage
|
|
for spectating support, we have to loop through the clients and emit to the spectator client too
|
|
note that a clientNum == -1 means send to everyone
|
|
===============
|
|
*/
|
|
void idGameLocal::SendUnreliableMessage( const idBitMsg &msg, const int clientNum ) {
|
|
int icl;
|
|
idPlayer *player;
|
|
|
|
for ( icl = 0; icl < numClients; icl++ ) {
|
|
if ( icl == localClientNum ) {
|
|
// not to local client
|
|
// note that if local is spectated he will still get it
|
|
continue;
|
|
}
|
|
if ( !entities[ icl ] ) {
|
|
continue;
|
|
}
|
|
if ( icl != clientNum ) {
|
|
player = static_cast< idPlayer * >( entities[ icl ] );
|
|
// drop all clients except the ones that follow the client we emit to
|
|
if ( !player->spectating || player->spectator != clientNum ) {
|
|
continue;
|
|
}
|
|
}
|
|
unreliableMessages[ icl ].Add( msg.GetData(), msg.GetSize(), false );
|
|
}
|
|
|
|
if ( demoState == DEMO_RECORDING ) {
|
|
// record the type and destination for remap on readback
|
|
idBitMsg dest;
|
|
byte msgBuf[ 16 ];
|
|
|
|
dest.Init( msgBuf, sizeof( msgBuf ) );
|
|
dest.WriteByte( GAME_UNRELIABLE_RECORD_CLIENTNUM );
|
|
dest.WriteByte( clientNum );
|
|
|
|
unreliableMessages[ MAX_CLIENTS ].AddConcat( dest.GetData(), dest.GetSize(), msg.GetData(), msg.GetSize(), false );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::SendUnreliableMessagePVS
|
|
instanceEnt to NULL for no instance checks
|
|
excludeClient to -1 for no exclusions
|
|
===============
|
|
*/
|
|
void idGameLocal::SendUnreliableMessagePVS( const idBitMsg &msg, const idEntity *instanceEnt, int area1, int area2 ) {
|
|
int icl;
|
|
int matchInstance = instanceEnt ? instanceEnt->GetInstance() : -1;
|
|
idPlayer *player;
|
|
int areas[ 2 ];
|
|
int numEvAreas;
|
|
|
|
numEvAreas = 0;
|
|
if ( area1 != -1 ) {
|
|
areas[ 0 ] = area1;
|
|
numEvAreas++;
|
|
}
|
|
if ( area2 != -1 ) {
|
|
areas[ numEvAreas ] = area2;
|
|
numEvAreas++;
|
|
}
|
|
|
|
for ( icl = 0; icl < numClients; icl++ ) {
|
|
if ( icl == localClientNum ) {
|
|
// local client is always excluded
|
|
continue;
|
|
}
|
|
if ( !entities[ icl ] ) {
|
|
continue;
|
|
}
|
|
if ( matchInstance >= 0 && entities[ icl ]->GetInstance() != matchInstance ) {
|
|
continue;
|
|
}
|
|
if ( clientsPVS[ icl ].i < 0 ) {
|
|
// clients for which we don't have PVS info won't get anything
|
|
continue;
|
|
}
|
|
player = static_cast< idPlayer * >( entities[ icl ] );
|
|
|
|
// if no areas are given, this is a global emit
|
|
if ( numEvAreas ) {
|
|
// ony send if pvs says this client can see it
|
|
if ( !pvs.InCurrentPVS( clientsPVS[ icl ], areas, numEvAreas ) ) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
unreliableMessages[ icl ].Add( msg.GetData(), msg.GetSize(), false );
|
|
}
|
|
|
|
if ( demoState == DEMO_RECORDING ) {
|
|
// record the target areas to the message
|
|
idBitMsg dest;
|
|
byte msgBuf[ 16 ];
|
|
|
|
// Tourney games: only record from instance 0
|
|
if ( !instanceEnt || instanceEnt->GetInstance() == 0 ) {
|
|
|
|
dest.Init( msgBuf, sizeof( msgBuf ) );
|
|
dest.WriteByte( GAME_UNRELIABLE_RECORD_AREAS );
|
|
dest.WriteLong( area1 );
|
|
dest.WriteLong( area2 );
|
|
|
|
unreliableMessages[ MAX_CLIENTS ].AddConcat( dest.GetData(), dest.GetSize(), msg.GetData(), msg.GetSize(), false );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ClientReadUnreliableMessages
|
|
===============
|
|
*/
|
|
void idGameLocal::ClientReadUnreliableMessages( const idBitMsg &_msg ) {
|
|
idMsgQueue localQueue;
|
|
int size;
|
|
byte msgBuf[MAX_GAME_MESSAGE_SIZE];
|
|
idBitMsg msg;
|
|
|
|
localQueue.ReadFrom( _msg );
|
|
|
|
msg.Init( msgBuf, sizeof( msgBuf ) );
|
|
while ( localQueue.Get( msg.GetData(), msg.GetMaxSize(), size, false ) ) {
|
|
msg.SetSize( size );
|
|
msg.BeginReading();
|
|
ProcessUnreliableMessage( msg );
|
|
msg.BeginWriting();
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::DemoReplayInAreas
|
|
checks if our current demo replay view ( server demo ) matches the areas given
|
|
===============
|
|
*/
|
|
bool idGameLocal::IsDemoReplayInAreas( int area1, int area2 ) {
|
|
int areas[2];
|
|
int numAreas;
|
|
|
|
idVec3 view;
|
|
pvsHandle_t handle;
|
|
|
|
bool ret;
|
|
|
|
numAreas = 0;
|
|
if ( area1 != -1 ) {
|
|
areas[ 0 ] = area1;
|
|
numAreas++;
|
|
}
|
|
if ( area2 != -1 ) {
|
|
areas[ numAreas ] = area2;
|
|
numAreas++;
|
|
}
|
|
|
|
assert( serverDemo );
|
|
assert( demoState == DEMO_PLAYING );
|
|
|
|
if ( followPlayer == -1 ) {
|
|
view = freeView.GetOrigin();
|
|
} else {
|
|
view = entities[ followPlayer ]->GetPhysics()->GetOrigin();
|
|
}
|
|
|
|
// could probably factorize this, at least for processing all unreliable messages, maybe at a higher level of the loop?
|
|
handle = pvs.SetupCurrentPVS( view );
|
|
ret = pvs.InCurrentPVS( handle, areas, numAreas );
|
|
pvs.FreeCurrentPVS( handle );
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ProcessUnreliableMessage
|
|
===============
|
|
*/
|
|
void idGameLocal::ProcessUnreliableMessage( const idBitMsg &msg ) {
|
|
if ( serverDemo ) {
|
|
assert( demoState == DEMO_PLAYING );
|
|
int record_type = msg.ReadByte();
|
|
assert( record_type < GAME_UNRELIABLE_RECORD_COUNT );
|
|
switch ( record_type ) {
|
|
case GAME_UNRELIABLE_RECORD_CLIENTNUM: {
|
|
int client = msg.ReadByte();
|
|
if ( client != -1 ) {
|
|
// unreliable was targetted
|
|
if ( followPlayer != client ) {
|
|
// either free flying, or following someone else
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case GAME_UNRELIABLE_RECORD_AREAS: {
|
|
int area1 = msg.ReadLong();
|
|
int area2 = msg.ReadLong();
|
|
if ( !IsDemoReplayInAreas( area1, area2 ) ) {
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int type = msg.ReadByte();
|
|
switch ( type ) {
|
|
case GAME_UNRELIABLE_MESSAGE_EVENT: {
|
|
idEntityPtr<idEntity> p;
|
|
int spawnId = msg.ReadBits( 32 );
|
|
p.SetSpawnId( spawnId );
|
|
|
|
if ( p.GetEntity() ) {
|
|
p.GetEntity()->ClientReceiveEvent( msg.ReadByte(), time, msg );
|
|
} else {
|
|
Warning( "ProcessUnreliableMessage: no local entity 0x%x for event %d", spawnId & ( ( 1 << GENTITYNUM_BITS ) - 1 ), msg.ReadByte() );
|
|
}
|
|
break;
|
|
}
|
|
case GAME_UNRELIABLE_MESSAGE_EFFECT: {
|
|
idCQuat quat;
|
|
idVec3 origin, origin2;
|
|
rvClientEffect* effect;
|
|
effectCategory_t category;
|
|
const idDecl *decl;
|
|
|
|
decl = idGameLocal::ReadDecl( msg, DECL_EFFECT );
|
|
|
|
origin.x = msg.ReadFloat( );
|
|
origin.y = msg.ReadFloat( );
|
|
origin.z = msg.ReadFloat( );
|
|
|
|
quat.x = msg.ReadFloat( );
|
|
quat.y = msg.ReadFloat( );
|
|
quat.z = msg.ReadFloat( );
|
|
|
|
bool loop = msg.ReadBits( 1 ) != 0;
|
|
|
|
origin2.x = msg.ReadFloat( );
|
|
origin2.y = msg.ReadFloat( );
|
|
origin2.z = msg.ReadFloat( );
|
|
|
|
category = ( effectCategory_t )msg.ReadByte();
|
|
|
|
if ( bse->CanPlayRateLimited( category ) ) {
|
|
effect = new rvClientEffect( decl );
|
|
effect->SetOrigin( origin );
|
|
effect->SetAxis( quat.ToMat3() );
|
|
effect->Play( time, loop, origin2 );
|
|
}
|
|
|
|
break;
|
|
}
|
|
case GAME_UNRELIABLE_MESSAGE_HITSCAN: {
|
|
ClientHitScan( msg );
|
|
break;
|
|
}
|
|
#ifdef _USE_VOICECHAT
|
|
case GAME_UNRELIABLE_MESSAGE_VOICEDATA_SERVER: {
|
|
mpGame.ReceiveAndPlayVoiceData( msg );
|
|
break;
|
|
}
|
|
#else
|
|
case GAME_UNRELIABLE_MESSAGE_VOICEDATA_SERVER: {
|
|
break;
|
|
}
|
|
#endif
|
|
default: {
|
|
Error( "idGameLocal::ProcessUnreliableMessage() - Unknown unreliable message '%d'\n", type );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::WriteNetworkInfo
|
|
===============
|
|
*/
|
|
void idGameLocal::WriteNetworkInfo( idFile* file, int clientNum ) {
|
|
int i, j;
|
|
snapshot_t *snapshot;
|
|
entityState_t *entityState;
|
|
|
|
if ( !IsServerDemo() ) {
|
|
|
|
// save the current states
|
|
for ( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
entityState = clientEntityStates[clientNum][i];
|
|
file->WriteBool( !!entityState );
|
|
if ( entityState ) {
|
|
file->WriteInt( entityState->entityNumber );
|
|
file->WriteInt( entityState->state.GetSize() );
|
|
file->Write( entityState->state.GetData(), entityState->state.GetSize() );
|
|
}
|
|
}
|
|
|
|
// save the PVS states
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
for ( j = 0; j < ENTITY_PVS_SIZE; j++ ) {
|
|
file->WriteInt( clientPVS[i][j] );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// players ( including local client )
|
|
j = 0;
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( !entities[i] ) {
|
|
continue;
|
|
}
|
|
j++;
|
|
}
|
|
file->WriteInt( j );
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( !entities[i] ) {
|
|
continue;
|
|
}
|
|
file->WriteInt( i );
|
|
file->WriteInt( spawnIds[ i ] );
|
|
}
|
|
|
|
if ( !IsServerDemo() ) {
|
|
|
|
// write number of snapshots so on readback we know how many to allocate
|
|
i = 0;
|
|
for ( snapshot = clientSnapshots[ clientNum ]; snapshot; snapshot = snapshot->next ) {
|
|
i++;
|
|
}
|
|
file->WriteInt( i );
|
|
|
|
for ( snapshot = clientSnapshots[ clientNum ]; snapshot; snapshot = snapshot->next ) {
|
|
file->WriteInt( snapshot->sequence );
|
|
|
|
// write number of entity states in the snapshot
|
|
i = 0;
|
|
for ( entityState = snapshot->firstEntityState; entityState; entityState = entityState->next ) {
|
|
i++;
|
|
}
|
|
file->WriteInt( i );
|
|
|
|
for ( entityState = snapshot->firstEntityState; entityState; entityState = entityState->next ) {
|
|
file->WriteInt( entityState->entityNumber );
|
|
file->WriteInt( entityState->state.GetSize() );
|
|
file->Write( entityState->state.GetData(), entityState->state.GetSize() );
|
|
}
|
|
|
|
file->Write( snapshot->pvs, sizeof( snapshot->pvs ) );
|
|
}
|
|
|
|
}
|
|
|
|
// write the 'initial reliables' data
|
|
mpGame.WriteNetworkInfo( file, clientNum );
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ReadNetworkInfo
|
|
===============
|
|
*/
|
|
void idGameLocal::ReadNetworkInfo( int gameTime, idFile* file, int clientNum ) {
|
|
int i, j, num, numStates, stateSize;
|
|
snapshot_t *snapshot, **lastSnap;
|
|
entityState_t *entityState, **lastState;
|
|
int proto69TypeNum = 0;
|
|
bool proto69 = ( gameLocal.GetCurrentDemoProtocol() == 69 );
|
|
|
|
assert( clientNum == MAX_CLIENTS || !IsServerDemo() );
|
|
InitLocalClient( clientNum );
|
|
|
|
time = gameTime;
|
|
previousTime = gameTime;
|
|
|
|
// force new frame
|
|
realClientTime = 0;
|
|
isNewFrame = true;
|
|
|
|
// clear the snapshot entity list
|
|
snapshotEntities.Clear();
|
|
|
|
if ( !IsServerDemo() ) {
|
|
|
|
for ( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
bool isValid;
|
|
|
|
file->ReadBool( isValid );
|
|
if ( isValid ) {
|
|
clientEntityStates[clientNum][i] = entityStateAllocator.Alloc();
|
|
entityState = clientEntityStates[clientNum][i];
|
|
entityState->next = NULL;
|
|
file->ReadInt( entityState->entityNumber );
|
|
file->ReadInt( stateSize );
|
|
entityState->state.Init( entityState->stateBuf, sizeof( entityState->stateBuf ) );
|
|
entityState->state.SetSize( stateSize );
|
|
file->Read( entityState->state.GetData(), stateSize );
|
|
} else {
|
|
clientEntityStates[clientNum][i] = NULL;
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
for ( j = 0; j < ENTITY_PVS_SIZE; j++ ) {
|
|
file->ReadInt( clientPVS[i][j] );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// spawn player entities. ( numClients is not a count but the watermark of client indexes )
|
|
file->ReadInt( num );
|
|
for ( i = 0; i < num; i++ ) {
|
|
int icl, spawnId;
|
|
file->ReadInt( icl );
|
|
file->ReadInt( spawnId );
|
|
SpawnPlayer( icl );
|
|
spawnIds[ icl ] = spawnId;
|
|
numClients = icl + 1;
|
|
}
|
|
|
|
if ( !IsServerDemo() ) {
|
|
|
|
file->ReadInt( num );
|
|
lastSnap = &clientSnapshots[ localClientNum ];
|
|
for ( i = 0; i < num; i++ ) {
|
|
snapshot = snapshotAllocator.Alloc();
|
|
snapshot->firstEntityState = NULL;
|
|
snapshot->next = NULL;
|
|
file->ReadInt( snapshot->sequence );
|
|
|
|
file->ReadInt( numStates );
|
|
lastState = &snapshot->firstEntityState;
|
|
for ( j = 0; j < numStates; j++ ) {
|
|
entityState = entityStateAllocator.Alloc();
|
|
file->ReadInt( entityState->entityNumber );
|
|
file->ReadInt( stateSize );
|
|
entityState->state.Init( entityState->stateBuf, sizeof( entityState->stateBuf ) );
|
|
entityState->state.SetSize( stateSize );
|
|
file->Read( entityState->state.GetData(), stateSize );
|
|
entityState->next = NULL;
|
|
assert( !(*lastState ) );
|
|
*lastState = entityState;
|
|
lastState = &entityState->next;
|
|
}
|
|
|
|
file->Read( snapshot->pvs, sizeof( snapshot->pvs ) );
|
|
|
|
assert( !(*lastSnap) );
|
|
*lastSnap = snapshot;
|
|
lastSnap = &snapshot->next;
|
|
}
|
|
|
|
// spawn entities
|
|
for ( i = 0; i < ENTITYNUM_NONE; i++ ) {
|
|
int spawnId, entityDefNumber;
|
|
idBitMsgDelta deltaMsg;
|
|
idDict args;
|
|
entityState_t *base = clientEntityStates[clientNum][i];
|
|
idEntity *ent = entities[i];
|
|
const idDeclEntityDef *decl;
|
|
|
|
if ( !base ) {
|
|
continue;
|
|
}
|
|
base->state.BeginReading();
|
|
deltaMsg.InitReading( &base->state, NULL, NULL );
|
|
|
|
spawnId = deltaMsg.ReadBits( 32 - GENTITYNUM_BITS );
|
|
if ( proto69 ) {
|
|
proto69TypeNum = deltaMsg.ReadBits( idClass::GetTypeNumBits() );
|
|
}
|
|
entityDefNumber = deltaMsg.ReadBits( entityDefBits );
|
|
|
|
if ( !ent || ent->entityDefNumber != entityDefNumber || spawnId != spawnIds[ i ] ) {
|
|
|
|
delete ent;
|
|
|
|
spawnCount = spawnId;
|
|
|
|
args.Clear();
|
|
args.SetInt( "spawn_entnum", i );
|
|
args.Set( "name", va( "entity%d", i ) );
|
|
|
|
// assume any items spawned from a server-snapshot are in our instance
|
|
if( gameLocal.GetLocalPlayer() ) {
|
|
args.SetInt( "instance", gameLocal.GetLocalPlayer()->GetInstance() );
|
|
}
|
|
|
|
if ( entityDefNumber >= 0 ) {
|
|
if ( entityDefNumber >= declManager->GetNumDecls( DECL_ENTITYDEF ) ) {
|
|
Error( "server has %d entityDefs instead of %d", entityDefNumber, declManager->GetNumDecls( DECL_ENTITYDEF ) );
|
|
}
|
|
decl = static_cast< const idDeclEntityDef * >( declManager->DeclByIndex( DECL_ENTITYDEF, entityDefNumber, false ) );
|
|
assert( decl && decl->GetType() == DECL_ENTITYDEF );
|
|
args.Set( "classname", decl->GetName() );
|
|
if ( !SpawnEntityDef( args, &ent ) || !entities[i] ) {
|
|
Error( "Failed to spawn entity with classname '%s' of type '%s'", decl->GetName(), decl->dict.GetString("spawnclass") );
|
|
}
|
|
} else {
|
|
// we no longer support spawning entities by type num only. we would only hit this when playing 1.2 demos for backward compatibility
|
|
assert( proto69 );
|
|
switch ( proto69TypeNum ) {
|
|
case 183:
|
|
ent = SpawnEntityType( rvViewWeapon::GetClassType(), &args, true );
|
|
break;
|
|
case 182:
|
|
ent = SpawnEntityType( idAnimatedEntity::GetClassType(), &args, true );
|
|
ent->fl.networkSync = true;
|
|
break;
|
|
default:
|
|
Error( "Unexpected protocol 69 typenum (%d) for spawning entity by type", proto69TypeNum );
|
|
}
|
|
if ( !entities[i] ) {
|
|
Error( "Failed to spawn entity by typenum %d ( protocol 69 backwards compat )", proto69TypeNum );
|
|
}
|
|
}
|
|
}
|
|
|
|
// add the entity to the snapshot list
|
|
ent->snapshotNode.AddToEnd( snapshotEntities );
|
|
|
|
// read the class specific data from the snapshot
|
|
ent->ReadFromSnapshot( deltaMsg );
|
|
|
|
// this is useful. for instance on idPlayer, resets stuff so powerups actually appear
|
|
ent->ClientUnstale();
|
|
}
|
|
|
|
{
|
|
// specific state read for game and player state
|
|
idBitMsgDelta deltaMsg;
|
|
entityState_t *base = clientEntityStates[clientNum][ENTITYNUM_NONE];
|
|
idPlayer *player;
|
|
int targetPlayer;
|
|
|
|
// it's possible to have a recording start right at CS_INGAME and not have a base for reading this yet
|
|
if ( base ) {
|
|
base->state.BeginReading();
|
|
deltaMsg.InitReading( &base->state, NULL, NULL );
|
|
|
|
targetPlayer = deltaMsg.ReadBits( idMath::BitsForInteger( MAX_CLIENTS ) );
|
|
player = static_cast< idPlayer* >( entities[ targetPlayer ] );
|
|
if ( !player ) {
|
|
Error( "ReadNetworkInfo: no local player entity" );
|
|
return;
|
|
}
|
|
player->ReadPlayerStateFromSnapshot( deltaMsg );
|
|
ReadGameStateFromSnapshot( deltaMsg );
|
|
}
|
|
}
|
|
|
|
// set self spectating state according to userinfo settings
|
|
GetLocalPlayer()->Spectate( idStr::Icmp( userInfo[ clientNum ].GetString( "ui_spectate" ), "Spectate" ) == 0 );
|
|
|
|
}
|
|
|
|
// read the 'initial reliables' data
|
|
mpGame.ReadNetworkInfo( file, clientNum );
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::SetDemoState
|
|
============
|
|
*/
|
|
void idGameLocal::SetDemoState( demoState_t state, bool _serverDemo, bool _timeDemo ) {
|
|
if ( demoState == DEMO_RECORDING && state == DEMO_NONE ) {
|
|
ServerClientDisconnect( MAX_CLIENTS );
|
|
}
|
|
demoState = state;
|
|
serverDemo = _serverDemo;
|
|
timeDemo = _timeDemo;
|
|
if ( demoState == DEMO_NONE ) {
|
|
demo_hud = NULL;
|
|
demo_mphud = NULL;
|
|
demo_cursor = NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ValidateDemoProtocol
|
|
===============
|
|
*/
|
|
bool idGameLocal::ValidateDemoProtocol( int minor_ref, int minor ) {
|
|
// 1.1 beta : 67
|
|
// 1.1 final: 68
|
|
// 1.2 : 69
|
|
// 1.3 : 71
|
|
|
|
// let 1.3 play 1.2 demos - keep a careful eye on snapshotting changes
|
|
demo_protocol = minor;
|
|
return ( minor_ref == minor || ( minor_ref == 71 && minor == 69 ) );
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::RandomSpawn
|
|
===============
|
|
*/
|
|
idPlayerStart *idGameLocal::RandomSpawn( void ) {
|
|
return spawnSpots[ random.RandomInt( spawnSpots.Num() ) ];
|
|
}
|