mirror of
https://github.com/dhewm/dhewm3.git
synced 2024-11-27 06:32:27 +00:00
4438 lines
111 KiB
C++
4438 lines
111 KiB
C++
/*
|
|
===========================================================================
|
|
|
|
Doom 3 GPL Source Code
|
|
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.
|
|
|
|
This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").
|
|
|
|
Doom 3 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 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 Source Code. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
In addition, the Doom 3 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 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 <SDL_endian.h>
|
|
|
|
#include "sys/platform.h"
|
|
#include "idlib/LangDict.h"
|
|
#include "idlib/Timer.h"
|
|
#include "framework/async/NetworkSystem.h"
|
|
#include "framework/BuildVersion.h"
|
|
#include "framework/DeclEntityDef.h"
|
|
#include "framework/FileSystem.h"
|
|
#include "renderer/ModelManager.h"
|
|
|
|
#include "gamesys/SysCvar.h"
|
|
#include "gamesys/SysCmds.h"
|
|
#include "script/Script_Thread.h"
|
|
#include "ai/AI.h"
|
|
#include "anim/Anim_Testmodel.h"
|
|
#include "Camera.h"
|
|
#include "SmokeParticles.h"
|
|
#include "Player.h"
|
|
#include "WorldSpawn.h"
|
|
#include "Misc.h"
|
|
#include "Trigger.h"
|
|
|
|
#include "framework/Licensee.h" // DG: for ID__DATE__
|
|
|
|
#include "Game_local.h"
|
|
|
|
const int NUM_RENDER_PORTAL_BITS = idMath::BitsForInteger( PS_BLOCK_ALL );
|
|
|
|
const float DEFAULT_GRAVITY = 1066.0f;
|
|
const idVec3 DEFAULT_GRAVITY_VEC3( 0, 0, -DEFAULT_GRAVITY );
|
|
const int CINEMATIC_SKIP_DELAY = SEC2MS( 2.0f );
|
|
|
|
#ifdef GAME_DLL
|
|
|
|
idSys * sys = NULL;
|
|
idCommon * common = NULL;
|
|
idCmdSystem * cmdSystem = NULL;
|
|
idCVarSystem * cvarSystem = NULL;
|
|
idFileSystem * fileSystem = NULL;
|
|
idNetworkSystem * networkSystem = 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"
|
|
};
|
|
|
|
/*
|
|
===========
|
|
GetGameAPI
|
|
============
|
|
*/
|
|
extern "C" ID_GAME_API gameExport_t *GetGameAPI( gameImport_t *import ) {
|
|
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;
|
|
networkSystem = import->networkSystem;
|
|
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;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
TestGameAPI
|
|
============
|
|
*/
|
|
void TestGameAPI( void ) {
|
|
gameImport_t testImport;
|
|
gameExport_t testExport;
|
|
|
|
testImport.sys = ::sys;
|
|
testImport.common = ::common;
|
|
testImport.cmdSystem = ::cmdSystem;
|
|
testImport.cvarSystem = ::cvarSystem;
|
|
testImport.fileSystem = ::fileSystem;
|
|
testImport.networkSystem = ::networkSystem;
|
|
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( void ) {
|
|
int i;
|
|
|
|
serverInfo.Clear();
|
|
numClients = 0;
|
|
for ( i = 0; i < MAX_CLIENTS; i++ ) {
|
|
userInfo[i].Clear();
|
|
persistentPlayerInfo[i].Clear();
|
|
}
|
|
memset( usercmds, 0, sizeof( usercmds ) );
|
|
memset( entities, 0, sizeof( entities ) );
|
|
memset( spawnIds, -1, sizeof( spawnIds ) );
|
|
firstFreeIndex = 0;
|
|
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;
|
|
cinematicSkipTime = 0;
|
|
cinematicStopTime = 0;
|
|
cinematicMaxSkipTime = 0;
|
|
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;
|
|
skipCinematic = false;
|
|
influenceActive = false;
|
|
|
|
localClientNum = 0;
|
|
isMultiplayer = false;
|
|
isServer = false;
|
|
isClient = false;
|
|
realClientTime = 0;
|
|
isNewFrame = true;
|
|
clientSmoothing = 0.1f;
|
|
entityDefBits = 0;
|
|
|
|
nextGibTime = 0;
|
|
globalMaterial = NULL;
|
|
newInfo.Clear();
|
|
lastGUIEnt = NULL;
|
|
lastGUI = 0;
|
|
|
|
memset( clientEntityStates, 0, sizeof( clientEntityStates ) );
|
|
memset( clientPVS, 0, sizeof( clientPVS ) );
|
|
memset( clientSnapshots, 0, sizeof( clientSnapshots ) );
|
|
|
|
eventQueue.Init();
|
|
savedEventQueue.Init();
|
|
|
|
memset( lagometer, 0, sizeof( lagometer ) );
|
|
}
|
|
|
|
|
|
// DG: hack to support the Demo version of Doom3
|
|
// NOTE: I couldn't just make this a global bool variable that's initialized
|
|
// in idGameLocal::Init(), because we decide whether it's the demo
|
|
// after loading and initializing the game DLL (when loading the main menu)
|
|
static bool (*isDemoFnPtr)(void) = NULL;
|
|
bool IsDoom3DemoVersion()
|
|
{
|
|
bool ret = isDemoFnPtr ? isDemoFnPtr() : false;
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::Init
|
|
|
|
initialize the game object, only happens once at startup, not each level load
|
|
============
|
|
*/
|
|
void idGameLocal::Init( void ) {
|
|
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", ID__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();
|
|
|
|
// load default scripts
|
|
program.Startup( SCRIPT_DEFAULT );
|
|
|
|
smokeParticles = new idSmokeParticles;
|
|
|
|
// set up the aas
|
|
dict = FindEntityDefDict( "aas_types" );
|
|
if ( !dict ) {
|
|
Error( "Unable to find entityDef for 'aas_types'" );
|
|
}
|
|
|
|
// 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() );
|
|
|
|
|
|
// DG: hack to support the Demo version of Doom3
|
|
common->GetAdditionalFunction(idCommon::FT_IsDemo, (idCommon::FunctionPointer*)&isDemoFnPtr, NULL);
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::Shutdown
|
|
|
|
shut down the entire game
|
|
============
|
|
*/
|
|
void idGameLocal::Shutdown( void ) {
|
|
|
|
if ( !common ) {
|
|
return;
|
|
}
|
|
|
|
Printf( "----- Game Shutdown -----\n" );
|
|
|
|
mpGame.Shutdown();
|
|
|
|
MapShutdown();
|
|
|
|
aasList.DeleteContents( true );
|
|
aasNames.Clear();
|
|
|
|
idAI::FreeObstacleAvoidanceNodes();
|
|
|
|
// shutdown the model exporter
|
|
idModelExport::Shutdown();
|
|
|
|
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();
|
|
|
|
#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
|
|
}
|
|
|
|
/*
|
|
===========
|
|
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 ) {
|
|
int i;
|
|
idEntity *ent;
|
|
idEntity *link;
|
|
|
|
idSaveGame savegame( f );
|
|
|
|
if (g_flushSave.GetBool( ) == true ) {
|
|
// force flushing with each write... for tracking down
|
|
// save game bugs.
|
|
f->ForceFlush();
|
|
}
|
|
|
|
savegame.WriteBuildNumber( BUILD_NUMBER );
|
|
|
|
// DG: add some more information to savegame to make future quirks easier
|
|
savegame.WriteInt( INTERNAL_SAVEGAME_VERSION ); // to be independent of BUILD_NUMBER
|
|
savegame.WriteString( D3_OSTYPE ); // operating system - from CMake
|
|
savegame.WriteString( D3_ARCH ); // CPU architecture (e.g. "x86" or "x86_64") - from CMake
|
|
savegame.WriteString( ENGINE_VERSION );
|
|
savegame.WriteShort( (short)sizeof(void*) ); // tells us if it's from a 32bit (4) or 64bit system (8)
|
|
savegame.WriteShort( SDL_BYTEORDER ) ; // SDL_LIL_ENDIAN or SDL_BIG_ENDIAN
|
|
// DG end
|
|
|
|
// 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.WriteDict( &serverInfo );
|
|
|
|
savegame.WriteInt( numClients );
|
|
for( i = 0; i < numClients; i++ ) {
|
|
savegame.WriteDict( &userInfo[ i ] );
|
|
savegame.WriteUsercmd( usercmds[ i ] );
|
|
savegame.WriteDict( &persistentPlayerInfo[ i ] );
|
|
}
|
|
|
|
for( i = 0; i < MAX_GENTITIES; i++ ) {
|
|
savegame.WriteObject( entities[ i ] );
|
|
savegame.WriteInt( spawnIds[ i ] );
|
|
}
|
|
|
|
savegame.WriteInt( firstFreeIndex );
|
|
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.WriteInt( cinematicSkipTime );
|
|
savegame.WriteInt( cinematicStopTime );
|
|
savegame.WriteInt( cinematicMaxSkipTime );
|
|
savegame.WriteBool( inCinematic );
|
|
savegame.WriteBool( skipCinematic );
|
|
|
|
savegame.WriteBool( isMultiplayer );
|
|
savegame.WriteInt( gameType );
|
|
|
|
savegame.WriteInt( framenum );
|
|
savegame.WriteInt( previousTime );
|
|
savegame.WriteInt( time );
|
|
|
|
savegame.WriteInt( vacuumAreaNum );
|
|
|
|
savegame.WriteInt( entityDefBits );
|
|
savegame.WriteBool( isServer );
|
|
savegame.WriteBool( isClient );
|
|
|
|
savegame.WriteInt( localClientNum );
|
|
|
|
// snapshotEntities is used for multiplayer only
|
|
|
|
savegame.WriteInt( realClientTime );
|
|
savegame.WriteBool( isNewFrame );
|
|
savegame.WriteFloat( clientSmoothing );
|
|
|
|
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();
|
|
}
|
|
|
|
/*
|
|
===========
|
|
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::SetLocalClient
|
|
============
|
|
*/
|
|
void idGameLocal::SetLocalClient( int clientNum ) {
|
|
localClientNum = clientNum;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SetUserInfo
|
|
============
|
|
*/
|
|
const idDict* idGameLocal::SetUserInfo( int clientNum, const idDict &userInfo, bool isClient, bool canModify ) {
|
|
int i;
|
|
bool modifiedInfo = false;
|
|
|
|
this->isClient = isClient;
|
|
|
|
if ( clientNum >= 0 && clientNum < MAX_CLIENTS ) {
|
|
idGameLocal::userInfo[ clientNum ] = userInfo;
|
|
|
|
// server sanity
|
|
if ( canModify ) {
|
|
|
|
// don't let numeric nicknames, it can be exploited to go around kick and ban commands from the server
|
|
if ( idStr::IsNumeric( this->userInfo[ clientNum ].GetString( "ui_name" ) ) ) {
|
|
idGameLocal::userInfo[ clientNum ].Set( "ui_name", va( "%s_", idGameLocal::userInfo[ clientNum ].GetString( "ui_name" ) ) );
|
|
modifiedInfo = true;
|
|
}
|
|
|
|
// don't allow dupe nicknames
|
|
for ( i = 0; i < numClients; i++ ) {
|
|
if ( i == clientNum ) {
|
|
continue;
|
|
}
|
|
if ( entities[ i ] && entities[ i ]->IsType( idPlayer::Type ) ) {
|
|
if ( !idStr::Icmp( idGameLocal::userInfo[ clientNum ].GetString( "ui_name" ), idGameLocal::userInfo[ i ].GetString( "ui_name" ) ) ) {
|
|
idGameLocal::userInfo[ clientNum ].Set( "ui_name", va( "%s_", idGameLocal::userInfo[ clientNum ].GetString( "ui_name" ) ) );
|
|
modifiedInfo = true;
|
|
i = -1; // rescan
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( entities[ clientNum ] && entities[ clientNum ]->IsType( idPlayer::Type ) ) {
|
|
modifiedInfo |= static_cast<idPlayer *>( entities[ clientNum ] )->UserInfoChanged( canModify );
|
|
}
|
|
|
|
if ( !isClient ) {
|
|
// now mark this client in game
|
|
mpGame.EnterGame( clientNum );
|
|
}
|
|
}
|
|
|
|
if ( modifiedInfo ) {
|
|
assert( canModify );
|
|
newInfo = idGameLocal::userInfo[ clientNum ];
|
|
return &newInfo;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetUserInfo
|
|
============
|
|
*/
|
|
const idDict* idGameLocal::GetUserInfo( int clientNum ) {
|
|
if ( entities[ clientNum ] && entities[ clientNum ]->IsType( idPlayer::Type ) ) {
|
|
return &userInfo[ clientNum ];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SetServerInfo
|
|
============
|
|
*/
|
|
void idGameLocal::SetServerInfo( const idDict &_serverInfo ) {
|
|
idBitMsg outMsg;
|
|
byte msgBuf[MAX_GAME_MESSAGE_SIZE];
|
|
|
|
serverInfo = _serverInfo;
|
|
UpdateServerInfoFlags();
|
|
|
|
if ( !isClient ) {
|
|
// Let our clients know the server info changed
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_SERVERINFO );
|
|
outMsg.WriteDeltaDict( gameLocal.serverInfo, NULL );
|
|
networkSystem->ServerSendReliableMessage( -1, outMsg );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::LoadMap
|
|
|
|
Initializes all map variables common to both save games and spawned games.
|
|
===================
|
|
*/
|
|
void idGameLocal::LoadMap( const char *mapName, int randseed ) {
|
|
int i;
|
|
bool sameMap = (mapFile && idStr::Icmp(mapFileName, mapName) == 0);
|
|
|
|
// clear the sound system
|
|
gameSoundWorld->ClearAllSoundEmitters();
|
|
|
|
InitAsyncNetwork();
|
|
|
|
if ( !sameMap || ( mapFile && mapFile->NeedsReload() ) ) {
|
|
// load the .map file
|
|
if ( mapFile ) {
|
|
delete mapFile;
|
|
}
|
|
mapFile = new 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 );
|
|
|
|
numClients = 0;
|
|
|
|
// initialize all entities for this game
|
|
memset( entities, 0, sizeof( entities ) );
|
|
memset( usercmds, 0, sizeof( usercmds ) );
|
|
memset( spawnIds, -1, sizeof( spawnIds ) );
|
|
spawnCount = INITIAL_SPAWN_COUNT;
|
|
|
|
spawnedEntities.Clear();
|
|
activeEntities.Clear();
|
|
numEntitiesToDeactivate = 0;
|
|
sortTeamMasters = false;
|
|
sortPushers = false;
|
|
lastGUIEnt = NULL;
|
|
lastGUI = 0;
|
|
|
|
globalMaterial = NULL;
|
|
|
|
memset( globalShaderParms, 0, sizeof( globalShaderParms ) );
|
|
|
|
// 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;
|
|
firstFreeIndex = MAX_CLIENTS;
|
|
|
|
// reset the random number generator.
|
|
random.SetSeed( isMultiplayer ? randseed : 0 );
|
|
|
|
camera = NULL;
|
|
world = NULL;
|
|
testmodel = NULL;
|
|
testFx = NULL;
|
|
|
|
lastAIAlertEntity = NULL;
|
|
lastAIAlertTime = 0;
|
|
|
|
previousTime = 0;
|
|
time = 0;
|
|
framenum = 0;
|
|
sessionCommand = "";
|
|
nextGibTime = 0;
|
|
|
|
vacuumAreaNum = -1; // if an info_vacuum is spawned, it will set this
|
|
|
|
if ( !editEntities ) {
|
|
editEntities = new idEditEntities;
|
|
}
|
|
|
|
gravity.Set( 0, 0, -g_gravity.GetFloat() );
|
|
|
|
spawnArgs.Clear();
|
|
|
|
skipCinematic = false;
|
|
inCinematic = false;
|
|
cinematicSkipTime = 0;
|
|
cinematicStopTime = 0;
|
|
cinematicMaxSkipTime = 0;
|
|
|
|
clip.Init();
|
|
pvs.Init();
|
|
playerPVS.i = -1;
|
|
playerConnectedAreas.i = -1;
|
|
|
|
// load navigation system for all the different monster sizes
|
|
for( 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();
|
|
|
|
// cache miscellanious media references
|
|
FindEntityDef( "preCacheExtras", false );
|
|
|
|
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();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::MapRestart
|
|
===================
|
|
*/
|
|
void idGameLocal::MapRestart( ) {
|
|
idBitMsg outMsg;
|
|
byte msgBuf[MAX_GAME_MESSAGE_SIZE];
|
|
idDict newInfo;
|
|
int i;
|
|
const idKeyValue *keyval, *keyval2;
|
|
|
|
if ( isClient ) {
|
|
LocalMapRestart();
|
|
} else {
|
|
newInfo = *cvarSystem->MoveCVarsToDict( CVAR_SERVERINFO );
|
|
for ( i = 0; i < newInfo.GetNumKeyVals(); i++ ) {
|
|
keyval = newInfo.GetKeyVal( i );
|
|
keyval2 = serverInfo.FindKey( keyval->GetKey() );
|
|
if ( !keyval2 ) {
|
|
break;
|
|
}
|
|
// a select set of si_ changes will cause a full restart of the server
|
|
if ( keyval->GetValue().Cmp( keyval2->GetValue() ) &&
|
|
( !keyval->GetKey().Cmp( "si_pure" ) || !keyval->GetKey().Cmp( "si_map" ) ) ) {
|
|
break;
|
|
}
|
|
}
|
|
cmdSystem->BufferCommandText( CMD_EXEC_NOW, "rescanSI" );
|
|
if ( i != newInfo.GetNumKeyVals() ) {
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "nextMap" );
|
|
} else {
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_RESTART );
|
|
outMsg.WriteBits( 1, 1 );
|
|
outMsg.WriteDeltaDict( serverInfo, NULL );
|
|
networkSystem->ServerSendReliableMessage( -1, outMsg );
|
|
|
|
LocalMapRestart();
|
|
mpGame.MapRestart();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::MapRestart_f
|
|
===================
|
|
*/
|
|
void idGameLocal::MapRestart_f( const idCmdArgs &args ) {
|
|
if ( !gameLocal.isMultiplayer || gameLocal.isClient ) {
|
|
common->Printf( "server is not running - use spawnServer\n" );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "spawnServer\n" );
|
|
return;
|
|
}
|
|
|
|
gameLocal.MapRestart( );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::NextMap
|
|
===================
|
|
*/
|
|
bool idGameLocal::NextMap( void ) {
|
|
const function_t *func;
|
|
idThread *thread;
|
|
idDict newInfo;
|
|
const idKeyValue *keyval, *keyval2;
|
|
int i;
|
|
|
|
if ( !g_mapCycle.GetString()[0] ) {
|
|
Printf( common->GetLanguageDict()->GetString( "#str_04294" ) );
|
|
return false;
|
|
}
|
|
if ( fileSystem->ReadFile( g_mapCycle.GetString(), NULL, NULL ) < 0 ) {
|
|
if ( fileSystem->ReadFile( va( "%s.scriptcfg", g_mapCycle.GetString() ), NULL, NULL ) < 0 ) {
|
|
Printf( "map cycle script '%s': not found\n", g_mapCycle.GetString() );
|
|
return false;
|
|
} else {
|
|
g_mapCycle.SetString( va( "%s.scriptcfg", g_mapCycle.GetString() ) );
|
|
}
|
|
}
|
|
|
|
Printf( "map cycle script: '%s'\n", g_mapCycle.GetString() );
|
|
func = program.FindFunction( "mapcycle::cycle" );
|
|
if ( !func ) {
|
|
program.CompileFile( g_mapCycle.GetString() );
|
|
func = program.FindFunction( "mapcycle::cycle" );
|
|
}
|
|
if ( !func ) {
|
|
Printf( "Couldn't find mapcycle::cycle\n" );
|
|
return false;
|
|
}
|
|
thread = new idThread( func );
|
|
thread->Start();
|
|
delete thread;
|
|
|
|
newInfo = *cvarSystem->MoveCVarsToDict( CVAR_SERVERINFO );
|
|
for ( i = 0; i < newInfo.GetNumKeyVals(); i++ ) {
|
|
keyval = newInfo.GetKeyVal( i );
|
|
keyval2 = serverInfo.FindKey( keyval->GetKey() );
|
|
if ( !keyval2 || keyval->GetValue().Cmp( keyval2->GetValue() ) ) {
|
|
break;
|
|
}
|
|
}
|
|
return ( i != newInfo.GetNumKeyVals() );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::NextMap_f
|
|
===================
|
|
*/
|
|
void idGameLocal::NextMap_f( const idCmdArgs &args ) {
|
|
if ( !gameLocal.isMultiplayer || gameLocal.isClient ) {
|
|
common->Printf( "server is not running\n" );
|
|
return;
|
|
}
|
|
|
|
gameLocal.NextMap( );
|
|
// next map was either voted for or triggered by a server command - always restart
|
|
gameLocal.MapRestart( );
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::MapPopulate
|
|
===================
|
|
*/
|
|
void idGameLocal::MapPopulate( void ) {
|
|
|
|
if ( 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();
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::InitFromNewMap
|
|
===================
|
|
*/
|
|
void idGameLocal::InitFromNewMap( const char *mapName, idRenderWorld *renderWorld, idSoundWorld *soundWorld, bool isServer, bool isClient, int randseed, int activeEditors) {
|
|
|
|
this->isServer = isServer;
|
|
this->isClient = isClient;
|
|
this->isMultiplayer = isServer || isClient;
|
|
|
|
if ( mapFileName.Length() ) {
|
|
MapShutdown();
|
|
}
|
|
|
|
Printf( "----- Game Map Init -----\n" );
|
|
|
|
//exposing editor flag so debugger does not miss any script calls during load/startup
|
|
editors = activeEditors;
|
|
gamestate = GAMESTATE_STARTUP;
|
|
|
|
gameRenderWorld = renderWorld;
|
|
gameSoundWorld = soundWorld;
|
|
|
|
LoadMap( mapName, randseed );
|
|
|
|
InitScriptForMap();
|
|
|
|
MapPopulate();
|
|
|
|
mpGame.Reset();
|
|
|
|
mpGame.Precache();
|
|
|
|
// free up any unused animations
|
|
animationLib.FlushUnusedAnims();
|
|
|
|
gamestate = GAMESTATE_ACTIVE;
|
|
}
|
|
|
|
/*
|
|
=================
|
|
idGameLocal::InitFromSaveGame
|
|
=================
|
|
*/
|
|
bool idGameLocal::InitFromSaveGame( const char *mapName, idRenderWorld *renderWorld, idSoundWorld *soundWorld, idFile *saveGameFile, int activeEditors ) {
|
|
int i;
|
|
int num;
|
|
idEntity *ent;
|
|
idDict si;
|
|
|
|
if ( mapFileName.Length() ) {
|
|
MapShutdown();
|
|
}
|
|
|
|
Printf( "----- Game Map Init SaveGame -----\n" );
|
|
|
|
//exposing editor flag so debugger does not miss any script calls during load/startup
|
|
editors = activeEditors;
|
|
gamestate = GAMESTATE_STARTUP;
|
|
|
|
gameRenderWorld = renderWorld;
|
|
gameSoundWorld = soundWorld;
|
|
|
|
idRestoreGame savegame( saveGameFile );
|
|
|
|
savegame.ReadBuildNumber();
|
|
|
|
// DG: I enhanced the information in savegames a bit for dhewm3 1.5.1
|
|
// for which I bumped th BUILD_NUMBER to 1305
|
|
if( savegame.GetBuildNumber() >= 1305 )
|
|
{
|
|
savegame.ReadInternalSavegameVersion();
|
|
if( savegame.GetInternalSavegameVersion() > INTERNAL_SAVEGAME_VERSION ) {
|
|
Warning( "Savegame from newer dhewm3 version, don't know how to load! (its version is %d, only up to %d supported)",
|
|
savegame.GetInternalSavegameVersion(), INTERNAL_SAVEGAME_VERSION );
|
|
return false;
|
|
}
|
|
idStr osType;
|
|
idStr cpuArch;
|
|
idStr engineVersion;
|
|
short ptrSize = 0;
|
|
short byteorder = 0;
|
|
savegame.ReadString( osType ); // operating system the savegame was crated on (written from D3_OSTYPE)
|
|
savegame.ReadString( cpuArch ); // written from D3_ARCH (which is set in CMake), like "x86" or "x86_64"
|
|
savegame.ReadString( engineVersion ); // written from ENGINE_VERSION
|
|
savegame.ReadShort( ptrSize ); // sizeof(void*) of system that created the savegame, 4 on 32bit systems, 8 on 64bit systems
|
|
savegame.ReadShort( byteorder ); // SDL_LIL_ENDIAN or SDL_BIG_ENDIAN
|
|
|
|
Printf( "Savegame was created by %s on %s %s. BuildNumber was %d, savegameversion %d\n",
|
|
engineVersion.c_str(), osType.c_str(), cpuArch.c_str(), savegame.GetBuildNumber(),
|
|
savegame.GetInternalSavegameVersion() );
|
|
|
|
// right now I have no further use for this information, but in the future
|
|
// it can be used for quirks for (then-) old savegames
|
|
}
|
|
// DG end
|
|
|
|
// 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;
|
|
}
|
|
|
|
// load the map needed for this savegame
|
|
LoadMap( mapName, 0 );
|
|
|
|
savegame.ReadInt( i );
|
|
g_skill.SetInteger( i );
|
|
|
|
// precache the player
|
|
FindEntityDef( "player_doommarine", false );
|
|
|
|
// precache any media specified in the map
|
|
for ( i = 0; i < mapFile->GetNumEntities(); i++ ) {
|
|
idMapEntity *mapEnt = mapFile->GetEntity( i );
|
|
|
|
if ( !InhibitEntitySpawn( mapEnt->epairs ) ) {
|
|
CacheDictionaryMedia( &mapEnt->epairs );
|
|
const char *classname;
|
|
if ( mapEnt->epairs.GetString( "classname", "", &classname ) ) {
|
|
FindEntityDef( classname, false );
|
|
}
|
|
}
|
|
}
|
|
|
|
savegame.ReadDict( &si );
|
|
SetServerInfo( si );
|
|
|
|
savegame.ReadInt( numClients );
|
|
for( i = 0; i < numClients; i++ ) {
|
|
savegame.ReadDict( &userInfo[ i ] );
|
|
savegame.ReadUsercmd( usercmds[ i ] );
|
|
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;
|
|
}
|
|
}
|
|
|
|
savegame.ReadInt( firstFreeIndex );
|
|
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.ReadInt( cinematicSkipTime );
|
|
savegame.ReadInt( cinematicStopTime );
|
|
savegame.ReadInt( cinematicMaxSkipTime );
|
|
savegame.ReadBool( inCinematic );
|
|
savegame.ReadBool( skipCinematic );
|
|
|
|
savegame.ReadBool( isMultiplayer );
|
|
savegame.ReadInt( (int &)gameType );
|
|
|
|
savegame.ReadInt( framenum );
|
|
savegame.ReadInt( previousTime );
|
|
savegame.ReadInt( time );
|
|
|
|
savegame.ReadInt( vacuumAreaNum );
|
|
|
|
savegame.ReadInt( entityDefBits );
|
|
savegame.ReadBool( isServer );
|
|
savegame.ReadBool( isClient );
|
|
|
|
savegame.ReadInt( localClientNum );
|
|
|
|
// snapshotEntities is used for multiplayer only
|
|
|
|
savegame.ReadInt( realClientTime );
|
|
savegame.ReadBool( isNewFrame );
|
|
savegame.ReadFloat( clientSmoothing );
|
|
|
|
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 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;
|
|
|
|
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( void ) {
|
|
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 );
|
|
|
|
// reset the script to the state it was before the map was started
|
|
program.Restart();
|
|
|
|
if ( smokeParticles ) {
|
|
smokeParticles->Shutdown();
|
|
}
|
|
|
|
pvs.Shutdown();
|
|
|
|
clip.Shutdown();
|
|
idClipModel::ClearTraceModelCache();
|
|
|
|
ShutdownAsyncNetwork();
|
|
|
|
mapFileName.Clear();
|
|
|
|
gameRenderWorld = NULL;
|
|
gameSoundWorld = NULL;
|
|
|
|
gamestate = GAMESTATE_NOMAP;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::DumpOggSounds
|
|
===================
|
|
*/
|
|
void idGameLocal::DumpOggSounds( void ) {
|
|
int i, j, k, size, totalSize;
|
|
idFile *file;
|
|
idStrList oggSounds, weaponSounds;
|
|
const idSoundShader *soundShader;
|
|
const soundShaderParms_t *parms;
|
|
idStr soundName;
|
|
|
|
for ( i = 0; i < declManager->GetNumDecls( DECL_SOUND ); i++ ) {
|
|
soundShader = static_cast<const idSoundShader *>(declManager->DeclByIndex( DECL_SOUND, i, false ));
|
|
parms = soundShader->GetParms();
|
|
|
|
if ( soundShader->EverReferenced() && soundShader->GetState() != DS_DEFAULTED ) {
|
|
|
|
const_cast<idSoundShader *>(soundShader)->EnsureNotPurged();
|
|
|
|
for ( j = 0; j < soundShader->GetNumSounds(); j++ ) {
|
|
soundName = soundShader->GetSound( j );
|
|
soundName.BackSlashesToSlashes();
|
|
|
|
// don't OGG sounds that cause a shake because that would
|
|
// cause continuous seeking on the OGG file which is expensive
|
|
if ( parms->shakes != 0.0f ) {
|
|
shakeSounds.AddUnique( soundName );
|
|
continue;
|
|
}
|
|
|
|
// if not voice over or combat chatter
|
|
if ( soundName.Find( "/vo/", false ) == -1 &&
|
|
soundName.Find( "/combat_chatter/", false ) == -1 &&
|
|
soundName.Find( "/bfgcarnage/", false ) == -1 &&
|
|
soundName.Find( "/enpro/", false ) == - 1 &&
|
|
soundName.Find( "/soulcube/energize_01.wav", false ) == -1 ) {
|
|
// don't OGG weapon sounds
|
|
if ( soundName.Find( "weapon", false ) != -1 ||
|
|
soundName.Find( "gun", false ) != -1 ||
|
|
soundName.Find( "bullet", false ) != -1 ||
|
|
soundName.Find( "bfg", false ) != -1 ||
|
|
soundName.Find( "plasma", false ) != -1 ) {
|
|
weaponSounds.AddUnique( soundName );
|
|
continue;
|
|
}
|
|
}
|
|
|
|
for ( k = 0; k < shakeSounds.Num(); k++ ) {
|
|
if ( shakeSounds[k].IcmpPath( soundName ) == 0 ) {
|
|
break;
|
|
}
|
|
}
|
|
if ( k < shakeSounds.Num() ) {
|
|
continue;
|
|
}
|
|
|
|
oggSounds.AddUnique( soundName );
|
|
}
|
|
}
|
|
}
|
|
|
|
file = fileSystem->OpenFileWrite( "makeogg.bat", "fs_savepath" );
|
|
if ( file == NULL ) {
|
|
common->Warning( "Couldn't open makeogg.bat" );
|
|
return;
|
|
}
|
|
|
|
// list all the shake sounds
|
|
totalSize = 0;
|
|
for ( i = 0; i < shakeSounds.Num(); i++ ) {
|
|
size = fileSystem->ReadFile( shakeSounds[i], NULL, NULL );
|
|
totalSize += size;
|
|
shakeSounds[i].Replace( "/", "\\" );
|
|
file->Printf( "echo \"%s\" (%d kB)\n", shakeSounds[i].c_str(), size >> 10 );
|
|
}
|
|
file->Printf( "echo %d kB in shake sounds\n\n\n", totalSize >> 10 );
|
|
|
|
// list all the weapon sounds
|
|
totalSize = 0;
|
|
for ( i = 0; i < weaponSounds.Num(); i++ ) {
|
|
size = fileSystem->ReadFile( weaponSounds[i], NULL, NULL );
|
|
totalSize += size;
|
|
weaponSounds[i].Replace( "/", "\\" );
|
|
file->Printf( "echo \"%s\" (%d kB)\n", weaponSounds[i].c_str(), size >> 10 );
|
|
}
|
|
file->Printf( "echo %d kB in weapon sounds\n\n\n", totalSize >> 10 );
|
|
|
|
// list commands to convert all other sounds to ogg
|
|
totalSize = 0;
|
|
for ( i = 0; i < oggSounds.Num(); i++ ) {
|
|
size = fileSystem->ReadFile( oggSounds[i], NULL, NULL );
|
|
totalSize += size;
|
|
oggSounds[i].Replace( "/", "\\" );
|
|
file->Printf( "w:\\doom\\ogg\\oggenc -q 0 \"c:\\doom\\base\\%s\"\n", oggSounds[i].c_str() );
|
|
file->Printf( "del \"c:\\doom\\base\\%s\"\n", oggSounds[i].c_str() );
|
|
}
|
|
file->Printf( "\n\necho %d kB in OGG sounds\n\n\n", totalSize >> 10 );
|
|
|
|
fileSystem->CloseFile( file );
|
|
|
|
shakeSounds.Clear();
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::GetShakeSounds
|
|
===================
|
|
*/
|
|
void idGameLocal::GetShakeSounds( const idDict *dict ) {
|
|
const idSoundShader *soundShader;
|
|
const char *soundShaderName;
|
|
idStr soundName;
|
|
|
|
if ( dict->GetString( "s_shader", "", &soundShaderName )
|
|
&& dict->GetFloat( "s_shakes" ) != 0.0f )
|
|
{
|
|
soundShader = declManager->FindSound( soundShaderName );
|
|
|
|
for ( int i = 0; i < soundShader->GetNumSounds(); i++ ) {
|
|
soundName = soundShader->GetSound( i );
|
|
soundName.BackSlashesToSlashes();
|
|
|
|
shakeSounds.AddUnique( soundName );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
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;
|
|
|
|
if ( dict == NULL ) {
|
|
if ( cvarSystem->GetCVarBool( "com_makingBuild") ) {
|
|
DumpOggSounds();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ( cvarSystem->GetCVarBool( "com_makingBuild" ) ) {
|
|
GetShakeSounds( dict );
|
|
}
|
|
|
|
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(), true );
|
|
}
|
|
}
|
|
kv = dict->MatchPrefix( "model", kv );
|
|
}
|
|
|
|
kv = dict->FindKey( "s_shader" );
|
|
if ( kv && kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_SOUND, kv->GetValue() );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "snd", NULL );
|
|
while( kv ) {
|
|
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 && kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_MATERIAL, kv->GetValue() );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "mtr", NULL );
|
|
while( kv ) {
|
|
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 ) {
|
|
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 && 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 ) {
|
|
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 ) {
|
|
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 ) {
|
|
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 ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
FindEntityDef( kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "def", kv );
|
|
}
|
|
|
|
kv = dict->MatchPrefix( "pda_name", NULL );
|
|
while( kv ) {
|
|
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 ) {
|
|
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 ) {
|
|
if ( kv->GetValue().Length() ) {
|
|
declManager->FindType( DECL_AUDIO, kv->GetValue().c_str(), false );
|
|
}
|
|
kv = dict->MatchPrefix( "audio", kv );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::InitScriptForMap
|
|
============
|
|
*/
|
|
void idGameLocal::InitScriptForMap( void ) {
|
|
// 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::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 ) );
|
|
args.Set( "classname", isMultiplayer ? "player_doommarine_mp" : "player_doommarine" );
|
|
if ( !SpawnEntityDef( args, &ent ) || !entities[ clientNum ] ) {
|
|
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() );
|
|
}
|
|
|
|
if ( clientNum >= numClients ) {
|
|
numClients = clientNum + 1;
|
|
}
|
|
|
|
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::GetClientByName
|
|
================
|
|
*/
|
|
idPlayer *idGameLocal::GetClientByName( const char *name ) const {
|
|
int i;
|
|
idEntity *ent;
|
|
for ( i = 0 ; i < numClients ; i++ ) {
|
|
ent = entities[ i ];
|
|
if ( ent && ent->IsType( idPlayer::Type ) ) {
|
|
if ( idStr::IcmpNoColor( name, userInfo[ i ].GetString( "ui_name" ) ) == 0 ) {
|
|
return static_cast<idPlayer *>( ent );
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetClientByCmdArgs
|
|
================
|
|
*/
|
|
idPlayer *idGameLocal::GetClientByCmdArgs( const idCmdArgs &args ) const {
|
|
idPlayer *player;
|
|
idStr client = args.Argv( 1 );
|
|
if ( !client.Length() ) {
|
|
return NULL;
|
|
}
|
|
// we don't allow numeric ui_name so this can't go wrong
|
|
if ( client.IsNumeric() ) {
|
|
player = GetClientByNum( atoi( client.c_str() ) );
|
|
} else {
|
|
player = GetClientByName( client.c_str() );
|
|
}
|
|
if ( !player ) {
|
|
common->Printf( "Player '%s' not found\n", client.c_str() );
|
|
}
|
|
return player;
|
|
}
|
|
|
|
/*
|
|
================
|
|
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 ( localClientNum < 0 ) {
|
|
return NULL;
|
|
}
|
|
|
|
if ( !entities[ localClientNum ] || !entities[ localClientNum ]->IsType( idPlayer::Type ) ) {
|
|
// not fully in game yet
|
|
return NULL;
|
|
}
|
|
return static_cast<idPlayer *>( entities[ localClientNum ] );
|
|
}
|
|
|
|
/*
|
|
================
|
|
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( void ) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::FreePlayerPVS
|
|
================
|
|
*/
|
|
void idGameLocal::FreePlayerPVS( void ) {
|
|
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( void ) {
|
|
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( void ) 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( void ) {
|
|
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::RunFrame
|
|
================
|
|
*/
|
|
gameReturn_t idGameLocal::RunFrame( const usercmd_t *clientCmds, int activeEditors ) {
|
|
idEntity * ent;
|
|
int num;
|
|
float ms;
|
|
idTimer timer_think, timer_events, timer_singlethink;
|
|
gameReturn_t ret;
|
|
idPlayer *player;
|
|
const renderView_t *view;
|
|
|
|
//exposing editor flag so debugger does not miss any script calls during load/startup
|
|
editors = activeEditors;
|
|
|
|
#ifdef _DEBUG
|
|
if ( isMultiplayer ) {
|
|
assert( !isClient );
|
|
}
|
|
#endif
|
|
|
|
player = GetLocalPlayer();
|
|
|
|
if ( !isMultiplayer && g_stopTime.GetBool() ) {
|
|
// clear any debug lines from a previous frame
|
|
gameRenderWorld->DebugClearLines( time + 1 );
|
|
|
|
// set the user commands for this frame
|
|
memcpy( usercmds, clientCmds, numClients * sizeof( usercmds[ 0 ] ) );
|
|
|
|
if ( player ) {
|
|
player->Think();
|
|
}
|
|
} else do {
|
|
// update the game time
|
|
framenum++;
|
|
previousTime = time;
|
|
time += msec;
|
|
realClientTime = time;
|
|
|
|
#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 );
|
|
|
|
// set the user commands for this frame
|
|
memcpy( usercmds, clientCmds, numClients * sizeof( usercmds[ 0 ] ) );
|
|
|
|
// 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();
|
|
ent->Think();
|
|
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;
|
|
}
|
|
ent->Think();
|
|
num++;
|
|
}
|
|
} else {
|
|
num = 0;
|
|
for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) {
|
|
ent->Think();
|
|
num++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
timer_events.Stop();
|
|
|
|
// free the player pvs
|
|
FreePlayerPVS();
|
|
|
|
// do multiplayer related stuff
|
|
if ( isMultiplayer ) {
|
|
mpGame.Run();
|
|
}
|
|
|
|
// display how long it took to calculate the current game frame
|
|
if ( g_frametime.GetBool() ) {
|
|
Printf( "game %d: all:%u th:%u ev:%u %d ents \n",
|
|
time, timer_think.Milliseconds() + timer_events.Milliseconds(),
|
|
timer_think.Milliseconds(), timer_events.Milliseconds(), num );
|
|
}
|
|
|
|
// build the return value
|
|
ret.consistencyHash = 0;
|
|
ret.sessionCommand[0] = 0;
|
|
|
|
if ( !isMultiplayer && player ) {
|
|
ret.health = player->health;
|
|
ret.heartRate = player->heartRate;
|
|
ret.stamina = idMath::FtoiFast( player->stamina );
|
|
// combat is a 0-100 value based on lastHitTime and lastDmgTime
|
|
// each make up 50% of the time spread over 10 seconds
|
|
ret.combat = 0;
|
|
if ( player->lastDmgTime > 0 && time < player->lastDmgTime + 10000 ) {
|
|
ret.combat += 50.0f * (float) ( time - player->lastDmgTime ) / 10000;
|
|
}
|
|
if ( player->lastHitTime > 0 && time < player->lastHitTime + 10000 ) {
|
|
ret.combat += 50.0f * (float) ( time - player->lastHitTime ) / 10000;
|
|
}
|
|
}
|
|
|
|
// see if a target_sessionCommand has forced a changelevel
|
|
if ( sessionCommand.Length() ) {
|
|
strncpy( ret.sessionCommand, sessionCommand, sizeof( ret.sessionCommand ) );
|
|
break;
|
|
}
|
|
|
|
// make sure we don't loop forever when skipping a cinematic
|
|
if ( skipCinematic && ( time > cinematicMaxSkipTime ) ) {
|
|
Warning( "Exceeded maximum cinematic skip length. Cinematic may be looping infinitely." );
|
|
skipCinematic = false;
|
|
break;
|
|
}
|
|
} while( ( inCinematic || ( time < cinematicStopTime ) ) && skipCinematic );
|
|
|
|
ret.syncNextGameFrame = skipCinematic;
|
|
if ( skipCinematic ) {
|
|
soundSystem->SetMute( false );
|
|
skipCinematic = false;
|
|
}
|
|
|
|
// show any debug info for this frame
|
|
RunDebugInfo();
|
|
D_DrawDebugLines();
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/*
|
|
======================================================================
|
|
|
|
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 {
|
|
float x;
|
|
float y;
|
|
float ratio_x;
|
|
float ratio_y;
|
|
|
|
// first, calculate the vertical fov based on a 640x480 view
|
|
x = 640.0f / tan( base_fov / 360.0f * idMath::PI );
|
|
y = atan2( 480.0f, x );
|
|
fov_y = y * 360.0f / idMath::PI;
|
|
|
|
// FIXME: somehow, this is happening occasionally
|
|
assert( fov_y > 0 );
|
|
if ( fov_y <= 0 ) {
|
|
Error( "idGameLocal::CalcFov: bad result, fov_y == %f, base_fov == %f", fov_y, base_fov );
|
|
}
|
|
|
|
switch( r_aspectRatio.GetInteger() ) {
|
|
default :
|
|
case -1 :
|
|
// auto mode => use aspect ratio from resolution, assuming screen's pixels are squares
|
|
ratio_x = renderSystem->GetScreenWidth();
|
|
ratio_y = renderSystem->GetScreenHeight();
|
|
if(ratio_x <= 0.0f || ratio_y <= 0.0f)
|
|
{
|
|
// for some reason (maybe this is a dedicated server?) GetScreenWidth()/Height()
|
|
// returned 0. Assume default 4:3 to avoid assert()/Error() below.
|
|
fov_x = base_fov;
|
|
return;
|
|
}
|
|
break;
|
|
case 0 :
|
|
// 4:3
|
|
fov_x = base_fov;
|
|
return;
|
|
break;
|
|
|
|
case 1 :
|
|
// 16:9
|
|
ratio_x = 16.0f;
|
|
ratio_y = 9.0f;
|
|
break;
|
|
|
|
case 2 :
|
|
// 16:10
|
|
ratio_x = 16.0f;
|
|
ratio_y = 10.0f;
|
|
break;
|
|
}
|
|
|
|
y = ratio_y / tan( fov_y / 360.0f * idMath::PI );
|
|
fov_x = atan2( ratio_x, y ) * 360.0f / idMath::PI;
|
|
|
|
if ( fov_x < base_fov ) {
|
|
fov_x = base_fov;
|
|
x = ratio_x / tan( fov_x / 360.0f * idMath::PI );
|
|
fov_y = atan2( ratio_y, x ) * 360.0f / idMath::PI;
|
|
}
|
|
|
|
// FIXME: somehow, this is happening occasionally
|
|
assert( ( fov_x > 0 ) && ( fov_y > 0 ) );
|
|
if ( ( fov_y <= 0 ) || ( fov_x <= 0 ) ) {
|
|
Error( "idGameLocal::CalcFov: bad result" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::Draw
|
|
|
|
makes rendering and sound system calls
|
|
================
|
|
*/
|
|
bool idGameLocal::Draw( int clientNum ) {
|
|
if ( isMultiplayer ) {
|
|
return mpGame.Draw( clientNum );
|
|
}
|
|
|
|
idPlayer *player = static_cast<idPlayer *>(entities[ clientNum ]);
|
|
|
|
if ( !player ) {
|
|
return false;
|
|
}
|
|
|
|
// render the scene
|
|
player->playerView.RenderPlayerView( player->hud );
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::HandleESC
|
|
================
|
|
*/
|
|
escReply_t idGameLocal::HandleESC( idUserInterface **gui ) {
|
|
if ( isMultiplayer ) {
|
|
*gui = StartMenu();
|
|
// we may set the gui back to NULL to hide it
|
|
return ESC_GUI;
|
|
}
|
|
idPlayer *player = GetLocalPlayer();
|
|
if ( player ) {
|
|
if ( player->HandleESC() ) {
|
|
return ESC_IGNORE;
|
|
} else {
|
|
return ESC_MAIN;
|
|
}
|
|
}
|
|
return ESC_MAIN;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::StartMenu
|
|
================
|
|
*/
|
|
idUserInterface* idGameLocal::StartMenu( void ) {
|
|
if ( !isMultiplayer ) {
|
|
return NULL;
|
|
}
|
|
return mpGame.StartMenu();
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::HandleGuiCommands
|
|
================
|
|
*/
|
|
const char* idGameLocal::HandleGuiCommands( const char *menuCommand ) {
|
|
if ( !isMultiplayer ) {
|
|
return NULL;
|
|
}
|
|
return mpGame.HandleGuiCommands( menuCommand );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::HandleMainMenuCommands
|
|
================
|
|
*/
|
|
void idGameLocal::HandleMainMenuCommands( const char *menuCommand, idUserInterface *gui ) { }
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetLevelMap
|
|
|
|
should only be used for in-game level editing
|
|
================
|
|
*/
|
|
idMapFile *idGameLocal::GetLevelMap( void ) {
|
|
if ( mapFile && mapFile->HasPrimitiveData()) {
|
|
return mapFile;
|
|
}
|
|
if ( !mapFileName.Length() ) {
|
|
return NULL;
|
|
}
|
|
|
|
if ( mapFile ) {
|
|
delete mapFile;
|
|
}
|
|
|
|
mapFile = new idMapFile;
|
|
if ( !mapFile->Parse( mapFileName ) ) {
|
|
delete mapFile;
|
|
mapFile = NULL;
|
|
}
|
|
|
|
return mapFile;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetMapName
|
|
================
|
|
*/
|
|
const char *idGameLocal::GetMapName( void ) 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( void ) {
|
|
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( void ) {
|
|
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( void ) 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 id_attribute((unused));
|
|
|
|
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( void ) {
|
|
int i;
|
|
|
|
for( i = 0; i < aasList.Num(); i++ ) {
|
|
aasList[ i ]->RemoveAllObstacles();
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idGameLocal::CheatsOk
|
|
==================
|
|
*/
|
|
bool idGameLocal::CheatsOk( bool requirePlayer ) {
|
|
idPlayer *player;
|
|
|
|
if ( isMultiplayer && !cvarSystem->GetCVarBool( "net_allowCheats" ) ) {
|
|
Printf( "Not allowed in multiplayer.\n" );
|
|
return false;
|
|
}
|
|
|
|
if ( developer.GetBool() ) {
|
|
return true;
|
|
}
|
|
|
|
player = GetLocalPlayer();
|
|
if ( !requirePlayer || ( player && ( 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 spawn_entnum;
|
|
|
|
if ( spawnCount >= ( 1 << ( 32 - GENTITYNUM_BITS ) ) ) {
|
|
Error( "idGameLocal::RegisterEntity: spawn count overflow" );
|
|
}
|
|
|
|
if ( !spawnArgs.GetInt( "spawn_entnum", "0", spawn_entnum ) ) {
|
|
while( entities[firstFreeIndex] && firstFreeIndex < ENTITYNUM_MAX_NORMAL ) {
|
|
firstFreeIndex++;
|
|
}
|
|
if ( firstFreeIndex >= ENTITYNUM_MAX_NORMAL ) {
|
|
Error( "no free entities" );
|
|
}
|
|
spawn_entnum = firstFreeIndex++;
|
|
}
|
|
|
|
entities[ spawn_entnum ] = ent;
|
|
spawnIds[ spawn_entnum ] = spawnCount++;
|
|
ent->entityNumber = spawn_entnum;
|
|
ent->spawnNode.AddToEnd( spawnedEntities );
|
|
ent->spawnArgs.TransferKeyValues( spawnArgs );
|
|
|
|
if ( spawn_entnum >= num_entities ) {
|
|
num_entities++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idGameLocal::UnregisterEntity
|
|
===================
|
|
*/
|
|
void idGameLocal::UnregisterEntity( idEntity *ent ) {
|
|
assert( ent );
|
|
|
|
if ( editEntities ) {
|
|
editEntities->RemoveSelectedEntity( ent );
|
|
}
|
|
|
|
if ( ( ent->entityNumber != ENTITYNUM_NONE ) && ( entities[ ent->entityNumber ] == ent ) ) {
|
|
ent->spawnNode.Remove();
|
|
entities[ ent->entityNumber ] = NULL;
|
|
spawnIds[ ent->entityNumber ] = -1;
|
|
if ( ent->entityNumber >= MAX_CLIENTS && ent->entityNumber < firstFreeIndex ) {
|
|
firstFreeIndex = ent->entityNumber;
|
|
}
|
|
ent->entityNumber = ENTITYNUM_NONE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SpawnEntityType
|
|
================
|
|
*/
|
|
idEntity *idGameLocal::SpawnEntityType( const idTypeInfo &classdef, const idDict *args, bool bIsClientReadSnapshot ) {
|
|
idClass *obj;
|
|
|
|
#if _DEBUG
|
|
if ( 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 );
|
|
|
|
// 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 ( 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 ( 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 );
|
|
}
|
|
|
|
const char *name;
|
|
if ( g_skill.GetInteger() == 3 ) {
|
|
name = spawnArgs.GetString( "classname" );
|
|
if ( idStr::Icmp( name, "item_medkit" ) == 0 || idStr::Icmp( name, "item_medkit_small" ) == 0 ) {
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
if ( gameLocal.isMultiplayer ) {
|
|
name = spawnArgs.GetString( "classname" );
|
|
if ( idStr::Icmp( name, "weapon_bfg" ) == 0 || idStr::Icmp( name, "weapon_soulcube" ) == 0 ) {
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SetSkill
|
|
================
|
|
*/
|
|
void idGameLocal::SetSkill( int value ) {
|
|
int skill_level;
|
|
|
|
if ( value < 0 ) {
|
|
skill_level = 0;
|
|
} else if ( value > 3 ) {
|
|
skill_level = 3;
|
|
} else {
|
|
skill_level = value;
|
|
}
|
|
|
|
g_skill.SetInteger( skill_level );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::GameState
|
|
|
|
Used to allow entities to know if they're being spawned during the initial spawn.
|
|
==============
|
|
*/
|
|
gameState_t idGameLocal::GameState( void ) const {
|
|
return gamestate;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
idGameLocal::SpawnMapEntities
|
|
|
|
Parses textual entity definitions out of an entstring and spawns gentities.
|
|
==============
|
|
*/
|
|
void idGameLocal::SpawnMapEntities( void ) {
|
|
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;
|
|
}
|
|
|
|
SetSkill( g_skill.GetInteger() );
|
|
|
|
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++ ) {
|
|
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
|
|
if ( hit->IsType( idPlayer::Type ) && static_cast< idPlayer * >( hit )->IsInTeleport() ) {
|
|
static_cast< idPlayer * >( hit )->TeleportDeath( ent->entityNumber );
|
|
} else if ( !catch_teleport ) {
|
|
hit->Damage( ent, ent, vec3_origin, "damage_telefrag", 1.0f, INVALID_JOINT );
|
|
}
|
|
|
|
if ( !gameLocal.isMultiplayer ) {
|
|
// let the mapper know about it
|
|
Warning( "'%s' telefragged '%s'", ent->name.c_str(), hit->name.c_str() );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
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 + msec;
|
|
lastAIAlertEntity = static_cast<idActor *>( ent );
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
idGameLocal::GetAlertEntity
|
|
============
|
|
*/
|
|
idActor *idGameLocal::GetAlertEntity( void ) {
|
|
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 ( 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;
|
|
}
|
|
|
|
ent->Damage( inflictor, attacker, dir, damageDefName, damageScale, INVALID_JOINT );
|
|
}
|
|
}
|
|
|
|
// 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[ INTSIGNBITSET( 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 ), time );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
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;
|
|
|
|
if ( skipCinematic && camera->spawnArgs.GetBool( "disconnect" ) ) {
|
|
camera->spawnArgs.SetBool( "disconnect", false );
|
|
cvarSystem->SetCVarFloat( "r_znear", 3.0f );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" );
|
|
skipCinematic = false;
|
|
return;
|
|
}
|
|
|
|
if ( time > cinematicStopTime ) {
|
|
cinematicSkipTime = time + CINEMATIC_SKIP_DELAY;
|
|
}
|
|
|
|
// 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;
|
|
cinematicStopTime = time + msec;
|
|
|
|
// 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( void ) const {
|
|
return camera;
|
|
}
|
|
|
|
/*
|
|
=============
|
|
idGameLocal::SkipCinematic
|
|
=============
|
|
*/
|
|
bool idGameLocal::SkipCinematic( void ) {
|
|
if ( camera ) {
|
|
if ( camera->spawnArgs.GetBool( "disconnect" ) ) {
|
|
camera->spawnArgs.SetBool( "disconnect", false );
|
|
cvarSystem->SetCVarFloat( "r_znear", 3.0f );
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" );
|
|
skipCinematic = false;
|
|
return false;
|
|
}
|
|
|
|
if ( camera->spawnArgs.GetBool( "instantSkip" ) ) {
|
|
camera->Stop();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
soundSystem->SetMute( true );
|
|
if ( !skipCinematic ) {
|
|
skipCinematic = true;
|
|
cinematicMaxSkipTime = gameLocal.time + SEC2MS( g_cinematicMaxSkipTime.GetFloat() );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
======================
|
|
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 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 ) {
|
|
idBitMsg outMsg;
|
|
byte msgBuf[ MAX_GAME_MESSAGE_SIZE ];
|
|
|
|
if ( !gameLocal.isClient ) {
|
|
outMsg.Init( msgBuf, sizeof( msgBuf ) );
|
|
outMsg.WriteByte( GAME_RELIABLE_MESSAGE_PORTAL );
|
|
outMsg.WriteInt( portal );
|
|
outMsg.WriteBits( blockingBits, NUM_RENDER_PORTAL_BITS );
|
|
networkSystem->ServerSendReliableMessage( -1, outMsg );
|
|
}
|
|
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( void ) {
|
|
spawnSpot_t spot;
|
|
int i, j;
|
|
idEntity *ent;
|
|
|
|
if ( !isMultiplayer || isClient ) {
|
|
return;
|
|
}
|
|
spawnSpots.Clear();
|
|
initialSpots.Clear();
|
|
spot.dist = 0;
|
|
spot.ent = FindEntityUsingDef( NULL, "info_player_deathmatch" );
|
|
while( spot.ent ) {
|
|
spawnSpots.Append( spot );
|
|
if ( spot.ent->spawnArgs.GetBool( "initial" ) ) {
|
|
initialSpots.Append( spot.ent );
|
|
}
|
|
spot.ent = FindEntityUsingDef( spot.ent, "info_player_deathmatch" );
|
|
}
|
|
if ( !spawnSpots.Num() ) {
|
|
common->Warning( "no info_player_deathmatch in map" );
|
|
return;
|
|
}
|
|
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 ( 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;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
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 ( !isMultiplayer || !spawnSpots.Num() ) {
|
|
spot.ent = FindEntityUsingDef( NULL, "info_player_start" );
|
|
if ( !spot.ent ) {
|
|
Error( "No info_player_start on map.\n" );
|
|
}
|
|
return spot.ent;
|
|
}
|
|
if ( player->spectating ) {
|
|
// plain random spot, don't bother
|
|
return spawnSpots[ random.RandomInt( spawnSpots.Num() ) ].ent;
|
|
} else if ( player->useInitialSpawns && currentInitialSpot < initialSpots.Num() ) {
|
|
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 ) {
|
|
// don't do distance-based
|
|
return spawnSpots[ random.RandomInt( spawnSpots.Num() ) ].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::UpdateServerInfoFlags
|
|
================
|
|
*/
|
|
void idGameLocal::UpdateServerInfoFlags() {
|
|
gameType = GAME_SP;
|
|
if ( ( idStr::Icmp( serverInfo.GetString( "si_gameType" ), "deathmatch" ) == 0 ) ) {
|
|
gameType = GAME_DM;
|
|
} else if ( ( idStr::Icmp( serverInfo.GetString( "si_gameType" ), "Tourney" ) == 0 ) ) {
|
|
gameType = GAME_TOURNEY;
|
|
} else if ( ( idStr::Icmp( serverInfo.GetString( "si_gameType" ), "Team DM" ) == 0 ) ) {
|
|
gameType = GAME_TDM;
|
|
} else if ( ( idStr::Icmp( serverInfo.GetString( "si_gameType" ), "Last Man" ) == 0 ) ) {
|
|
gameType = GAME_LASTMAN;
|
|
}
|
|
if ( gameType == GAME_LASTMAN ) {
|
|
if ( !serverInfo.GetInt( "si_warmup" ) ) {
|
|
common->Warning( "Last Man Standing - forcing warmup on" );
|
|
serverInfo.SetInt( "si_warmup", 1 );
|
|
}
|
|
if ( serverInfo.GetInt( "si_fraglimit" ) <= 0 ) {
|
|
common->Warning( "Last Man Standing - setting fraglimit 1" );
|
|
serverInfo.SetInt( "si_fraglimit", 1 );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
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;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::ThrottleUserInfo
|
|
================
|
|
*/
|
|
void idGameLocal::ThrottleUserInfo( void ) {
|
|
mpGame.ThrottleUserInfo();
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::SelectTimeGroup
|
|
============
|
|
*/
|
|
void idGameLocal::SelectTimeGroup( int timeGroup ) { }
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetTimeGroupTime
|
|
============
|
|
*/
|
|
int idGameLocal::GetTimeGroupTime( int timeGroup ) {
|
|
return gameLocal.time;
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::GetBestGameType
|
|
============
|
|
*/
|
|
void idGameLocal::GetBestGameType( const char* map, const char* gametype, char buf[ MAX_STRING_CHARS ] ) {
|
|
strncpy( buf, gametype, MAX_STRING_CHARS );
|
|
buf[ MAX_STRING_CHARS - 1 ] = '\0';
|
|
}
|
|
|
|
/*
|
|
===========
|
|
idGameLocal::NeedRestart
|
|
============
|
|
*/
|
|
bool idGameLocal::NeedRestart() {
|
|
|
|
idDict newInfo;
|
|
const idKeyValue *keyval, *keyval2;
|
|
|
|
newInfo = *cvarSystem->MoveCVarsToDict( CVAR_SERVERINFO );
|
|
|
|
for ( int i = 0; i < newInfo.GetNumKeyVals(); i++ ) {
|
|
keyval = newInfo.GetKeyVal( i );
|
|
keyval2 = serverInfo.FindKey( keyval->GetKey() );
|
|
if ( !keyval2 ) {
|
|
return true;
|
|
}
|
|
// a select set of si_ changes will cause a full restart of the server
|
|
if ( keyval->GetValue().Cmp( keyval2->GetValue() ) && ( !keyval->GetKey().Cmp( "si_pure" ) || !keyval->GetKey().Cmp( "si_map" ) ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idGameLocal::GetClientStats
|
|
================
|
|
*/
|
|
void idGameLocal::GetClientStats( int clientNum, char *data, const int len ) {
|
|
mpGame.PlayerStats( clientNum, data, len );
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
idGameLocal::SwitchTeam
|
|
================
|
|
*/
|
|
void idGameLocal::SwitchTeam( int clientNum, int team ) {
|
|
|
|
idPlayer * player;
|
|
player = clientNum >= 0 ? static_cast<idPlayer *>( gameLocal.entities[ clientNum ] ) : NULL;
|
|
|
|
if ( !player )
|
|
return;
|
|
|
|
int oldTeam = player->team;
|
|
|
|
// Put in spectator mode
|
|
if ( team == -1 ) {
|
|
static_cast< idPlayer * >( entities[ clientNum ] )->Spectate( true );
|
|
}
|
|
// Switch to a team
|
|
else {
|
|
mpGame.SwitchToTeam ( clientNum, oldTeam, team );
|
|
}
|
|
}
|
|
|
|
/*
|
|
===============
|
|
idGameLocal::GetMapLoadingGUI
|
|
===============
|
|
*/
|
|
void idGameLocal::GetMapLoadingGUI( char gui[ MAX_STRING_CHARS ] ) { }
|