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.
1368 lines
32 KiB
1368 lines
32 KiB
/* |
|
host.c - dedicated and normal host |
|
Copyright (C) 2007 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 "build.h" |
|
#ifdef XASH_SDL |
|
#include <SDL.h> |
|
#endif // XASH_SDL |
|
#include <stdarg.h> // va_args |
|
#include <errno.h> // errno |
|
#include <string.h> // strerror |
|
#if !XASH_WIN32 |
|
#include <unistd.h> // fork |
|
#include <sys/types.h> |
|
#include <sys/stat.h> |
|
#include <fcntl.h> |
|
#endif |
|
#if XASH_EMSCRIPTEN |
|
#include <emscripten/emscripten.h> |
|
#endif |
|
#include <errno.h> |
|
#include "common.h" |
|
#include "base_cmd.h" |
|
#include "client.h" |
|
#include "server.h" |
|
#include "netchan.h" |
|
#include "protocol.h" |
|
#include "mod_local.h" |
|
#include "xash3d_mathlib.h" |
|
#include "input.h" |
|
#include "enginefeatures.h" |
|
#include "render_api.h" // decallist_t |
|
#include "tests.h" |
|
|
|
pfnChangeGame pChangeGame = NULL; |
|
host_parm_t host; // host parms |
|
sysinfo_t SI; |
|
|
|
#ifdef XASH_ENGINE_TESTS |
|
struct tests_stats_s tests_stats; |
|
#endif |
|
|
|
CVAR_DEFINE( host_developer, "developer", "0", FCVAR_FILTERABLE, "engine is in development-mode" ); |
|
CVAR_DEFINE_AUTO( sys_timescale, "1.0", FCVAR_FILTERABLE, "scale frame time" ); |
|
CVAR_DEFINE_AUTO( sys_ticrate, "100", 0, "framerate in dedicated mode" ); |
|
|
|
static CVAR_DEFINE_AUTO( host_serverstate, "0", FCVAR_READ_ONLY, "displays current server state" ); |
|
static CVAR_DEFINE_AUTO( host_gameloaded, "0", FCVAR_READ_ONLY, "inidcates a loaded game.dll" ); |
|
static CVAR_DEFINE_AUTO( host_clientloaded, "0", FCVAR_READ_ONLY, "inidcates a loaded client.dll" ); |
|
CVAR_DEFINE_AUTO( host_limitlocal, "0", 0, "apply cl_cmdrate and rate to loopback connection" ); |
|
CVAR_DEFINE( host_maxfps, "fps_max", "72", FCVAR_ARCHIVE|FCVAR_FILTERABLE, "host fps upper limit" ); |
|
static CVAR_DEFINE_AUTO( host_framerate, "0", FCVAR_FILTERABLE, "locks frame timing to this value in seconds" ); |
|
static CVAR_DEFINE( host_sleeptime, "sleeptime", "1", FCVAR_ARCHIVE|FCVAR_FILTERABLE, "milliseconds to sleep for each frame. higher values reduce fps accuracy" ); |
|
static CVAR_DEFINE_AUTO( host_sleeptime_debug, "0", 0, "print sleeps between frames" ); |
|
CVAR_DEFINE( con_gamemaps, "con_mapfilter", "1", FCVAR_ARCHIVE, "when true show only maps in game folder" ); |
|
|
|
void Sys_PrintUsage( void ) |
|
{ |
|
string version_str; |
|
const char *usage_str; |
|
|
|
Q_snprintf( version_str, sizeof( version_str ), |
|
XASH_ENGINE_NAME " %i/" XASH_VERSION " (%s-%s build %i)", PROTOCOL_VERSION, Q_buildos(), Q_buildarch(), Q_buildnum( )); |
|
|
|
#if XASH_WIN32 |
|
#define XASH_EXE "(xash).exe" |
|
#else |
|
#define XASH_EXE "(xash)" |
|
#endif |
|
|
|
#define O( x, y ) " "x" "y"\n" |
|
|
|
usage_str = S_USAGE XASH_EXE " [options] [+command] [+command2 arg] ...\n" |
|
|
|
"\nCommon options:\n" |
|
O("-dev [level] ", "set log verbosity 0-2") |
|
O("-log ", "write log to \"engine.log\"") |
|
O("-nowriteconfig ", "disable config save") |
|
O("-noch ", "disable crashhandler") |
|
#if XASH_WIN32 // !!!! |
|
O("-minidumps ", "enable writing minidumps when game is crashed") |
|
#endif |
|
O("-rodir <path> ", "set read-only base directory") |
|
O("-bugcomp ", "enable precise bug compatibility. Will break games that don't require it") |
|
O(" ", "Refer to engine documentation for more info") |
|
O("-disablehelp ", "disable this message") |
|
#if !XASH_DEDICATED |
|
O("-dedicated ", "run engine in dedicated mode") |
|
#endif |
|
|
|
"\nNetworking options:\n" |
|
O("-noip ", "disable IPv4") |
|
O("-ip <ip> ", "set IPv4 address") |
|
O("-port <port> ", "set IPv4 port") |
|
O("-noip6 ", "disable IPv6") |
|
O("-ip6 <ip> ", "set IPv6 address") |
|
O("-port6 <port> ", "set IPv6 port") |
|
O("-clockwindow <cw>", "adjust clockwindow used to ignore client commands to prevent speed hacks") |
|
|
|
"\nGame options:\n" |
|
O("-dll <path> ", "override server DLL path") |
|
#if !XASH_DEDICATED |
|
O("-clientlib <path>", "override client DLL path") |
|
O("-console ", "run engine with console enabled") |
|
O("-toconsole ", "run engine witn console open") |
|
O("-oldfont ", "enable unused Quake font in Half-Life") |
|
O("-width <n> ", "set window width") |
|
O("-height <n> ", "set window height") |
|
O("-borderless ", "run engine in fullscreen borderless mode") |
|
O("-fullscreen ", "run engine in fullscreen mode") |
|
O("-windowed ", "run engine in windowed mode") |
|
O("-ref <name> ", "use selected renderer dll") |
|
O("-gldebug ", "enable OpenGL debug log") |
|
#if XASH_WIN32 |
|
O("-noavi ", "disable AVI support") |
|
O("-nointro ", "disable intro video") |
|
#endif |
|
O("-noenginejoy ", "disable engine builtin joystick support") |
|
O("-noenginemouse ", "disable engine builtin mouse support") |
|
O("-nosound ", "disable sound output") |
|
O("-timedemo ", "run timedemo and exit") |
|
#endif |
|
|
|
"\nPlatform-specific options:\n" |
|
#if !XASH_MOBILE_PLATFORM |
|
O("-daemonize ", "run engine as a daemon") |
|
#endif |
|
#if XASH_SDL == 2 |
|
O("-sdl_joy_old_api ","use SDL legacy joystick API") |
|
O("-sdl_renderer <n>","use alternative SDL_Renderer for software") |
|
#endif // XASH_SDL |
|
#if XASH_ANDROID && !XASH_SDL |
|
O("-nativeegl ","use native egl implementation. Use if screen does not update or black") |
|
#endif // XASH_ANDROID |
|
#if XASH_DOS |
|
O("-novesa ","disable vesa") |
|
#endif // XASH_DOS |
|
#if XASH_VIDEO == VIDEO_FBDEV |
|
O("-fbdev <path> ","open selected framebuffer") |
|
O("-ttygfx ","set graphics mode in tty") |
|
O("-doublebuffer ","enable doublebuffering") |
|
#endif // XASH_VIDEO == VIDEO_FBDEV |
|
#if XASH_SOUND == SOUND_ALSA |
|
O("-alsadev <dev> ","open selected ALSA device") |
|
#endif // XASH_SOUND == SOUND_ALSA |
|
; |
|
#undef O |
|
|
|
// HACKHACK: pretty output in dedicated |
|
#if XASH_MESSAGEBOX != MSGBOX_STDERR |
|
Platform_MessageBox( version_str, usage_str, false ); |
|
#else |
|
fprintf( stderr, "%s\n%s", version_str, usage_str ); |
|
#endif |
|
|
|
Sys_Quit(); |
|
} |
|
|
|
int Host_CompareFileTime( int ft1, int ft2 ) |
|
{ |
|
if( ft1 < ft2 ) |
|
{ |
|
return -1; |
|
} |
|
else if( ft1 > ft2 ) |
|
{ |
|
return 1; |
|
} |
|
return 0; |
|
} |
|
|
|
void Host_ShutdownServer( void ) |
|
{ |
|
SV_Shutdown( "Server was killed\n" ); |
|
} |
|
|
|
/* |
|
================ |
|
Host_PrintEngineFeatures |
|
================ |
|
*/ |
|
void Host_PrintEngineFeatures( void ) |
|
{ |
|
if( FBitSet( host.features, ENGINE_WRITE_LARGE_COORD )) |
|
Con_Reportf( "^3EXT:^7 big world support enabled\n" ); |
|
|
|
if( FBitSet( host.features, ENGINE_LOAD_DELUXEDATA )) |
|
Con_Reportf( "^3EXT:^7 deluxemap support enabled\n" ); |
|
|
|
if( FBitSet( host.features, ENGINE_PHYSICS_PUSHER_EXT )) |
|
Con_Reportf( "^3EXT:^7 Improved MOVETYPE_PUSH is used\n" ); |
|
|
|
if( FBitSet( host.features, ENGINE_LARGE_LIGHTMAPS )) |
|
Con_Reportf( "^3EXT:^7 Large lightmaps enabled\n" ); |
|
|
|
if( FBitSet( host.features, ENGINE_COMPENSATE_QUAKE_BUG )) |
|
Con_Reportf( "^3EXT:^7 Compensate quake bug enabled\n" ); |
|
} |
|
|
|
/* |
|
============== |
|
Host_IsQuakeCompatible |
|
|
|
============== |
|
*/ |
|
qboolean Host_IsQuakeCompatible( void ) |
|
{ |
|
// feature set |
|
if( FBitSet( host.features, ENGINE_QUAKE_COMPATIBLE )) |
|
return true; |
|
|
|
#if !XASH_DEDICATED |
|
// quake demo playing |
|
if( cls.demoplayback == DEMO_QUAKE1 ) |
|
return true; |
|
#endif // XASH_DEDICATED |
|
|
|
return false; |
|
} |
|
|
|
/* |
|
================ |
|
Host_EndGame |
|
================ |
|
*/ |
|
void Host_EndGame( qboolean abort, const char *message, ... ) |
|
{ |
|
va_list argptr; |
|
static char string[MAX_SYSPATH]; |
|
|
|
va_start( argptr, message ); |
|
Q_vsnprintf( string, sizeof( string ), message, argptr ); |
|
va_end( argptr ); |
|
|
|
Con_Printf( "Host_EndGame: %s\n", string ); |
|
|
|
SV_Shutdown( "\n" ); |
|
#if !XASH_DEDICATED |
|
CL_Disconnect(); |
|
|
|
// recreate world if needs |
|
CL_ClearEdicts (); |
|
#endif |
|
|
|
// release all models |
|
Mod_FreeAll(); |
|
|
|
if( abort ) Host_AbortCurrentFrame (); |
|
} |
|
|
|
/* |
|
================ |
|
Host_AbortCurrentFrame |
|
|
|
aborts the current host frame and goes on with the next one |
|
================ |
|
*/ |
|
void Host_AbortCurrentFrame( void ) |
|
{ |
|
longjmp( host.abortframe, 1 ); |
|
} |
|
|
|
/* |
|
================== |
|
Host_CalcSleep |
|
================== |
|
*/ |
|
static int Host_CalcSleep( void ) |
|
{ |
|
#ifndef XASH_DEDICATED |
|
// never sleep in timedemo for benchmarking purposes |
|
// also don't sleep with vsync for less lag |
|
if( CL_IsTimeDemo( ) || gl_vsync.value ) |
|
return 0; |
|
#endif |
|
|
|
if( Host_IsDedicated() ) |
|
{ |
|
// let the dedicated server some sleep |
|
return host_sleeptime.value; |
|
} |
|
|
|
switch( host.status ) |
|
{ |
|
case HOST_NOFOCUS: |
|
if( SV_Active() && CL_IsInGame()) |
|
return host_sleeptime.value; |
|
// fallthrough |
|
case HOST_SLEEP: |
|
return 20; |
|
} |
|
|
|
return host_sleeptime.value; |
|
} |
|
|
|
void Host_NewInstance( const char *name, const char *finalmsg ) |
|
{ |
|
if( !pChangeGame ) return; |
|
|
|
host.change_game = true; |
|
Q_strncpy( host.finalmsg, finalmsg, sizeof( host.finalmsg )); |
|
|
|
if( !Sys_NewInstance( name )) |
|
pChangeGame( name ); // call from hl.exe |
|
} |
|
|
|
/* |
|
================= |
|
Host_ChangeGame_f |
|
|
|
Change game modification |
|
================= |
|
*/ |
|
void Host_ChangeGame_f( void ) |
|
{ |
|
int i; |
|
|
|
if( Cmd_Argc() != 2 ) |
|
{ |
|
Con_Printf( S_USAGE "game <directory>\n" ); |
|
return; |
|
} |
|
|
|
// validate gamedir |
|
for( i = 0; i < FI->numgames; i++ ) |
|
{ |
|
if( !Q_stricmp( FI->games[i]->gamefolder, Cmd_Argv( 1 ))) |
|
break; |
|
} |
|
|
|
if( i == FI->numgames ) |
|
{ |
|
Con_Printf( "%s not exist\n", Cmd_Argv( 1 )); |
|
} |
|
else if( !Q_stricmp( GI->gamefolder, Cmd_Argv( 1 ))) |
|
{ |
|
Con_Printf( "%s already active\n", Cmd_Argv( 1 )); |
|
} |
|
else |
|
{ |
|
char finalmsg[MAX_VA_STRING]; |
|
|
|
Q_snprintf( finalmsg, sizeof( finalmsg ), "change game to '%s'", FI->games[i]->title ); |
|
Host_NewInstance( Cmd_Argv( 1 ), finalmsg ); |
|
} |
|
} |
|
|
|
/* |
|
=============== |
|
Host_Exec_f |
|
=============== |
|
*/ |
|
void Host_Exec_f( void ) |
|
{ |
|
string cfgpath; |
|
byte *f; |
|
char *txt; |
|
fs_offset_t len; |
|
const char *arg; |
|
|
|
if( Cmd_Argc() != 2 ) |
|
{ |
|
Con_Printf( S_USAGE "exec <filename>\n" ); |
|
return; |
|
} |
|
|
|
arg = Cmd_Argv( 1 ); |
|
|
|
#ifndef XASH_DEDICATED |
|
if( !Cmd_CurrentCommandIsPrivileged() ) |
|
{ |
|
const char *unprivilegedWhitelist[] = |
|
{ |
|
NULL, "mapdefault.cfg", "scout.cfg", "sniper.cfg", |
|
"soldier.cfg", "demoman.cfg", "medic.cfg", "hwguy.cfg", |
|
"pyro.cfg", "spy.cfg", "engineer.cfg", "civilian.cfg" |
|
}; |
|
int i; |
|
char temp[MAX_VA_STRING]; |
|
qboolean allow = false; |
|
|
|
Q_snprintf( temp, sizeof( temp ), "%s.cfg", clgame.mapname ); |
|
unprivilegedWhitelist[0] = temp; |
|
|
|
for( i = 0; i < ARRAYSIZE( unprivilegedWhitelist ); i++ ) |
|
{ |
|
if( !Q_strcmp( arg, unprivilegedWhitelist[i] )) |
|
{ |
|
allow = true; |
|
break; |
|
} |
|
} |
|
|
|
if( !allow ) |
|
{ |
|
Con_Printf( "exec %s: not privileged or in whitelist\n", arg ); |
|
return; |
|
} |
|
} |
|
#endif // XASH_DEDICATED |
|
|
|
if( !Q_stricmp( "game.cfg", arg )) |
|
{ |
|
// don't execute game.cfg in singleplayer |
|
if( SV_GetMaxClients() == 1 ) |
|
return; |
|
} |
|
|
|
Q_strncpy( cfgpath, arg, sizeof( cfgpath )); |
|
COM_DefaultExtension( cfgpath, ".cfg", sizeof( cfgpath )); // append as default |
|
|
|
f = FS_LoadFile( cfgpath, &len, false ); |
|
if( !f ) |
|
{ |
|
Con_Reportf( "couldn't exec %s\n", Cmd_Argv( 1 )); |
|
return; |
|
} |
|
|
|
if( !Q_stricmp( "config.cfg", arg )) |
|
host.config_executed = true; |
|
|
|
// adds \n\0 at end of the file |
|
txt = Z_Calloc( len + 2 ); |
|
memcpy( txt, f, len ); |
|
Q_strncat( txt, "\n", len + 2 ); |
|
Mem_Free( f ); |
|
|
|
if( !host.apply_game_config ) |
|
Con_Printf( "execing %s\n", arg ); |
|
Cbuf_InsertText( txt ); |
|
Mem_Free( txt ); |
|
} |
|
|
|
/* |
|
=============== |
|
Host_MemStats_f |
|
=============== |
|
*/ |
|
void Host_MemStats_f( void ) |
|
{ |
|
switch( Cmd_Argc( )) |
|
{ |
|
case 1: |
|
Mem_PrintList( 1<<30 ); |
|
Mem_PrintStats(); |
|
break; |
|
case 2: |
|
Mem_PrintList( Q_atoi( Cmd_Argv( 1 )) * 1024 ); |
|
Mem_PrintStats(); |
|
break; |
|
default: |
|
Con_Printf( S_USAGE "memlist <all>\n" ); |
|
break; |
|
} |
|
} |
|
|
|
void Host_Minimize_f( void ) |
|
{ |
|
#ifdef XASH_SDL |
|
if( host.hWnd ) SDL_MinimizeWindow( host.hWnd ); |
|
#endif |
|
} |
|
|
|
/* |
|
================= |
|
Host_IsLocalGame |
|
|
|
singleplayer game detect |
|
================= |
|
*/ |
|
qboolean Host_IsLocalGame( void ) |
|
{ |
|
if( SV_Active( )) |
|
{ |
|
return ( SV_GetMaxClients() == 1 ) ? true : false; |
|
} |
|
else |
|
{ |
|
return ( CL_GetMaxClients() == 1 ) ? true : false; |
|
} |
|
} |
|
|
|
qboolean Host_IsLocalClient( void ) |
|
{ |
|
// only the local client have the active server |
|
if( CL_Initialized( ) && SV_Initialized( )) |
|
return true; |
|
return false; |
|
} |
|
|
|
/* |
|
================= |
|
Host_RegisterDecal |
|
================= |
|
*/ |
|
qboolean Host_RegisterDecal( const char *name, int *count ) |
|
{ |
|
char shortname[MAX_QPATH]; |
|
int i; |
|
|
|
if( !COM_CheckString( name )) |
|
return 0; |
|
|
|
COM_FileBase( name, shortname, sizeof( shortname )); |
|
|
|
for( i = 1; i < MAX_DECALS && host.draw_decals[i][0]; i++ ) |
|
{ |
|
if( !Q_stricmp( host.draw_decals[i], shortname )) |
|
return true; |
|
} |
|
|
|
if( i == MAX_DECALS ) |
|
{ |
|
Con_DPrintf( S_ERROR "MAX_DECALS limit exceeded (%d)\n", MAX_DECALS ); |
|
return false; |
|
} |
|
|
|
// register new decal |
|
Q_strncpy( host.draw_decals[i], shortname, sizeof( host.draw_decals[i] )); |
|
*count += 1; |
|
|
|
return true; |
|
} |
|
|
|
/* |
|
================= |
|
Host_InitDecals |
|
================= |
|
*/ |
|
void Host_InitDecals( void ) |
|
{ |
|
int i, num_decals = 0; |
|
search_t *t; |
|
|
|
// NOTE: only once resource without which engine can't continue work |
|
if( !FS_FileExists( "gfx/conchars", false )) |
|
Sys_Error( "W_LoadWadFile: couldn't load gfx.wad\n" ); |
|
|
|
memset( host.draw_decals, 0, sizeof( host.draw_decals )); |
|
|
|
// lookup all the decals in decals.wad (basedir, gamedir, falldir) |
|
t = FS_Search( "decals.wad/*.*", true, false ); |
|
|
|
for( i = 0; t && i < t->numfilenames; i++ ) |
|
{ |
|
if( !Host_RegisterDecal( t->filenames[i], &num_decals )) |
|
break; |
|
} |
|
|
|
if( t ) Mem_Free( t ); |
|
Con_Reportf( "InitDecals: %i decals\n", num_decals ); |
|
} |
|
|
|
/* |
|
=================== |
|
Host_GetCommands |
|
|
|
Add them exactly as if they had been typed at the console |
|
=================== |
|
*/ |
|
void Host_GetCommands( void ) |
|
{ |
|
char *cmd; |
|
|
|
while( ( cmd = Sys_Input() ) ) |
|
{ |
|
Cbuf_AddText( cmd ); |
|
Cbuf_Execute(); |
|
} |
|
} |
|
|
|
/* |
|
=================== |
|
Host_CalcFPS |
|
|
|
compute actual FPS for various modes |
|
=================== |
|
*/ |
|
double Host_CalcFPS( void ) |
|
{ |
|
double fps = 0.0; |
|
|
|
if( Host_IsDedicated() ) |
|
{ |
|
fps = sys_ticrate.value; |
|
} |
|
#if !XASH_DEDICATED |
|
else if( CL_IsPlaybackDemo() || CL_IsRecordDemo( )) // NOTE: we should play demos with same fps as it was recorded |
|
{ |
|
fps = CL_GetDemoFramerate(); |
|
} |
|
else if( Host_IsLocalGame( )) |
|
{ |
|
if( !gl_vsync.value ) |
|
fps = host_maxfps.value; |
|
} |
|
else |
|
{ |
|
if( !gl_vsync.value ) |
|
{ |
|
fps = host_maxfps.value; |
|
if( fps == 0.0 ) fps = MAX_FPS; |
|
fps = bound( MIN_FPS, fps, MAX_FPS ); |
|
} |
|
} |
|
#endif |
|
|
|
return fps; |
|
} |
|
|
|
static qboolean Host_Autosleep( double dt, double scale ) |
|
{ |
|
double targetframetime, fps; |
|
int sleep; |
|
|
|
fps = Host_CalcFPS(); |
|
|
|
if( fps <= 0 ) |
|
return true; |
|
|
|
// limit fps to withing tolerable range |
|
fps = bound( MIN_FPS, fps, MAX_FPS ); |
|
|
|
if( Host_IsDedicated( )) |
|
targetframetime = ( 1.0 / ( fps + 1.0 )); |
|
else targetframetime = ( 1.0 / fps ); |
|
|
|
sleep = Host_CalcSleep(); |
|
if( sleep == 0 ) // no sleeps between frames, much simpler code |
|
{ |
|
if( dt < targetframetime * scale ) |
|
return false; |
|
} |
|
else |
|
{ |
|
static double timewindow; // allocate a time window for sleeps |
|
static int counter; // for debug |
|
static double realsleeptime; |
|
const double sleeptime = sleep * 0.001; |
|
|
|
if( dt < targetframetime * scale ) |
|
{ |
|
// if we have allocated time window, try to sleep |
|
if( timewindow > realsleeptime ) |
|
{ |
|
// Sys_Sleep isn't guaranteed to sleep an exact amount of milliseconds |
|
// so we measure the real sleep time and use it to decrease the window |
|
double t1 = Sys_DoubleTime(), t2; |
|
Sys_Sleep( sleep ); // in msec! |
|
t2 = Sys_DoubleTime(); |
|
realsleeptime = t2 - t1; |
|
|
|
timewindow -= realsleeptime; |
|
|
|
if( host_sleeptime_debug.value ) |
|
{ |
|
counter++; |
|
|
|
Con_NPrintf( counter, "%d: %.4f %.4f", counter, timewindow, realsleeptime ); |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
// if we exhausted this time window, allocate a new one after new frame |
|
if( timewindow <= realsleeptime ) |
|
{ |
|
double targetsleeptime = targetframetime - host.pureframetime * 2; |
|
|
|
if( targetsleeptime > 0 ) |
|
timewindow = targetsleeptime; |
|
else timewindow = 0; |
|
|
|
realsleeptime = sleeptime; // reset in case CPU was too busy |
|
|
|
if( host_sleeptime_debug.value ) |
|
{ |
|
counter = 0; |
|
|
|
Con_NPrintf( 0, "tgt = %.4f, pft = %.4f, wnd = %.4f", targetframetime, host.pureframetime, timewindow ); |
|
} |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
/* |
|
=================== |
|
Host_FilterTime |
|
|
|
Returns false if the time is too short to run a frame |
|
=================== |
|
*/ |
|
qboolean Host_FilterTime( float time ) |
|
{ |
|
static double oldtime; |
|
double dt; |
|
double scale = sys_timescale.value; |
|
|
|
host.realtime += time * scale; |
|
dt = host.realtime - oldtime; |
|
|
|
// clamp the fps in multiplayer games |
|
if( !Host_Autosleep( dt, scale )) |
|
return false; |
|
|
|
host.frametime = host.realtime - oldtime; |
|
host.realframetime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME ); |
|
oldtime = host.realtime; |
|
|
|
// NOTE: allow only in singleplayer while demos are not active |
|
if( host_framerate.value > 0.0f && Host_IsLocalGame() && !CL_IsPlaybackDemo() && !CL_IsRecordDemo( )) |
|
host.frametime = bound( MIN_FRAMETIME, host_framerate.value * scale, MAX_FRAMETIME ); |
|
else host.frametime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME ); |
|
|
|
return true; |
|
} |
|
|
|
/* |
|
================= |
|
Host_Frame |
|
================= |
|
*/ |
|
void Host_Frame( float time ) |
|
{ |
|
double t1, t2; |
|
|
|
// decide the simulation time |
|
if( !Host_FilterTime( time )) |
|
return; |
|
|
|
t1 = Sys_DoubleTime(); |
|
|
|
if( host.framecount == 0 ) |
|
Con_DPrintf( "Time to first frame: %.3f seconds\n", t1 - host.starttime ); |
|
|
|
Host_InputFrame (); // input frame |
|
Host_ClientBegin (); // begin client |
|
Host_GetCommands (); // dedicated in |
|
Host_ServerFrame (); // server frame |
|
Host_ClientFrame (); // client frame |
|
HTTP_Run(); // both server and client |
|
|
|
t2 = Sys_DoubleTime(); |
|
|
|
host.pureframetime = t2 - t1; |
|
|
|
host.framecount++; |
|
} |
|
|
|
/* |
|
================= |
|
Host_Error |
|
================= |
|
*/ |
|
void GAME_EXPORT Host_Error( const char *error, ... ) |
|
{ |
|
static char hosterror1[MAX_SYSPATH]; |
|
static char hosterror2[MAX_SYSPATH]; |
|
static qboolean recursive = false; |
|
va_list argptr; |
|
|
|
va_start( argptr, error ); |
|
Q_vsnprintf( hosterror1, sizeof( hosterror1 ), error, argptr ); |
|
va_end( argptr ); |
|
|
|
CL_WriteMessageHistory (); // before Q_error call |
|
|
|
if( host.framecount < 3 ) |
|
{ |
|
Sys_Error( "Host_InitError: %s", hosterror1 ); |
|
} |
|
else if( host.framecount == host.errorframe ) |
|
{ |
|
Sys_Error( "Host_MultiError: %s", hosterror2 ); |
|
} |
|
else |
|
{ |
|
if( host.allow_console ) |
|
{ |
|
UI_SetActiveMenu( false ); |
|
Key_SetKeyDest( key_console ); |
|
Con_Printf( "Host_Error: %s", hosterror1 ); |
|
} |
|
else Platform_MessageBox( "Host Error", hosterror1, true ); |
|
} |
|
|
|
// host is shutting down. don't invoke infinite loop |
|
if( host.status == HOST_SHUTDOWN ) return; |
|
|
|
if( recursive ) |
|
{ |
|
Con_Printf( "Host_RecursiveError: %s", hosterror2 ); |
|
Sys_Error( "%s", hosterror1 ); |
|
} |
|
|
|
recursive = true; |
|
Q_strncpy( hosterror2, hosterror1, MAX_SYSPATH ); |
|
host.errorframe = host.framecount; // to avoid multply calls per frame |
|
Q_snprintf( host.finalmsg, sizeof( host.finalmsg ), "Server crashed: %s", hosterror1 ); |
|
|
|
// clearing cmd buffer to prevent execute any commands |
|
COM_InitHostState(); |
|
Cbuf_Clear(); |
|
|
|
Host_ShutdownServer(); |
|
CL_Drop(); // drop clients |
|
|
|
// recreate world if needs |
|
CL_ClearEdicts (); |
|
|
|
// release all models |
|
Mod_FreeAll(); |
|
|
|
recursive = false; |
|
Host_AbortCurrentFrame(); |
|
} |
|
|
|
void Host_Error_f( void ) |
|
{ |
|
const char *error = Cmd_Argv( 1 ); |
|
|
|
if( !*error ) error = "Invoked host error"; |
|
Host_Error( "%s\n", error ); |
|
} |
|
|
|
void Sys_Error_f( void ) |
|
{ |
|
const char *error = Cmd_Argv( 1 ); |
|
|
|
if( !*error ) error = "Invoked sys error"; |
|
Sys_Error( "%s\n", error ); |
|
} |
|
|
|
/* |
|
================= |
|
Host_Crash_f |
|
================= |
|
*/ |
|
static void Host_Crash_f( void ) |
|
{ |
|
*(volatile int *)0 = 0xffffffff; |
|
} |
|
|
|
/* |
|
================= |
|
Host_Userconfigd_f |
|
================= |
|
*/ |
|
void Host_Userconfigd_f( void ) |
|
{ |
|
search_t *t; |
|
int i; |
|
|
|
t = FS_Search( "userconfig.d/*.cfg", true, false ); |
|
if( !t ) return; |
|
|
|
for( i = 0; i < t->numfilenames; i++ ) |
|
{ |
|
Cbuf_AddTextf( "exec %s\n", t->filenames[i] ); |
|
} |
|
|
|
Mem_Free( t ); |
|
} |
|
|
|
#if XASH_ENGINE_TESTS |
|
static void Host_RunTests( int stage ) |
|
{ |
|
switch( stage ) |
|
{ |
|
case 0: // early engine load |
|
memset( &tests_stats, 0, sizeof( tests_stats )); |
|
TEST_LIST_0; |
|
#if !XASH_DEDICATED |
|
TEST_LIST_0_CLIENT; |
|
#endif /* XASH_DEDICATED */ |
|
break; |
|
case 1: // after FS load |
|
TEST_LIST_1; |
|
#if !XASH_DEDICATED |
|
TEST_LIST_1_CLIENT; |
|
#endif |
|
Msg( "Done! %d passed, %d failed\n", tests_stats.passed, tests_stats.failed ); |
|
Sys_Quit(); |
|
} |
|
} |
|
#endif |
|
|
|
/* |
|
================= |
|
Host_InitCommon |
|
================= |
|
*/ |
|
void Host_InitCommon( int argc, char **argv, const char *progname, qboolean bChangeGame ) |
|
{ |
|
char dev_level[4]; |
|
int developer = DEFAULT_DEV; |
|
const char *baseDir; |
|
char ticrate[16]; |
|
int len; |
|
|
|
// some commands may turn engine into infinite loop, |
|
// e.g. xash.exe +game xash -game xash |
|
// so we clear all cmd_args, but leave dbg states as well |
|
Sys_ParseCommandLine( argc, argv ); |
|
|
|
if( !Sys_CheckParm( "-disablehelp" ) ) |
|
{ |
|
if( Sys_CheckParm( "-help" ) || Sys_CheckParm( "-h" ) || Sys_CheckParm( "--help" ) ) |
|
{ |
|
Sys_PrintUsage(); |
|
} |
|
} |
|
|
|
if( !Sys_CheckParm( "-noch" ) ) |
|
Sys_SetupCrashHandler(); |
|
|
|
host.enabledll = !Sys_CheckParm( "-nodll" ); |
|
|
|
host.change_game = bChangeGame || Sys_CheckParm( "-changegame" ); |
|
host.config_executed = false; |
|
host.status = HOST_INIT; // initialzation started |
|
|
|
Memory_Init(); // init memory subsystem |
|
|
|
host.mempool = Mem_AllocPool( "Zone Engine" ); |
|
|
|
host.allow_console = DEFAULT_ALLOWCONSOLE; |
|
|
|
// HACKHACK: Quake console is always allowed |
|
// TODO: determine if we are running QWrap more reliable |
|
if( !host.allow_console && ( Sys_CheckParm( "-console" ) || !Q_stricmp( SI.exeName, "quake" ))) |
|
host.allow_console = true; |
|
|
|
if( Sys_CheckParm( "-dev" )) |
|
{ |
|
host.allow_console = true; |
|
developer = DEV_NORMAL; |
|
|
|
if( Sys_GetParmFromCmdLine( "-dev", dev_level )) |
|
{ |
|
if( Q_isdigit( dev_level )) |
|
developer = bound( DEV_NONE, abs( Q_atoi( dev_level )), DEV_EXTENDED ); |
|
} |
|
} |
|
|
|
#if XASH_ENGINE_TESTS |
|
if( Sys_CheckParm( "-runtests" )) |
|
{ |
|
host.allow_console = true; |
|
developer = DEV_EXTENDED; |
|
} |
|
#endif |
|
|
|
host.con_showalways = true; |
|
|
|
#if XASH_DEDICATED |
|
host.type = HOST_DEDICATED; // predict state |
|
#else |
|
if( Sys_CheckParm("-dedicated") || progname[0] == '#' ) |
|
{ |
|
host.type = HOST_DEDICATED; |
|
} |
|
else |
|
{ |
|
host.type = HOST_NORMAL; |
|
} |
|
#endif |
|
|
|
// set default gamedir |
|
if( progname[0] == '#' ) |
|
progname++; |
|
|
|
Q_strncpy( SI.exeName, progname, sizeof( SI.exeName )); |
|
Q_strncpy( SI.basedirName, progname, sizeof( SI.exeName )); |
|
|
|
if( Host_IsDedicated() ) |
|
{ |
|
Sys_MergeCommandLine( ); |
|
|
|
host.allow_console = true; |
|
} |
|
else |
|
{ |
|
// don't show console as default |
|
if( developer <= DEV_NORMAL ) |
|
host.con_showalways = false; |
|
} |
|
|
|
// member console allowing |
|
host.allow_console_init = host.allow_console; |
|
|
|
if( Sys_CheckParm( "-bugcomp" )) |
|
{ |
|
// add argument check here when we add other levels |
|
// of bugcompatibility |
|
host.bugcomp = BUGCOMP_GOLDSRC; |
|
} |
|
|
|
// timeBeginPeriod( 1 ); // a1ba: Do we need this? |
|
|
|
// NOTE: this message couldn't be passed into game console but it doesn't matter |
|
// Con_Reportf( "Sys_LoadLibrary: Loading xash.dll - ok\n" ); |
|
|
|
// get default screen res |
|
VID_InitDefaultResolution(); |
|
|
|
// init host state machine |
|
COM_InitHostState(); |
|
|
|
// init hashed commands |
|
BaseCmd_Init(); |
|
|
|
// startup cmds and cvars subsystem |
|
Cmd_Init(); |
|
Cvar_Init(); |
|
|
|
// share developer level across all dlls |
|
Q_snprintf( dev_level, sizeof( dev_level ), "%i", developer ); |
|
Cvar_DirectSet( &host_developer, dev_level ); |
|
Cvar_RegisterVariable( &sys_ticrate ); |
|
|
|
if( Sys_GetParmFromCmdLine( "-sys_ticrate", ticrate )) |
|
{ |
|
double fps = bound( MIN_FPS, atof( ticrate ), MAX_FPS ); |
|
Cvar_SetValue( "sys_ticrate", fps ); |
|
} |
|
|
|
Con_Init(); // early console running to catch all the messages |
|
|
|
#if XASH_ENGINE_TESTS |
|
if( Sys_CheckParm( "-runtests" )) |
|
Host_RunTests( 0 ); |
|
#endif |
|
|
|
Platform_Init(); |
|
|
|
baseDir = getenv( "XASH3D_BASEDIR" ); |
|
|
|
if( COM_CheckString( baseDir ) ) |
|
{ |
|
Q_strncpy( host.rootdir, baseDir, sizeof( host.rootdir )); |
|
} |
|
else |
|
{ |
|
#if TARGET_OS_IOS |
|
Q_strncpy( host.rootdir, IOS_GetDocsDir(), sizeof( host.rootdir )); |
|
#elif XASH_ANDROID && XASH_SDL |
|
Q_strncpy( host.rootdir, SDL_AndroidGetExternalStoragePath(), sizeof( host.rootdir )); |
|
#elif XASH_PSVITA |
|
if ( !PSVita_GetBasePath( host.rootdir, sizeof( host.rootdir ))) |
|
{ |
|
Sys_Error( "couldn't find xash3d data directory" ); |
|
host.rootdir[0] = 0; |
|
} |
|
#elif (XASH_SDL == 2) && !XASH_NSWITCH // GetBasePath not impl'd in switch-sdl2 |
|
char *szBasePath = SDL_GetBasePath(); |
|
if( szBasePath ) |
|
{ |
|
Q_strncpy( host.rootdir, szBasePath, sizeof( host.rootdir )); |
|
SDL_free( szBasePath ); |
|
} |
|
else |
|
{ |
|
#if XASH_POSIX || XASH_WIN32 |
|
if( !getcwd( host.rootdir, sizeof( host.rootdir ))) |
|
Sys_Error( "couldn't determine current directory: %s, getcwd: %s", SDL_GetError(), strerror( errno )); |
|
#else |
|
Sys_Error( "couldn't determine current directory: %s", SDL_GetError() ); |
|
#endif |
|
} |
|
#else |
|
if( !getcwd( host.rootdir, sizeof( host.rootdir ))) |
|
{ |
|
Sys_Error( "couldn't determine current directory: %s", strerror( errno ) ); |
|
host.rootdir[0] = 0; |
|
} |
|
#endif |
|
} |
|
|
|
#if XASH_WIN32 |
|
COM_FixSlashes( host.rootdir ); |
|
#endif |
|
|
|
len = Q_strlen( host.rootdir ); |
|
|
|
if( len && host.rootdir[len - 1] == '/' ) |
|
host.rootdir[len - 1] = 0; |
|
|
|
// get readonly root. The order is: check for arg, then env. |
|
// if still not got it, rodir is disabled. |
|
host.rodir[0] = '\0'; |
|
if( !Sys_GetParmFromCmdLine( "-rodir", host.rodir )) |
|
{ |
|
char *roDir = getenv( "XASH3D_RODIR" ); |
|
|
|
if( COM_CheckString( roDir )) |
|
Q_strncpy( host.rodir, roDir, sizeof( host.rodir )); |
|
} |
|
|
|
#if XASH_WIN32 |
|
COM_FixSlashes( host.rootdir ); |
|
#endif |
|
|
|
len = Q_strlen( host.rodir ); |
|
|
|
if( len && host.rodir[len - 1] == '/' ) |
|
host.rodir[len - 1] = 0; |
|
|
|
if( !COM_CheckStringEmpty( host.rootdir )) |
|
{ |
|
Sys_Error( "Changing working directory failed (empty working directory)\n" ); |
|
return; |
|
} |
|
|
|
FS_LoadProgs(); |
|
|
|
// TODO: this function will cause engine to stop in case of fail |
|
// when it will have an option to return string error, restore Sys_Error |
|
FS_SetCurrentDirectory( host.rootdir ); |
|
|
|
FS_Init(); |
|
|
|
Sys_InitLog(); |
|
|
|
// print bugcompatibility level here, after log was initialized |
|
if( host.bugcomp == BUGCOMP_GOLDSRC ) |
|
{ |
|
Con_Printf( "^3BUGCOMP^7: GoldSrc bug-compatibility enabled\n" ); |
|
} |
|
|
|
Cmd_AddCommand( "exec", Host_Exec_f, "execute a script file" ); |
|
Cmd_AddCommand( "memlist", Host_MemStats_f, "prints memory pool information" ); |
|
Cmd_AddRestrictedCommand( "userconfigd", Host_Userconfigd_f, "execute all scripts from userconfig.d" ); |
|
|
|
Image_Init(); |
|
Sound_Init(); |
|
|
|
#if XASH_ENGINE_TESTS |
|
if( Sys_CheckParm( "-runtests" )) |
|
Host_RunTests( 1 ); |
|
#endif |
|
|
|
FS_LoadGameInfo( NULL ); |
|
Cvar_PostFSInit(); |
|
|
|
if( FS_FileExists( va( "%s.rc", SI.basedirName ), false )) |
|
Q_strncpy( SI.rcName, SI.basedirName, sizeof( SI.rcName )); // e.g. valve.rc |
|
else Q_strncpy( SI.rcName, SI.exeName, sizeof( SI.rcName )); // e.g. quake.rc |
|
|
|
Q_strncpy( host.gamefolder, GI->gamefolder, sizeof( host.gamefolder )); |
|
|
|
Image_CheckPaletteQ1 (); |
|
Host_InitDecals (); // reload decals |
|
|
|
// DEPRECATED: by FWGS fork |
|
#if 0 |
|
if( GI->secure ) |
|
{ |
|
// clear all developer levels when game is protected |
|
Cvar_DirectSet( &host_developer, "0" ); |
|
host.allow_console_init = false; |
|
host.con_showalways = false; |
|
host.allow_console = false; |
|
} |
|
#endif |
|
HPAK_Init(); |
|
|
|
IN_Init(); |
|
Key_Init(); |
|
} |
|
|
|
void Host_FreeCommon( void ) |
|
{ |
|
Image_Shutdown(); |
|
Sound_Shutdown(); |
|
Netchan_Shutdown(); |
|
HPAK_FlushHostQueue(); |
|
FS_Shutdown(); |
|
} |
|
|
|
/* |
|
================= |
|
Host_Main |
|
================= |
|
*/ |
|
int EXPORT Host_Main( int argc, char **argv, const char *progname, int bChangeGame, pfnChangeGame func ) |
|
{ |
|
static double oldtime, newtime; |
|
string demoname; |
|
|
|
host.starttime = Sys_DoubleTime(); |
|
|
|
pChangeGame = func; // may be NULL |
|
|
|
Host_InitCommon( argc, argv, progname, bChangeGame ); |
|
|
|
// init commands and vars |
|
if( host_developer.value >= DEV_EXTENDED ) |
|
{ |
|
Cmd_AddRestrictedCommand ( "sys_error", Sys_Error_f, "just throw a fatal error to test shutdown procedures"); |
|
Cmd_AddRestrictedCommand ( "host_error", Host_Error_f, "just throw a host error to test shutdown procedures"); |
|
Cmd_AddRestrictedCommand ( "crash", Host_Crash_f, "a way to force a bus error for development reasons"); |
|
} |
|
|
|
Cvar_RegisterVariable( &host_serverstate ); |
|
Cvar_RegisterVariable( &host_maxfps ); |
|
Cvar_RegisterVariable( &host_framerate ); |
|
Cvar_RegisterVariable( &host_sleeptime ); |
|
Cvar_RegisterVariable( &host_sleeptime_debug ); |
|
Cvar_RegisterVariable( &host_gameloaded ); |
|
Cvar_RegisterVariable( &host_clientloaded ); |
|
Cvar_RegisterVariable( &host_limitlocal ); |
|
Cvar_RegisterVariable( &con_gamemaps ); |
|
Cvar_RegisterVariable( &sys_timescale ); |
|
|
|
Cvar_Getf( "buildnum", FCVAR_READ_ONLY, "returns a current build number", "%i", Q_buildnum_compat()); |
|
Cvar_Getf( "ver", FCVAR_READ_ONLY, "shows an engine version", "%i/%s (hw build %i)", PROTOCOL_VERSION, XASH_COMPAT_VERSION, Q_buildnum_compat()); |
|
Cvar_Getf( "host_ver", FCVAR_READ_ONLY, "detailed info about this build", "%i " XASH_VERSION " %s %s %s", Q_buildnum(), Q_buildos(), Q_buildarch(), Q_buildcommit()); |
|
Cvar_Getf( "host_lowmemorymode", FCVAR_READ_ONLY, "indicates if engine compiled for low RAM consumption (0 - normal, 1 - low engine limits, 2 - low protocol limits)", "%i", XASH_LOW_MEMORY ); |
|
|
|
Mod_Init(); |
|
NET_Init(); |
|
NET_InitMasters(); |
|
Netchan_Init(); |
|
|
|
// allow to change game from the console |
|
if( pChangeGame != NULL ) |
|
{ |
|
Cmd_AddRestrictedCommand( "game", Host_ChangeGame_f, "change game" ); |
|
Cvar_Get( "host_allow_changegame", "1", FCVAR_READ_ONLY, "allows to change games" ); |
|
} |
|
else |
|
{ |
|
Cvar_Get( "host_allow_changegame", "0", FCVAR_READ_ONLY, "allows to change games" ); |
|
} |
|
|
|
SV_Init(); |
|
CL_Init(); |
|
|
|
HTTP_Init(); |
|
ID_Init(); |
|
|
|
if( Host_IsDedicated() ) |
|
{ |
|
#ifdef _WIN32 |
|
Wcon_InitConsoleCommands (); |
|
#endif |
|
|
|
Cmd_AddRestrictedCommand( "quit", Sys_Quit, "quit the game" ); |
|
Cmd_AddRestrictedCommand( "exit", Sys_Quit, "quit the game" ); |
|
} |
|
else Cmd_AddRestrictedCommand( "minimize", Host_Minimize_f, "minimize main window to tray" ); |
|
|
|
HPAK_CheckIntegrity( CUSTOM_RES_PATH ); |
|
|
|
host.errorframe = 0; |
|
|
|
// post initializations |
|
switch( host.type ) |
|
{ |
|
case HOST_NORMAL: |
|
#ifdef _WIN32 |
|
Wcon_ShowConsole( false ); // hide console |
|
#endif |
|
// execute startup config and cmdline |
|
Cbuf_AddTextf( "exec %s.rc\n", SI.rcName ); |
|
Cbuf_Execute(); |
|
if( !host.config_executed ) |
|
{ |
|
Cbuf_AddText( "exec config.cfg\n" ); |
|
Cbuf_Execute(); |
|
} |
|
// exec all files from userconfig.d |
|
Host_Userconfigd_f(); |
|
break; |
|
case HOST_DEDICATED: |
|
// allways parse commandline in dedicated-mode |
|
host.stuffcmds_pending = true; |
|
break; |
|
} |
|
|
|
host.change_game = false; // done |
|
Cmd_RemoveCommand( "setgl" ); |
|
Cbuf_ExecStuffCmds(); // execute stuffcmds (commandline) |
|
SCR_CheckStartupVids(); // must be last |
|
|
|
if( Sys_GetParmFromCmdLine( "-timedemo", demoname )) |
|
Cbuf_AddTextf( "timedemo %s\n", demoname ); |
|
|
|
oldtime = Sys_DoubleTime() - 0.1; |
|
|
|
if( Host_IsDedicated( )) |
|
{ |
|
// in dedicated server input system can't set HOST_FRAME status |
|
// so set it here as we're finished initializing |
|
host.status = HOST_FRAME; |
|
|
|
if( GameState->nextstate == STATE_RUNFRAME ) |
|
Con_Printf( "Type 'map <mapname>' to start game... (TAB-autocomplete is working too)\n" ); |
|
|
|
// execute server.cfg after commandline |
|
// so we have a chance to set servercfgfile |
|
Cbuf_AddTextf( "exec %s\n", Cvar_VariableString( "servercfgfile" )); |
|
Cbuf_Execute(); |
|
} |
|
|
|
// main window message loop |
|
while( !host.crashed ) |
|
{ |
|
newtime = Sys_DoubleTime (); |
|
COM_Frame( newtime - oldtime ); |
|
oldtime = newtime; |
|
} |
|
|
|
// never reached |
|
return 0; |
|
} |
|
|
|
/* |
|
================= |
|
Host_Shutdown |
|
================= |
|
*/ |
|
void EXPORT Host_Shutdown( void ) |
|
{ |
|
if( host.shutdown_issued ) return; |
|
host.shutdown_issued = true; |
|
|
|
if( host.status != HOST_ERR_FATAL ) host.status = HOST_SHUTDOWN; // prepare host to normal shutdown |
|
if( !host.change_game ) Q_strncpy( host.finalmsg, "Server shutdown", sizeof( host.finalmsg )); |
|
|
|
#if !XASH_DEDICATED |
|
if( host.type == HOST_NORMAL ) |
|
Host_WriteConfig(); |
|
#endif |
|
|
|
SV_Shutdown( "Server shutdown\n" ); |
|
SV_UnloadProgs(); |
|
SV_ShutdownFilter(); |
|
CL_Shutdown(); |
|
|
|
Mod_Shutdown(); |
|
NET_Shutdown(); |
|
HTTP_Shutdown(); |
|
Host_FreeCommon(); |
|
Platform_Shutdown(); |
|
|
|
// must be last, console uses this |
|
Mem_FreePool( &host.mempool ); |
|
|
|
// restore filter |
|
Sys_RestoreCrashHandler(); |
|
Sys_CloseLog(); |
|
}
|
|
|