/* 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. */ #ifdef XASH_SDL #include #endif // XASH_SDL #include // va_args #include // errno #include // strerror #ifndef _WIN32 #include // fork #include #include #include #endif #ifdef __EMSCRIPTEN__ #include #endif #include #include "common.h" #include "netchan.h" #include "protocol.h" #include "mod_local.h" #include "mathlib.h" #include "input.h" #include "enginefeatures.h" #include "render_api.h" // decallist_t typedef void (*pfnChangeGame)( const char *progname ); pfnChangeGame pChangeGame = NULL; host_parm_t host; // host parms sysinfo_t SI; CVAR_DEFINE( host_developer, "developer", "0", 0, "engine is in development-mode" ); CVAR_DEFINE_AUTO( sys_ticrate, "100", 0, "framerate in dedicated mode" ); convar_t *host_gameloaded; convar_t *host_clientloaded; convar_t *host_limitlocal; convar_t *host_maxfps; convar_t *host_framerate; convar_t *con_gamemaps; convar_t *build, *ver; int Host_CompareFileTime( long ft1, long 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 )) MsgDev( D_REPORT, "^3EXT:^7 big world support enabled\n" ); if( FBitSet( host.features, ENGINE_LOAD_DELUXEDATA )) MsgDev( D_REPORT, "^3EXT:^7 deluxemap support enabled\n" ); if( FBitSet( host.features, ENGINE_PHYSICS_PUSHER_EXT )) MsgDev( D_REPORT, "^3EXT:^7 Improved MOVETYPE_PUSH is used\n" ); if( FBitSet( host.features, ENGINE_LARGE_LIGHTMAPS )) MsgDev( D_REPORT, "^3EXT:^7 Large lightmaps enabled\n" ); if( FBitSet( host.features, ENGINE_COMPENSATE_QUAKE_BUG )) MsgDev( D_REPORT, "^3EXT:^7 Compensate quake bug enabled\n" ); } /* ================ 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 ); MsgDev( D_INFO, "Host_EndGame: %s\n", string ); SV_Shutdown( "\n" ); #ifndef 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_CheckSleep ================== */ void Host_CheckSleep( void ) { if( Host_IsDedicated() ) { // let the dedicated server some sleep Sys_Sleep( 1 ); } else { if( host.status == HOST_NOFOCUS ) { if( SV_Active() && CL_IsInGame( )) Sys_Sleep( 1 ); // listenserver else Sys_Sleep( 20 ); // sleep 20 ms otherwise } else if( host.status == HOST_SLEEP ) { // completely sleep in minimized state Sys_Sleep( 20 ); } } } void Host_NewInstance( const char *name, const char *finalmsg ) { if( !pChangeGame ) return; host.change_game = true; Q_strncpy( host.finalmsg, finalmsg, sizeof( host.finalmsg )); 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 \n" ); return; } // validate gamedir for( i = 0; i < SI.numgames; i++ ) { if( !Q_stricmp( SI.games[i]->gamefolder, Cmd_Argv( 1 ))) break; } if( i == SI.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 { const char *arg1 = va( "%s%s", (host.type == HOST_NORMAL) ? "" : "#", Cmd_Argv( 1 )); const char *arg2 = va( "change game to '%s'", SI.games[i]->title ); Host_NewInstance( arg1, arg2 ); } } /* =============== Host_Exec_f =============== */ void Host_Exec_f( void ) { string cfgpath; char *f, *txt; size_t len; if( Cmd_Argc() != 2 ) { Con_Printf( S_USAGE "exec \n" ); return; } if( !Q_stricmp( "game.cfg", Cmd_Argv( 1 ))) { // don't execute game.cfg in singleplayer if( SV_GetMaxClients() == 1 ) return; } Q_strncpy( cfgpath, Cmd_Argv( 1 ), sizeof( cfgpath )); COM_DefaultExtension( cfgpath, ".cfg" ); // append as default f = FS_LoadFile( cfgpath, &len, false ); if( !f ) { MsgDev( D_NOTE, "couldn't exec %s\n", Cmd_Argv( 1 )); return; } if( !Q_stricmp( "config.cfg", Cmd_Argv( 1 ))) host.config_executed = true; // adds \n\0 at end of the file txt = Z_Malloc( len + 2 ); memcpy( txt, f, len ); Q_strncat( txt, "\n", len + 2 ); Mem_Free( f ); if( !host.apply_game_config ) MsgDev( D_INFO, "execing %s\n", Cmd_Argv( 1 )); 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 \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 ); 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 ) { MsgDev( D_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; 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; // NOTE: we should play demos with same fps as it was recorded #ifndef XASH_DEDICATED if( CL_IsPlaybackDemo() || CL_IsRecordDemo( )) { fps = CL_GetDemoFramerate(); } else if( Host_IsLocalGame( )) { fps = host_maxfps->value; } else #endif if( Host_IsDedicated() ) { fps = sys_ticrate.value; } else { fps = host_maxfps->value; fps = bound( MIN_FPS, fps, MAX_FPS ); } #ifndef XASH_DEDICATED // probably left part of this condition is redundant :-) if( host.type != HOST_DEDICATED && Host_IsLocalGame( ) && !CL_IsTimeDemo( )) { // ajdust fps for vertical synchronization if( gl_vsync != NULL && gl_vsync->value ) { if( vid_displayfrequency->value != 0.0f ) fps = vid_displayfrequency->value; else fps = 60.0; // default } } #endif return fps; } /* =================== Host_FilterTime Returns false if the time is too short to run a frame =================== */ qboolean Host_FilterTime( float time ) { static double oldtime; double fps; host.realtime += time; fps = Host_CalcFPS( ); // clamp the fps in multiplayer games if( fps != 0.0 ) { // limit fps to withing tolerable range fps = bound( MIN_FPS, fps, MAX_FPS ); if( Host_IsDedicated() ) { if(( host.realtime - oldtime ) < ( 1.0 / ( fps + 1.0 ))) return false; } else { if(( host.realtime - oldtime ) < ( 1.0 / fps )) 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, MAX_FRAMETIME ); else host.frametime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME ); return true; } /* ================= Host_Frame ================= */ void Host_Frame( float time ) { Host_CheckSleep(); // decide the simulation time if( !Host_FilterTime( time )) return; Host_InputFrame (); // input frame Host_ClientBegin (); // begin client Host_GetCommands (); // dedicated in Host_ServerFrame (); // server frame Host_ClientFrame (); // client frame host.framecount++; } /* ================= Host_Error ================= */ void Host_Error( const char *error, ... ) { static char hosterror1[MAX_SYSPATH]; static char hosterror2[MAX_SYSPATH]; static qboolean recursive = false; va_list argptr; if( host.mouse_visible && !CL_IsInMenu( )) { // hide VGUI mouse #ifdef XASH_SDL SDL_ShowCursor( 0 ); #endif host.mouse_visible = false; } va_start( argptr, error ); Q_vsprintf( 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 ); return; } else { if( host.allow_console ) { UI_SetActiveMenu( false ); Key_SetKeyDest( key_console ); Con_Printf( "Host_Error: %s", hosterror1 ); } else MSGBOX2( hosterror1 ); } // 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 ); return; // don't multiple executes } recursive = true; Q_strncpy( hosterror2, hosterror1, MAX_SYSPATH ); host.errorframe = host.framecount; // to avoid multply calls per frame Q_sprintf( 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 ) { *(int *)0 = 0xffffffff; } /* ================= Host_InitCommon ================= */ void Host_InitCommon( int argc, char **argv, const char *progname, qboolean bChangeGame ) { char dev_level[4]; int developer = 0; const char *baseDir; char ticrate[16]; // 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( "-noch" ) ) Sys_SetupCrashHandler(); // to be accessed later if( ( host.daemonized = Sys_CheckParm( "-daemonize" ) ) ) { #if defined(_POSIX_VERSION) && !defined(XASH_MOBILE_PLATFORM) pid_t daemon; daemon = fork(); if( daemon < 0 ) { Host_Error( "fork() failed: %s\n", strerror( errno ) ); } if( daemon > 0 ) { // parent MsgDev( D_INFO, "Child pid: %i\n", daemon ); exit( 0 ); } else { // don't be closed by parent if( setsid() < 0 ) { Host_Error( "setsid() failed: %s\n", strerror( errno ) ); } // set permissions umask( 0 ); // engine will still use stdin/stdout, // so just redirect them to /dev/null close( STDIN_FILENO ); close( STDOUT_FILENO ); close( STDERR_FILENO ); open("/dev/null", O_RDONLY); // becomes stdin open("/dev/null", O_RDWR); // stdout open("/dev/null", O_RDWR); // stderr // fallthrough } #elif defined(XASH_MOBILE_PLATFORM) Sys_Error( "Can't run in background on mobile platforms!" ); #else Sys_Error( "Daemonize not supported on this platform!" ); #endif } if( ( baseDir = getenv( "XASH3D_BASEDIR" ) ) ) { Q_strncpy( host.rootdir, baseDir, sizeof(host.rootdir) ); } else { #if TARGET_OS_IOS const char *IOS_GetDocsDir(); Q_strncpy( host.rootdir, IOS_GetDocsDir(), sizeof(host.rootdir) ); #elif defined(XASH_SDL) if( !( baseDir = SDL_GetBasePath() ) ) Sys_Error( "couldn't determine current directory: %s", SDL_GetError() ); Q_strncpy( host.rootdir, baseDir, sizeof( host.rootdir ) ); SDL_free( (void*)baseDir ); #else if( !getcwd( host.rootdir, sizeof(host.rootdir) ) ) { Sys_Error( "couldn't determine current directory: %s", strerror( errno ) ); host.rootdir[0] = 0; } #endif } if( host.rootdir[Q_strlen( host.rootdir ) - 1] == '/' ) host.rootdir[Q_strlen( host.rootdir ) - 1] = 0; host.enabledll = !Sys_CheckParm( "-nodll" ); #ifdef DLL_LOADER if( host.enabledll ) Setup_LDT_Keeper( ); // Must call before creating any thread #endif host.change_game = bChangeGame; host.config_executed = false; host.status = HOST_INIT; // initialzation started Memory_Init(); // init memory subsystem host.mempool = Mem_AllocPool( "Zone Engine" ); // HACKHACK: Quake console is always allowed if( Sys_CheckParm( "-console" ) || !Q_stricmp( progname, "id1" )) 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 ); } } host.con_showalways = true; #ifdef 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 #ifdef XASH_SDL // should work even if it failed SDL_Init( SDL_INIT_TIMER ); if( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_EVENTS ) ) { Sys_Warn( "SDL_Init failed: %s", SDL_GetError() ); host.type = HOST_DEDICATED; } SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); #if defined XASH_GLES && !defined __EMSCRIPTEN__ && !TARGET_OS_IOS SDL_SetHint( SDL_HINT_OPENGL_ES_DRIVER, "1" ); #endif SDL_StopTextInput(); #endif if ( !host.rootdir[0] || SetCurrentDirectory( host.rootdir ) != 0) MsgDev( D_INFO, "%s is working directory now\n", host.rootdir ); else Sys_Error( "Changing working directory to %s failed.\n", host.rootdir ); Sys_InitLog(); // 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; #ifdef _WIN32 Wcon_CreateConsole(); // system console used by dedicated server or show fatal errors #endif // timeBeginPeriod( 1 ); // a1ba: Do we need this? // NOTE: this message couldn't be passed into game console but it doesn't matter MsgDev( D_NOTE, "Sys_LoadLibrary: Loading xash.dll - ok\n" ); // get default screen res VID_InitDefaultResolution(); // init host state machine COM_InitHostState(); // 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 Cmd_AddCommand( "exec", Host_Exec_f, "execute a script file" ); Cmd_AddCommand( "memlist", Host_MemStats_f, "prints memory pool information" ); FS_Init(); Image_Init(); Sound_Init(); FS_LoadGameInfo( NULL ); Q_strncpy( host.gamefolder, GI->gamefolder, sizeof( host.gamefolder )); // 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; pChangeGame = func; // may be NULL Host_InitCommon( argc, argv, progname, bChangeGame ); // init commands and vars if( host_developer.value >= DEV_EXTENDED ) { Cmd_AddCommand ( "sys_error", Sys_Error_f, "just throw a fatal error to test shutdown procedures"); Cmd_AddCommand ( "host_error", Host_Error_f, "just throw a host error to test shutdown procedures"); Cmd_AddCommand ( "crash", Host_Crash_f, "a way to force a bus error for development reasons"); } host_maxfps = Cvar_Get( "fps_max", "72", FCVAR_ARCHIVE, "host fps upper limit" ); host_framerate = Cvar_Get( "host_framerate", "0", 0, "locks frame timing to this value in seconds" ); host_gameloaded = Cvar_Get( "host_gameloaded", "0", FCVAR_READ_ONLY, "inidcates a loaded game.dll" ); host_clientloaded = Cvar_Get( "host_clientloaded", "0", FCVAR_READ_ONLY, "inidcates a loaded client.dll" ); host_limitlocal = Cvar_Get( "host_limitlocal", "0", 0, "apply cl_cmdrate and rate to loopback connection" ); con_gamemaps = Cvar_Get( "con_mapfilter", "1", FCVAR_ARCHIVE, "when true show only maps in game folder" ); build = Cvar_Get( "build", va( "%i", Q_buildnum()), FCVAR_READ_ONLY, "returns a current build number" ); ver = Cvar_Get( "ver", va( "%i/%s (hw build %i)", PROTOCOL_VERSION, XASH_VERSION, Q_buildnum()), FCVAR_READ_ONLY, "shows an engine version" ); Mod_Init(); NET_Init(); NET_InitMasters(); Netchan_Init(); // allow to change game from the console if( pChangeGame != NULL ) { Cmd_AddCommand( "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(); ID_Init(); if( Host_IsDedicated() ) { #ifdef _WIN32 Wcon_InitConsoleCommands (); #endif Cmd_AddCommand( "quit", Sys_Quit, "quit the game" ); Cmd_AddCommand( "exit", Sys_Quit, "quit the game" ); } else Cmd_AddCommand( "minimize", Host_Minimize_f, "minimize main window to tray" ); 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_AddText( va( "exec %s.rc\n", SI.rcName )); Cbuf_Execute(); if( !host.config_executed ) { Cbuf_AddText( "exec config.cfg\n" ); Cbuf_Execute(); } 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 oldtime = Sys_DoubleTime() - 0.1; if( Host_IsDedicated() && GameState->nextstate == STATE_RUNFRAME ) Con_Printf( "type 'map ' to run server... (TAB-autocomplete is working too)\n" ); // 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 )); #ifndef XASH_DEDICATED if( host.type == HOST_NORMAL ) Host_WriteConfig(); #endif SV_Shutdown( "Server shutdown\n" ); CL_Shutdown(); Mod_Shutdown(); NET_Shutdown(); Host_FreeCommon(); #ifdef _WIN32 Wcon_DestroyConsole(); #endif // must be last, console uses this Mem_FreePool( &host.mempool ); // restore filter Sys_RestoreCrashHandler(); Sys_CloseLog(); }