mirror of
https://github.com/id-Software/DOOM-3-BFG.git
synced 2024-11-24 13:01:27 +00:00
5085 lines
131 KiB
C++
5085 lines
131 KiB
C++
/*
|
|
===========================================================================
|
|
|
|
Doom 3 BFG Edition GPL Source Code
|
|
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company.
|
|
|
|
This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").
|
|
|
|
Doom 3 BFG Edition Source Code is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Doom 3 BFG Edition Source Code is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Doom 3 BFG Edition Source Code. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code. If not, please request a copy in writing from id Software at the address below.
|
|
|
|
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
|
|
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "../idlib/precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "Game_local.h"
|
|
|
|
#ifdef GAME_DLL
|
|
|
|
idSys * sys = NULL;
|
|
idCommon * common = NULL;
|
|
idCmdSystem * cmdSystem = NULL;
|
|
idCVarSystem * cvarSystem = NULL;
|
|
idFileSystem * fileSystem = NULL;
|
|
idRenderSystem * renderSystem = NULL;
|
|
idSoundSystem * soundSystem = NULL;
|
|
idRenderModelManager * renderModelManager = NULL;
|
|
idUserInterfaceManager * uiManager = NULL;
|
|
idDeclManager * declManager = NULL;
|
|
idAASFileManager * AASFileManager = NULL;
|
|
idCollisionModelManager * collisionModelManager = NULL;
|
|
idCVar * idCVar::staticVars = NULL;
|
|
|
|
idCVar com_forceGenericSIMD( "com_forceGenericSIMD", "0", CVAR_BOOL|CVAR_SYSTEM, "force generic platform independent SIMD" );
|
|
|
|
#endif
|
|
|
|
idRenderWorld * gameRenderWorld = NULL; // all drawing is done to this world
|
|
idSoundWorld * gameSoundWorld = NULL; // all audio goes to this world
|
|
|
|
static gameExport_t gameExport;
|
|
|
|
// global animation lib
|
|
idAnimManager animationLib;
|
|
|
|
// the rest of the engine will only reference the "game" variable, while all local aspects stay hidden
|
|
idGameLocal gameLocal;
|
|
idGame * game = &gameLocal; // statically pointed at an idGameLocal
|
|
|
|
const char *idGameLocal::sufaceTypeNames[ MAX_SURFACE_TYPES ] = {
|
|
"none", "metal", "stone", "flesh", "wood", "cardboard", "liquid", "glass", "plastic",
|
|
"ricochet", "surftype10", "surftype11", "surftype12", "surftype13", "surftype14", "surftype15"
|
|
};
|
|
|
|
idCVar net_usercmd_timing_debug( "net_usercmd_timing_debug", "0", CVAR_BOOL, "Print messages about usercmd timing." );
|
|
|
|
|
|
// List of all defs used by the player that will stay on the fast timeline
|
|
static char* fastEntityList[] = {
|
|
"player_doommarine",
|
|
"weapon_chainsaw",
|
|
"weapon_fists",
|
|
"weapon_flashlight",
|
|
"weapon_rocketlauncher",
|
|
"projectile_rocket",
|
|
"weapon_machinegun",
|
|
"projectile_bullet_machinegun",
|
|
"weapon_pistol",
|
|
"projectile_bullet_pistol",
|
|
"weapon_handgrenade",
|
|
"projectile_grenade",
|
|
"weapon_bfg",
|
|
"projectile_bfg",
|
|
"weapon_chaingun",
|
|
"projectile_chaingunbullet",
|
|
"weapon_pda",
|
|
"weapon_plasmagun",
|
|
"projectile_plasmablast",
|
|
"weapon_shotgun",
|
|
"projectile_bullet_shotgun",
|
|
"weapon_soulcube",
|
|
"projectile_soulblast",
|
|
"weapon_shotgun_double",
|
|
"projectile_shotgunbullet_double",
|
|
"weapon_grabber",
|
|
"weapon_bloodstone_active1",
|
|
"weapon_bloodstone_active2",
|
|
"weapon_bloodstone_active3",
|
|
"weapon_bloodstone_passive",
|
|
NULL };
|
|
/*
|
|
===========
|
|
GetGameAPI
|
|
============
|
|
*/
|
|
#if __MWERKS__
|
|
#pragma export on
|
|
#endif
|
|
#if __GNUC__ >= 4
|
|
#pragma GCC visibility push(default)
|
|
#endif
|
|
extern "C" gameExport_t *GetGameAPI( gameImport_t *import ) {
|
|
#if __MWERKS__
|
|
#pragma export off
|
|
#endif
|
|
|
|
if ( import->version == GAME_API_VERSION ) {
|
|
|
|
// set interface pointers used by the game
|
|
sys = import->sys;
|
|
common = import->common;
|
|
cmdSystem = import->cmdSystem;
|
|
cvarSystem = import->cvarSystem;
|
|
fileSystem = import->fileSystem;
|
|
renderSystem = import->renderSystem;
|
|
soundSystem = import->soundSystem;
|
|
renderModelManager = import->renderModelManager;
|
|
uiManager = import->uiManager;
|
|
declManager = import->declManager;
|
|
AASFileManager = import->AASFileManager;
|
|
collisionModelManager = import->collisionModelManager;
|
|
}
|
|
|
|
// set interface pointers used by idLib
|
|
idLib::sys = sys;
|
|
idLib::common = common;
|
|
idLib::cvarSystem = cvarSystem;
|
|
idLib::fileSystem = fileSystem;
|
|
|
|
// setup export interface
|
|
gameExport.version = GAME_API_VERSION;
|
|
gameExport.game = game;
|
|
gameExport.gameEdit = gameEdit;
|
|
|
|
return &gameExport;
|
|
}
|
|
#if __GNUC__ >= 4
|
|
#pragma GCC visibility pop
|
|
#endif
|
|
|
|
/*
|
|
===========
|
|
TestGameAPI
|
|
============
|
|
*/
|
|
void TestGameAPI() {
|
|
gameImport_t testImport;
|
|
gameExport_t testExport;
|
|
|
|
testImport.sys = ::sys;
|
|
testImport.common = ::common;
|
|
testImport.cmdSystem = ::cmdSystem;
|
|
testImport.cvarSystem = ::cvarSystem;
|
|
testImport.fileSystem = ::fileSystem;
|
|
testImport.renderSystem = ::renderSystem;
|
|
testImport.soundSystem = ::soundSystem;
|
|
testImport.renderModelManager = ::renderModelManager;
|
|
testImport.uiManager = ::uiManager;
|
|
testImport.declManager = ::declManager;
|
|
testImport.AASFileManager = ::AASFileManager;
|
|
testImport.collisionModelManager = ::collisionModelManager;
|
|
|
|
testExport = *GetGameAPI( &testImport );
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::idGameLocal
|
|
============
|
|
*/
|
|
idGameLocal::idGameLocal() {
|
|
Clear();
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::Clear
|
|
============
|
|
*/
|
|
void idGameLocal::Clear() {
|
|
int i;
|
|
|
|
serverInfo.Clear();
|
|
numClients = 0;
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
persistentPlayerInfo[i].Clear();
|
|
}
|
|
memset( entities, 0, sizeof( entities ) );
|
|
memset( spawnIds, -1, sizeof( spawnIds ) );
|
|
firstFreeEntityIndex[0] = 0;
|
|
firstFreeEntityIndex[1] = ENTITYNUM_FIRST_NON_REPLICATED;
|
|
num_entities = 0;
|
|
spawnedEntities.Clear();
|
|
activeEntities.Clear();
|
|
numEntitiesToDeactivate = 0;
|
|
sortPushers = false;
|
|
sortTeamMasters = false;
|
|
persistentLevelInfo.Clear();
|
|
memset( globalShaderParms, 0, sizeof( globalShaderParms ) );
|
|
random.SetSeed( 0 );
|
|
world = NULL;
|
|
frameCommandThread = NULL;
|
|
testmodel = NULL;
|
|
testFx = NULL;
|
|
clip.Shutdown();
|
|
pvs.Shutdown();
|
|
sessionCommand.Clear();
|
|
locationEntities = NULL;
|
|
smokeParticles = NULL;
|
|
editEntities = NULL;
|
|
entityHash.Clear( 1024, MAX_GENTITIES );
|
|
inCinematic = false;
|
|
framenum = 0;
|
|
previousTime = 0;
|
|
time = 0;
|
|
vacuumAreaNum = 0;
|
|
mapFileName.Clear();
|
|
mapFile = NULL;
|
|
spawnCount = INITIAL_SPAWN_COUNT;
|
|
mapSpawnCount = 0;
|
|
camera = NULL;
|
|
aasList.Clear();
|
|
aasNames.Clear();
|
|
lastAIAlertEntity = NULL;
|
|
lastAIAlertTime = 0;
|
|
spawnArgs.Clear();
|
|
gravity.Set( 0, 0, -1 );
|
|
playerPVS.h = (unsigned int)-1;
|
|
playerConnectedAreas.h = (unsigned int)-1;
|
|
gamestate = GAMESTATE_UNINITIALIZED;
|
|
influenceActive = false;
|
|
|
|
realClientTime = 0;
|
|
isNewFrame = true;
|
|
clientSmoothing = 0.1f;
|
|
entityDefBits = 0;
|
|
|
|
nextGibTime = 0;
|
|
globalMaterial = NULL;
|
|
newInfo.Clear();
|
|
lastGUIEnt = NULL;
|
|
lastGUI = 0;
|
|
|
|
eventQueue.Init();
|
|
savedEventQueue.Init();
|
|
|
|
shellHandler = NULL;
|
|
selectedGroup = 0;
|
|
portalSkyEnt = NULL;
|
|
portalSkyActive = false;
|
|
|
|
ResetSlowTimeVars();
|
|
|
|
lastCmdRunTimeOnClient.Zero();
|
|
lastCmdRunTimeOnServer.Zero();
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::Init
|
|
|
|
initialize the game object, only happens once at startup, not each level load
|
|
============
|
|
*/
|
|
void idGameLocal::Init() {
|
|
const idDict *dict;
|
|
idAAS *aas;
|
|
|
|
#ifndef GAME_DLL
|
|
|
|
TestGameAPI();
|
|
|
|
#else
|
|
|
|
// initialize idLib
|
|
idLib::Init();
|
|
|
|
// register static cvars declared in the game
|
|
idCVar::RegisterStaticVars();
|
|
|
|
// initialize processor specific SIMD
|
|
idSIMD::InitProcessor( "game", com_forceGenericSIMD.GetBool() );
|
|
|
|
#endif
|
|
|
|
Printf( "--------- Initializing Game ----------\n" );
|
|
Printf( "gamename: %s\n", GAME_VERSION );
|
|
Printf( "gamedate: %s\n", __DATE__ );
|
|
|
|
// register game specific decl types
|
|
declManager->RegisterDeclType( "model", DECL_MODELDEF, idDeclAllocator<idDeclModelDef> );
|
|
declManager->RegisterDeclType( "export", DECL_MODELEXPORT, idDeclAllocator<idDecl> );
|
|
|
|
// register game specific decl folders
|
|
declManager->RegisterDeclFolder( "def", ".def", DECL_ENTITYDEF );
|
|
declManager->RegisterDeclFolder( "fx", ".fx", DECL_FX );
|
|
declManager->RegisterDeclFolder( "particles", ".prt", DECL_PARTICLE );
|
|
declManager->RegisterDeclFolder( "af", ".af", DECL_AF );
|
|
declManager->RegisterDeclFolder( "newpdas", ".pda", DECL_PDA );
|
|
|
|
cmdSystem->AddCommand( "listModelDefs", idListDecls_f<DECL_MODELDEF>, CMD_FL_SYSTEM|CMD_FL_GAME, "lists model defs" );
|
|
cmdSystem->AddCommand( "printModelDefs", idPrintDecls_f<DECL_MODELDEF>, CMD_FL_SYSTEM|CMD_FL_GAME, "prints a model def", idCmdSystem::ArgCompletion_Decl<DECL_MODELDEF> );
|
|
|
|
Clear();
|
|
|
|
idEvent::Init();
|
|
idClass::Init();
|
|
|
|
InitConsoleCommands();
|
|
|
|
shellHandler = new (TAG_SWF) idMenuHandler_Shell();
|
|
|
|
if(!g_xp_bind_run_once.GetBool()) {
|
|
//The default config file contains remapped controls that support the XP weapons
|
|
//We want to run this once after the base doom config file has run so we can
|
|
//have the correct xp binds
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "exec default.cfg\n" );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "seta g_xp_bind_run_once 1\n" );
|
|
cmdSystem->ExecuteCommandBuffer();
|
|
}
|
|
|
|
// load default scripts
|
|
program.Startup( SCRIPT_DEFAULT );
|
|
|
|
smokeParticles = new (TAG_PARTICLE) idSmokeParticles;
|
|
|
|
// set up the aas
|
|
dict = FindEntityDefDict( "aas_types" );
|
|
if ( dict == NULL ) {
|
|
Error( "Unable to find entityDef for 'aas_types'" );
|
|
return;
|
|
}
|
|
|
|
// allocate space for the aas
|
|
const idKeyValue *kv = dict->MatchPrefix( "type" );
|
|
while( kv != NULL ) {
|
|
aas = idAAS::Alloc();
|
|
aasList.Append( aas );
|
|
aasNames.Append( kv->GetValue() );
|
|
kv = dict->MatchPrefix( "type", kv );
|
|
}
|
|
|
|
gamestate = GAMESTATE_NOMAP;
|
|
|
|
Printf( "...%d aas types\n", aasList.Num() );
|
|
Printf( "game initialized.\n" );
|
|
Printf( "--------------------------------------\n" );
|
|
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::Shutdown
|
|
|
|
shut down the entire game
|
|
============
|
|
*/
|
|
void idGameLocal::Shutdown() {
|
|
|
|
if ( !common ) {
|
|
return;
|
|
}
|
|
|
|
Printf( "------------ Game Shutdown -----------\n" );
|
|
|
|
Shell_Cleanup();
|
|
|
|
mpGame.Shutdown();
|
|
|
|
MapShutdown();
|
|
|
|
aasList.DeleteContents( true );
|
|
aasNames.Clear();
|
|
|
|
idAI::FreeObstacleAvoidanceNodes();
|
|
|
|
idEvent::Shutdown();
|
|
|
|
delete[] locationEntities;
|
|
locationEntities = NULL;
|
|
|
|
delete smokeParticles;
|
|
smokeParticles = NULL;
|
|
|
|
idClass::Shutdown();
|
|
|
|
// clear list with forces
|
|
idForce::ClearForceList();
|
|
|
|
// free the program data
|
|
program.FreeData();
|
|
|
|
// delete the .map file
|
|
delete mapFile;
|
|
mapFile = NULL;
|
|
|
|
// free the collision map
|
|
collisionModelManager->FreeMap();
|
|
|
|
ShutdownConsoleCommands();
|
|
|
|
// free memory allocated by class objects
|
|
Clear();
|
|
|
|
// shut down the animation manager
|
|
animationLib.Shutdown();
|
|
|
|
Printf( "--------------------------------------\n" );
|
|
|
|
#ifdef GAME_DLL
|
|
|
|
// remove auto-completion function pointers pointing into this DLL
|
|
cvarSystem->RemoveFlaggedAutoCompletion( CVAR_GAME );
|
|
|
|
// enable leak test
|
|
Mem_EnableLeakTest( "game" );
|
|
|
|
// shutdown idLib
|
|
idLib::ShutDown();
|
|
|
|
#endif
|
|
}
|
|
|
|
idCVar g_recordSaveGameTrace( "g_recordSaveGameTrace", "0", CVAR_BOOL, "" );
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SaveGame
|
|
|
|
save the current player state, level name, and level state
|
|
the session may have written some data to the file already
|
|
============
|
|
*/
|
|
void idGameLocal::SaveGame( idFile *f, idFile *strings ) {
|
|
int i;
|
|
idEntity *ent;
|
|
idEntity *link;
|
|
|
|
int startTimeMs = Sys_Milliseconds();
|
|
if ( g_recordSaveGameTrace.GetBool() ) {
|
|
bool result = BeginTraceRecording( "e:\\savegame_trace.pix2" );
|
|
if ( !result ) {
|
|
//idLib::Printf( "BeginTraceRecording: error %d\n", GetLastError() );
|
|
}
|
|
}
|
|
|
|
idSaveGame savegame( f, strings, BUILD_NUMBER );
|
|
|
|
if ( g_flushSave.GetBool( ) == true ) {
|
|
// force flushing with each write... for tracking down
|
|
// save game bugs.
|
|
f->ForceFlush();
|
|
}
|
|
|
|
// go through all entities and threads and add them to the object list
|
|
for( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
ent = entities[i];
|
|
|
|
if ( ent ) {
|
|
if ( ent->GetTeamMaster() && ent->GetTeamMaster() != ent ) {
|
|
continue;
|
|
}
|
|
for ( link = ent; link != NULL; link = link->GetNextTeamEntity() ) {
|
|
savegame.AddObject( link );
|
|
}
|
|
}
|
|
}
|
|
|
|
idList<idThread *> threads;
|
|
threads = idThread::GetThreads();
|
|
|
|
for( i = 0; i < threads.Num(); i++ ) {
|
|
savegame.AddObject( threads[i] );
|
|
}
|
|
|
|
// write out complete object list
|
|
savegame.WriteObjectList();
|
|
|
|
program.Save( &savegame );
|
|
|
|
savegame.WriteInt( g_skill.GetInteger() );
|
|
|
|
savegame.WriteDecls();
|
|
|
|
savegame.WriteDict( &serverInfo );
|
|
|
|
savegame.WriteInt( numClients );
|
|
for( i = 0; i < numClients; i++ ) {
|
|
//savegame.WriteUsercmd( usercmds[ i ] );
|
|
// Now that usercmds are handled by the idUserCmdMgr,
|
|
// do we need another solution here?
|
|
usercmd_t dummy;
|
|
savegame.WriteUsercmd( dummy );
|
|
|
|
savegame.WriteDict( &persistentPlayerInfo[ i ] );
|
|
}
|
|
|
|
for( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
savegame.WriteObject( entities[ i ] );
|
|
savegame.WriteInt( spawnIds[ i ] );
|
|
}
|
|
|
|
// There shouldn't be any non-replicated entities in SP,
|
|
// so we shouldn't have to save the first free replicated entity index.
|
|
savegame.WriteInt( firstFreeEntityIndex[0] );
|
|
savegame.WriteInt( num_entities );
|
|
|
|
// enityHash is restored by idEntity::Restore setting the entity name.
|
|
|
|
savegame.WriteObject( world );
|
|
|
|
savegame.WriteInt( spawnedEntities.Num() );
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
savegame.WriteObject( ent );
|
|
}
|
|
|
|
savegame.WriteInt( activeEntities.Num() );
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
savegame.WriteObject( ent );
|
|
}
|
|
|
|
savegame.WriteInt( numEntitiesToDeactivate );
|
|
savegame.WriteBool( sortPushers );
|
|
savegame.WriteBool( sortTeamMasters );
|
|
savegame.WriteDict( &persistentLevelInfo );
|
|
|
|
for( i = 0; i < MAX_GLOBAL_SHADER_PARMS; i++ ) {
|
|
savegame.WriteFloat( globalShaderParms[ i ] );
|
|
}
|
|
|
|
savegame.WriteInt( random.GetSeed() );
|
|
savegame.WriteObject( frameCommandThread );
|
|
|
|
// clip
|
|
// push
|
|
// pvs
|
|
|
|
testmodel = NULL;
|
|
testFx = NULL;
|
|
|
|
savegame.WriteString( sessionCommand );
|
|
|
|
// FIXME: save smoke particles
|
|
|
|
savegame.WriteBool( inCinematic );
|
|
|
|
savegame.WriteInt( gameType );
|
|
|
|
savegame.WriteInt( framenum );
|
|
savegame.WriteInt( previousTime );
|
|
savegame.WriteInt( time );
|
|
|
|
savegame.WriteInt( vacuumAreaNum );
|
|
|
|
savegame.WriteInt( entityDefBits );
|
|
|
|
savegame.WriteInt( GetLocalClientNum() );
|
|
|
|
// snapshotEntities is used for multiplayer only
|
|
|
|
savegame.WriteInt( realClientTime );
|
|
savegame.WriteBool( isNewFrame );
|
|
savegame.WriteFloat( clientSmoothing );
|
|
|
|
portalSkyEnt.Save( &savegame );
|
|
savegame.WriteBool( portalSkyActive );
|
|
|
|
fast.Save( &savegame );
|
|
slow.Save( &savegame );
|
|
|
|
savegame.WriteInt( slowmoState );
|
|
savegame.WriteFloat( slowmoScale );
|
|
savegame.WriteBool( quickSlowmoReset );
|
|
|
|
savegame.WriteBool( mapCycleLoaded );
|
|
savegame.WriteInt( spawnCount );
|
|
|
|
if ( !locationEntities ) {
|
|
savegame.WriteInt( 0 );
|
|
} else {
|
|
savegame.WriteInt( gameRenderWorld->NumAreas() );
|
|
for( i = 0; i < gameRenderWorld->NumAreas(); i++ ) {
|
|
savegame.WriteObject( locationEntities[ i ] );
|
|
}
|
|
}
|
|
|
|
savegame.WriteObject( camera );
|
|
|
|
savegame.WriteMaterial( globalMaterial );
|
|
|
|
lastAIAlertEntity.Save( &savegame );
|
|
savegame.WriteInt( lastAIAlertTime );
|
|
|
|
savegame.WriteDict( &spawnArgs );
|
|
|
|
savegame.WriteInt( playerPVS.i );
|
|
savegame.WriteInt( playerPVS.h );
|
|
savegame.WriteInt( playerConnectedAreas.i );
|
|
savegame.WriteInt( playerConnectedAreas.h );
|
|
|
|
savegame.WriteVec3( gravity );
|
|
|
|
// gamestate
|
|
|
|
savegame.WriteBool( influenceActive );
|
|
savegame.WriteInt( nextGibTime );
|
|
|
|
// spawnSpots
|
|
// initialSpots
|
|
// currentInitialSpot
|
|
// newInfo
|
|
// makingBuild
|
|
// shakeSounds
|
|
|
|
// write out pending events
|
|
idEvent::Save( &savegame );
|
|
|
|
savegame.Close();
|
|
|
|
int endTimeMs = Sys_Milliseconds();
|
|
idLib::Printf( "Save time: %dms\n", ( endTimeMs - startTimeMs ) );
|
|
|
|
if ( g_recordSaveGameTrace.GetBool() ) {
|
|
EndTraceRecording();
|
|
g_recordSaveGameTrace.SetBool( false );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetSaveGameDetails
|
|
============
|
|
*/
|
|
void idGameLocal::GetSaveGameDetails( idSaveGameDetails & gameDetails ) {
|
|
idLocationEntity * locationEnt = LocationForPoint( gameLocal.GetLocalPlayer()->GetEyePosition() );
|
|
const char * locationStr = locationEnt ? locationEnt->GetLocation() : idLocalization::GetString( "#str_02911" );
|
|
|
|
idStrStatic< MAX_OSPATH > shortMapName = mapFileName;
|
|
shortMapName.StripFileExtension();
|
|
shortMapName.StripLeading( "maps/" );
|
|
|
|
const idDeclEntityDef * mapDef = static_cast<const idDeclEntityDef *>(declManager->FindType( DECL_MAPDEF, shortMapName, false ));
|
|
const char * mapPrettyName = mapDef ? idLocalization::GetString( mapDef->dict.GetString( "name", shortMapName ) ) : shortMapName.c_str();
|
|
idPlayer * player = GetClientByNum( 0 );
|
|
int playTime = player ? player->GetPlayedTime() : 0;
|
|
gameExpansionType_t expansionType = player ? player->GetExpansionType() : GAME_BASE;
|
|
|
|
gameDetails.descriptors.Clear();
|
|
gameDetails.descriptors.SetInt( SAVEGAME_DETAIL_FIELD_EXPANSION, expansionType );
|
|
gameDetails.descriptors.Set( SAVEGAME_DETAIL_FIELD_MAP, mapPrettyName );
|
|
gameDetails.descriptors.Set( SAVEGAME_DETAIL_FIELD_MAP_LOCATE, locationStr );
|
|
gameDetails.descriptors.SetInt( SAVEGAME_DETAIL_FIELD_SAVE_VERSION, BUILD_NUMBER );
|
|
gameDetails.descriptors.SetInt( SAVEGAME_DETAIL_FIELD_DIFFICULTY, g_skill.GetInteger() );
|
|
gameDetails.descriptors.SetInt( SAVEGAME_DETAIL_FIELD_PLAYTIME, playTime );
|
|
|
|
// PS3 only strings that use the dict just set
|
|
|
|
// even though we don't use this when we enumerate, when we save, we use this descriptors file later so we need the date populated now
|
|
gameDetails.date = ::time( NULL );
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetPersistentPlayerInfo
|
|
============
|
|
*/
|
|
const idDict &idGameLocal::GetPersistentPlayerInfo( int clientNum ) {
|
|
idEntity *ent;
|
|
|
|
persistentPlayerInfo[ clientNum ].Clear();
|
|
ent = entities[ clientNum ];
|
|
if ( ent && ent->IsType( idPlayer::Type ) ) {
|
|
static_cast<idPlayer *>(ent)->SavePersistantInfo();
|
|
}
|
|
|
|
return persistentPlayerInfo[ clientNum ];
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SetPersistentPlayerInfo
|
|
============
|
|
*/
|
|
void idGameLocal::SetPersistentPlayerInfo( int clientNum, const idDict &playerInfo ) {
|
|
persistentPlayerInfo[ clientNum ] = playerInfo;
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::Printf
|
|
============
|
|
*/
|
|
void idGameLocal::Printf( const char *fmt, ... ) const {
|
|
va_list argptr;
|
|
char text[MAX_STRING_CHARS];
|
|
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
common->Printf( "%s", text );
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::DPrintf
|
|
============
|
|
*/
|
|
void idGameLocal::DPrintf( const char *fmt, ... ) const {
|
|
va_list argptr;
|
|
char text[MAX_STRING_CHARS];
|
|
|
|
if ( !developer.GetBool() ) {
|
|
return;
|
|
}
|
|
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
common->Printf( "%s", text );
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::Warning
|
|
============
|
|
*/
|
|
void idGameLocal::Warning( const char *fmt, ... ) const {
|
|
va_list argptr;
|
|
char text[MAX_STRING_CHARS];
|
|
idThread * thread;
|
|
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
thread = idThread::CurrentThread();
|
|
if ( thread ) {
|
|
thread->Warning( "%s", text );
|
|
} else {
|
|
common->Warning( "%s", text );
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::DWarning
|
|
============
|
|
*/
|
|
void idGameLocal::DWarning( const char *fmt, ... ) const {
|
|
va_list argptr;
|
|
char text[MAX_STRING_CHARS];
|
|
idThread * thread;
|
|
|
|
if ( !developer.GetBool() ) {
|
|
return;
|
|
}
|
|
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
thread = idThread::CurrentThread();
|
|
if ( thread ) {
|
|
thread->Warning( "%s", text );
|
|
} else {
|
|
common->DWarning( "%s", text );
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::Error
|
|
============
|
|
*/
|
|
void idGameLocal::Error( const char *fmt, ... ) const {
|
|
va_list argptr;
|
|
char text[MAX_STRING_CHARS];
|
|
idThread * thread;
|
|
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
thread = idThread::CurrentThread();
|
|
if ( thread ) {
|
|
thread->Error( "%s", text );
|
|
} else {
|
|
common->Error( "%s", text );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
gameError
|
|
===============
|
|
*/
|
|
void gameError( const char *fmt, ... ) {
|
|
va_list argptr;
|
|
char text[MAX_STRING_CHARS];
|
|
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( text, sizeof( text ), fmt, argptr );
|
|
va_end( argptr );
|
|
|
|
gameLocal.Error( "%s", text );
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::SetServerGameTimeMs
|
|
========================
|
|
*/
|
|
void idGameLocal::SetServerGameTimeMs( const int time ) {
|
|
previousServerTime = this->serverTime;
|
|
this->serverTime = time;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::GetServerGameTimeMs
|
|
========================
|
|
*/
|
|
int idGameLocal::GetServerGameTimeMs() const {
|
|
return serverTime;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SetServerInfo
|
|
============
|
|
*/
|
|
void idGameLocal::SetServerInfo( const idDict &_serverInfo ) {
|
|
serverInfo = _serverInfo;
|
|
|
|
if ( gameType == GAME_LASTMAN ) {
|
|
if ( serverInfo.GetInt( "si_fraglimit" ) <= 0 ) {
|
|
common->Warning( "Last Man Standing - setting fraglimit 1" );
|
|
serverInfo.SetInt( "si_fraglimit", 1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetServerInfo
|
|
============
|
|
*/
|
|
const idDict & idGameLocal::GetServerInfo() {
|
|
return serverInfo;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::LoadMap
|
|
|
|
Initializes all map variables common to both save games and spawned games.
|
|
===================
|
|
*/
|
|
void idGameLocal::LoadMap( const char * mapName, int randseed ) {
|
|
bool sameMap = (mapFile && idStr::Icmp(mapFileName, mapName) == 0);
|
|
|
|
// clear the sound system
|
|
gameSoundWorld->ClearAllSoundEmitters();
|
|
|
|
// clear envirosuit sound fx
|
|
gameSoundWorld->SetEnviroSuit( false );
|
|
gameSoundWorld->SetSlowmoSpeed( 1.0f );
|
|
|
|
InitAsyncNetwork();
|
|
|
|
if ( !sameMap || ( mapFile && mapFile->NeedsReload() ) ) {
|
|
// load the .map file
|
|
if ( mapFile ) {
|
|
delete mapFile;
|
|
}
|
|
mapFile = new (TAG_GAME) idMapFile;
|
|
if ( !mapFile->Parse( idStr( mapName ) + ".map" ) ) {
|
|
delete mapFile;
|
|
mapFile = NULL;
|
|
Error( "Couldn't load %s", mapName );
|
|
}
|
|
}
|
|
mapFileName = mapFile->GetName();
|
|
|
|
// load the collision map
|
|
collisionModelManager->LoadMap( mapFile );
|
|
collisionModelManager->Preload( mapName );
|
|
|
|
numClients = 0;
|
|
|
|
// initialize all entities for this game
|
|
memset( entities, 0, sizeof( entities ) );
|
|
memset( spawnIds, -1, sizeof( spawnIds ) );
|
|
spawnCount = INITIAL_SPAWN_COUNT;
|
|
|
|
spawnedEntities.Clear();
|
|
activeEntities.Clear();
|
|
aimAssistEntities.Clear();
|
|
numEntitiesToDeactivate = 0;
|
|
sortTeamMasters = false;
|
|
sortPushers = false;
|
|
lastGUIEnt = NULL;
|
|
lastGUI = 0;
|
|
|
|
globalMaterial = NULL;
|
|
|
|
memset( globalShaderParms, 0, sizeof( globalShaderParms ) );
|
|
|
|
// These used to be a non-pot adjustment for portal skies
|
|
// they're no longer needed, but we can't update the materials
|
|
globalShaderParms[4] = 1.0f;
|
|
globalShaderParms[5] = 1.0f;
|
|
|
|
// always leave room for the max number of clients,
|
|
// even if they aren't all used, so numbers inside that
|
|
// range are NEVER anything but clients
|
|
num_entities = MAX_CLIENTS;
|
|
firstFreeEntityIndex[0] = MAX_CLIENTS;
|
|
|
|
// reset the random number generator.
|
|
random.SetSeed( common->IsMultiplayer() ? randseed : 0 );
|
|
|
|
camera = NULL;
|
|
world = NULL;
|
|
testmodel = NULL;
|
|
testFx = NULL;
|
|
|
|
lastAIAlertEntity = NULL;
|
|
lastAIAlertTime = 0;
|
|
|
|
previousTime = 0;
|
|
time = 0;
|
|
framenum = 0;
|
|
sessionCommand = "";
|
|
nextGibTime = 0;
|
|
|
|
portalSkyEnt = NULL;
|
|
portalSkyActive = false;
|
|
|
|
ResetSlowTimeVars();
|
|
|
|
vacuumAreaNum = -1; // if an info_vacuum is spawned, it will set this
|
|
|
|
if ( !editEntities ) {
|
|
editEntities = new (TAG_GAME) idEditEntities;
|
|
}
|
|
|
|
gravity.Set( 0, 0, -g_gravity.GetFloat() );
|
|
|
|
spawnArgs.Clear();
|
|
|
|
inCinematic = false;
|
|
|
|
clip.Init();
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
pvs.Init();
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
playerPVS.i = -1;
|
|
playerConnectedAreas.i = -1;
|
|
|
|
// load navigation system for all the different monster sizes
|
|
for ( int i = 0; i < aasNames.Num(); i++ ) {
|
|
aasList[ i ]->Init( idStr( mapFileName ).SetFileExtension( aasNames[ i ] ).c_str(), mapFile->GetGeometryCRC() );
|
|
}
|
|
|
|
// clear the smoke particle free list
|
|
smokeParticles->Init();
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
// cache miscellaneous media references
|
|
FindEntityDef( "preCacheExtras", false );
|
|
FindEntityDef( "ammo_types", false );
|
|
FindEntityDef( "ammo_names", false );
|
|
FindEntityDef( "ammo_types_d3xp", false );
|
|
|
|
FindEntityDef( "damage_noair", false );
|
|
FindEntityDef( "damage_moverCrush", false );
|
|
FindEntityDef( "damage_crush", false );
|
|
FindEntityDef( "damage_triggerhurt_1000", false );
|
|
FindEntityDef( "damage_telefrag", false );
|
|
FindEntityDef( "damage_suicide", false );
|
|
FindEntityDef( "damage_explosion", false );
|
|
FindEntityDef( "damage_generic", false );
|
|
FindEntityDef( "damage_painTrigger", false );
|
|
FindEntityDef( "damage_thrown_ragdoll", false );
|
|
FindEntityDef( "damage_gib", false );
|
|
FindEntityDef( "damage_softfall", false );
|
|
FindEntityDef( "damage_hardfall", false );
|
|
FindEntityDef( "damage_fatalfall", false );
|
|
|
|
FindEntityDef( "envirosuit_light", false );
|
|
|
|
declManager->FindType( DECL_EMAIL, "highScore", false );
|
|
declManager->FindType( DECL_EMAIL, "MartianBuddyGameComplete", false );
|
|
|
|
declManager->FindMaterial( "itemHighlightShell" );
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
if ( !sameMap ) {
|
|
mapFile->RemovePrimitiveData();
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::LocalMapRestart
|
|
===================
|
|
*/
|
|
void idGameLocal::LocalMapRestart( ) {
|
|
int i, latchSpawnCount;
|
|
|
|
Printf( "----------- Game Map Restart ------------\n" );
|
|
|
|
gamestate = GAMESTATE_SHUTDOWN;
|
|
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( entities[ i ] && entities[ i ]->IsType( idPlayer::Type ) ) {
|
|
static_cast< idPlayer * >( entities[ i ] )->PrepareForRestart();
|
|
}
|
|
}
|
|
|
|
eventQueue.Shutdown();
|
|
savedEventQueue.Shutdown();
|
|
|
|
MapClear( false );
|
|
|
|
|
|
|
|
// clear the smoke particle free list
|
|
smokeParticles->Init();
|
|
|
|
// clear the sound system
|
|
if ( gameSoundWorld ) {
|
|
gameSoundWorld->ClearAllSoundEmitters();
|
|
// clear envirosuit sound fx
|
|
gameSoundWorld->SetEnviroSuit( false );
|
|
gameSoundWorld->SetSlowmoSpeed( 1.0f );
|
|
}
|
|
|
|
// the spawnCount is reset to zero temporarily to spawn the map entities with the same spawnId
|
|
// if we don't do that, network clients are confused and don't show any map entities
|
|
latchSpawnCount = spawnCount;
|
|
spawnCount = INITIAL_SPAWN_COUNT;
|
|
|
|
gamestate = GAMESTATE_STARTUP;
|
|
|
|
program.Restart();
|
|
|
|
InitScriptForMap();
|
|
|
|
MapPopulate();
|
|
|
|
// once the map is populated, set the spawnCount back to where it was so we don't risk any collision
|
|
// (note that if there are no players in the game, we could just leave it at it's current value)
|
|
spawnCount = latchSpawnCount;
|
|
|
|
// setup the client entities again
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( entities[ i ] && entities[ i ]->IsType( idPlayer::Type ) ) {
|
|
static_cast< idPlayer * >( entities[ i ] )->Restart();
|
|
}
|
|
}
|
|
|
|
gamestate = GAMESTATE_ACTIVE;
|
|
|
|
Printf( "--------------------------------------\n" );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::MapRestart
|
|
===================
|
|
*/
|
|
void idGameLocal::MapRestart() {
|
|
if ( common->IsClient() ) {
|
|
LocalMapRestart();
|
|
} else {
|
|
idBitMsg msg;
|
|
session->GetActingGameStateLobbyBase().SendReliable( GAME_RELIABLE_MESSAGE_RESTART, msg, false );
|
|
|
|
LocalMapRestart();
|
|
mpGame.MapRestart();
|
|
}
|
|
|
|
if ( common->IsMultiplayer() ) {
|
|
gameLocal.mpGame.ReloadScoreboard();
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::MapRestart_f
|
|
===================
|
|
*/
|
|
void idGameLocal::MapRestart_f( const idCmdArgs &args ) {
|
|
if ( !common->IsMultiplayer() || common->IsClient() ) {
|
|
common->Printf( "server is not running - use spawnServer\n" );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "spawnServer\n" );
|
|
return;
|
|
}
|
|
|
|
gameLocal.MapRestart( );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::MapPopulate
|
|
===================
|
|
*/
|
|
void idGameLocal::MapPopulate() {
|
|
|
|
if ( common->IsMultiplayer() ) {
|
|
cvarSystem->SetCVarBool( "r_skipSpecular", false );
|
|
}
|
|
// parse the key/value pairs and spawn entities
|
|
SpawnMapEntities();
|
|
|
|
// mark location entities in all connected areas
|
|
SpreadLocations();
|
|
|
|
// prepare the list of randomized initial spawn spots
|
|
RandomizeInitialSpawns();
|
|
|
|
// spawnCount - 1 is the number of entities spawned into the map, their indexes started at MAX_CLIENTS (included)
|
|
// mapSpawnCount is used as the max index of map entities, it's the first index of non-map entities
|
|
mapSpawnCount = MAX_CLIENTS + spawnCount - 1;
|
|
|
|
// execute pending events before the very first game frame
|
|
// this makes sure the map script main() function is called
|
|
// before the physics are run so entities can bind correctly
|
|
Printf( "==== Processing events ====\n" );
|
|
idEvent::ServiceEvents();
|
|
|
|
// Must set GAME_FPS for script after populating, because some maps run their own scripts
|
|
// when spawning the world, and GAME_FPS will not be found before then.
|
|
SetScriptFPS( com_engineHz_latched );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::InitFromNewMap
|
|
===================
|
|
*/
|
|
void idGameLocal::InitFromNewMap( const char *mapName, idRenderWorld *renderWorld, idSoundWorld *soundWorld, int gameMode, int randseed ) {
|
|
|
|
this->gameType = (gameType_t)idMath::ClampInt( GAME_SP, GAME_COUNT-1, gameMode );
|
|
|
|
if ( mapFileName.Length() ) {
|
|
MapShutdown();
|
|
}
|
|
|
|
Printf( "----------- Game Map Init ------------\n" );
|
|
|
|
gamestate = GAMESTATE_STARTUP;
|
|
|
|
gameRenderWorld = renderWorld;
|
|
gameSoundWorld = soundWorld;
|
|
|
|
if ( common->IsMultiplayer() ) {
|
|
g_skill.SetInteger( 1 );
|
|
} else {
|
|
g_skill.SetInteger( idMath::ClampInt( 0, 3, g_skill.GetInteger() ) );
|
|
}
|
|
|
|
LoadMap( mapName, randseed );
|
|
|
|
InitScriptForMap();
|
|
|
|
MapPopulate();
|
|
|
|
mpGame.Reset();
|
|
mpGame.Precache();
|
|
|
|
SyncPlayersWithLobbyUsers( true );
|
|
|
|
// free up any unused animations
|
|
animationLib.FlushUnusedAnims();
|
|
|
|
gamestate = GAMESTATE_ACTIVE;
|
|
|
|
Printf( "--------------------------------------\n" );
|
|
}
|
|
|
|
/*
|
|
=================
|
|
idGameLocal::InitFromSaveGame
|
|
=================
|
|
*/
|
|
bool idGameLocal::InitFromSaveGame( const char *mapName, idRenderWorld *renderWorld, idSoundWorld *soundWorld, idFile * saveGameFile, idFile * stringTableFile, int saveGameVersion ) {
|
|
int i;
|
|
int num;
|
|
idEntity *ent;
|
|
idDict si;
|
|
|
|
if ( mapFileName.Length() ) {
|
|
MapShutdown();
|
|
}
|
|
|
|
Printf( "------- Game Map Init SaveGame -------\n" );
|
|
|
|
gamestate = GAMESTATE_STARTUP;
|
|
|
|
gameRenderWorld = renderWorld;
|
|
gameSoundWorld = soundWorld;
|
|
|
|
SetScriptFPS( com_engineHz_latched );
|
|
|
|
// load the map needed for this savegame
|
|
LoadMap( mapName, 0 );
|
|
|
|
idFile_SaveGamePipelined * pipelineFile = new (TAG_SAVEGAMES) idFile_SaveGamePipelined();
|
|
pipelineFile->OpenForReading( saveGameFile );
|
|
idRestoreGame savegame( pipelineFile, stringTableFile, saveGameVersion );
|
|
|
|
// Create the list of all objects in the game
|
|
savegame.CreateObjects();
|
|
|
|
// Load the idProgram, also checking to make sure scripting hasn't changed since the savegame
|
|
if ( program.Restore( &savegame ) == false ) {
|
|
|
|
// Abort the load process, and let the session know so that it can restart the level
|
|
// with the player persistent data.
|
|
savegame.DeleteObjects();
|
|
program.Restart();
|
|
return false;
|
|
}
|
|
|
|
savegame.ReadInt( i );
|
|
g_skill.SetInteger( i );
|
|
|
|
// precache any media specified in the map
|
|
savegame.ReadDecls();
|
|
|
|
savegame.ReadDict( &si );
|
|
SetServerInfo( si );
|
|
|
|
savegame.ReadInt( numClients );
|
|
for( i = 0; i < numClients; i++ ) {
|
|
//savegame.ReadUsercmd( usercmds[ i ] );
|
|
// Now that usercmds are handled by the idUserCmdMgr,
|
|
// do we need another solution here?
|
|
usercmd_t dummy;
|
|
savegame.ReadUsercmd( dummy );
|
|
savegame.ReadDict( &persistentPlayerInfo[ i ] );
|
|
}
|
|
|
|
for( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( entities[ i ] ) );
|
|
savegame.ReadInt( spawnIds[ i ] );
|
|
|
|
// restore the entityNumber
|
|
if ( entities[ i ] != NULL ) {
|
|
entities[ i ]->entityNumber = i;
|
|
}
|
|
}
|
|
|
|
// Connect players with lobby users
|
|
// There should only be 1 player and 1 lobby user, but I'm using a loop just to be safe
|
|
idLobbyBase & lobby = session->GetActingGameStateLobbyBase();
|
|
int numLobbyUsers = lobby.GetNumLobbyUsers();
|
|
int lobbyUserNum = 0;
|
|
assert( numLobbyUsers == 1 );
|
|
for ( int i = 0; i < MAX_PLAYERS && lobbyUserNum < numLobbyUsers; i++ ) {
|
|
if ( entities[i] == NULL ) {
|
|
continue;
|
|
}
|
|
lobbyUserIDs[i] = lobby.GetLobbyUserIdByOrdinal( lobbyUserNum++ );
|
|
}
|
|
|
|
savegame.ReadInt( firstFreeEntityIndex[0] );
|
|
savegame.ReadInt( num_entities );
|
|
|
|
// enityHash is restored by idEntity::Restore setting the entity name.
|
|
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( world ) );
|
|
|
|
savegame.ReadInt( num );
|
|
for( i = 0; i < num; i++ ) {
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( ent ) );
|
|
assert( ent );
|
|
if ( ent ) {
|
|
ent->spawnNode.AddToEnd( spawnedEntities );
|
|
}
|
|
}
|
|
|
|
savegame.ReadInt( num );
|
|
for( i = 0; i < num; i++ ) {
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( ent ) );
|
|
assert( ent );
|
|
if ( ent ) {
|
|
ent->activeNode.AddToEnd( activeEntities );
|
|
}
|
|
}
|
|
|
|
savegame.ReadInt( numEntitiesToDeactivate );
|
|
savegame.ReadBool( sortPushers );
|
|
savegame.ReadBool( sortTeamMasters );
|
|
savegame.ReadDict( &persistentLevelInfo );
|
|
|
|
for( i = 0; i < MAX_GLOBAL_SHADER_PARMS; i++ ) {
|
|
savegame.ReadFloat( globalShaderParms[ i ] );
|
|
}
|
|
|
|
savegame.ReadInt( i );
|
|
random.SetSeed( i );
|
|
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( frameCommandThread ) );
|
|
|
|
// clip
|
|
// push
|
|
// pvs
|
|
|
|
// testmodel = "<NULL>"
|
|
// testFx = "<NULL>"
|
|
|
|
savegame.ReadString( sessionCommand );
|
|
|
|
// FIXME: save smoke particles
|
|
|
|
savegame.ReadBool( inCinematic );
|
|
|
|
savegame.ReadInt( (int &)gameType );
|
|
|
|
savegame.ReadInt( framenum );
|
|
savegame.ReadInt( previousTime );
|
|
savegame.ReadInt( time );
|
|
|
|
savegame.ReadInt( vacuumAreaNum );
|
|
|
|
savegame.ReadInt( entityDefBits );
|
|
|
|
// the localClientNum member of idGameLocal was removed,
|
|
// but to preserve savegame compatibility, we still need
|
|
// to read an int here even though it's not used.
|
|
int dummyLocalClientNum = 0;
|
|
savegame.ReadInt( dummyLocalClientNum );
|
|
|
|
// snapshotEntities is used for multiplayer only
|
|
|
|
savegame.ReadInt( realClientTime );
|
|
savegame.ReadBool( isNewFrame );
|
|
savegame.ReadFloat( clientSmoothing );
|
|
|
|
portalSkyEnt.Restore( &savegame );
|
|
savegame.ReadBool( portalSkyActive );
|
|
|
|
fast.Restore( &savegame );
|
|
slow.Restore( &savegame );
|
|
|
|
framenum = MSEC_TO_FRAME_FLOOR( fast.time );
|
|
|
|
int blah;
|
|
savegame.ReadInt( blah );
|
|
slowmoState = (slowmoState_t)blah;
|
|
|
|
savegame.ReadFloat( slowmoScale );
|
|
savegame.ReadBool( quickSlowmoReset );
|
|
|
|
if ( gameSoundWorld ) {
|
|
gameSoundWorld->SetSlowmoSpeed( slowmoScale );
|
|
}
|
|
|
|
savegame.ReadBool( mapCycleLoaded );
|
|
savegame.ReadInt( spawnCount );
|
|
|
|
savegame.ReadInt( num );
|
|
if ( num ) {
|
|
if ( num != gameRenderWorld->NumAreas() ) {
|
|
savegame.Error( "idGameLocal::InitFromSaveGame: number of areas in map differs from save game." );
|
|
}
|
|
|
|
locationEntities = new (TAG_GAME) idLocationEntity *[ num ];
|
|
for( i = 0; i < num; i++ ) {
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( locationEntities[ i ] ) );
|
|
}
|
|
}
|
|
|
|
savegame.ReadObject( reinterpret_cast<idClass *&>( camera ) );
|
|
|
|
savegame.ReadMaterial( globalMaterial );
|
|
|
|
lastAIAlertEntity.Restore( &savegame );
|
|
savegame.ReadInt( lastAIAlertTime );
|
|
|
|
savegame.ReadDict( &spawnArgs );
|
|
|
|
savegame.ReadInt( playerPVS.i );
|
|
savegame.ReadInt( (int &)playerPVS.h );
|
|
savegame.ReadInt( playerConnectedAreas.i );
|
|
savegame.ReadInt( (int &)playerConnectedAreas.h );
|
|
|
|
savegame.ReadVec3( gravity );
|
|
|
|
// gamestate is restored after restoring everything else
|
|
|
|
savegame.ReadBool( influenceActive );
|
|
savegame.ReadInt( nextGibTime );
|
|
|
|
// spawnSpots
|
|
// initialSpots
|
|
// currentInitialSpot
|
|
// newInfo
|
|
// makingBuild
|
|
// shakeSounds
|
|
|
|
// Read out pending events
|
|
idEvent::Restore( &savegame );
|
|
|
|
savegame.RestoreObjects();
|
|
|
|
mpGame.Reset();
|
|
|
|
mpGame.Precache();
|
|
|
|
// free up any unused animations
|
|
animationLib.FlushUnusedAnims();
|
|
|
|
gamestate = GAMESTATE_ACTIVE;
|
|
|
|
Printf( "--------------------------------------\n" );
|
|
|
|
delete pipelineFile;
|
|
pipelineFile = NULL;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::MapClear
|
|
===========
|
|
*/
|
|
void idGameLocal::MapClear( bool clearClients ) {
|
|
int i;
|
|
|
|
for( i = ( clearClients ? 0 : MAX_CLIENTS ); i < MAX_GENTITIES; i++ ) {
|
|
delete entities[ i ];
|
|
// ~idEntity is in charge of setting the pointer to NULL
|
|
// it will also clear pending events for this entity
|
|
assert( !entities[ i ] );
|
|
spawnIds[ i ] = -1;
|
|
}
|
|
|
|
entityHash.Clear( 1024, MAX_GENTITIES );
|
|
|
|
if ( !clearClients ) {
|
|
// add back the hashes of the clients
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
if ( !entities[ i ] ) {
|
|
continue;
|
|
}
|
|
entityHash.Add( entityHash.GenerateKey( entities[ i ]->name.c_str(), true ), i );
|
|
}
|
|
}
|
|
|
|
delete frameCommandThread;
|
|
frameCommandThread = NULL;
|
|
|
|
if ( editEntities ) {
|
|
delete editEntities;
|
|
editEntities = NULL;
|
|
}
|
|
|
|
delete[] locationEntities;
|
|
locationEntities = NULL;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::MapShutdown
|
|
============
|
|
*/
|
|
void idGameLocal::MapShutdown() {
|
|
Printf( "--------- Game Map Shutdown ----------\n" );
|
|
|
|
gamestate = GAMESTATE_SHUTDOWN;
|
|
|
|
if ( gameRenderWorld ) {
|
|
// clear any debug lines, text, and polygons
|
|
gameRenderWorld->DebugClearLines( 0 );
|
|
gameRenderWorld->DebugClearPolygons( 0 );
|
|
}
|
|
|
|
// clear out camera if we're in a cinematic
|
|
if ( inCinematic ) {
|
|
camera = NULL;
|
|
inCinematic = false;
|
|
}
|
|
|
|
MapClear( true );
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
// reset the script to the state it was before the map was started
|
|
program.Restart();
|
|
|
|
if ( smokeParticles ) {
|
|
smokeParticles->Shutdown();
|
|
}
|
|
|
|
pvs.Shutdown();
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
clip.Shutdown();
|
|
idClipModel::ClearTraceModelCache();
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
collisionModelManager->FreeMap(); // Fixes an issue where when maps were reloaded the materials wouldn't get their surfaceFlags re-set. Now we free the map collision model forcing materials to be reparsed.
|
|
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
ShutdownAsyncNetwork();
|
|
|
|
idStrStatic< MAX_OSPATH > mapName = mapFileName;
|
|
mapName.StripPath();
|
|
mapName.StripFileExtension();
|
|
fileSystem->UnloadMapResources( mapName );
|
|
|
|
mapFileName.Clear();
|
|
|
|
gameRenderWorld = NULL;
|
|
gameSoundWorld = NULL;
|
|
|
|
gamestate = GAMESTATE_NOMAP;
|
|
|
|
Printf( "--------------------------------------\n" );
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::GetAimAssistAngles
|
|
========================
|
|
*/
|
|
void idGameLocal::GetAimAssistAngles( idAngles & angles ) {
|
|
angles.Zero();
|
|
|
|
// Take a look at serializing this to the clients
|
|
idPlayer * player = GetLocalPlayer();
|
|
if ( player == NULL ) {
|
|
return;
|
|
}
|
|
|
|
idAimAssist * aimAssist = player->GetAimAssist();
|
|
if ( aimAssist == NULL ) {
|
|
return;
|
|
}
|
|
|
|
aimAssist->GetAngleCorrection( angles );
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::GetAimAssistSensitivity
|
|
========================
|
|
*/
|
|
float idGameLocal::GetAimAssistSensitivity() {
|
|
// Take a look at serializing this to the clients
|
|
idPlayer * player = GetLocalPlayer();
|
|
if ( player == NULL ) {
|
|
return 1.0f;
|
|
}
|
|
|
|
idAimAssist * aimAssist = player->GetAimAssist();
|
|
if ( aimAssist == NULL ) {
|
|
return 1.0f;
|
|
}
|
|
|
|
return aimAssist->GetFrictionScalar();
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::MapPeerToClient
|
|
========================
|
|
*/
|
|
int idGameLocal::MapPeerToClient( int peer ) const {
|
|
idLobbyBase & lobby = session->GetActingGameStateLobbyBase();
|
|
for ( int userNum = 0; userNum < lobbyUserIDs.Num(); ++userNum ) {
|
|
const int peerForUser = lobby.PeerIndexFromLobbyUser( lobbyUserIDs[userNum] );
|
|
if ( peerForUser == peer ) {
|
|
return userNum;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::GetLocalClientNum
|
|
========================
|
|
*/
|
|
int idGameLocal::GetLocalClientNum() const {
|
|
localUserHandle_t localUserHandle = session->GetSignInManager().GetMasterLocalUserHandle();
|
|
if ( !localUserHandle.IsValid() ) {
|
|
return 0;
|
|
}
|
|
|
|
for ( int i = 0; i < lobbyUserIDs.Num(); i++ ) {
|
|
lobbyUserID_t lobbyUserID = lobbyUserIDs[i];
|
|
if ( localUserHandle == lobbyUserID.GetLocalUserHandle() ) {
|
|
return i;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::Preload
|
|
|
|
===================
|
|
*/
|
|
void idGameLocal::Preload( const idPreloadManifest &manifest ) {
|
|
animationLib.Preload( manifest );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::CacheDictionaryMedia
|
|
|
|
This is called after parsing an EntityDef and for each entity spawnArgs before
|
|
merging the entitydef. It could be done post-merge, but that would
|
|
avoid the fast pre-cache check associated with each entityDef
|
|
===================
|
|
*/
|
|
void idGameLocal::CacheDictionaryMedia( const idDict *dict ) {
|
|
const idKeyValue *kv;
|
|
|
|
kv = dict->MatchPrefix( "model" );
|
|
while( kv ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->MediaPrint( "Precaching model %s\n", kv->GetValue().c_str() );
|
|
// precache model/animations
|
|
if ( declManager->FindType( DECL_MODELDEF, kv->GetValue(), false ) == NULL ) {
|
|
// precache the render model
|
|
renderModelManager->FindModel( kv->GetValue() );
|
|
// precache .cm files only
|
|
collisionModelManager->LoadModel( kv->GetValue() );
|
|
}
|
|
}
|
|
kv = dict->MatchPrefix( "model", kv );
|
|
}
|
|
|
|
kv = dict->FindKey( "s_shader" );
|
|
if ( kv != NULL && kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_SOUND, kv->GetValue() );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "snd", NULL );
|
|
while ( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_SOUND, kv->GetValue() );
|
|
}
|
|
kv = dict->MatchPrefix( "snd", kv );
|
|
}
|
|
|
|
|
|
kv = dict->MatchPrefix( "gui", NULL );
|
|
while( kv ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
if ( !idStr::Icmp( kv->GetKey(), "gui_noninteractive" )
|
|
|| !idStr::Icmpn( kv->GetKey(), "gui_parm", 8 )
|
|
|| !idStr::Icmp( kv->GetKey(), "gui_inventory" ) ) {
|
|
// unfortunate flag names, they aren't actually a gui
|
|
} else {
|
|
declManager->MediaPrint( "Precaching gui %s\n", kv->GetValue().c_str() );
|
|
idUserInterface *gui = uiManager->Alloc();
|
|
if ( gui ) {
|
|
gui->InitFromFile( kv->GetValue() );
|
|
uiManager->DeAlloc( gui );
|
|
}
|
|
}
|
|
}
|
|
kv = dict->MatchPrefix( "gui", kv );
|
|
}
|
|
|
|
kv = dict->FindKey( "texture" );
|
|
if ( kv != NULL && kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_MATERIAL, kv->GetValue() );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "mtr", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_MATERIAL, kv->GetValue() );
|
|
}
|
|
kv = dict->MatchPrefix( "mtr", kv );
|
|
}
|
|
|
|
// handles hud icons
|
|
kv = dict->MatchPrefix( "inv_icon", NULL );
|
|
while ( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_MATERIAL, kv->GetValue() );
|
|
}
|
|
kv = dict->MatchPrefix( "inv_icon", kv );
|
|
}
|
|
|
|
// handles teleport fx.. this is not ideal but the actual decision on which fx to use
|
|
// is handled by script code based on the teleport number
|
|
kv = dict->MatchPrefix( "teleport", NULL );
|
|
if ( kv != NULL && kv->GetValue().Length() ) {
|
|
int teleportType = atoi( kv->GetValue() );
|
|
const char *p = ( teleportType ) ? va( "fx/teleporter%i.fx", teleportType ) : "fx/teleporter.fx";
|
|
declManager->FindType( DECL_FX, p );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "fx", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->MediaPrint( "Precaching fx %s\n", kv->GetValue().c_str() );
|
|
declManager->FindType( DECL_FX, kv->GetValue() );
|
|
}
|
|
kv = dict->MatchPrefix( "fx", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "smoke", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
idStr prtName = kv->GetValue();
|
|
int dash = prtName.Find('-');
|
|
if ( dash > 0 ) {
|
|
prtName = prtName.Left( dash );
|
|
}
|
|
declManager->FindType( DECL_PARTICLE, prtName );
|
|
}
|
|
kv = dict->MatchPrefix( "smoke", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "skin", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->MediaPrint( "Precaching skin %s\n", kv->GetValue().c_str() );
|
|
declManager->FindType( DECL_SKIN, kv->GetValue() );
|
|
}
|
|
kv = dict->MatchPrefix( "skin", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "def", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
FindEntityDef( kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "def", kv );
|
|
}
|
|
|
|
// Precache all available grabber "catch" damage decls
|
|
kv = dict->MatchPrefix( "def_damage", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
FindEntityDef( kv->GetValue() + "_catch", false );
|
|
}
|
|
kv = dict->MatchPrefix( "def_damage", kv );
|
|
}
|
|
|
|
// Should have been def_monster_damage!!
|
|
kv = dict->FindKey( "monster_damage" );
|
|
if ( kv != NULL && kv->GetValue().Length() ) {
|
|
FindEntityDef( kv->GetValue(), false );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "item", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
FindEntityDefDict( kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "item", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "pda_name", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_PDA, kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "pda_name", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "video", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_VIDEO, kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "video", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "audio", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_AUDIO, kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "audio", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "email", NULL );
|
|
while( kv != NULL ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_EMAIL, kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "email", kv );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::InitScriptForMap
|
|
============
|
|
*/
|
|
void idGameLocal::InitScriptForMap() {
|
|
// create a thread to run frame commands on
|
|
frameCommandThread = new idThread();
|
|
frameCommandThread->ManualDelete();
|
|
frameCommandThread->SetThreadName( "frameCommands" );
|
|
|
|
// run the main game script function (not the level specific main)
|
|
const function_t *func = program.FindFunction( SCRIPT_DEFAULTFUNC );
|
|
if ( func != NULL ) {
|
|
idThread *thread = new idThread( func );
|
|
if ( thread->Start() ) {
|
|
// thread has finished executing, so delete it
|
|
delete thread;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::SetScriptFPS
|
|
===================
|
|
*/
|
|
void idGameLocal::SetScriptFPS( const float engineHz ) {
|
|
idVarDef * fpsDef = program.GetDef( &type_float, "GAME_FPS", &def_namespace );
|
|
if ( fpsDef != NULL ) {
|
|
eval_t fpsValue;
|
|
fpsValue._float = engineHz;
|
|
fpsDef->SetValue( fpsValue, false );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetMPPlayerDefName
|
|
============
|
|
*/
|
|
const char * idGameLocal::GetMPPlayerDefName() const {
|
|
if ( gameType == GAME_CTF ) {
|
|
return "player_doommarine_ctf";
|
|
}
|
|
|
|
return "player_doommarine_mp";
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SpawnPlayer
|
|
============
|
|
*/
|
|
void idGameLocal::SpawnPlayer( int clientNum ) {
|
|
idEntity *ent;
|
|
idDict args;
|
|
|
|
// they can connect
|
|
Printf( "SpawnPlayer: %i\n", clientNum );
|
|
|
|
args.SetInt( "spawn_entnum", clientNum );
|
|
args.Set( "name", va( "player%d", clientNum + 1 ) );
|
|
if ( common->IsMultiplayer() ) {
|
|
args.Set( "classname", GetMPPlayerDefName() );
|
|
} else {
|
|
// precache the player
|
|
args.Set( "classname", gameLocal.world->spawnArgs.GetString( "def_player", "player_doommarine" ) );
|
|
}
|
|
|
|
// It's important that we increment numClients before calling SpawnEntityDef, because some
|
|
// entities want to check gameLocal.numClients to see who to operate on (such as target_removeweapons)
|
|
if ( clientNum >= numClients ) {
|
|
numClients = clientNum + 1;
|
|
}
|
|
|
|
if ( !SpawnEntityDef( args, &ent ) || clientNum >= MAX_GENTITIES || entities[ clientNum ] == NULL ) {
|
|
Error( "Failed to spawn player as '%s'", args.GetString( "classname" ) );
|
|
}
|
|
|
|
// make sure it's a compatible class
|
|
if ( !ent->IsType( idPlayer::Type ) ) {
|
|
Error( "'%s' spawned the player as a '%s'. Player spawnclass must be a subclass of idPlayer.", args.GetString( "classname" ), ent->GetClassname() );
|
|
}
|
|
mpGame.SpawnPlayer( clientNum );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetClientByNum
|
|
================
|
|
*/
|
|
idPlayer *idGameLocal::GetClientByNum( int current ) const {
|
|
if ( current < 0 || current >= numClients ) {
|
|
current = 0;
|
|
}
|
|
if ( entities[current] ) {
|
|
return static_cast<idPlayer *>( entities[ current ] );
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetNextClientNum
|
|
================
|
|
*/
|
|
int idGameLocal::GetNextClientNum( int _current ) const {
|
|
int i, current;
|
|
|
|
current = 0;
|
|
for ( i = 0; i < numClients; i++) {
|
|
current = ( _current + i + 1 ) % numClients;
|
|
if ( entities[ current ] && entities[ current ]->IsType( idPlayer::Type ) ) {
|
|
return current;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetLocalPlayer
|
|
|
|
Nothing in the game tic should EVER make a decision based on what the
|
|
local client number is, it shouldn't even be aware that there is a
|
|
draw phase even happening. This just returns client 0, which will
|
|
be correct for single player.
|
|
================
|
|
*/
|
|
idPlayer *idGameLocal::GetLocalPlayer() const {
|
|
if ( GetLocalClientNum() < 0 ) {
|
|
return NULL;
|
|
}
|
|
|
|
if ( !entities[ GetLocalClientNum() ] || !entities[ GetLocalClientNum() ]->IsType( idPlayer::Type ) ) {
|
|
// not fully in game yet
|
|
return NULL;
|
|
}
|
|
return static_cast<idPlayer *>( entities[ GetLocalClientNum() ] );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SetupClientPVS
|
|
================
|
|
*/
|
|
pvsHandle_t idGameLocal::GetClientPVS( idPlayer *player, pvsType_t type ) {
|
|
if ( player->GetPrivateCameraView() ) {
|
|
return pvs.SetupCurrentPVS( player->GetPrivateCameraView()->GetPVSAreas(), player->GetPrivateCameraView()->GetNumPVSAreas() );
|
|
} else if ( camera ) {
|
|
return pvs.SetupCurrentPVS( camera->GetPVSAreas(), camera->GetNumPVSAreas() );
|
|
} else {
|
|
return pvs.SetupCurrentPVS( player->GetPVSAreas(), player->GetNumPVSAreas() );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SetupPlayerPVS
|
|
================
|
|
*/
|
|
void idGameLocal::SetupPlayerPVS() {
|
|
int i;
|
|
idEntity * ent;
|
|
idPlayer * player;
|
|
pvsHandle_t otherPVS, newPVS;
|
|
|
|
playerPVS.i = -1;
|
|
for ( i = 0; i < numClients; i++ ) {
|
|
ent = entities[i];
|
|
if ( !ent || !ent->IsType( idPlayer::Type ) ) {
|
|
continue;
|
|
}
|
|
|
|
player = static_cast<idPlayer *>(ent);
|
|
|
|
if ( playerPVS.i == -1 ) {
|
|
playerPVS = GetClientPVS( player, PVS_NORMAL );
|
|
} else {
|
|
otherPVS = GetClientPVS( player, PVS_NORMAL );
|
|
newPVS = pvs.MergeCurrentPVS( playerPVS, otherPVS );
|
|
pvs.FreeCurrentPVS( playerPVS );
|
|
pvs.FreeCurrentPVS( otherPVS );
|
|
playerPVS = newPVS;
|
|
}
|
|
|
|
if ( playerConnectedAreas.i == -1 ) {
|
|
playerConnectedAreas = GetClientPVS( player, PVS_CONNECTED_AREAS );
|
|
} else {
|
|
otherPVS = GetClientPVS( player, PVS_CONNECTED_AREAS );
|
|
newPVS = pvs.MergeCurrentPVS( playerConnectedAreas, otherPVS );
|
|
pvs.FreeCurrentPVS( playerConnectedAreas );
|
|
pvs.FreeCurrentPVS( otherPVS );
|
|
playerConnectedAreas = newPVS;
|
|
}
|
|
|
|
// if portalSky is preset, then merge into pvs so we get rotating brushes, etc
|
|
if ( portalSkyEnt.GetEntity() ) {
|
|
idEntity *skyEnt = portalSkyEnt.GetEntity();
|
|
|
|
otherPVS = pvs.SetupCurrentPVS( skyEnt->GetPVSAreas(), skyEnt->GetNumPVSAreas() );
|
|
newPVS = pvs.MergeCurrentPVS( playerPVS, otherPVS );
|
|
pvs.FreeCurrentPVS( playerPVS );
|
|
pvs.FreeCurrentPVS( otherPVS );
|
|
playerPVS = newPVS;
|
|
|
|
otherPVS = pvs.SetupCurrentPVS( skyEnt->GetPVSAreas(), skyEnt->GetNumPVSAreas() );
|
|
newPVS = pvs.MergeCurrentPVS( playerConnectedAreas, otherPVS );
|
|
pvs.FreeCurrentPVS( playerConnectedAreas );
|
|
pvs.FreeCurrentPVS( otherPVS );
|
|
playerConnectedAreas = newPVS;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::FreePlayerPVS
|
|
================
|
|
*/
|
|
void idGameLocal::FreePlayerPVS() {
|
|
if ( playerPVS.i != -1 ) {
|
|
pvs.FreeCurrentPVS( playerPVS );
|
|
playerPVS.i = -1;
|
|
}
|
|
if ( playerConnectedAreas.i != -1 ) {
|
|
pvs.FreeCurrentPVS( playerConnectedAreas );
|
|
playerConnectedAreas.i = -1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::InPlayerPVS
|
|
|
|
should only be called during entity thinking and event handling
|
|
================
|
|
*/
|
|
bool idGameLocal::InPlayerPVS( idEntity *ent ) const {
|
|
if ( playerPVS.i == -1 ) {
|
|
return false;
|
|
}
|
|
return pvs.InCurrentPVS( playerPVS, ent->GetPVSAreas(), ent->GetNumPVSAreas() );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::InPlayerConnectedArea
|
|
|
|
should only be called during entity thinking and event handling
|
|
================
|
|
*/
|
|
bool idGameLocal::InPlayerConnectedArea( idEntity *ent ) const {
|
|
if ( playerConnectedAreas.i == -1 ) {
|
|
return false;
|
|
}
|
|
return pvs.InCurrentPVS( playerConnectedAreas, ent->GetPVSAreas(), ent->GetNumPVSAreas() );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::UpdateGravity
|
|
================
|
|
*/
|
|
void idGameLocal::UpdateGravity() {
|
|
idEntity *ent;
|
|
|
|
if ( g_gravity.IsModified() ) {
|
|
if ( g_gravity.GetFloat() == 0.0f ) {
|
|
g_gravity.SetFloat( 1.0f );
|
|
}
|
|
gravity.Set( 0, 0, -g_gravity.GetFloat() );
|
|
|
|
// update all physics objects
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
if ( ent->IsType( idAFEntity_Generic::Type ) ) {
|
|
idPhysics *phys = ent->GetPhysics();
|
|
if ( phys ) {
|
|
phys->SetGravity( gravity );
|
|
}
|
|
}
|
|
}
|
|
g_gravity.ClearModified();
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetGravity
|
|
================
|
|
*/
|
|
const idVec3 &idGameLocal::GetGravity() const {
|
|
return gravity;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SortActiveEntityList
|
|
|
|
Sorts the active entity list such that pushing entities come first,
|
|
actors come next and physics team slaves appear after their master.
|
|
================
|
|
*/
|
|
void idGameLocal::SortActiveEntityList() {
|
|
idEntity *ent, *next_ent, *master, *part;
|
|
|
|
// if the active entity list needs to be reordered to place physics team masters at the front
|
|
if ( sortTeamMasters ) {
|
|
for ( ent = activeEntities.Next(); ent != NULL; ent = next_ent ) {
|
|
next_ent = ent->activeNode.Next();
|
|
master = ent->GetTeamMaster();
|
|
if ( master && master == ent ) {
|
|
ent->activeNode.Remove();
|
|
ent->activeNode.AddToFront( activeEntities );
|
|
}
|
|
}
|
|
}
|
|
|
|
// if the active entity list needs to be reordered to place pushers at the front
|
|
if ( sortPushers ) {
|
|
|
|
for ( ent = activeEntities.Next(); ent != NULL; ent = next_ent ) {
|
|
next_ent = ent->activeNode.Next();
|
|
master = ent->GetTeamMaster();
|
|
if ( !master || master == ent ) {
|
|
// check if there is an actor on the team
|
|
for ( part = ent; part != NULL; part = part->GetNextTeamEntity() ) {
|
|
if ( part->GetPhysics()->IsType( idPhysics_Actor::Type ) ) {
|
|
break;
|
|
}
|
|
}
|
|
// if there is an actor on the team
|
|
if ( part ) {
|
|
ent->activeNode.Remove();
|
|
ent->activeNode.AddToFront( activeEntities );
|
|
}
|
|
}
|
|
}
|
|
|
|
for ( ent = activeEntities.Next(); ent != NULL; ent = next_ent ) {
|
|
next_ent = ent->activeNode.Next();
|
|
master = ent->GetTeamMaster();
|
|
if ( !master || master == ent ) {
|
|
// check if there is an entity on the team using parametric physics
|
|
for ( part = ent; part != NULL; part = part->GetNextTeamEntity() ) {
|
|
if ( part->GetPhysics()->IsType( idPhysics_Parametric::Type ) ) {
|
|
break;
|
|
}
|
|
}
|
|
// if there is an entity on the team using parametric physics
|
|
if ( part ) {
|
|
ent->activeNode.Remove();
|
|
ent->activeNode.AddToFront( activeEntities );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sortTeamMasters = false;
|
|
sortPushers = false;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::SetInterpolation
|
|
========================
|
|
*/
|
|
void idGameLocal::SetInterpolation( const float fraction, const int serverGameMS, const int ssStartTime, const int ssEndTime ) {
|
|
netInterpolationInfo.previousServerGameMs = netInterpolationInfo.serverGameMs;
|
|
netInterpolationInfo.pct = fraction;
|
|
netInterpolationInfo.serverGameMs = serverGameMS;
|
|
netInterpolationInfo.ssStartTime = ssStartTime;
|
|
netInterpolationInfo.ssEndTime = ssEndTime;
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RunTimeGroup2
|
|
================
|
|
*/
|
|
void idGameLocal::RunTimeGroup2( idUserCmdMgr & userCmdMgr ) {
|
|
idEntity *ent;
|
|
int num = 0;
|
|
|
|
SelectTimeGroup( true );
|
|
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
if ( ent->timeGroup != TIME_GROUP2 ) {
|
|
continue;
|
|
}
|
|
RunEntityThink( *ent, userCmdMgr );
|
|
num++;
|
|
}
|
|
|
|
SelectTimeGroup( false );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RunEntityThink
|
|
================
|
|
*/
|
|
void idGameLocal::RunEntityThink( idEntity & ent, idUserCmdMgr & userCmdMgr ) {
|
|
if ( ent.entityNumber < MAX_PLAYERS ) {
|
|
// Players may run more than one think per frame in MP,
|
|
// if there is a large buffer of usercmds from the network.
|
|
// Players will always run exactly one think in singleplayer.
|
|
RunAllUserCmdsForPlayer( userCmdMgr, ent.entityNumber );
|
|
} else {
|
|
// Non-player entities always run one think.
|
|
ent.Think();
|
|
}
|
|
}
|
|
|
|
idCVar g_recordTrace( "g_recordTrace", "0", CVAR_BOOL, "" );
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RunFrame
|
|
================
|
|
*/
|
|
void idGameLocal::RunFrame( idUserCmdMgr & cmdMgr, gameReturn_t & ret ) {
|
|
idEntity * ent;
|
|
int num;
|
|
float ms;
|
|
idTimer timer_think, timer_events, timer_singlethink;
|
|
|
|
idPlayer *player;
|
|
const renderView_t *view;
|
|
|
|
if ( g_recordTrace.GetBool() ) {
|
|
bool result = BeginTraceRecording( "e:\\gametrace.pix2" );
|
|
if ( !result ) {
|
|
//idLib::Printf( "BeginTraceRecording: error %d\n", GetLastError() );
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
if ( common->IsMultiplayer() ) {
|
|
assert( !common->IsClient() );
|
|
}
|
|
#endif
|
|
|
|
if ( gameRenderWorld == NULL ) {
|
|
return;
|
|
}
|
|
|
|
SyncPlayersWithLobbyUsers( false );
|
|
ServerSendNetworkSyncCvars();
|
|
|
|
player = GetLocalPlayer();
|
|
|
|
if ( !common->IsMultiplayer() && g_stopTime.GetBool() ) {
|
|
// clear any debug lines from a previous frame
|
|
gameRenderWorld->DebugClearLines( time + 1 );
|
|
|
|
// set the user commands for this frame
|
|
if ( player ) {
|
|
player->HandleUserCmds( cmdMgr.GetUserCmdForPlayer( GetLocalClientNum() ) );
|
|
cmdMgr.MakeReadPtrCurrentForPlayer( GetLocalClientNum() );
|
|
player->Think();
|
|
}
|
|
} else {
|
|
// update the game time
|
|
framenum++;
|
|
fast.previousTime = FRAME_TO_MSEC( framenum - 1 );
|
|
fast.time = FRAME_TO_MSEC( framenum );
|
|
fast.realClientTime = fast.time;
|
|
SetServerGameTimeMs( fast.time );
|
|
|
|
ComputeSlowScale();
|
|
|
|
slow.previousTime = slow.time;
|
|
slow.time += idMath::Ftoi( ( fast.time - fast.previousTime ) * slowmoScale );
|
|
slow.realClientTime = slow.time;
|
|
|
|
SelectTimeGroup( false );
|
|
|
|
#ifdef GAME_DLL
|
|
// allow changing SIMD usage on the fly
|
|
if ( com_forceGenericSIMD.IsModified() ) {
|
|
idSIMD::InitProcessor( "game", com_forceGenericSIMD.GetBool() );
|
|
}
|
|
#endif
|
|
|
|
// make sure the random number counter is used each frame so random events
|
|
// are influenced by the player's actions
|
|
random.RandomInt();
|
|
|
|
if ( player ) {
|
|
// update the renderview so that any gui videos play from the right frame
|
|
view = player->GetRenderView();
|
|
if ( view ) {
|
|
gameRenderWorld->SetRenderView( view );
|
|
}
|
|
}
|
|
|
|
// clear any debug lines from a previous frame
|
|
gameRenderWorld->DebugClearLines( time );
|
|
|
|
// clear any debug polygons from a previous frame
|
|
gameRenderWorld->DebugClearPolygons( time );
|
|
|
|
// free old smoke particles
|
|
smokeParticles->FreeSmokes();
|
|
|
|
// process events on the server
|
|
ServerProcessEntityNetworkEventQueue();
|
|
|
|
// update our gravity vector if needed.
|
|
UpdateGravity();
|
|
|
|
// create a merged pvs for all players
|
|
SetupPlayerPVS();
|
|
|
|
// sort the active entity list
|
|
SortActiveEntityList();
|
|
|
|
timer_think.Clear();
|
|
timer_think.Start();
|
|
|
|
// let entities think
|
|
if ( g_timeentities.GetFloat() ) {
|
|
num = 0;
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
if ( g_cinematic.GetBool() && inCinematic && !ent->cinematic ) {
|
|
ent->GetPhysics()->UpdateTime( time );
|
|
continue;
|
|
}
|
|
timer_singlethink.Clear();
|
|
timer_singlethink.Start();
|
|
RunEntityThink( *ent, cmdMgr );
|
|
timer_singlethink.Stop();
|
|
ms = timer_singlethink.Milliseconds();
|
|
if ( ms >= g_timeentities.GetFloat() ) {
|
|
Printf( "%d: entity '%s': %.1f ms\n", time, ent->name.c_str(), ms );
|
|
}
|
|
num++;
|
|
}
|
|
} else {
|
|
if ( inCinematic ) {
|
|
num = 0;
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
if ( g_cinematic.GetBool() && !ent->cinematic ) {
|
|
ent->GetPhysics()->UpdateTime( time );
|
|
continue;
|
|
}
|
|
RunEntityThink( *ent, cmdMgr );
|
|
num++;
|
|
}
|
|
} else {
|
|
num = 0;
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
if ( ent->timeGroup != TIME_GROUP1 ) {
|
|
continue;
|
|
}
|
|
RunEntityThink( *ent, cmdMgr );
|
|
num++;
|
|
}
|
|
}
|
|
}
|
|
|
|
RunTimeGroup2( cmdMgr );
|
|
|
|
// Run catch-up for any client projectiles.
|
|
// This is done after the main think so that all projectiles will be up-to-date
|
|
// when snapshots are created.
|
|
//if ( common->IsMultiplayer() ) {
|
|
//while ( SimulateProjectiles() ) {
|
|
//clientGame.gameLibEffects.Update( clientGame.GetGameMs(), clientGame.GetGameMsPerFrame(), clientGame.GetServerGameTime() );
|
|
//}
|
|
//}
|
|
|
|
|
|
// remove any entities that have stopped thinking
|
|
if ( numEntitiesToDeactivate ) {
|
|
idEntity *next_ent;
|
|
int c = 0;
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = next_ent ) {
|
|
next_ent = ent->activeNode.Next();
|
|
if ( !ent->thinkFlags ) {
|
|
ent->activeNode.Remove();
|
|
c++;
|
|
}
|
|
}
|
|
//assert( numEntitiesToDeactivate == c );
|
|
numEntitiesToDeactivate = 0;
|
|
}
|
|
|
|
timer_think.Stop();
|
|
timer_events.Clear();
|
|
timer_events.Start();
|
|
|
|
// service any pending events
|
|
idEvent::ServiceEvents();
|
|
|
|
// service pending fast events
|
|
SelectTimeGroup( true );
|
|
idEvent::ServiceFastEvents();
|
|
SelectTimeGroup( false );
|
|
|
|
timer_events.Stop();
|
|
|
|
// free the player pvs
|
|
FreePlayerPVS();
|
|
|
|
// do multiplayer related stuff
|
|
if ( common->IsMultiplayer() ) {
|
|
mpGame.Run();
|
|
}
|
|
|
|
// display how long it took to calculate the current game frame
|
|
if ( g_frametime.GetBool() ) {
|
|
Printf( "game %d: all:%.1f th:%.1f ev:%.1f %d ents \n",
|
|
time, timer_think.Milliseconds() + timer_events.Milliseconds(),
|
|
timer_think.Milliseconds(), timer_events.Milliseconds(), num );
|
|
}
|
|
|
|
BuildReturnValue( ret );
|
|
}
|
|
|
|
// show any debug info for this frame
|
|
RunDebugInfo();
|
|
D_DrawDebugLines();
|
|
|
|
if ( g_recordTrace.GetBool() ) {
|
|
EndTraceRecording();
|
|
g_recordTrace.SetBool( false );
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
idGameLocal::BuildReturnValue
|
|
|
|
Fills out gameReturn_t, called on server and clients.
|
|
====================
|
|
*/
|
|
void idGameLocal::BuildReturnValue( gameReturn_t & ret ) {
|
|
ret.sessionCommand[0] = 0;
|
|
|
|
if ( GetLocalPlayer() != NULL ) {
|
|
GetLocalPlayer()->GetControllerShake( ret.vibrationLow, ret.vibrationHigh );
|
|
} else {
|
|
// Dedicated server?
|
|
ret.vibrationLow = 0;
|
|
ret.vibrationHigh = 0;
|
|
}
|
|
|
|
// see if a target_sessionCommand has forced a changelevel
|
|
if ( sessionCommand.Length() ) {
|
|
strncpy( ret.sessionCommand, sessionCommand, sizeof( ret.sessionCommand ) );
|
|
sessionCommand.Clear(); // Fixes a double loading bug for the e3 demo. Since we run the game thread in SMP mode, we could run this twice and re-set the ret.sessionCommand to a stale sessionCommand (since that doesn't get cleared until LoadMap is called!)
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
idGameLocal::RunSinglelUserCmd
|
|
|
|
Runs a Think or a ClientThink for a player. Will write the client's
|
|
position and firecount to the usercmd.
|
|
====================
|
|
*/
|
|
void idGameLocal::RunSingleUserCmd( usercmd_t & cmd, idPlayer & player ) {
|
|
player.HandleUserCmds( cmd );
|
|
|
|
// To fix the stupid chaingun script that depends on frame counts instead of
|
|
// milliseconds in the case of the server running at 60Hz and the client running
|
|
// at 120Hz, we need to set the script's GAME_FPS value to the client's effective rate.
|
|
// (I'd like to just fix the script to use milliseconds, but we don't want to change assets
|
|
// at this point.)
|
|
if ( !player.IsLocallyControlled() ) {
|
|
const float usercmdMillisecondDelta = player.usercmd.clientGameMilliseconds - player.oldCmd.clientGameMilliseconds;
|
|
const float clientEngineHz = 1000.0f / usercmdMillisecondDelta;
|
|
|
|
// Force to 60 or 120, those are the only values allowed in multiplayer.
|
|
const float forcedClientEngineHz = ( clientEngineHz < 90.0f ) ? 60.0f : 120.0f;
|
|
SetScriptFPS( forcedClientEngineHz );
|
|
}
|
|
|
|
if ( !common->IsMultiplayer() || common->IsServer() ) {
|
|
player.Think();
|
|
|
|
// Keep track of the client time of the usercmd we just ran. We will send this back to clients
|
|
// in a snapshot so they know when they can stop predicting certain things.
|
|
usercmdLastClientMilliseconds[ player.GetEntityNumber() ] = cmd.clientGameMilliseconds;
|
|
} else {
|
|
player.ClientThink( netInterpolationInfo.serverGameMs, netInterpolationInfo.pct, true );
|
|
}
|
|
|
|
// Since the client is authoritative on its position, we have to update the usercmd
|
|
// that will be sent over the network after the player thinks.
|
|
cmd.pos = player.usercmd.pos;
|
|
cmd.fireCount = player.GetClientFireCount();
|
|
if ( player.GetPhysics() ) {
|
|
cmd.speedSquared = player.GetPhysics()->GetLinearVelocity().LengthSqr();
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
idGameLocal::RunAllUserCmdsForPlayer
|
|
Runs a Think or ClientThink for each usercmd, but leaves a few cmds in the buffer
|
|
so that we have something to process while we wait for more from the network.
|
|
====================
|
|
*/
|
|
void idGameLocal::RunAllUserCmdsForPlayer( idUserCmdMgr & cmdMgr, const int playerNumber ) {
|
|
//idLib::Printf( "Frame: %i = [%i-%i] ", gameLocal.framenum,
|
|
//cmdMgr.readFrame[0], cmdMgr.writeFrame[0] ); // !@#
|
|
|
|
// Run thinks on any players that have queued up usercmds for networking.
|
|
assert( playerNumber < MAX_PLAYERS );
|
|
|
|
if ( entities[ playerNumber ] == NULL ) {
|
|
return;
|
|
}
|
|
|
|
idPlayer & player = static_cast< idPlayer & >( *entities[ playerNumber ] );
|
|
|
|
// Only run a single userCmd each game frame for local players, otherwise when
|
|
// we are running < 60fps things like footstep sounds may get started right on top
|
|
// of each other instead of spread out in time.
|
|
if ( player.IsLocallyControlled() ) {
|
|
if ( cmdMgr.HasUserCmdForPlayer( playerNumber ) ) {
|
|
if ( net_usercmd_timing_debug.GetBool() ) {
|
|
idLib::Printf( "[%d]Running local cmd for player %d, %d buffered.\n",
|
|
common->GetGameFrame(), playerNumber, cmdMgr.GetNumUnreadFrames( playerNumber ) );
|
|
}
|
|
RunSingleUserCmd( cmdMgr.GetWritableUserCmdForPlayer( playerNumber ), player );
|
|
} else {
|
|
RunSingleUserCmd( player.usercmd, player );
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Only the server runs remote commands.
|
|
if ( common->IsClient() ) {
|
|
return;
|
|
}
|
|
|
|
// Make sure to run a command for remote players. May duplicate the previous command
|
|
// if the server is running faster, or run an "empty" command if the buffer
|
|
// underflows.
|
|
if ( cmdMgr.HasUserCmdForPlayer( player.GetEntityNumber() ) ) {
|
|
const int clientTimeOfNextCommand = cmdMgr.GetNextUserCmdClientTime( playerNumber );
|
|
const int timeDeltaBetweenClientCommands = clientTimeOfNextCommand - lastCmdRunTimeOnClient[ playerNumber ];
|
|
const int timeSinceServerRanLastCommand = gameLocal.time - lastCmdRunTimeOnServer[ playerNumber ];
|
|
int clientTimeRunSoFar = 0;
|
|
|
|
// Handle clients who may be running faster than the server. Potentiallly runs multiple
|
|
// usercmds so that the server can catch up.
|
|
if ( timeDeltaBetweenClientCommands - timeSinceServerRanLastCommand <= 1 ) {
|
|
while ( clientTimeRunSoFar < ( timeSinceServerRanLastCommand - 1 ) && cmdMgr.HasUserCmdForPlayer( player.GetEntityNumber() ) ) {
|
|
usercmd_t & currentCommand = cmdMgr.GetWritableUserCmdForPlayer( playerNumber );
|
|
RunSingleUserCmd( currentCommand, player );
|
|
lastCmdRunTimeOnClient[ playerNumber ] = currentCommand.clientGameMilliseconds;
|
|
lastCmdRunTimeOnServer[ playerNumber ] = gameLocal.serverTime;
|
|
clientTimeRunSoFar += timeDeltaBetweenClientCommands;
|
|
if ( clientTimeRunSoFar == 0 ) {
|
|
// Hack to avoid infinite loop
|
|
break;
|
|
}
|
|
if ( net_usercmd_timing_debug.GetBool() ) {
|
|
idLib::Printf( "[%d]Running initial cmd for player %d, %d buffered, %d so far, %d serverDelta.\n",
|
|
common->GetGameFrame(), playerNumber, cmdMgr.GetNumUnreadFrames( playerNumber ),
|
|
clientTimeRunSoFar, timeSinceServerRanLastCommand );
|
|
}
|
|
}
|
|
} else {
|
|
// If we get here, it is likely that the client is running at 60Hz but the server
|
|
// is running at 120Hz. Duplicate the previous
|
|
// usercmd and run it so that the server doesn't starve.
|
|
usercmd_t lastPlayerCmd = player.usercmd;
|
|
RunSingleUserCmd( lastPlayerCmd, player );
|
|
if ( net_usercmd_timing_debug.GetBool() ) {
|
|
idLib::Printf( "[%d]Running duplicated command for player %d to prevent server from starving. clientCmdTimeDelta = %d, runCmdTimeDeltaOnServer = %d.\n",
|
|
common->GetGameFrame(), playerNumber, timeDeltaBetweenClientCommands, timeSinceServerRanLastCommand );
|
|
}
|
|
}
|
|
} else {
|
|
// Run an "empty" cmd, ran out of buffer.
|
|
usercmd_t emptyCmd = player.usercmd;
|
|
emptyCmd.forwardmove = 0;
|
|
emptyCmd.rightmove = 0;
|
|
RunSingleUserCmd( emptyCmd, player );
|
|
lastCmdRunTimeOnServer[ playerNumber ] = gameLocal.serverTime;
|
|
if ( net_usercmd_timing_debug.GetBool() ) {
|
|
idLib::Printf( "[%d]Ran out of commands for player %d.\n", common->GetGameFrame(), playerNumber );
|
|
}
|
|
}
|
|
|
|
// For remote players on the server, run enough commands
|
|
// to leave only a buffer that will hold us over for a
|
|
// number of milliseconds equal to the net_ucmdRate + one frame.
|
|
const int MaxExtraCommandsPerFrame = 15;
|
|
int numPasses = 0;
|
|
|
|
for ( ; numPasses < MaxExtraCommandsPerFrame; numPasses++ ) {
|
|
// Run remote player extra commands
|
|
extern idCVar net_ucmdRate;
|
|
// Add some extra time to smooth out network inconsistencies.
|
|
const int extraFrameMilliseconds = FRAME_TO_MSEC( common->GetGameFrame() + 2 ) - FRAME_TO_MSEC( common->GetGameFrame() );
|
|
const int millisecondBuffer = MSEC_ALIGN_TO_FRAME( net_ucmdRate.GetInteger() + extraFrameMilliseconds );
|
|
|
|
const bool hasNextCmd = cmdMgr.HasUserCmdForClientTimeBuffer( playerNumber, millisecondBuffer );
|
|
|
|
if ( hasNextCmd ) {
|
|
usercmd_t & currentCommand = cmdMgr.GetWritableUserCmdForPlayer( playerNumber );
|
|
|
|
if ( net_usercmd_timing_debug.GetBool() ) {
|
|
idLib::Printf( "[%d]Pass %d, running extra cmd for player %d, %d buffered\n", common->GetGameFrame(), numPasses, playerNumber, cmdMgr.GetNumUnreadFrames( playerNumber ) );
|
|
}
|
|
|
|
RunSingleUserCmd( currentCommand, player );
|
|
lastCmdRunTimeOnClient[ playerNumber ] = currentCommand.clientGameMilliseconds;
|
|
lastCmdRunTimeOnServer[ playerNumber ] = gameLocal.serverTime;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Reset the script FPS in case it was changed to accomodate an MP client
|
|
// running at a different framerate.
|
|
SetScriptFPS( com_engineHz_latched );
|
|
|
|
//idLib::Printf( "\n" );//!@#
|
|
}
|
|
|
|
|
|
/*
|
|
======================================================================
|
|
|
|
Game view drawing
|
|
|
|
======================================================================
|
|
*/
|
|
|
|
/*
|
|
====================
|
|
idGameLocal::CalcFov
|
|
|
|
Calculates the horizontal and vertical field of view based on a horizontal field of view and custom aspect ratio
|
|
====================
|
|
*/
|
|
void idGameLocal::CalcFov( float base_fov, float &fov_x, float &fov_y ) const {
|
|
const int width = renderSystem->GetWidth();
|
|
const int height = renderSystem->GetHeight();
|
|
if ( width == height ) {
|
|
// this is the Rift, so don't mess with our aspect ratio corrections
|
|
fov_x = base_fov;
|
|
fov_y = base_fov;
|
|
return;
|
|
}
|
|
|
|
// Calculate the fov_y based on an ideal aspect ratio
|
|
const float ideal_ratio_x = 16.0f;
|
|
const float ideal_ratio_y = 9.0f;
|
|
const float tanHalfX = idMath::Tan( DEG2RAD( base_fov * 0.5f ) );
|
|
fov_y = 2.0f * RAD2DEG( idMath::ATan( ideal_ratio_y * tanHalfX, ideal_ratio_x ) );
|
|
|
|
// Then calculate fov_x based on the true aspect ratio
|
|
const float ratio_x = width * renderSystem->GetPixelAspect();
|
|
const float ratio_y = height;
|
|
const float tanHalfY = idMath::Tan( DEG2RAD( fov_y * 0.5f ) );
|
|
fov_x = 2.0f * RAD2DEG( idMath::ATan( ratio_x * tanHalfY, ratio_y ) );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::Draw
|
|
|
|
makes rendering and sound system calls
|
|
================
|
|
*/
|
|
bool idGameLocal::Draw( int clientNum ) {
|
|
|
|
if( clientNum == -1 ) {
|
|
return false;
|
|
}
|
|
|
|
if ( common->IsMultiplayer() && session->GetState() == idSession::INGAME ) {
|
|
return mpGame.Draw( clientNum );
|
|
}
|
|
|
|
// chose the optimized or legacy device context code
|
|
uiManager->SetDrawingDC();
|
|
|
|
idPlayer *player = static_cast<idPlayer *>(entities[ clientNum ]);
|
|
|
|
if ( ( player == NULL ) || ( player->GetRenderView() == NULL ) ) {
|
|
return false;
|
|
}
|
|
|
|
// render the scene
|
|
player->playerView.RenderPlayerView( player->hudManager );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::HandleGuiCommands
|
|
================
|
|
*/
|
|
bool idGameLocal::HandlePlayerGuiEvent( const sysEvent_t * ev ) {
|
|
|
|
idPlayer * player = GetLocalPlayer();
|
|
bool handled = false;
|
|
|
|
if ( player != NULL ) {
|
|
handled = player->HandleGuiEvents( ev );
|
|
}
|
|
|
|
if ( common->IsMultiplayer() && !handled ) {
|
|
handled = mpGame.HandleGuiEvent( ev );
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetLevelMap
|
|
|
|
should only be used for in-game level editing
|
|
================
|
|
*/
|
|
idMapFile *idGameLocal::GetLevelMap() {
|
|
if ( mapFile && mapFile->HasPrimitiveData()) {
|
|
return mapFile;
|
|
}
|
|
if ( !mapFileName.Length() ) {
|
|
return NULL;
|
|
}
|
|
|
|
if ( mapFile ) {
|
|
delete mapFile;
|
|
}
|
|
|
|
mapFile = new (TAG_GAME) idMapFile;
|
|
if ( !mapFile->Parse( mapFileName ) ) {
|
|
delete mapFile;
|
|
mapFile = NULL;
|
|
}
|
|
|
|
return mapFile;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetMapName
|
|
================
|
|
*/
|
|
const char *idGameLocal::GetMapName() const {
|
|
return mapFileName.c_str();
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::CallFrameCommand
|
|
================
|
|
*/
|
|
void idGameLocal::CallFrameCommand( idEntity *ent, const function_t *frameCommand ) {
|
|
frameCommandThread->CallFunction( ent, frameCommand, true );
|
|
frameCommandThread->Execute();
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::CallObjectFrameCommand
|
|
================
|
|
*/
|
|
void idGameLocal::CallObjectFrameCommand( idEntity *ent, const char *frameCommand ) {
|
|
const function_t *func;
|
|
|
|
func = ent->scriptObject.GetFunction( frameCommand );
|
|
if ( !func ) {
|
|
if ( !ent->IsType( idTestModel::Type ) ) {
|
|
Error( "Unknown function '%s' called for frame command on entity '%s'", frameCommand, ent->name.c_str() );
|
|
}
|
|
} else {
|
|
frameCommandThread->CallFunction( ent, func, true );
|
|
frameCommandThread->Execute();
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ShowTargets
|
|
================
|
|
*/
|
|
void idGameLocal::ShowTargets() {
|
|
idMat3 axis = GetLocalPlayer()->viewAngles.ToMat3();
|
|
idVec3 up = axis[ 2 ] * 5.0f;
|
|
const idVec3 &viewPos = GetLocalPlayer()->GetPhysics()->GetOrigin();
|
|
idBounds viewTextBounds( viewPos );
|
|
idBounds viewBounds( viewPos );
|
|
idBounds box( idVec3( -4.0f, -4.0f, -4.0f ), idVec3( 4.0f, 4.0f, 4.0f ) );
|
|
idEntity *ent;
|
|
idEntity *target;
|
|
int i;
|
|
idBounds totalBounds;
|
|
|
|
viewTextBounds.ExpandSelf( 128.0f );
|
|
viewBounds.ExpandSelf( 512.0f );
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
totalBounds = ent->GetPhysics()->GetAbsBounds();
|
|
for( i = 0; i < ent->targets.Num(); i++ ) {
|
|
target = ent->targets[ i ].GetEntity();
|
|
if ( target ) {
|
|
totalBounds.AddBounds( target->GetPhysics()->GetAbsBounds() );
|
|
}
|
|
}
|
|
|
|
if ( !viewBounds.IntersectsBounds( totalBounds ) ) {
|
|
continue;
|
|
}
|
|
|
|
float dist;
|
|
idVec3 dir = totalBounds.GetCenter() - viewPos;
|
|
dir.NormalizeFast();
|
|
totalBounds.RayIntersection( viewPos, dir, dist );
|
|
float frac = ( 512.0f - dist ) / 512.0f;
|
|
if ( frac < 0.0f ) {
|
|
continue;
|
|
}
|
|
|
|
gameRenderWorld->DebugBounds( ( ent->IsHidden() ? colorLtGrey : colorOrange ) * frac, ent->GetPhysics()->GetAbsBounds() );
|
|
if ( viewTextBounds.IntersectsBounds( ent->GetPhysics()->GetAbsBounds() ) ) {
|
|
idVec3 center = ent->GetPhysics()->GetAbsBounds().GetCenter();
|
|
gameRenderWorld->DrawText( ent->name.c_str(), center - up, 0.1f, colorWhite * frac, axis, 1 );
|
|
gameRenderWorld->DrawText( ent->GetEntityDefName(), center, 0.1f, colorWhite * frac, axis, 1 );
|
|
gameRenderWorld->DrawText( va( "#%d", ent->entityNumber ), center + up, 0.1f, colorWhite * frac, axis, 1 );
|
|
}
|
|
|
|
for( i = 0; i < ent->targets.Num(); i++ ) {
|
|
target = ent->targets[ i ].GetEntity();
|
|
if ( target ) {
|
|
gameRenderWorld->DebugArrow( colorYellow * frac, ent->GetPhysics()->GetAbsBounds().GetCenter(), target->GetPhysics()->GetOrigin(), 10, 0 );
|
|
gameRenderWorld->DebugBounds( colorGreen * frac, box, target->GetPhysics()->GetOrigin() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RunDebugInfo
|
|
================
|
|
*/
|
|
void idGameLocal::RunDebugInfo() {
|
|
idEntity *ent;
|
|
idPlayer *player;
|
|
|
|
player = GetLocalPlayer();
|
|
if ( !player ) {
|
|
return;
|
|
}
|
|
|
|
const idVec3 &origin = player->GetPhysics()->GetOrigin();
|
|
|
|
if ( g_showEntityInfo.GetBool() ) {
|
|
idMat3 axis = player->viewAngles.ToMat3();
|
|
idVec3 up = axis[ 2 ] * 5.0f;
|
|
idBounds viewTextBounds( origin );
|
|
idBounds viewBounds( origin );
|
|
|
|
viewTextBounds.ExpandSelf( 128.0f );
|
|
viewBounds.ExpandSelf( 512.0f );
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
// don't draw the worldspawn
|
|
if ( ent == world ) {
|
|
continue;
|
|
}
|
|
|
|
// skip if the entity is very far away
|
|
if ( !viewBounds.IntersectsBounds( ent->GetPhysics()->GetAbsBounds() ) ) {
|
|
continue;
|
|
}
|
|
|
|
const idBounds &entBounds = ent->GetPhysics()->GetAbsBounds();
|
|
int contents = ent->GetPhysics()->GetContents();
|
|
if ( contents & CONTENTS_BODY ) {
|
|
gameRenderWorld->DebugBounds( colorCyan, entBounds );
|
|
} else if ( contents & CONTENTS_TRIGGER ) {
|
|
gameRenderWorld->DebugBounds( colorOrange, entBounds );
|
|
} else if ( contents & CONTENTS_SOLID ) {
|
|
gameRenderWorld->DebugBounds( colorGreen, entBounds );
|
|
} else {
|
|
if ( !entBounds.GetVolume() ) {
|
|
gameRenderWorld->DebugBounds( colorMdGrey, entBounds.Expand( 8.0f ) );
|
|
} else {
|
|
gameRenderWorld->DebugBounds( colorMdGrey, entBounds );
|
|
}
|
|
}
|
|
if ( viewTextBounds.IntersectsBounds( entBounds ) ) {
|
|
gameRenderWorld->DrawText( ent->name.c_str(), entBounds.GetCenter(), 0.1f, colorWhite, axis, 1 );
|
|
gameRenderWorld->DrawText( va( "#%d", ent->entityNumber ), entBounds.GetCenter() + up, 0.1f, colorWhite, axis, 1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
// debug tool to draw bounding boxes around active entities
|
|
if ( g_showActiveEntities.GetBool() ) {
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
idBounds b = ent->GetPhysics()->GetBounds();
|
|
if ( b.GetVolume() <= 0 ) {
|
|
b[0][0] = b[0][1] = b[0][2] = -8;
|
|
b[1][0] = b[1][1] = b[1][2] = 8;
|
|
}
|
|
if ( ent->fl.isDormant ) {
|
|
gameRenderWorld->DebugBounds( colorYellow, b, ent->GetPhysics()->GetOrigin() );
|
|
} else {
|
|
gameRenderWorld->DebugBounds( colorGreen, b, ent->GetPhysics()->GetOrigin() );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( g_showTargets.GetBool() ) {
|
|
ShowTargets();
|
|
}
|
|
|
|
if ( g_showTriggers.GetBool() ) {
|
|
idTrigger::DrawDebugInfo();
|
|
}
|
|
|
|
if ( ai_showCombatNodes.GetBool() ) {
|
|
idCombatNode::DrawDebugInfo();
|
|
}
|
|
|
|
if ( ai_showPaths.GetBool() ) {
|
|
idPathCorner::DrawDebugInfo();
|
|
}
|
|
|
|
if ( g_editEntityMode.GetBool() ) {
|
|
editEntities->DisplayEntities();
|
|
}
|
|
|
|
if ( g_showCollisionWorld.GetBool() ) {
|
|
collisionModelManager->DrawModel( 0, vec3_origin, mat3_identity, origin, 128.0f );
|
|
}
|
|
|
|
if ( g_showCollisionModels.GetBool() ) {
|
|
clip.DrawClipModels( player->GetEyePosition(), g_maxShowDistance.GetFloat(), pm_thirdPerson.GetBool() ? NULL : player );
|
|
}
|
|
|
|
if ( g_showCollisionTraces.GetBool() ) {
|
|
clip.PrintStatistics();
|
|
}
|
|
|
|
if ( g_showPVS.GetInteger() ) {
|
|
pvs.DrawPVS( origin, ( g_showPVS.GetInteger() == 2 ) ? PVS_ALL_PORTALS_OPEN : PVS_NORMAL );
|
|
}
|
|
|
|
if ( aas_test.GetInteger() >= 0 ) {
|
|
idAAS *aas = GetAAS( aas_test.GetInteger() );
|
|
if ( aas ) {
|
|
aas->Test( origin );
|
|
if ( ai_testPredictPath.GetBool() ) {
|
|
idVec3 velocity;
|
|
predictedPath_t path;
|
|
|
|
velocity.x = cos( DEG2RAD( player->viewAngles.yaw ) ) * 100.0f;
|
|
velocity.y = sin( DEG2RAD( player->viewAngles.yaw ) ) * 100.0f;
|
|
velocity.z = 0.0f;
|
|
idAI::PredictPath( player, aas, origin, velocity, 1000, 100, SE_ENTER_OBSTACLE | SE_BLOCKED | SE_ENTER_LEDGE_AREA, path );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ai_showObstacleAvoidance.GetInteger() == 2 ) {
|
|
idAAS *aas = GetAAS( 0 );
|
|
if ( aas ) {
|
|
idVec3 seekPos;
|
|
obstaclePath_t path;
|
|
|
|
seekPos = player->GetPhysics()->GetOrigin() + player->viewAxis[0] * 200.0f;
|
|
idAI::FindPathAroundObstacles( player->GetPhysics(), aas, NULL, player->GetPhysics()->GetOrigin(), seekPos, path );
|
|
}
|
|
}
|
|
|
|
// collision map debug output
|
|
collisionModelManager->DebugOutput( player->GetEyePosition() );
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::NumAAS
|
|
==================
|
|
*/
|
|
int idGameLocal::NumAAS() const {
|
|
return aasList.Num();
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::GetAAS
|
|
==================
|
|
*/
|
|
idAAS *idGameLocal::GetAAS( int num ) const {
|
|
if ( ( num >= 0 ) && ( num < aasList.Num() ) ) {
|
|
if ( aasList[ num ] && aasList[ num ]->GetSettings() ) {
|
|
return aasList[ num ];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::GetAAS
|
|
==================
|
|
*/
|
|
idAAS *idGameLocal::GetAAS( const char *name ) const {
|
|
int i;
|
|
|
|
for ( i = 0; i < aasNames.Num(); i++ ) {
|
|
if ( aasNames[ i ] == name ) {
|
|
if ( !aasList[ i ]->GetSettings() ) {
|
|
return NULL;
|
|
} else {
|
|
return aasList[ i ];
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::SetAASAreaState
|
|
==================
|
|
*/
|
|
void idGameLocal::SetAASAreaState( const idBounds &bounds, const int areaContents, bool closed ) {
|
|
int i;
|
|
|
|
for( i = 0; i < aasList.Num(); i++ ) {
|
|
aasList[ i ]->SetAreaState( bounds, areaContents, closed );
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::AddAASObstacle
|
|
==================
|
|
*/
|
|
aasHandle_t idGameLocal::AddAASObstacle( const idBounds &bounds ) {
|
|
int i;
|
|
aasHandle_t obstacle;
|
|
aasHandle_t check;
|
|
|
|
if ( !aasList.Num() ) {
|
|
return -1;
|
|
}
|
|
|
|
obstacle = aasList[ 0 ]->AddObstacle( bounds );
|
|
for( i = 1; i < aasList.Num(); i++ ) {
|
|
check = aasList[ i ]->AddObstacle( bounds );
|
|
assert( check == obstacle );
|
|
}
|
|
|
|
return obstacle;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::RemoveAASObstacle
|
|
==================
|
|
*/
|
|
void idGameLocal::RemoveAASObstacle( const aasHandle_t handle ) {
|
|
int i;
|
|
|
|
for( i = 0; i < aasList.Num(); i++ ) {
|
|
aasList[ i ]->RemoveObstacle( handle );
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::RemoveAllAASObstacles
|
|
==================
|
|
*/
|
|
void idGameLocal::RemoveAllAASObstacles() {
|
|
int i;
|
|
|
|
for( i = 0; i < aasList.Num(); i++ ) {
|
|
aasList[ i ]->RemoveAllObstacles();
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::CheatsOk
|
|
==================
|
|
*/
|
|
bool idGameLocal::CheatsOk( bool requirePlayer ) {
|
|
extern idCVar net_allowCheats;
|
|
if ( common->IsMultiplayer() && !net_allowCheats.GetBool() ) {
|
|
Printf( "Not allowed in multiplayer.\n" );
|
|
return false;
|
|
}
|
|
|
|
if ( developer.GetBool() ) {
|
|
return true;
|
|
}
|
|
|
|
idPlayer * player = GetLocalPlayer();
|
|
if ( !requirePlayer || ( player != NULL && ( player->health > 0 ) ) ) {
|
|
return true;
|
|
}
|
|
|
|
Printf( "You must be alive to use this command.\n" );
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::RegisterEntity
|
|
===================
|
|
*/
|
|
void idGameLocal::RegisterEntity( idEntity *ent, int forceSpawnId, const idDict & spawnArgsToCopy ) {
|
|
int spawn_entnum;
|
|
|
|
ent->fl.skipReplication = spawnArgsToCopy.GetBool( "net_skip_replication", false );
|
|
|
|
if ( spawnCount >= ( 1 << ( 32 - GENTITYNUM_BITS ) ) ) {
|
|
Error( "idGameLocal::RegisterEntity: spawn count overflow" );
|
|
}
|
|
|
|
if ( !spawnArgsToCopy.GetInt( "spawn_entnum", "0", spawn_entnum ) ) {
|
|
const int freeListType = ( common->IsMultiplayer() && ent->GetSkipReplication() ) ? 1 : 0;
|
|
const int maxEntityNum = ( common->IsMultiplayer() && !ent->GetSkipReplication() ) ? ENTITYNUM_FIRST_NON_REPLICATED : ENTITYNUM_MAX_NORMAL;
|
|
int freeIndex = firstFreeEntityIndex[ freeListType ];
|
|
|
|
while( entities[ freeIndex ] != NULL && freeIndex < maxEntityNum ) {
|
|
freeIndex++;
|
|
}
|
|
if ( freeIndex >= maxEntityNum ) {
|
|
Error( "no free entities[%d]", freeListType );
|
|
}
|
|
spawn_entnum = freeIndex++;
|
|
|
|
firstFreeEntityIndex[ freeListType ] = freeIndex;
|
|
}
|
|
|
|
entities[ spawn_entnum ] = ent;
|
|
spawnIds[ spawn_entnum ] = ( forceSpawnId >= 0 ) ? forceSpawnId : spawnCount++;
|
|
ent->entityNumber = spawn_entnum;
|
|
ent->spawnNode.AddToEnd( spawnedEntities );
|
|
|
|
// Make a copy because TransferKeyValues clears the input parameter.
|
|
idDict copiedArgs = spawnArgsToCopy;
|
|
ent->spawnArgs.TransferKeyValues( copiedArgs );
|
|
|
|
if ( spawn_entnum >= num_entities ) {
|
|
num_entities++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::UnregisterEntity
|
|
===================
|
|
*/
|
|
void idGameLocal::UnregisterEntity( idEntity *ent ) {
|
|
assert( ent );
|
|
|
|
if ( editEntities ) {
|
|
editEntities->RemoveSelectedEntity( ent );
|
|
}
|
|
|
|
if ( !verify( ent->entityNumber < MAX_GENTITIES ) ) {
|
|
idLib::Error( "invalid entity number" );
|
|
return;
|
|
}
|
|
|
|
if ( ( ent->entityNumber != ENTITYNUM_NONE ) && ( entities[ ent->entityNumber ] == ent ) ) {
|
|
ent->spawnNode.Remove();
|
|
entities[ ent->entityNumber ] = NULL;
|
|
spawnIds[ ent->entityNumber ] = -1;
|
|
|
|
int freeListType = ( common->IsMultiplayer() && ent->entityNumber >= ENTITYNUM_FIRST_NON_REPLICATED ) ? 1 : 0;
|
|
|
|
if ( ent->entityNumber >= MAX_CLIENTS && ent->entityNumber < firstFreeEntityIndex[ freeListType ] ) {
|
|
firstFreeEntityIndex[ freeListType ] = ent->entityNumber;
|
|
}
|
|
ent->entityNumber = ENTITYNUM_NONE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SpawnEntityType
|
|
================
|
|
*/
|
|
idEntity *idGameLocal::SpawnEntityType( const idTypeInfo &classdef, const idDict *args, bool bIsClientReadSnapshot ) {
|
|
idClass *obj;
|
|
|
|
#if _DEBUG
|
|
if ( common->IsClient() ) {
|
|
assert( bIsClientReadSnapshot );
|
|
}
|
|
#endif
|
|
|
|
if ( !classdef.IsType( idEntity::Type ) ) {
|
|
Error( "Attempted to spawn non-entity class '%s'", classdef.classname );
|
|
}
|
|
|
|
try {
|
|
if ( args ) {
|
|
spawnArgs = *args;
|
|
} else {
|
|
spawnArgs.Clear();
|
|
}
|
|
obj = classdef.CreateInstance();
|
|
obj->CallSpawn();
|
|
}
|
|
|
|
catch( idAllocError & ) {
|
|
obj = NULL;
|
|
}
|
|
spawnArgs.Clear();
|
|
|
|
return static_cast<idEntity *>(obj);
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::SpawnEntityDef
|
|
|
|
Finds the spawn function for the entity and calls it,
|
|
returning false if not found
|
|
===================
|
|
*/
|
|
bool idGameLocal::SpawnEntityDef( const idDict &args, idEntity **ent, bool setDefaults ) {
|
|
const char *classname;
|
|
const char *spawn;
|
|
idTypeInfo *cls;
|
|
idClass *obj;
|
|
idStr error;
|
|
const char *name;
|
|
|
|
if ( ent ) {
|
|
*ent = NULL;
|
|
}
|
|
|
|
spawnArgs = args;
|
|
|
|
if ( spawnArgs.GetString( "name", "", &name ) ) {
|
|
sprintf( error, " on '%s'", name);
|
|
}
|
|
|
|
spawnArgs.GetString( "classname", NULL, &classname );
|
|
|
|
const idDeclEntityDef *def = FindEntityDef( classname, false );
|
|
|
|
if ( !def ) {
|
|
Warning( "Unknown classname '%s'%s.", classname, error.c_str() );
|
|
return false;
|
|
}
|
|
|
|
spawnArgs.SetDefaults( &def->dict );
|
|
|
|
if ( !spawnArgs.FindKey( "slowmo" ) ) {
|
|
bool slowmo = true;
|
|
|
|
for ( int i = 0; fastEntityList[i]; i++ ) {
|
|
if ( !idStr::Cmp( classname, fastEntityList[i] ) ) {
|
|
slowmo = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !slowmo ) {
|
|
spawnArgs.SetBool( "slowmo", slowmo );
|
|
}
|
|
}
|
|
|
|
// check if we should spawn a class object
|
|
spawnArgs.GetString( "spawnclass", NULL, &spawn );
|
|
if ( spawn ) {
|
|
|
|
cls = idClass::GetClass( spawn );
|
|
if ( !cls ) {
|
|
Warning( "Could not spawn '%s'. Class '%s' not found%s.", classname, spawn, error.c_str() );
|
|
return false;
|
|
}
|
|
|
|
obj = cls->CreateInstance();
|
|
if ( !obj ) {
|
|
Warning( "Could not spawn '%s'. Instance could not be created%s.", classname, error.c_str() );
|
|
return false;
|
|
}
|
|
|
|
obj->CallSpawn();
|
|
|
|
if ( ent && obj->IsType( idEntity::Type ) ) {
|
|
*ent = static_cast<idEntity *>(obj);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// check if we should call a script function to spawn
|
|
spawnArgs.GetString( "spawnfunc", NULL, &spawn );
|
|
if ( spawn ) {
|
|
const function_t *func = program.FindFunction( spawn );
|
|
if ( !func ) {
|
|
Warning( "Could not spawn '%s'. Script function '%s' not found%s.", classname, spawn, error.c_str() );
|
|
return false;
|
|
}
|
|
idThread *thread = new idThread( func );
|
|
thread->DelayedStart( 0 );
|
|
return true;
|
|
}
|
|
|
|
Warning( "%s doesn't include a spawnfunc or spawnclass%s.", classname, error.c_str() );
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::FindEntityDef
|
|
================
|
|
*/
|
|
const idDeclEntityDef *idGameLocal::FindEntityDef( const char *name, bool makeDefault ) const {
|
|
const idDecl *decl = NULL;
|
|
if ( common->IsMultiplayer() ) {
|
|
decl = declManager->FindType( DECL_ENTITYDEF, va( "%s_mp", name ), false );
|
|
}
|
|
if ( !decl ) {
|
|
decl = declManager->FindType( DECL_ENTITYDEF, name, makeDefault );
|
|
}
|
|
return static_cast<const idDeclEntityDef *>( decl );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::FindEntityDefDict
|
|
================
|
|
*/
|
|
const idDict *idGameLocal::FindEntityDefDict( const char *name, bool makeDefault ) const {
|
|
const idDeclEntityDef *decl = FindEntityDef( name, makeDefault );
|
|
return decl ? &decl->dict : NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::InhibitEntitySpawn
|
|
================
|
|
*/
|
|
bool idGameLocal::InhibitEntitySpawn( idDict &spawnArgs ) {
|
|
|
|
bool result = false;
|
|
|
|
if ( common->IsMultiplayer() ) {
|
|
spawnArgs.GetBool( "not_multiplayer", "0", result );
|
|
} else if ( g_skill.GetInteger() == 0 ) {
|
|
spawnArgs.GetBool( "not_easy", "0", result );
|
|
} else if ( g_skill.GetInteger() == 1 ) {
|
|
spawnArgs.GetBool( "not_medium", "0", result );
|
|
} else {
|
|
spawnArgs.GetBool( "not_hard", "0", result );
|
|
if ( !result && g_skill.GetInteger() == 3 ) {
|
|
spawnArgs.GetBool( "not_nightmare", "0", result );
|
|
}
|
|
}
|
|
|
|
if ( g_skill.GetInteger() == 3 ) {
|
|
const char * name = spawnArgs.GetString( "classname" );
|
|
// _D3XP :: remove moveable medkit packs also
|
|
if ( idStr::Icmp( name, "item_medkit" ) == 0 || idStr::Icmp( name, "item_medkit_small" ) == 0 ||
|
|
idStr::Icmp( name, "moveable_item_medkit" ) == 0 || idStr::Icmp( name, "moveable_item_medkit_small" ) == 0 ) {
|
|
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
if ( common->IsMultiplayer() ) {
|
|
const char * name = spawnArgs.GetString( "classname" );
|
|
if ( idStr::Icmp( name, "weapon_bfg" ) == 0 || idStr::Icmp( name, "weapon_soulcube" ) == 0 ) {
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::GameState
|
|
|
|
Used to allow entities to know if they're being spawned during the initial spawn.
|
|
==============
|
|
*/
|
|
gameState_t idGameLocal::GameState() const {
|
|
return gamestate;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::SpawnMapEntities
|
|
|
|
Parses textual entity definitions out of an entstring and spawns gentities.
|
|
==============
|
|
*/
|
|
void idGameLocal::SpawnMapEntities() {
|
|
int i;
|
|
int num;
|
|
int inhibit;
|
|
idMapEntity *mapEnt;
|
|
int numEntities;
|
|
idDict args;
|
|
|
|
Printf( "Spawning entities\n" );
|
|
|
|
if ( mapFile == NULL ) {
|
|
Printf("No mapfile present\n");
|
|
return;
|
|
}
|
|
|
|
numEntities = mapFile->GetNumEntities();
|
|
if ( numEntities == 0 ) {
|
|
Error( "...no entities" );
|
|
}
|
|
|
|
// the worldspawn is a special that performs any global setup
|
|
// needed by a level
|
|
mapEnt = mapFile->GetEntity( 0 );
|
|
args = mapEnt->epairs;
|
|
args.SetInt( "spawn_entnum", ENTITYNUM_WORLD );
|
|
if ( !SpawnEntityDef( args ) || !entities[ ENTITYNUM_WORLD ] || !entities[ ENTITYNUM_WORLD ]->IsType( idWorldspawn::Type ) ) {
|
|
Error( "Problem spawning world entity" );
|
|
}
|
|
|
|
num = 1;
|
|
inhibit = 0;
|
|
|
|
for ( i = 1 ; i < numEntities ; i++ ) {
|
|
common->UpdateLevelLoadPacifier();
|
|
|
|
|
|
mapEnt = mapFile->GetEntity( i );
|
|
args = mapEnt->epairs;
|
|
|
|
if ( !InhibitEntitySpawn( args ) ) {
|
|
// precache any media specified in the map entity
|
|
CacheDictionaryMedia( &args );
|
|
|
|
SpawnEntityDef( args );
|
|
num++;
|
|
} else {
|
|
inhibit++;
|
|
}
|
|
}
|
|
|
|
Printf( "...%i entities spawned, %i inhibited\n\n", num, inhibit );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::AddEntityToHash
|
|
================
|
|
*/
|
|
void idGameLocal::AddEntityToHash( const char *name, idEntity *ent ) {
|
|
if ( FindEntity( name ) ) {
|
|
Error( "Multiple entities named '%s'", name );
|
|
}
|
|
entityHash.Add( entityHash.GenerateKey( name, true ), ent->entityNumber );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RemoveEntityFromHash
|
|
================
|
|
*/
|
|
bool idGameLocal::RemoveEntityFromHash( const char *name, idEntity *ent ) {
|
|
int hash, i;
|
|
|
|
hash = entityHash.GenerateKey( name, true );
|
|
for ( i = entityHash.First( hash ); i != -1; i = entityHash.Next( i ) ) {
|
|
if ( entities[i] && entities[i] == ent && entities[i]->name.Icmp( name ) == 0 ) {
|
|
entityHash.Remove( hash, i );
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetTargets
|
|
================
|
|
*/
|
|
int idGameLocal::GetTargets( const idDict &args, idList< idEntityPtr<idEntity> > &list, const char *ref ) const {
|
|
int i, num, refLength;
|
|
const idKeyValue *arg;
|
|
idEntity *ent;
|
|
|
|
list.Clear();
|
|
|
|
refLength = strlen( ref );
|
|
num = args.GetNumKeyVals();
|
|
for( i = 0; i < num; i++ ) {
|
|
|
|
arg = args.GetKeyVal( i );
|
|
if ( arg->GetKey().Icmpn( ref, refLength ) == 0 ) {
|
|
|
|
ent = FindEntity( arg->GetValue() );
|
|
if ( ent ) {
|
|
idEntityPtr<idEntity> &entityPtr = list.Alloc();
|
|
entityPtr = ent;
|
|
}
|
|
}
|
|
}
|
|
|
|
return list.Num();
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::GetTraceEntity
|
|
|
|
returns the master entity of a trace. for example, if the trace entity is the player's head, it will return the player.
|
|
=============
|
|
*/
|
|
idEntity *idGameLocal::GetTraceEntity( const trace_t &trace ) const {
|
|
idEntity *master;
|
|
|
|
if ( !entities[ trace.c.entityNum ] ) {
|
|
return NULL;
|
|
}
|
|
master = entities[ trace.c.entityNum ]->GetBindMaster();
|
|
if ( master ) {
|
|
return master;
|
|
}
|
|
return entities[ trace.c.entityNum ];
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::ArgCompletion_EntityName
|
|
|
|
Argument completion for entity names
|
|
=============
|
|
*/
|
|
void idGameLocal::ArgCompletion_EntityName( const idCmdArgs &args, void(*callback)( const char *s ) ) {
|
|
int i;
|
|
|
|
for( i = 0; i < gameLocal.num_entities; i++ ) {
|
|
if ( gameLocal.entities[ i ] ) {
|
|
callback( va( "%s %s", args.Argv( 0 ), gameLocal.entities[ i ]->name.c_str() ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::FindEntity
|
|
|
|
Returns the entity whose name matches the specified string.
|
|
=============
|
|
*/
|
|
idEntity *idGameLocal::FindEntity( const char *name ) const {
|
|
int hash, i;
|
|
|
|
hash = entityHash.GenerateKey( name, true );
|
|
for ( i = entityHash.First( hash ); i != -1; i = entityHash.Next( i ) ) {
|
|
if ( entities[i] && entities[i]->name.Icmp( name ) == 0 ) {
|
|
return entities[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::FindEntityUsingDef
|
|
|
|
Searches all active entities for the next one using the specified entityDef.
|
|
|
|
Searches beginning at the entity after from, or the beginning if NULL
|
|
NULL will be returned if the end of the list is reached.
|
|
=============
|
|
*/
|
|
idEntity *idGameLocal::FindEntityUsingDef( idEntity *from, const char *match ) const {
|
|
idEntity *ent;
|
|
|
|
if ( !from ) {
|
|
ent = spawnedEntities.Next();
|
|
} else {
|
|
ent = from->spawnNode.Next();
|
|
}
|
|
|
|
for ( ; ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
assert( ent );
|
|
if ( idStr::Icmp( ent->GetEntityDefName(), match ) == 0 ) {
|
|
return ent;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::FindTraceEntity
|
|
|
|
Searches all active entities for the closest ( to start ) match that intersects
|
|
the line start,end
|
|
=============
|
|
*/
|
|
idEntity *idGameLocal::FindTraceEntity( idVec3 start, idVec3 end, const idTypeInfo &c, const idEntity *skip ) const {
|
|
idEntity *ent;
|
|
idEntity *bestEnt;
|
|
float scale;
|
|
float bestScale;
|
|
idBounds b;
|
|
|
|
bestEnt = NULL;
|
|
bestScale = 1.0f;
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
if ( ent->IsType( c ) && ent != skip ) {
|
|
b = ent->GetPhysics()->GetAbsBounds().Expand( 16 );
|
|
if ( b.RayIntersection( start, end-start, scale ) ) {
|
|
if ( scale >= 0.0f && scale < bestScale ) {
|
|
bestEnt = ent;
|
|
bestScale = scale;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestEnt;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::EntitiesWithinRadius
|
|
================
|
|
*/
|
|
int idGameLocal::EntitiesWithinRadius( const idVec3 org, float radius, idEntity **entityList, int maxCount ) const {
|
|
idEntity *ent;
|
|
idBounds bo( org );
|
|
int entCount = 0;
|
|
|
|
bo.ExpandSelf( radius );
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
if ( ent->GetPhysics()->GetAbsBounds().IntersectsBounds( bo ) ) {
|
|
entityList[entCount++] = ent;
|
|
}
|
|
}
|
|
|
|
return entCount;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
idGameLocal::KillBox
|
|
|
|
Kills all entities that would touch the proposed new positioning of ent. The ent itself will not being killed.
|
|
Checks if player entities are in the teleporter, and marks them to die at teleport exit instead of immediately.
|
|
If catch_teleport, this only marks teleport players for death on exit
|
|
=================
|
|
*/
|
|
void idGameLocal::KillBox( idEntity *ent, bool catch_teleport ) {
|
|
int i;
|
|
int num;
|
|
idEntity * hit;
|
|
idClipModel *cm;
|
|
idClipModel *clipModels[ MAX_GENTITIES ];
|
|
idPhysics *phys;
|
|
|
|
phys = ent->GetPhysics();
|
|
if ( !phys->GetNumClipModels() ) {
|
|
return;
|
|
}
|
|
|
|
num = clip.ClipModelsTouchingBounds( phys->GetAbsBounds(), phys->GetClipMask(), clipModels, MAX_GENTITIES );
|
|
for ( i = 0; i < num; i++ ) {
|
|
cm = clipModels[ i ];
|
|
|
|
// don't check render entities
|
|
if ( cm->IsRenderModel() ) {
|
|
continue;
|
|
}
|
|
|
|
hit = cm->GetEntity();
|
|
if ( ( hit == ent ) || !hit->fl.takedamage ) {
|
|
continue;
|
|
}
|
|
|
|
if ( !phys->ClipContents( cm ) ) {
|
|
continue;
|
|
}
|
|
|
|
// nail it
|
|
idPlayer *otherPlayer = NULL;
|
|
if ( hit->IsType( idPlayer::Type ) ) {
|
|
otherPlayer = static_cast< idPlayer * >( hit );
|
|
}
|
|
if ( otherPlayer != NULL ) {
|
|
if ( otherPlayer->IsInTeleport() ) {
|
|
otherPlayer->TeleportDeath( ent->entityNumber );
|
|
} else if ( !catch_teleport ) {
|
|
hit->Damage( ent, ent, vec3_origin, "damage_telefrag", 1.0f, INVALID_JOINT );
|
|
}
|
|
} else if ( !catch_teleport ) {
|
|
hit->Damage( ent, ent, vec3_origin, "damage_telefrag", 1.0f, INVALID_JOINT );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::RequirementMet
|
|
================
|
|
*/
|
|
bool idGameLocal::RequirementMet( idEntity *activator, const idStr &requires, int removeItem ) {
|
|
if ( requires.Length() ) {
|
|
if ( activator->IsType( idPlayer::Type ) ) {
|
|
idPlayer *player = static_cast<idPlayer *>(activator);
|
|
idDict *item = player->FindInventoryItem( requires );
|
|
if ( item ) {
|
|
if ( removeItem ) {
|
|
player->RemoveInventoryItem( item );
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::AlertAI
|
|
============
|
|
*/
|
|
void idGameLocal::AlertAI( idEntity *ent ) {
|
|
if ( ent && ent->IsType( idActor::Type ) ) {
|
|
// alert them for the next frame
|
|
lastAIAlertTime = time + 1;
|
|
lastAIAlertEntity = static_cast<idActor *>( ent );
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::GetAlertEntity
|
|
============
|
|
*/
|
|
idActor *idGameLocal::GetAlertEntity() {
|
|
int timeGroup = 0;
|
|
if ( lastAIAlertTime && lastAIAlertEntity.GetEntity() ) {
|
|
timeGroup = lastAIAlertEntity.GetEntity()->timeGroup;
|
|
}
|
|
SetTimeState ts( timeGroup );
|
|
|
|
if ( lastAIAlertTime >= time ) {
|
|
return lastAIAlertEntity.GetEntity();
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::RadiusDamage
|
|
============
|
|
*/
|
|
void idGameLocal::RadiusDamage( const idVec3 &origin, idEntity *inflictor, idEntity *attacker, idEntity *ignoreDamage, idEntity *ignorePush, const char *damageDefName, float dmgPower ) {
|
|
float dist, damageScale, attackerDamageScale, attackerPushScale;
|
|
idEntity * ent;
|
|
idEntity * entityList[ MAX_GENTITIES ];
|
|
int numListedEntities;
|
|
idBounds bounds;
|
|
idVec3 v, damagePoint, dir;
|
|
int i, e, damage, radius, push;
|
|
|
|
const idDict *damageDef = FindEntityDefDict( damageDefName, false );
|
|
if ( !damageDef ) {
|
|
Warning( "Unknown damageDef '%s'", damageDefName );
|
|
return;
|
|
}
|
|
|
|
damageDef->GetInt( "damage", "20", damage );
|
|
damageDef->GetInt( "radius", "50", radius );
|
|
damageDef->GetInt( "push", va( "%d", damage * 100 ), push );
|
|
damageDef->GetFloat( "attackerDamageScale", "0.5", attackerDamageScale );
|
|
damageDef->GetFloat( "attackerPushScale", "0", attackerPushScale );
|
|
|
|
if ( radius < 1 ) {
|
|
radius = 1;
|
|
}
|
|
|
|
bounds = idBounds( origin ).Expand( radius );
|
|
|
|
// get all entities touching the bounds
|
|
numListedEntities = clip.EntitiesTouchingBounds( bounds, -1, entityList, MAX_GENTITIES );
|
|
|
|
if ( inflictor && inflictor->IsType( idAFAttachment::Type ) ) {
|
|
inflictor = static_cast<idAFAttachment*>(inflictor)->GetBody();
|
|
}
|
|
if ( attacker && attacker->IsType( idAFAttachment::Type ) ) {
|
|
attacker = static_cast<idAFAttachment*>(attacker)->GetBody();
|
|
}
|
|
if ( ignoreDamage && ignoreDamage->IsType( idAFAttachment::Type ) ) {
|
|
ignoreDamage = static_cast<idAFAttachment*>(ignoreDamage)->GetBody();
|
|
}
|
|
|
|
// apply damage to the entities
|
|
for ( e = 0; e < numListedEntities; e++ ) {
|
|
ent = entityList[ e ];
|
|
assert( ent );
|
|
|
|
if ( !ent->fl.takedamage ) {
|
|
continue;
|
|
}
|
|
|
|
if ( ent == inflictor || ( ent->IsType( idAFAttachment::Type ) && static_cast<idAFAttachment*>(ent)->GetBody() == inflictor ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( ent == ignoreDamage || ( ent->IsType( idAFAttachment::Type ) && static_cast<idAFAttachment*>(ent)->GetBody() == ignoreDamage ) ) {
|
|
continue;
|
|
}
|
|
|
|
// don't damage a dead player
|
|
if ( common->IsMultiplayer() && ent->entityNumber < MAX_CLIENTS && ent->IsType( idPlayer::Type ) && static_cast< idPlayer * >( ent )->health < 0 ) {
|
|
continue;
|
|
}
|
|
|
|
// find the distance from the edge of the bounding box
|
|
for ( i = 0; i < 3; i++ ) {
|
|
if ( origin[ i ] < ent->GetPhysics()->GetAbsBounds()[0][ i ] ) {
|
|
v[ i ] = ent->GetPhysics()->GetAbsBounds()[0][ i ] - origin[ i ];
|
|
} else if ( origin[ i ] > ent->GetPhysics()->GetAbsBounds()[1][ i ] ) {
|
|
v[ i ] = origin[ i ] - ent->GetPhysics()->GetAbsBounds()[1][ i ];
|
|
} else {
|
|
v[ i ] = 0;
|
|
}
|
|
}
|
|
|
|
dist = v.Length();
|
|
if ( dist >= radius ) {
|
|
continue;
|
|
}
|
|
|
|
if ( ent->CanDamage( origin, damagePoint ) ) {
|
|
// push the center of mass higher than the origin so players
|
|
// get knocked into the air more
|
|
dir = ent->GetPhysics()->GetOrigin() - origin;
|
|
dir[ 2 ] += 24;
|
|
|
|
// get the damage scale
|
|
damageScale = dmgPower * ( 1.0f - dist / radius );
|
|
if ( ent == attacker || ( ent->IsType( idAFAttachment::Type ) && static_cast<idAFAttachment*>(ent)->GetBody() == attacker ) ) {
|
|
damageScale *= attackerDamageScale;
|
|
}
|
|
|
|
bool killedBySplash = true;
|
|
|
|
if( ent->health <= 0 ) {
|
|
killedBySplash = false;
|
|
}
|
|
|
|
ent->Damage( inflictor, attacker, dir, damageDefName, damageScale, INVALID_JOINT );
|
|
|
|
|
|
// If the player is local. SHAkkkkkkeeee!
|
|
if( !common->IsMultiplayer() && ent->entityNumber == GetLocalClientNum() ) {
|
|
|
|
if( ent->IsType( idPlayer::Type ) ) {
|
|
idPlayer * player = static_cast< idPlayer* >( ent );
|
|
if( player ) {
|
|
player->ControllerShakeFromDamage( damage );
|
|
}
|
|
}
|
|
}
|
|
|
|
// if our inflictor is a projectile, we want to count up how many kills the splash damage has chalked up.
|
|
if( ent->health <= 0 && killedBySplash ) {
|
|
if( inflictor && inflictor->IsType( idProjectile::Type ) ) {
|
|
if ( attacker && attacker->IsType( idPlayer::Type ) ) {
|
|
if ( ent->IsType( idActor::Type ) && ent != attacker ) {
|
|
idPlayer *player = static_cast<idPlayer *>( attacker );
|
|
player->AddProjectileKills();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// push physics objects
|
|
if ( push ) {
|
|
RadiusPush( origin, radius, push * dmgPower, attacker, ignorePush, attackerPushScale, false );
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::RadiusPush
|
|
==============
|
|
*/
|
|
void idGameLocal::RadiusPush( const idVec3 &origin, const float radius, const float push, const idEntity *inflictor, const idEntity *ignore, float inflictorScale, const bool quake ) {
|
|
int i, numListedClipModels;
|
|
idClipModel *clipModel;
|
|
idClipModel *clipModelList[ MAX_GENTITIES ];
|
|
idVec3 dir;
|
|
idBounds bounds;
|
|
modelTrace_t result;
|
|
idEntity *ent;
|
|
float scale;
|
|
|
|
dir.Set( 0.0f, 0.0f, 1.0f );
|
|
|
|
bounds = idBounds( origin ).Expand( radius );
|
|
|
|
// get all clip models touching the bounds
|
|
numListedClipModels = clip.ClipModelsTouchingBounds( bounds, -1, clipModelList, MAX_GENTITIES );
|
|
|
|
if ( inflictor && inflictor->IsType( idAFAttachment::Type ) ) {
|
|
inflictor = static_cast<const idAFAttachment*>(inflictor)->GetBody();
|
|
}
|
|
if ( ignore && ignore->IsType( idAFAttachment::Type ) ) {
|
|
ignore = static_cast<const idAFAttachment*>(ignore)->GetBody();
|
|
}
|
|
|
|
// apply impact to all the clip models through their associated physics objects
|
|
for ( i = 0; i < numListedClipModels; i++ ) {
|
|
|
|
clipModel = clipModelList[i];
|
|
|
|
// never push render models
|
|
if ( clipModel->IsRenderModel() ) {
|
|
continue;
|
|
}
|
|
|
|
ent = clipModel->GetEntity();
|
|
|
|
// never push projectiles
|
|
if ( ent->IsType( idProjectile::Type ) ) {
|
|
continue;
|
|
}
|
|
|
|
// players use "knockback" in idPlayer::Damage
|
|
if ( ent->IsType( idPlayer::Type ) && !quake ) {
|
|
continue;
|
|
}
|
|
|
|
// don't push the ignore entity
|
|
if ( ent == ignore || ( ent->IsType( idAFAttachment::Type ) && static_cast<idAFAttachment*>(ent)->GetBody() == ignore ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( gameRenderWorld->FastWorldTrace( result, origin, clipModel->GetOrigin() ) ) {
|
|
continue;
|
|
}
|
|
|
|
// scale the push for the inflictor
|
|
if ( ent == inflictor || ( ent->IsType( idAFAttachment::Type ) && static_cast<idAFAttachment*>(ent)->GetBody() == inflictor ) ) {
|
|
scale = inflictorScale;
|
|
} else {
|
|
scale = 1.0f;
|
|
}
|
|
|
|
if ( quake ) {
|
|
clipModel->GetEntity()->ApplyImpulse( world, clipModel->GetId(), clipModel->GetOrigin(), scale * push * dir );
|
|
} else {
|
|
RadiusPushClipModel( origin, scale * push, clipModel );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::RadiusPushClipModel
|
|
==============
|
|
*/
|
|
void idGameLocal::RadiusPushClipModel( const idVec3 &origin, const float push, const idClipModel *clipModel ) {
|
|
int i, j;
|
|
float dot, dist, area;
|
|
const idTraceModel *trm;
|
|
const traceModelPoly_t *poly;
|
|
idFixedWinding w;
|
|
idVec3 v, localOrigin, center, impulse;
|
|
|
|
trm = clipModel->GetTraceModel();
|
|
if ( !trm || 1 ) {
|
|
impulse = clipModel->GetAbsBounds().GetCenter() - origin;
|
|
impulse.Normalize();
|
|
impulse.z += 1.0f;
|
|
clipModel->GetEntity()->ApplyImpulse( world, clipModel->GetId(), clipModel->GetOrigin(), push * impulse );
|
|
return;
|
|
}
|
|
|
|
localOrigin = ( origin - clipModel->GetOrigin() ) * clipModel->GetAxis().Transpose();
|
|
for ( i = 0; i < trm->numPolys; i++ ) {
|
|
poly = &trm->polys[i];
|
|
|
|
center.Zero();
|
|
for ( j = 0; j < poly->numEdges; j++ ) {
|
|
v = trm->verts[ trm->edges[ abs(poly->edges[j]) ].v[ INT32_SIGNBITSET( poly->edges[j] ) ] ];
|
|
center += v;
|
|
v -= localOrigin;
|
|
v.NormalizeFast(); // project point on a unit sphere
|
|
w.AddPoint( v );
|
|
}
|
|
center /= poly->numEdges;
|
|
v = center - localOrigin;
|
|
dist = v.NormalizeFast();
|
|
dot = v * poly->normal;
|
|
if ( dot > 0.0f ) {
|
|
continue;
|
|
}
|
|
area = w.GetArea();
|
|
// impulse in polygon normal direction
|
|
impulse = poly->normal * clipModel->GetAxis();
|
|
// always push up for nicer effect
|
|
impulse.z -= 1.0f;
|
|
// scale impulse based on visible surface area and polygon angle
|
|
impulse *= push * ( dot * area * ( 1.0f / ( 4.0f * idMath::PI ) ) );
|
|
// scale away distance for nicer effect
|
|
impulse *= ( dist * 2.0f );
|
|
// impulse is applied to the center of the polygon
|
|
center = clipModel->GetOrigin() + center * clipModel->GetAxis();
|
|
|
|
clipModel->GetEntity()->ApplyImpulse( world, clipModel->GetId(), center, impulse );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::ProjectDecal
|
|
===============
|
|
*/
|
|
void idGameLocal::ProjectDecal( const idVec3 &origin, const idVec3 &dir, float depth, bool parallel, float size, const char *material, float angle ) {
|
|
float s, c;
|
|
idMat3 axis, axistemp;
|
|
idFixedWinding winding;
|
|
idVec3 windingOrigin, projectionOrigin;
|
|
|
|
static idVec3 decalWinding[4] = {
|
|
idVec3( 1.0f, 1.0f, 0.0f ),
|
|
idVec3( -1.0f, 1.0f, 0.0f ),
|
|
idVec3( -1.0f, -1.0f, 0.0f ),
|
|
idVec3( 1.0f, -1.0f, 0.0f )
|
|
};
|
|
|
|
if ( !g_decals.GetBool() ) {
|
|
return;
|
|
}
|
|
|
|
// randomly rotate the decal winding
|
|
idMath::SinCos16( ( angle ) ? angle : random.RandomFloat() * idMath::TWO_PI, s, c );
|
|
|
|
// winding orientation
|
|
axis[2] = dir;
|
|
axis[2].Normalize();
|
|
axis[2].NormalVectors( axistemp[0], axistemp[1] );
|
|
axis[0] = axistemp[ 0 ] * c + axistemp[ 1 ] * -s;
|
|
axis[1] = axistemp[ 0 ] * -s + axistemp[ 1 ] * -c;
|
|
|
|
windingOrigin = origin + depth * axis[2];
|
|
if ( parallel ) {
|
|
projectionOrigin = origin - depth * axis[2];
|
|
} else {
|
|
projectionOrigin = origin;
|
|
}
|
|
|
|
size *= 0.5f;
|
|
|
|
winding.Clear();
|
|
winding += idVec5( windingOrigin + ( axis * decalWinding[0] ) * size, idVec2( 1, 1 ) );
|
|
winding += idVec5( windingOrigin + ( axis * decalWinding[1] ) * size, idVec2( 0, 1 ) );
|
|
winding += idVec5( windingOrigin + ( axis * decalWinding[2] ) * size, idVec2( 0, 0 ) );
|
|
winding += idVec5( windingOrigin + ( axis * decalWinding[3] ) * size, idVec2( 1, 0 ) );
|
|
gameRenderWorld->ProjectDecalOntoWorld( winding, projectionOrigin, parallel, depth * 0.5f, declManager->FindMaterial( material ), gameLocal.slow.time /* _D3XP */ );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::BloodSplat
|
|
==============
|
|
*/
|
|
void idGameLocal::BloodSplat( const idVec3 &origin, const idVec3 &dir, float size, const char *material ) {
|
|
float halfSize = size * 0.5f;
|
|
idVec3 verts[] = { idVec3( 0.0f, +halfSize, +halfSize ),
|
|
idVec3( 0.0f, +halfSize, -halfSize ),
|
|
idVec3( 0.0f, -halfSize, -halfSize ),
|
|
idVec3( 0.0f, -halfSize, +halfSize ) };
|
|
idTraceModel trm;
|
|
idClipModel mdl;
|
|
trace_t results;
|
|
|
|
// FIXME: get from damage def
|
|
if ( !g_bloodEffects.GetBool() ) {
|
|
return;
|
|
}
|
|
|
|
size = halfSize + random.RandomFloat() * halfSize;
|
|
trm.SetupPolygon( verts, 4 );
|
|
mdl.LoadModel( trm );
|
|
clip.Translation( results, origin, origin + dir * 64.0f, &mdl, mat3_identity, CONTENTS_SOLID, NULL );
|
|
ProjectDecal( results.endpos, dir, 2.0f * size, true, size, material );
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::SetCamera
|
|
=============
|
|
*/
|
|
void idGameLocal::SetCamera( idCamera *cam ) {
|
|
int i;
|
|
idEntity *ent;
|
|
idAI *ai;
|
|
|
|
// this should fix going into a cinematic when dead.. rare but happens
|
|
idPlayer *client = GetLocalPlayer();
|
|
if ( client->health <= 0 || client->AI_DEAD ) {
|
|
return;
|
|
}
|
|
|
|
camera = cam;
|
|
if ( camera ) {
|
|
inCinematic = true;
|
|
|
|
// set r_znear so that transitioning into/out of the player's head doesn't clip through the view
|
|
cvarSystem->SetCVarFloat( "r_znear", 1.0f );
|
|
|
|
// hide all the player models
|
|
for( i = 0; i < numClients; i++ ) {
|
|
if ( entities[ i ] ) {
|
|
client = static_cast< idPlayer* >( entities[ i ] );
|
|
client->EnterCinematic();
|
|
}
|
|
}
|
|
|
|
if ( !cam->spawnArgs.GetBool( "ignore_enemies" ) ) {
|
|
// kill any active monsters that are enemies of the player
|
|
for ( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
if ( ent->cinematic || ent->fl.isDormant ) {
|
|
// only kill entities that aren't needed for cinematics and aren't dormant
|
|
continue;
|
|
}
|
|
|
|
if ( ent->IsType( idAI::Type ) ) {
|
|
ai = static_cast<idAI *>( ent );
|
|
if ( !ai->GetEnemy() || !ai->IsActive() ) {
|
|
// no enemy, or inactive, so probably safe to ignore
|
|
continue;
|
|
}
|
|
} else if ( ent->IsType( idProjectile::Type ) ) {
|
|
// remove all projectiles
|
|
} else if ( ent->spawnArgs.GetBool( "cinematic_remove" ) ) {
|
|
// remove anything marked to be removed during cinematics
|
|
} else {
|
|
// ignore everything else
|
|
continue;
|
|
}
|
|
|
|
// remove it
|
|
DPrintf( "removing '%s' for cinematic\n", ent->GetName() );
|
|
ent->PostEventMS( &EV_Remove, 0 );
|
|
}
|
|
}
|
|
|
|
} else {
|
|
inCinematic = false;
|
|
|
|
// restore r_znear
|
|
cvarSystem->SetCVarFloat( "r_znear", 3.0f );
|
|
|
|
// show all the player models
|
|
for( i = 0; i < numClients; i++ ) {
|
|
if ( entities[ i ] ) {
|
|
idPlayer *client = static_cast< idPlayer* >( entities[ i ] );
|
|
client->ExitCinematic();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::GetCamera
|
|
=============
|
|
*/
|
|
idCamera *idGameLocal::GetCamera() const {
|
|
return camera;
|
|
}
|
|
|
|
/*
|
|
======================
|
|
idGameLocal::SpreadLocations
|
|
|
|
Now that everything has been spawned, associate areas with location entities
|
|
======================
|
|
*/
|
|
void idGameLocal::SpreadLocations() {
|
|
idEntity *ent;
|
|
|
|
// allocate the area table
|
|
int numAreas = gameRenderWorld->NumAreas();
|
|
locationEntities = new (TAG_GAME) idLocationEntity *[ numAreas ];
|
|
memset( locationEntities, 0, numAreas * sizeof( *locationEntities ) );
|
|
|
|
// for each location entity, make pointers from every area it touches
|
|
for( ent = spawnedEntities.Next(); ent != NULL; ent = ent->spawnNode.Next() ) {
|
|
if ( !ent->IsType( idLocationEntity::Type ) ) {
|
|
continue;
|
|
}
|
|
idVec3 point = ent->spawnArgs.GetVector( "origin" );
|
|
int areaNum = gameRenderWorld->PointInArea( point );
|
|
if ( areaNum < 0 ) {
|
|
Printf( "SpreadLocations: location '%s' is not in a valid area\n", ent->spawnArgs.GetString( "name" ) );
|
|
continue;
|
|
}
|
|
if ( areaNum >= numAreas ) {
|
|
Error( "idGameLocal::SpreadLocations: areaNum >= gameRenderWorld->NumAreas()" );
|
|
}
|
|
if ( locationEntities[areaNum] ) {
|
|
Warning( "location entity '%s' overlaps '%s'", ent->spawnArgs.GetString( "name" ),
|
|
locationEntities[areaNum]->spawnArgs.GetString( "name" ) );
|
|
continue;
|
|
}
|
|
locationEntities[areaNum] = static_cast<idLocationEntity *>(ent);
|
|
|
|
// spread to all other connected areas
|
|
for ( int i = 0 ; i < numAreas ; i++ ) {
|
|
if ( i == areaNum ) {
|
|
continue;
|
|
}
|
|
if ( gameRenderWorld->AreasAreConnected( areaNum, i, PS_BLOCK_LOCATION ) ) {
|
|
locationEntities[i] = static_cast<idLocationEntity *>(ent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::LocationForPoint
|
|
|
|
The player checks the location each frame to update the HUD text display
|
|
May return NULL
|
|
===================
|
|
*/
|
|
idLocationEntity *idGameLocal::LocationForPoint( const idVec3 &point ) {
|
|
if ( !locationEntities ) {
|
|
// before SpreadLocations() has been called
|
|
return NULL;
|
|
}
|
|
|
|
int areaNum = gameRenderWorld->PointInArea( point );
|
|
if ( areaNum < 0 ) {
|
|
return NULL;
|
|
}
|
|
if ( areaNum >= gameRenderWorld->NumAreas() ) {
|
|
Error( "idGameLocal::LocationForPoint: areaNum >= gameRenderWorld->NumAreas()" );
|
|
}
|
|
|
|
return locationEntities[ areaNum ];
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::SetPortalState
|
|
============
|
|
*/
|
|
void idGameLocal::SetPortalState( qhandle_t portal, int blockingBits ) {
|
|
gameRenderWorld->SetPortalState( portal, blockingBits );
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::sortSpawnPoints
|
|
============
|
|
*/
|
|
int idGameLocal::sortSpawnPoints( const void *ptr1, const void *ptr2 ) {
|
|
const spawnSpot_t *spot1 = static_cast<const spawnSpot_t *>( ptr1 );
|
|
const spawnSpot_t *spot2 = static_cast<const spawnSpot_t *>( ptr2 );
|
|
float diff;
|
|
|
|
diff = spot1->dist - spot2->dist;
|
|
if ( diff < 0.0f ) {
|
|
return 1;
|
|
} else if ( diff > 0.0f ) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::RandomizeInitialSpawns
|
|
randomize the order of the initial spawns
|
|
prepare for a sequence of initial player spawns
|
|
============
|
|
*/
|
|
void idGameLocal::RandomizeInitialSpawns() {
|
|
spawnSpot_t spot;
|
|
int i, j;
|
|
int k;
|
|
|
|
idEntity *ent;
|
|
|
|
if ( !common->IsServer() ) {
|
|
return;
|
|
}
|
|
spawnSpots.Clear();
|
|
initialSpots.Clear();
|
|
teamSpawnSpots[0].Clear();
|
|
teamSpawnSpots[1].Clear();
|
|
teamInitialSpots[0].Clear();
|
|
teamInitialSpots[1].Clear();
|
|
|
|
spot.dist = 0;
|
|
spot.ent = FindEntityUsingDef( NULL, "info_player_deathmatch" );
|
|
while( spot.ent ) {
|
|
spot.ent->spawnArgs.GetInt( "team", "-1", spot.team );
|
|
|
|
if ( mpGame.IsGametypeFlagBased() ) /* CTF */
|
|
{
|
|
if ( spot.team == 0 || spot.team == 1 )
|
|
teamSpawnSpots[spot.team].Append( spot );
|
|
else
|
|
common->Warning( "info_player_deathmatch : invalid or no team attached to spawn point\n");
|
|
}
|
|
spawnSpots.Append( spot );
|
|
if ( spot.ent->spawnArgs.GetBool( "initial" ) ) {
|
|
if ( mpGame.IsGametypeFlagBased() ) /* CTF */
|
|
{
|
|
assert( spot.team == 0 || spot.team == 1 );
|
|
teamInitialSpots[ spot.team ].Append( spot.ent );
|
|
}
|
|
|
|
initialSpots.Append( spot.ent );
|
|
}
|
|
spot.ent = FindEntityUsingDef( spot.ent, "info_player_deathmatch" );
|
|
}
|
|
|
|
if ( mpGame.IsGametypeFlagBased() ) { /* CTF */
|
|
if ( !teamSpawnSpots[0].Num() )
|
|
common->Warning( "red team : no info_player_deathmatch in map" );
|
|
if ( !teamSpawnSpots[1].Num() )
|
|
common->Warning( "blue team : no info_player_deathmatch in map" );
|
|
|
|
if ( !teamSpawnSpots[0].Num() || !teamSpawnSpots[1].Num() )
|
|
return;
|
|
}
|
|
|
|
if ( !spawnSpots.Num() ) {
|
|
common->Warning( "no info_player_deathmatch in map" );
|
|
return;
|
|
}
|
|
|
|
if ( mpGame.IsGametypeFlagBased() ) { /* CTF */
|
|
common->Printf( "red team : %d spawns (%d initials)\n", teamSpawnSpots[ 0 ].Num(), teamInitialSpots[ 0 ].Num() );
|
|
// if there are no initial spots in the map, consider they can all be used as initial
|
|
if ( !teamInitialSpots[ 0 ].Num() ) {
|
|
common->Warning( "red team : no info_player_deathmatch entities marked initial in map" );
|
|
for ( i = 0; i < teamSpawnSpots[ 0 ].Num(); i++ ) {
|
|
teamInitialSpots[ 0 ].Append( teamSpawnSpots[ 0 ][ i ].ent );
|
|
}
|
|
}
|
|
|
|
common->Printf( "blue team : %d spawns (%d initials)\n", teamSpawnSpots[ 1 ].Num(), teamInitialSpots[ 1 ].Num() );
|
|
// if there are no initial spots in the map, consider they can all be used as initial
|
|
if ( !teamInitialSpots[ 1 ].Num() ) {
|
|
common->Warning( "blue team : no info_player_deathmatch entities marked initial in map" );
|
|
for ( i = 0; i < teamSpawnSpots[ 1 ].Num(); i++ ) {
|
|
teamInitialSpots[ 1 ].Append( teamSpawnSpots[ 1 ][ i ].ent );
|
|
}
|
|
}
|
|
}
|
|
|
|
common->Printf( "%d spawns (%d initials)\n", spawnSpots.Num(), initialSpots.Num() );
|
|
// if there are no initial spots in the map, consider they can all be used as initial
|
|
if ( !initialSpots.Num() ) {
|
|
common->Warning( "no info_player_deathmatch entities marked initial in map" );
|
|
for ( i = 0; i < spawnSpots.Num(); i++ ) {
|
|
initialSpots.Append( spawnSpots[ i ].ent );
|
|
}
|
|
}
|
|
|
|
for ( k = 0; k < 2; k++ ) {
|
|
for ( i = 0; i < teamInitialSpots[ k ].Num(); i++ ) {
|
|
j = random.RandomInt( teamInitialSpots[ k ].Num() );
|
|
ent = teamInitialSpots[ k ][ i ];
|
|
teamInitialSpots[ k ][ i ] = teamInitialSpots[ k ][ j ];
|
|
teamInitialSpots[ k ][ j ] = ent;
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < initialSpots.Num(); i++ ) {
|
|
j = random.RandomInt( initialSpots.Num() );
|
|
ent = initialSpots[ i ];
|
|
initialSpots[ i ] = initialSpots[ j ];
|
|
initialSpots[ j ] = ent;
|
|
}
|
|
// reset the counter
|
|
currentInitialSpot = 0;
|
|
|
|
teamCurrentInitialSpot[0] = 0;
|
|
teamCurrentInitialSpot[1] = 0;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SelectInitialSpawnPoint
|
|
spectators are spawned randomly anywhere
|
|
in-game clients are spawned based on distance to active players (randomized on the first half)
|
|
upon map restart, initial spawns are used (randomized ordered list of spawns flagged "initial")
|
|
if there are more players than initial spots, overflow to regular spawning
|
|
============
|
|
*/
|
|
idEntity *idGameLocal::SelectInitialSpawnPoint( idPlayer *player ) {
|
|
int i, j, which;
|
|
spawnSpot_t spot;
|
|
idVec3 pos;
|
|
float dist;
|
|
bool alone;
|
|
|
|
if ( !common->IsMultiplayer() || !spawnSpots.Num() || ( mpGame.IsGametypeFlagBased() && ( !teamSpawnSpots[0].Num() || !teamSpawnSpots[1].Num() ) ) ) { /* CTF */
|
|
spot.ent = FindEntityUsingDef( NULL, "info_player_start" );
|
|
if ( !spot.ent ) {
|
|
Error( "No info_player_start on map.\n" );
|
|
}
|
|
return spot.ent;
|
|
}
|
|
|
|
bool useInitialSpots = false;
|
|
if ( mpGame.IsGametypeFlagBased() ) { /* CTF */
|
|
assert( player->team == 0 || player->team == 1 );
|
|
useInitialSpots = player->useInitialSpawns && teamCurrentInitialSpot[ player->team ] < teamInitialSpots[ player->team ].Num();
|
|
} else {
|
|
useInitialSpots = player->useInitialSpawns && currentInitialSpot < initialSpots.Num();
|
|
}
|
|
|
|
if ( player->spectating ) {
|
|
// plain random spot, don't bother
|
|
return spawnSpots[ random.RandomInt( spawnSpots.Num() ) ].ent;
|
|
} else if ( useInitialSpots ) {
|
|
if ( mpGame.IsGametypeFlagBased() ) { /* CTF */
|
|
assert( player->team == 0 || player->team == 1 );
|
|
player->useInitialSpawns = false; // only use the initial spawn once
|
|
return teamInitialSpots[ player->team ][ teamCurrentInitialSpot[ player->team ]++ ];
|
|
}
|
|
return initialSpots[ currentInitialSpot++ ];
|
|
} else {
|
|
// check if we are alone in map
|
|
alone = true;
|
|
for ( j = 0; j < MAX_CLIENTS; j++ ) {
|
|
if ( entities[ j ] && entities[ j ] != player ) {
|
|
alone = false;
|
|
break;
|
|
}
|
|
}
|
|
if ( alone ) {
|
|
if ( mpGame.IsGametypeFlagBased() ) { /* CTF */
|
|
assert( player->team == 0 || player->team == 1 );
|
|
return teamSpawnSpots[ player->team ][ random.RandomInt( teamSpawnSpots[ player->team ].Num() ) ].ent;
|
|
}
|
|
// don't do distance-based
|
|
return spawnSpots[ random.RandomInt( spawnSpots.Num() ) ].ent;
|
|
}
|
|
|
|
if ( mpGame.IsGametypeFlagBased() ) { /* CTF */
|
|
// TODO : make as reusable method, same code as below
|
|
int team = player->team;
|
|
assert( team == 0 || team == 1 );
|
|
|
|
// find the distance to the closest active player for each spawn spot
|
|
for ( i = 0; i < teamSpawnSpots[ team ].Num(); i++ ) {
|
|
pos = teamSpawnSpots[ team ][ i ].ent->GetPhysics()->GetOrigin();
|
|
|
|
// skip initial spawn points for CTF
|
|
if ( teamSpawnSpots[ team ][ i ].ent->spawnArgs.GetBool("initial") ) {
|
|
teamSpawnSpots[ team ][ i ].dist = 0x0;
|
|
continue;
|
|
}
|
|
|
|
teamSpawnSpots[ team ][ i ].dist = 0x7fffffff;
|
|
|
|
for( j = 0; j < MAX_CLIENTS; j++ ) {
|
|
if ( !entities[ j ] || !entities[ j ]->IsType( idPlayer::Type )
|
|
|| entities[ j ] == player
|
|
|| static_cast< idPlayer * >( entities[ j ] )->spectating ) {
|
|
continue;
|
|
}
|
|
|
|
dist = ( pos - entities[ j ]->GetPhysics()->GetOrigin() ).LengthSqr();
|
|
if ( dist < teamSpawnSpots[ team ][ i ].dist ) {
|
|
teamSpawnSpots[ team ][ i ].dist = dist;
|
|
}
|
|
}
|
|
}
|
|
|
|
// sort the list
|
|
qsort( ( void * )teamSpawnSpots[ team ].Ptr(), teamSpawnSpots[ team ].Num(), sizeof( spawnSpot_t ), ( int (*)(const void *, const void *) )sortSpawnPoints );
|
|
|
|
// choose a random one in the top half
|
|
which = random.RandomInt( teamSpawnSpots[ team ].Num() / 2 );
|
|
spot = teamSpawnSpots[ team ][ which ];
|
|
// assert( teamSpawnSpots[ team ][ which ].dist != 0 );
|
|
|
|
return spot.ent;
|
|
}
|
|
|
|
// find the distance to the closest active player for each spawn spot
|
|
for( i = 0; i < spawnSpots.Num(); i++ ) {
|
|
pos = spawnSpots[ i ].ent->GetPhysics()->GetOrigin();
|
|
spawnSpots[ i ].dist = 0x7fffffff;
|
|
for( j = 0; j < MAX_CLIENTS; j++ ) {
|
|
if ( !entities[ j ] || !entities[ j ]->IsType( idPlayer::Type )
|
|
|| entities[ j ] == player
|
|
|| static_cast< idPlayer * >( entities[ j ] )->spectating ) {
|
|
continue;
|
|
}
|
|
|
|
dist = ( pos - entities[ j ]->GetPhysics()->GetOrigin() ).LengthSqr();
|
|
if ( dist < spawnSpots[ i ].dist ) {
|
|
spawnSpots[ i ].dist = dist;
|
|
}
|
|
}
|
|
}
|
|
|
|
// sort the list
|
|
qsort( ( void * )spawnSpots.Ptr(), spawnSpots.Num(), sizeof( spawnSpot_t ), ( int (*)(const void *, const void *) )sortSpawnPoints );
|
|
|
|
// choose a random one in the top half
|
|
which = random.RandomInt( spawnSpots.Num() / 2 );
|
|
spot = spawnSpots[ which ];
|
|
}
|
|
return spot.ent;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SetGlobalMaterial
|
|
================
|
|
*/
|
|
void idGameLocal::SetGlobalMaterial( const idMaterial *mat ) {
|
|
globalMaterial = mat;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetGlobalMaterial
|
|
================
|
|
*/
|
|
const idMaterial *idGameLocal::GetGlobalMaterial() {
|
|
return globalMaterial;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetSpawnId
|
|
================
|
|
*/
|
|
int idGameLocal::GetSpawnId( const idEntity* ent ) const {
|
|
return ( gameLocal.spawnIds[ ent->entityNumber ] << GENTITYNUM_BITS ) | ent->entityNumber;
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
idPlayer::SetPortalSkyEnt
|
|
=================
|
|
*/
|
|
void idGameLocal::SetPortalSkyEnt( idEntity *ent ) {
|
|
portalSkyEnt = ent;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
idPlayer::IsPortalSkyAcive
|
|
=================
|
|
*/
|
|
bool idGameLocal::IsPortalSkyAcive() {
|
|
return portalSkyActive;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SelectTimeGroup
|
|
============
|
|
*/
|
|
void idGameLocal::SelectTimeGroup( int timeGroup ) {
|
|
if ( timeGroup ) {
|
|
fast.Get( time, previousTime, realClientTime );
|
|
} else {
|
|
slow.Get( time, previousTime, realClientTime );
|
|
}
|
|
|
|
selectedGroup = timeGroup;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetTimeGroupTime
|
|
============
|
|
*/
|
|
int idGameLocal::GetTimeGroupTime( int timeGroup ) {
|
|
if ( timeGroup ) {
|
|
return fast.time;
|
|
} else {
|
|
return slow.time;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::ComputeSlowScale
|
|
============
|
|
*/
|
|
void idGameLocal::ComputeSlowScale() {
|
|
|
|
// check if we need to do a quick reset
|
|
if ( quickSlowmoReset ) {
|
|
quickSlowmoReset = false;
|
|
|
|
// stop the sounds
|
|
if ( gameSoundWorld ) {
|
|
gameSoundWorld->SetSlowmoSpeed( 1.0f );
|
|
}
|
|
|
|
// stop the state
|
|
slowmoState = SLOWMO_STATE_OFF;
|
|
slowmoScale = 1.0f;
|
|
}
|
|
|
|
// check the player state
|
|
idPlayer * player = GetLocalPlayer();
|
|
bool powerupOn = false;
|
|
|
|
if ( player != NULL && player->PowerUpActive( HELLTIME ) ) {
|
|
powerupOn = true;
|
|
}
|
|
else if ( g_enableSlowmo.GetBool() ) {
|
|
powerupOn = true;
|
|
}
|
|
|
|
// determine proper slowmo state
|
|
if ( powerupOn && slowmoState == SLOWMO_STATE_OFF ) {
|
|
slowmoState = SLOWMO_STATE_RAMPUP;
|
|
|
|
if ( gameSoundWorld ) {
|
|
gameSoundWorld->SetSlowmoSpeed( slowmoScale );
|
|
}
|
|
}
|
|
else if ( !powerupOn && slowmoState == SLOWMO_STATE_ON ) {
|
|
slowmoState = SLOWMO_STATE_RAMPDOWN;
|
|
|
|
// play the stop sound
|
|
if ( player != NULL ) {
|
|
player->PlayHelltimeStopSound();
|
|
}
|
|
}
|
|
|
|
// do any necessary ramping
|
|
if ( slowmoState == SLOWMO_STATE_RAMPUP ) {
|
|
float delta = ( 0.25f - slowmoScale );
|
|
|
|
if ( fabs( delta ) < g_slowmoStepRate.GetFloat() ) {
|
|
slowmoScale = 0.25f;
|
|
slowmoState = SLOWMO_STATE_ON;
|
|
} else {
|
|
slowmoScale += delta * g_slowmoStepRate.GetFloat();
|
|
}
|
|
|
|
if ( gameSoundWorld != NULL ) {
|
|
gameSoundWorld->SetSlowmoSpeed( slowmoScale );
|
|
}
|
|
}
|
|
else if ( slowmoState == SLOWMO_STATE_RAMPDOWN ) {
|
|
float delta = ( 1.0f - slowmoScale );
|
|
|
|
if ( fabs( delta ) < g_slowmoStepRate.GetFloat() ) {
|
|
slowmoScale = 1.0f;
|
|
slowmoState = SLOWMO_STATE_OFF;
|
|
} else {
|
|
slowmoScale += delta * g_slowmoStepRate.GetFloat();
|
|
}
|
|
|
|
if ( gameSoundWorld != NULL ) {
|
|
gameSoundWorld->SetSlowmoSpeed( slowmoScale );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::ResetSlowTimeVars
|
|
============
|
|
*/
|
|
void idGameLocal::ResetSlowTimeVars() {
|
|
slowmoScale = 1.0f;
|
|
slowmoState = SLOWMO_STATE_OFF;
|
|
|
|
fast.previousTime = 0;
|
|
fast.time = 0;
|
|
|
|
slow.previousTime = 0;
|
|
slow.time = 0;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::QuickSlowmoReset
|
|
============
|
|
*/
|
|
void idGameLocal::QuickSlowmoReset() {
|
|
quickSlowmoReset = true;
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetClientStats
|
|
================
|
|
*/
|
|
void idGameLocal::GetClientStats( int clientNum, char *data, const int len ) {
|
|
mpGame.PlayerStats( clientNum, data, len );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetMPGameModes
|
|
================
|
|
*/
|
|
int idGameLocal::GetMPGameModes( const char *** gameModes, const char *** gameModesDisplay ) {
|
|
return mpGame.GetGameModes( gameModes, gameModesDisplay );
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::IsPDAOpen
|
|
========================
|
|
*/
|
|
bool idGameLocal::IsPDAOpen() const {
|
|
return GetLocalPlayer() && GetLocalPlayer()->objectiveSystemOpen;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::IsPlayerChatting
|
|
========================
|
|
*/
|
|
bool idGameLocal::IsPlayerChatting() const {
|
|
return GetLocalPlayer() && ( GetLocalPlayer()->isChatting > 0 ) && !GetLocalPlayer()->spectating;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::InhibitControls
|
|
========================
|
|
*/
|
|
bool idGameLocal::InhibitControls() {
|
|
return ( Shell_IsActive() || IsPDAOpen() || IsPlayerChatting() || ( common->IsMultiplayer() && mpGame.IsScoreboardActive() ) );
|
|
}
|
|
|
|
/*
|
|
===============================
|
|
idGameLocal::Leaderboards_Init
|
|
===============================
|
|
*/
|
|
void idGameLocal::Leaderboards_Init() {
|
|
LeaderboardLocal_Init();
|
|
}
|
|
|
|
/*
|
|
===============================
|
|
idGameLocal::Leaderboards_Shutdown
|
|
===============================
|
|
*/
|
|
void idGameLocal::Leaderboards_Shutdown() {
|
|
LeaderboardLocal_Shutdown();
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_ClearRepeater
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_ClearRepeater() {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->ClearWidgetActionRepeater();
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_Init
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_Init( const char * filename, idSoundWorld * sw ) {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->Initialize( filename, sw );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_Init
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_Cleanup() {
|
|
if ( shellHandler != NULL ) {
|
|
delete shellHandler;
|
|
shellHandler = NULL;
|
|
}
|
|
|
|
mpGame.CleanupScoreboard();
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_CreateMenu
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_CreateMenu( bool inGame ) {
|
|
Shell_ResetMenu();
|
|
|
|
if ( shellHandler != NULL ) {
|
|
if ( !inGame ) {
|
|
shellHandler->SetInGame( false );
|
|
Shell_Init( "shell", common->MenuSW() );
|
|
} else {
|
|
shellHandler->SetInGame( true );
|
|
if ( common->IsMultiplayer() ) {
|
|
Shell_Init( "pause", common->SW() );
|
|
} else {
|
|
Shell_Init( "pause", common->MenuSW() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_ClosePause
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_ClosePause() {
|
|
if ( shellHandler != NULL ) {
|
|
|
|
if ( !common->IsMultiplayer() && GetLocalPlayer() && GetLocalPlayer()->health <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
if ( shellHandler->GetGameComplete() ) {
|
|
return;
|
|
}
|
|
|
|
shellHandler->SetNextScreen( SHELL_AREA_INVALID, MENU_TRANSITION_SIMPLE );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_Show
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_Show( bool show ) {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->ActivateMenu( show );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_IsActive
|
|
========================
|
|
*/
|
|
bool idGameLocal::Shell_IsActive() const {
|
|
if ( shellHandler != NULL ) {
|
|
return shellHandler->IsActive();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_HandleGuiEvent
|
|
========================
|
|
*/
|
|
bool idGameLocal::Shell_HandleGuiEvent( const sysEvent_t * sev ) {
|
|
if ( shellHandler != NULL ) {
|
|
return shellHandler->HandleGuiEvent( sev );
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_Render
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_Render() {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->Update();
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_ResetMenu
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_ResetMenu() {
|
|
if ( shellHandler != NULL ) {
|
|
delete shellHandler;
|
|
shellHandler = new (TAG_SWF) idMenuHandler_Shell();
|
|
}
|
|
}
|
|
|
|
/*
|
|
=================
|
|
idGameLocal::Shell_SyncWithSession
|
|
=================
|
|
*/
|
|
void idGameLocal::Shell_SyncWithSession() {
|
|
if ( shellHandler == NULL ) {
|
|
return;
|
|
}
|
|
switch ( session->GetState() ) {
|
|
case idSession::PRESS_START:
|
|
shellHandler->SetShellState( SHELL_STATE_PRESS_START );
|
|
break;
|
|
case idSession::INGAME:
|
|
shellHandler->SetShellState( SHELL_STATE_PAUSED );
|
|
break;
|
|
case idSession::IDLE:
|
|
shellHandler->SetShellState( SHELL_STATE_IDLE );
|
|
break;
|
|
case idSession::PARTY_LOBBY:
|
|
shellHandler->SetShellState( SHELL_STATE_PARTY_LOBBY );
|
|
break;
|
|
case idSession::GAME_LOBBY:
|
|
shellHandler->SetShellState( SHELL_STATE_GAME_LOBBY );
|
|
break;
|
|
case idSession::SEARCHING:
|
|
shellHandler->SetShellState( SHELL_STATE_SEARCHING );
|
|
break;
|
|
case idSession::LOADING:
|
|
shellHandler->SetShellState( SHELL_STATE_LOADING );
|
|
break;
|
|
case idSession::CONNECTING:
|
|
shellHandler->SetShellState( SHELL_STATE_CONNECTING );
|
|
break;
|
|
case idSession::BUSY:
|
|
shellHandler->SetShellState( SHELL_STATE_BUSY );
|
|
break;
|
|
}
|
|
}
|
|
|
|
void idGameLocal::Shell_SetGameComplete() {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->SetGameComplete();
|
|
Shell_Show( true );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_SetState_GameLobby
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_UpdateSavedGames() {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->UpdateSavedGames();
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_SetCanContinue
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_SetCanContinue( bool valid ) {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->SetCanContinue( valid );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_SetState_GameLobby
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_UpdateClientCountdown( int countdown ) {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->SetTimeRemaining( countdown );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::Shell_SetState_GameLobby
|
|
========================
|
|
*/
|
|
void idGameLocal::Shell_UpdateLeaderboard( const idLeaderboardCallback * callback ) {
|
|
if ( shellHandler != NULL ) {
|
|
shellHandler->UpdateLeaderboard( callback );
|
|
}
|
|
}
|
|
|
|
/*
|
|
========================
|
|
idGameLocal::SimulateProjectiles
|
|
========================
|
|
*/
|
|
bool idGameLocal::SimulateProjectiles() {
|
|
bool moreProjectiles = false;
|
|
// Simulate projectiles
|
|
for ( int i = 0; i < idProjectile::MAX_SIMULATED_PROJECTILES; i++ ) {
|
|
if ( idProjectile::projectilesToSimulate[i].projectile != NULL && idProjectile::projectilesToSimulate[i].startTime != 0 ) {
|
|
const int startTime = idProjectile::projectilesToSimulate[i].startTime;
|
|
const int startFrame = MSEC_TO_FRAME_FLOOR( startTime );
|
|
const int endFrame = startFrame + 1;
|
|
const int endTime = FRAME_TO_MSEC( endFrame );
|
|
|
|
idProjectile::projectilesToSimulate[i].projectile->SimulateProjectileFrame( endTime - startTime, endTime );
|
|
|
|
if ( idProjectile::projectilesToSimulate[i].projectile != NULL ) {
|
|
if ( endTime >= previousServerTime ) {
|
|
idProjectile::projectilesToSimulate[i].projectile->PostSimulate( endTime );
|
|
idProjectile::projectilesToSimulate[i].startTime = 0;
|
|
idProjectile::projectilesToSimulate[i].projectile = NULL;
|
|
} else {
|
|
idProjectile::projectilesToSimulate[i].startTime = endTime;
|
|
moreProjectiles = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return moreProjectiles;
|
|
}
|