Xash3D FWGS engine.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

948 lines
22 KiB

/*
sv_init.c - server initialize operations
Copyright (C) 2009 Uncle Mike
This program 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.
This program 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.
*/
#include "common.h"
#include "server.h"
#include "net_encode.h"
int SV_UPDATE_BACKUP = SINGLEPLAYER_BACKUP;
server_t sv; // local server
server_static_t svs; // persistant server info
svgame_static_t svgame; // persistant game info
/*
================
SV_AddResource
generic method to put the resources into array
================
*/
static void SV_AddResource( resourcetype_t type, const char *name, int size, byte flags, int index )
{
resource_t *pResource = &sv.resources[sv.num_resources];
if( sv.num_resources >= MAX_RESOURCES )
Host_Error( "MAX_RESOURCES limit exceeded (%d)\n", MAX_RESOURCES );
sv.num_resources++;
Q_strncpy( pResource->szFileName, name, sizeof( pResource->szFileName ));
pResource->nDownloadSize = size;
pResource->ucFlags = flags;
pResource->nIndex = index;
pResource->type = type;
}
/*
================
SV_SendSingleResource
hot precache on a flying
================
*/
void SV_SendSingleResource( const char *name, resourcetype_t type, int index, byte flags )
{
resource_t *pResource = &sv.resources[sv.num_resources];
int nSize = 0;
if( !COM_CheckString( name ))
return;
switch( type )
{
case t_model:
nSize = ( name[0] != '*' ) ? FS_FileSize( name, false ) : 0;
break;
case t_sound:
nSize = FS_FileSize( va( "%s%s", DEFAULT_SOUNDPATH, name ), false );
break;
default:
nSize = FS_FileSize( name, false );
break;
}
SV_AddResource( type, name, nSize, flags, index );
MSG_BeginServerCmd( &sv.reliable_datagram, svc_resource );
SV_SendResource( pResource, &sv.reliable_datagram );
}
/*
================
SV_ModelIndex
register unique model for a server and client
================
*/
int SV_ModelIndex( const char *filename )
{
char name[MAX_QPATH];
int i;
if( !COM_CheckString( filename ))
return 0;
if( *filename == '\\' || *filename == '/' )
filename++;
Q_strncpy( name, filename, sizeof( name ));
COM_FixSlashes( name );
for( i = 1; i < MAX_MODELS && sv.model_precache[i][0]; i++ )
{
if( !Q_stricmp( sv.model_precache[i], name ))
return i;
}
if( i == MAX_MODELS )
{
Host_Error( "MAX_MODELS limit exceeded (%d)\n", MAX_MODELS );
return 0;
}
// register new model
Q_strncpy( sv.model_precache[i], name, sizeof( sv.model_precache[i] ));
if( sv.state != ss_loading )
{
// send the update to everyone
SV_SendSingleResource( name, t_model, i, sv.model_precache_flags[i] );
Con_Printf( S_WARN "late precache of %s\n", name );
}
return i;
}
/*
================
SV_SoundIndex
register unique sound for client
================
*/
int SV_SoundIndex( const char *filename )
{
char name[MAX_QPATH];
int i;
// don't precache sentence names!
if( !COM_CheckString( filename ))
return 0;
if( filename[0] == '!' )
{
Con_Printf( S_WARN "'%s' do not precache sentence names!\n", filename );
return 0;
}
if( *filename == '\\' || *filename == '/' )
filename++;
Q_strncpy( name, filename, sizeof( name ));
COM_FixSlashes( name );
for( i = 1; i < MAX_SOUNDS && sv.sound_precache[i][0]; i++ )
{
if( !Q_stricmp( sv.sound_precache[i], name ))
return i;
}
if( i == MAX_SOUNDS )
{
Host_Error( "MAX_SOUNDS limit exceeded (%d)\n", MAX_SOUNDS );
return 0;
}
// register new sound
Q_strncpy( sv.sound_precache[i], name, sizeof( sv.sound_precache[i] ));
if( sv.state != ss_loading )
{
// send the update to everyone
SV_SendSingleResource( name, t_sound, i, 0 );
Con_Printf( S_WARN "late precache of %s\n", name );
}
return i;
}
/*
================
SV_EventIndex
register network event for a server and client
================
*/
int SV_EventIndex( const char *filename )
{
char name[MAX_QPATH];
int i;
if( !COM_CheckString( filename ))
return 0;
Q_strncpy( name, filename, sizeof( name ));
COM_FixSlashes( name );
for( i = 1; i < MAX_EVENTS && sv.event_precache[i][0]; i++ )
{
if( !Q_stricmp( sv.event_precache[i], name ))
return i;
}
if( i == MAX_EVENTS )
{
Host_Error( "MAX_EVENTS limit exceeded (%d)\n", MAX_EVENTS );
return 0;
}
// register new event
Q_strncpy( sv.event_precache[i], name, sizeof( sv.event_precache[i] ));
if( sv.state != ss_loading )
{
// send the update to everyone
SV_SendSingleResource( name, t_eventscript, i, RES_FATALIFMISSING );
}
return i;
}
/*
================
SV_GenericIndex
register generic resourse for a server and client
================
*/
int SV_GenericIndex( const char *filename )
{
char name[MAX_QPATH];
int i;
if( !COM_CheckString( filename ))
return 0;
Q_strncpy( name, filename, sizeof( name ));
COM_FixSlashes( name );
for( i = 1; i < MAX_CUSTOM && sv.files_precache[i][0]; i++ )
{
if( !Q_stricmp( sv.files_precache[i], name ))
return i;
}
if( i == MAX_CUSTOM )
{
Host_Error( "MAX_CUSTOM limit exceeded (%d)\n", MAX_CUSTOM );
return 0;
}
// register new generic resource
Q_strncpy( sv.files_precache[i], name, sizeof( sv.files_precache[i] ));
if( sv.state != ss_loading )
{
// send the update to everyone
SV_SendSingleResource( name, t_generic, i, RES_FATALIFMISSING );
}
return i;
}
/*
================
SV_ModelHandle
register unique model for a server and client
================
*/
model_t *SV_ModelHandle( int modelindex )
{
if( modelindex < 0 || modelindex >= MAX_MODELS )
return NULL;
return sv.models[modelindex];
}
void SV_CreateGenericResources( void )
{
string filename, token;
char *afile, *pfile;
Q_strncpy( filename, sv.model_precache[1], sizeof( filename ));
COM_ReplaceExtension( filename, ".res" );
COM_FixSlashes( filename );
afile = FS_LoadFile( filename, NULL, false );
if( !afile ) return;
pfile = afile;
Con_DPrintf( "Precaching from %s\n", filename );
Con_DPrintf( "----------------------------------\n" );
while(( pfile = COM_ParseFile( pfile, token )) != NULL )
{
if( !COM_IsSafeFileToDownload( token ))
continue;
Con_DPrintf( " %s\n", token );
SV_GenericIndex( token );
}
Con_DPrintf( "----------------------------------\n" );
Mem_Free( afile );
}
void SV_CreateResourceList( void )
{
qboolean ffirstsent = false;
int i, nSize;
char *s;
sv.num_resources = 0;
for( i = 1; i < MAX_CUSTOM; i++ )
{
s = sv.files_precache[i];
if( !COM_CheckString( s )) break; // end of list
nSize = FS_FileSize( s, false );
SV_AddResource( t_generic, s, nSize, RES_FATALIFMISSING, i );
}
for( i = 1; i < MAX_SOUNDS; i++ )
{
s = sv.sound_precache[i];
if( !COM_CheckString( s ))
break; // end of list
if( s[0] == '!' )
{
if( !ffirstsent )
{
SV_AddResource( t_sound, "!", 0, RES_FATALIFMISSING, i );
ffirstsent = true;
}
}
else
{
nSize = FS_FileSize( va( "%s%s", DEFAULT_SOUNDPATH, s ), false );
SV_AddResource( t_sound, s, nSize, 0, i );
}
}
for( i = 1; i < MAX_MODELS; i++ )
{
s = sv.model_precache[i];
if( !COM_CheckString( s )) break; // end of list
nSize = ( s[0] != '*' ) ? FS_FileSize( s, false ) : 0;
SV_AddResource( t_model, s, nSize, sv.model_precache_flags[i], i );
}
// just send names
for( i = 0; i < MAX_DECALS && host.draw_decals[i][0]; i++ )
{
SV_AddResource( t_decal, host.draw_decals[i], 0, 0, i );
}
for( i = 1; i < MAX_EVENTS; i++ )
{
s = sv.event_precache[i];
if( !COM_CheckString( s )) break; // end of list
nSize = FS_FileSize( s, false );
SV_AddResource( t_eventscript, s, nSize, RES_FATALIFMISSING, i );
}
}
/*
================
SV_CreateBaseline
Entity baselines are used to compress the update messages
to the clients -- only the fields that differ from the
baseline will be transmitted
INTERNAL RESOURCE
================
*/
void SV_CreateBaseline( void )
{
entity_state_t nullstate, *base;
int playermodel;
qboolean player;
int entnum;
if( FBitSet( host.features, ENGINE_QUAKE_COMPATIBLE ))
playermodel = SV_ModelIndex( DEFAULT_PLAYER_PATH_QUAKE );
else playermodel = SV_ModelIndex( DEFAULT_PLAYER_PATH_HALFLIFE );
memset( &nullstate, 0, sizeof( nullstate ));
for( entnum = 0; entnum < svgame.numEntities; entnum++ )
{
edict_t *pEdict = EDICT_NUM( entnum );
if( !SV_IsValidEdict( pEdict ))
continue;
if( entnum != 0 && entnum <= svs.maxclients )
{
player = true;
}
else
{
if( !pEdict->v.modelindex )
continue; // invisible
player = false;
}
// take current state as baseline
base = &svs.baselines[entnum];
base->number = entnum;
// set entity type
if( FBitSet( pEdict->v.flags, FL_CUSTOMENTITY ))
base->entityType = ENTITY_BEAM;
else base->entityType = ENTITY_NORMAL;
svgame.dllFuncs.pfnCreateBaseline( player, entnum, base, pEdict, playermodel, host.player_mins[0], host.player_maxs[0] );
sv.last_valid_baseline = entnum;
}
// create the instanced baselines
svgame.dllFuncs.pfnCreateInstancedBaselines();
// now put the baseline into the signon message.
MSG_BeginServerCmd( &sv.signon, svc_spawnbaseline );
for( entnum = 0; entnum < svgame.numEntities; entnum++ )
{
edict_t *pEdict = EDICT_NUM( entnum );
if( !SV_IsValidEdict( pEdict ))
continue;
if( entnum != 0 && entnum <= svs.maxclients )
{
player = true;
}
else
{
if( !pEdict->v.modelindex )
continue; // invisible
player = false;
}
// take current state as baseline
base = &svs.baselines[entnum];
MSG_WriteDeltaEntity( &nullstate, base, &sv.signon, true, player, 1.0f, 0 );
}
MSG_WriteUBitLong( &sv.signon, LAST_EDICT, MAX_ENTITY_BITS ); // end of baselines
MSG_WriteUBitLong( &sv.signon, sv.num_instanced, 6 );
for( entnum = 0; entnum < sv.num_instanced; entnum++ )
{
base = &sv.instanced[entnum].baseline;
MSG_WriteDeltaEntity( &nullstate, base, &sv.signon, true, false, 1.0f, 0 );
}
}
/*
================
SV_FreeOldEntities
remove immediate entities
================
*/
void SV_FreeOldEntities( void )
{
edict_t *ent;
int i;
// at end of frame kill all entities which supposed to it
for( i = svs.maxclients + 1; i < svgame.numEntities; i++ )
{
ent = EDICT_NUM( i );
if( !ent->free && FBitSet( ent->v.flags, FL_KILLME ))
SV_FreeEdict( ent );
}
// decrement svgame.numEntities if the highest number entities died
for( ; EDICT_NUM( svgame.numEntities - 1 )->free; svgame.numEntities-- );
}
/*
================
SV_ActivateServer
activate server on changed map, run physics
================
*/
void SV_ActivateServer( int runPhysics )
{
int i, numFrames;
byte msg_buf[MAX_INIT_MSG];
sizebuf_t msg;
sv_client_t *cl;
if( !svs.initialized )
return;
MSG_Init( &msg, "ActivateServer", msg_buf, sizeof( msg_buf ));
// always clearing newunit variable
Cvar_SetValue( "sv_newunit", 0 );
// relese all intermediate entities
SV_FreeOldEntities ();
// Activate the DLL server code
svgame.globals->time = sv.time;
svgame.dllFuncs.pfnServerActivate( svgame.edicts, svgame.numEntities, svs.maxclients );
// parse user-specified resources
SV_CreateGenericResources();
if( runPhysics )
{
numFrames = (svs.maxclients <= 1) ? 2 : 8;
sv.frametime = SV_SPAWN_TIME;
}
else
{
sv.frametime = 0.001;
numFrames = 1;
}
// run some frames to allow everything to settle
for( i = 0; i < numFrames; i++ )
SV_Physics();
// create a baseline for more efficient communications
SV_CreateBaseline();
// collect all info from precached resources
SV_CreateResourceList();
// check and count all files that marked by user as unmodified (typically is a player models etc)
SV_TransferConsistencyInfo();
// send serverinfo to all connected clients
for( i = 0, cl = svs.clients; i < svs.maxclients; i++, cl++ )
{
if( cl->state < cs_connected )
continue;
Netchan_Clear( &cl->netchan );
cl->delta_sequence = -1;
}
// invoke to refresh all movevars
memset( &svgame.oldmovevars, 0, sizeof( movevars_t ));
svgame.globals->changelevel = false;
// setup hostflags
sv.hostflags = 0;
HPAK_FlushHostQueue();
// tell what kind of server has been started.
if( svs.maxclients > 1 )
Con_Printf( "%i player server started\n", svs.maxclients );
else Con_Printf( "Game started\n" );
Log_Printf( "Started map \"%s\" (CRC \"%i\")\n", sv.name, sv.worldmapCRC );
// dedicated server purge unused resources here
if( host.type == HOST_DEDICATED )
Mod_FreeUnused ();
host.movevars_changed = true;
sv.state = ss_active;
Con_DPrintf( "level loaded at %.2f sec\n", Sys_DoubleTime() - svs.timestart );
if( sv.ignored_static_ents )
Con_Printf( S_WARN "%i static entities was rejected due buffer overflow\n", sv.ignored_static_ents );
if( sv.ignored_world_decals )
Con_Printf( S_WARN "%i static decals was rejected due buffer overflow\n", sv.ignored_world_decals );
if( svs.maxclients > 1 )
{
const char *cycle = Cvar_VariableString( "mapchangecfgfile" );
if( COM_CheckString( cycle ))
Cbuf_AddText( va( "exec %s\n", cycle ));
if( public_server->value )
Master_Add( );
}
}
/*
================
SV_DeactivateServer
deactivate server, free edicts, strings etc
================
*/
void SV_DeactivateServer( void )
{
int i;
if( !svs.initialized || sv.state == ss_dead )
return;
svgame.globals->time = sv.time;
svgame.dllFuncs.pfnServerDeactivate();
sv.state = ss_dead;
SV_FreeEdicts ();
SV_ClearPhysEnts ();
Mem_EmptyPool( svgame.stringspool );
for( i = 0; i < svs.maxclients; i++ )
{
// release client frames
if( svs.clients[i].frames )
Mem_Free( svs.clients[i].frames );
svs.clients[i].frames = NULL;
}
svgame.globals->maxEntities = GI->max_edicts;
svgame.globals->maxClients = svs.maxclients;
svgame.numEntities = svs.maxclients + 1; // clients + world
svgame.globals->startspot = 0;
svgame.globals->mapname = 0;
}
/*
==============
SV_InitGame
A brand new game has been started
==============
*/
qboolean SV_InitGame( void )
{
if( svs.initialized )
return true; // already initialized ?
// first initialize?
COM_ResetLibraryError();
if( !SV_LoadProgs( SI.gamedll ))
{
Con_Printf( S_ERROR "can't initialize %s: %s\n", SI.gamedll, COM_GetLibraryError() );
return false; // failed to loading server.dll
}
// client frames will be allocated in SV_ClientConnect
svs.initialized = true;
return true;
}
/*
==============
SV_ShutdownGame
prepare to close server
==============
*/
void SV_ShutdownGame( void )
{
if( !GameState->loadGame )
SV_ClearGameState();
SV_FinalMessage( "", true );
S_StopBackgroundTrack();
if( GameState->newGame )
{
Host_EndGame( false, DEFAULT_ENDGAME_MESSAGE );
}
else
{
S_StopAllSounds( true );
SV_DeactivateServer();
}
}
/*
================
SV_SetupClients
determine the game type and prepare clients
================
*/
void SV_SetupClients( void )
{
qboolean changed_maxclients = false;
// check if clients count was really changed
if( svs.maxclients != (int)sv_maxclients->value )
changed_maxclients = true;
if( !changed_maxclients ) return; // nothing to change
// if clients count was changed we need to run full shutdown procedure
if( svs.maxclients ) Host_ShutdownServer();
// copy the actual value from cvar
svs.maxclients = (int)sv_maxclients->value;
// dedicated servers are can't be single player and are usually DM
if( host.type == HOST_DEDICATED )
svs.maxclients = bound( 4, svs.maxclients, MAX_CLIENTS );
else svs.maxclients = bound( 1, svs.maxclients, MAX_CLIENTS );
if( svs.maxclients == 1 )
Cvar_SetValue( "deathmatch", 0.0f );
else Cvar_SetValue( "deathmatch", 1.0f );
// make cvars consistant
if( coop.value ) Cvar_SetValue( "deathmatch", 0.0f );
// feedback for cvar
Cvar_FullSet( "maxplayers", va( "%d", svs.maxclients ), FCVAR_LATCH );
SV_UPDATE_BACKUP = ( svs.maxclients == 1 ) ? SINGLEPLAYER_BACKUP : MULTIPLAYER_BACKUP;
svs.clients = Z_Realloc( svs.clients, sizeof( sv_client_t ) * svs.maxclients );
svs.num_client_entities = svs.maxclients * SV_UPDATE_BACKUP * NUM_PACKET_ENTITIES;
svs.packet_entities = Z_Realloc( svs.packet_entities, sizeof( entity_state_t ) * svs.num_client_entities );
Con_DPrintf( "%s alloced by server packet entities\n", Q_memprint( sizeof( entity_state_t ) * svs.num_client_entities ));
// init network stuff
NET_Config(( svs.maxclients > 1 ));
svgame.numEntities = svs.maxclients + 1; // clients + world
ClearBits( sv_maxclients->flags, FCVAR_CHANGED );
}
/*
================
SV_SpawnServer
Change the server to a new map, taking all connected
clients along with it.
================
*/
qboolean SV_SpawnServer( const char *mapname, const char *startspot, qboolean background )
{
int i, current_skill;
edict_t *ent;
SV_SetupClients();
if( !SV_InitGame( ))
return false;
Log_Open();
Log_Printf( "Loading map \"%s\"\n", mapname );
Log_PrintServerVars();
svs.timestart = Sys_DoubleTime();
svs.spawncount++; // any partially connected client will be restarted
// let's not have any servers with no name
if( !COM_CheckString( hostname.string ))
Cvar_Set( "hostname", svgame.dllFuncs.pfnGetGameDescription ? svgame.dllFuncs.pfnGetGameDescription() : FS_Title( ));
if( startspot )
{
Con_Printf( "Spawn Server: %s [%s]\n", mapname, startspot );
}
else
{
Con_DPrintf( "Spawn Server: %s\n", mapname );
}
memset( &sv, 0, sizeof( sv )); // wipe the entire per-level structure
sv.time = svgame.globals->time = 1.0f; // server spawn time it's always 1.0 second
sv.background = background;
// initialize buffers
MSG_Init( &sv.signon, "Signon", sv.signon_buf, sizeof( sv.signon_buf ));
MSG_Init( &sv.multicast, "Multicast", sv.multicast_buf, sizeof( sv.multicast_buf ));
MSG_Init( &sv.datagram, "Datagram", sv.datagram_buf, sizeof( sv.datagram_buf ));
MSG_Init( &sv.reliable_datagram, "Reliable Datagram", sv.reliable_datagram_buf, sizeof( sv.reliable_datagram_buf ));
MSG_Init( &sv.spec_datagram, "Spectator Datagram", sv.spectator_buf, sizeof( sv.spectator_buf ));
// clearing all the baselines
memset( svs.baselines, 0, sizeof( entity_state_t ) * GI->max_edicts );
// make cvars consistant
if( coop.value ) Cvar_SetValue( "deathmatch", 0 );
current_skill = Q_rint( skill.value );
current_skill = bound( 0, current_skill, 3 );
Cvar_SetValue( "skill", (float)current_skill );
// force normal player collisions for single player
if( svs.maxclients == 1 )
Cvar_SetValue( "sv_clienttrace", 1 );
// copy gamemode into svgame.globals
svgame.globals->deathmatch = deathmatch.value;
svgame.globals->coop = coop.value;
svgame.globals->maxClients = svs.maxclients;
if( sv.background )
{
// tell the game parts about background state
Cvar_FullSet( "sv_background", "1", FCVAR_READ_ONLY );
Cvar_FullSet( "cl_background", "1", FCVAR_READ_ONLY );
}
else
{
Cvar_FullSet( "sv_background", "0", FCVAR_READ_ONLY );
Cvar_FullSet( "cl_background", "0", FCVAR_READ_ONLY );
}
// force normal player collisions for single player
if( svs.maxclients == 1 ) Cvar_SetValue( "sv_clienttrace", 1 );
// make sure what server name doesn't contain path and extension
COM_FileBase( mapname, sv.name );
// precache and static commands can be issued during map initialization
sv.state = ss_loading;
if( startspot )
Q_strncpy( sv.startspot, startspot, sizeof( sv.startspot ));
else sv.startspot[0] = '\0';
Q_snprintf( sv.model_precache[WORLD_INDEX], sizeof( sv.model_precache[0] ), "maps/%s.bsp", sv.name );
SetBits( sv.model_precache_flags[WORLD_INDEX], RES_FATALIFMISSING );
sv.worldmodel = sv.models[WORLD_INDEX] = Mod_LoadWorld( sv.model_precache[WORLD_INDEX], true );
CRC32_MapFile( &sv.worldmapCRC, sv.model_precache[WORLD_INDEX], svs.maxclients > 1 );
if( FBitSet( host.features, ENGINE_QUAKE_COMPATIBLE ) && FS_FileExists( "progs.dat", false ))
{
file_t *f = FS_Open( "progs.dat", "rb", false );
FS_Seek( f, sizeof( int ), SEEK_SET );
FS_Read( f, &sv.progsCRC, sizeof( int ));
FS_Close( f );
}
for( i = WORLD_INDEX; i < sv.worldmodel->numsubmodels; i++ )
{
Q_sprintf( sv.model_precache[i+1], "*%i", i );
sv.models[i+1] = Mod_ForName( sv.model_precache[i+1], false, false );
SetBits( sv.model_precache_flags[i+1], RES_FATALIFMISSING );
}
// leave slots at start for clients only
for( i = 0; i < svs.maxclients; i++ )
{
// needs to reconnect
if( svs.clients[i].state > cs_connected )
svs.clients[i].state = cs_connected;
ent = EDICT_NUM( i + 1 );
svs.clients[i].pViewEntity = NULL;
svs.clients[i].edict = ent;
SV_InitEdict( ent );
}
// heartbeats will always be sent to the id master
svs.last_heartbeat = MAX_HEARTBEAT; // send immediately
// get actual movevars
SV_UpdateMovevars( true );
// clear physics interaction links
SV_ClearWorld();
return true;
}
qboolean SV_Active( void )
{
return (sv.state != ss_dead);
}
qboolean SV_Initialized( void )
{
return svs.initialized;
}
int SV_GetMaxClients( void )
{
return svs.maxclients;
}
void SV_InitGameProgs( void )
{
if( svgame.hInstance ) return; // already loaded
// just try to initialize
SV_LoadProgs( GI->game_dll );
}
void SV_FreeGameProgs( void )
{
if( svs.initialized ) return; // server is active
// unload progs (free cvars and commands)
SV_UnloadProgs();
}
/*
================
SV_ExecLoadLevel
State machine exec new map
================
*/
void SV_ExecLoadLevel( void )
{
if( SV_SpawnServer( GameState->levelName, NULL, GameState->backgroundMap ))
{
SV_SpawnEntities( GameState->levelName );
SV_ActivateServer( true );
}
}
/*
================
SV_ExecLoadGame
State machine exec load saved game
================
*/
void SV_ExecLoadGame( void )
{
if( SV_SpawnServer( GameState->levelName, NULL, false ))
{
if( !SV_LoadGameState( GameState->levelName ))
SV_SpawnEntities( GameState->levelName );
SV_ActivateServer( false );
}
}
/*
================
SV_ExecChangeLevel
State machine exec changelevel path
================
*/
void SV_ExecChangeLevel( void )
{
SV_ChangeLevel( GameState->loadGame, GameState->levelName, GameState->landmarkName, GameState->backgroundMap );
}