Files
xash3d-fwgs/engine/common/host.c
Alibek Omarov 9c6589fa03 engine: introduce vfs.cfg to properly control user set addon folders
vfs.cfg executed before game directory is mounted, this way we can know which
folders must be mounted before engine initialization.

This commit also removes buggy hot reload of the addon folders.
2025-09-01 01:42:37 +05:00

1424 lines
36 KiB
C

/*
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"
#include <stdarg.h> // va_args
#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 "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"
static pfnChangeGame pChangeGame = NULL;
host_parm_t host; // host parms
#if XASH_ANDROID
static jmp_buf return_from_main_buf;
/*
===============
Host_ExitInMain
On some platforms (e.g. Android) we can't exit with exit(3) as calling it would
kill wrapper process (e.g. app_process) too early, before all resources would
be freed, contexts released, files closed, etc, etc...
To fix this, we create jmp_buf in Host_Main function, when jumping into with
non-zero value will immediately return from it with `error_on_exit`.
===============
*/
void Host_ExitInMain( void )
{
longjmp( return_from_main_buf, 1 );
}
#endif // XASH_ANDROID
#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" );
static CVAR_DEFINE_AUTO( sys_ticrate, "100", FCVAR_SERVER, "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" );
CVAR_DEFINE_AUTO( fps_override, "0", FCVAR_FILTERABLE, "unlock higher framerate values, not supported" );
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_AUTO( host_allow_materials, "0", FCVAR_LATCH|FCVAR_ARCHIVE, "allow texture replacements from materials/ folder" );
CVAR_DEFINE( con_gamemaps, "con_mapfilter", "1", FCVAR_ARCHIVE, "when true show only maps in game folder" );
typedef struct feature_message_s
{
uint32_t mask;
const char *msg;
const char *arg;
} feature_message_t;
static const feature_message_t bugcomp_features[] =
{
{ BUGCOMP_PENTITYOFENTINDEX_FLAG, "pfnPEntityOfEntIndex bugfix revert", "peoei" },
{ BUGCOMP_MESSAGE_REWRITE_FACILITY_FLAG, "GoldSrc Message Rewrite Facility", "gsmrf" },
{ BUGCOMP_SPATIALIZE_SOUND_WITH_ATTN_NONE, "spatialize sounds with zero attenuation", "sp_attn_none" },
{ BUGCOMP_GET_GAME_DIR_FULL_PATH, "Return full path in GET_GAME_DIR()", "get_game_dir_full" }
};
static const feature_message_t engine_features[] =
{
{ ENGINE_WRITE_LARGE_COORD, "Big World Support" },
{ ENGINE_QUAKE_COMPATIBLE, "Quake Compatibility" },
{ ENGINE_LOAD_DELUXEDATA, "Deluxemap Support" },
{ ENGINE_PHYSICS_PUSHER_EXT, "Improved MOVETYPE_PUSH" },
{ ENGINE_LARGE_LIGHTMAPS, "Large Lightmaps" },
{ ENGINE_COMPENSATE_QUAKE_BUG, "Stupid Quake Bug Compensation" },
{ ENGINE_IMPROVED_LINETRACE, "Improved Trace Line" },
{ ENGINE_COMPUTE_STUDIO_LERP, "Studio MOVETYPE_STEP Lerping" },
{ ENGINE_LINEAR_GAMMA_SPACE, "Linear Gamma Space" },
{ ENGINE_STEP_POSHISTORY_LERP, "MOVETYPE_STEP Position History Based Lerping" },
};
static void Sys_MakeVersionString( char *out, size_t len )
{
Q_snprintf( out, len, XASH_ENGINE_NAME " %i/" XASH_VERSION " (%s-%s build %i)", PROTOCOL_VERSION, Q_buildos(), Q_buildarch(), Q_buildnum( ));
}
static void Sys_PrintUsage( const char *exename )
{
string version_str;
const char *usage_str;
Sys_MakeVersionString( version_str, sizeof( version_str ));
#if XASH_MESSAGEBOX != MSGBOX_STDERR
#if XASH_WIN32
#define XASH_EXE "(xash).exe"
#else
#define XASH_EXE "(xash)"
#endif
#else
#define XASH_EXE "%s"
#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 [file name] ", "write log to \"engine.log\" or [file name] if specified")
O("-logtime ", "enable writing timestamps to the log file")
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 [opts] ", "enable precise bug compatibility")
O(" ", "will break games that don't require it")
O(" ", "refer to engine documentation for more info")
O("-language <lang> ", "mount localization game directory")
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")
#if !XASH_DEDICATED
O("-clientport <port> ", "set IPv4 client port")
#endif
O("-noip6 ", "disable IPv6")
O("-ip6 <ip> ", "set IPv6 address")
O("-port6 <port> ", "set IPv6 port")
#if !XASH_DEDICATED
O("-clientport6 <port>", "set IPv6 client port")
#endif
O("-clockwindow <cw> ", "adjust clockwindow used to ignore client commands")
O(" ", "to prevent speed hacks")
"\nGame options:\n"
O("-game <directory> ", "set game directory to start engine with")
O("-dll <path> ", "override server DLL path")
#if !XASH_DEDICATED
O("-clientlib <path> ", "override client DLL path")
O("-menulib <path> ", "override menu 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_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
#undef XASH_EXE
// HACKHACK: pretty output in dedicated
#if XASH_MESSAGEBOX != MSGBOX_STDERR
Platform_MessageBox( version_str, usage_str, false );
#else
fprintf( stderr, "%s\n", version_str );
fprintf( stderr, usage_str, exename );
#endif
Sys_Quit( NULL );
}
static void Sys_PrintBugcompUsage( const char *exename )
{
string version_str;
char usage_str[4096];
char *p = usage_str;
int i;
Sys_MakeVersionString( version_str, sizeof( version_str ));
p += Q_snprintf( p, sizeof( usage_str ) - ( usage_str - p ), "Known bugcomp flags are:\n" );
for( i = 0; i < ARRAYSIZE( bugcomp_features ); i++ )
p += Q_snprintf( p, sizeof( usage_str ) - ( usage_str - p ), " %s: %s\n", bugcomp_features[i].arg, bugcomp_features[i].msg );
p += Q_snprintf( p, sizeof( usage_str ) - ( usage_str - p ), "\nIt is possible to combine multiple flags with '+' characters.\nExample: -bugcomp flag1+flag2+flag3...\n" );
// HACKHACK: pretty output in dedicated
#if XASH_MESSAGEBOX != MSGBOX_STDERR
Platform_MessageBox( version_str, usage_str, false );
#else
fprintf( stderr, "%s\n", version_str );
fprintf( stderr, usage_str, exename );
#endif
Sys_Quit( NULL );
}
/*
================
Host_PrintEngineFeatures
================
*/
static void Host_PrintFeatures( uint32_t flags, const char *s, const feature_message_t *features, size_t size )
{
size_t i;
for( i = 0; i < size; i++ )
{
if( FBitSet( flags, features[i].mask ))
Con_Printf( "^3%s:^7 %s is enabled\n", s, features[i].msg );
}
}
/*
==============
Host_ValidateEngineFeatures
validate features bits and set host.features
==============
*/
void Host_ValidateEngineFeatures( uint32_t mask, uint32_t features )
{
// don't allow unsupported bits
features &= mask;
// force bits for some games
if( !Q_stricmp( GI->gamefolder, "cstrike" ) || !Q_stricmp( GI->gamefolder, "czero" ))
SetBits( features, ENGINE_STEP_POSHISTORY_LERP );
// print requested first
Host_PrintFeatures( features, "EXT", engine_features, ARRAYSIZE( engine_features ));
// now warn about incompatible bits
if( FBitSet( features, ENGINE_STEP_POSHISTORY_LERP|ENGINE_COMPUTE_STUDIO_LERP ) == ( ENGINE_STEP_POSHISTORY_LERP|ENGINE_COMPUTE_STUDIO_LERP ))
Con_Printf( S_WARN "%s: incompatible ENGINE_STEP_POSHISTORY_LERP and ENGINE_COMPUTE_STUDIO_LERP are enabled!\n", __func__ );
// finally set global variable
host.features = features;
}
/*
==============
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_CalcSleep
==================
*/
static int Host_CalcSleep( void )
{
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;
}
static void Host_NewInstance( const char *name, const char *finalmsg )
{
if( !pChangeGame ) return;
host.change_game = true;
if( !Sys_NewInstance( name, finalmsg ))
pChangeGame( name ); // call from hl.exe
}
/*
=================
Host_ChangeGame_f
Change game modification
=================
*/
static 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
===============
*/
static void Host_Exec_f( void )
{
string cfgpath;
byte *f;
fs_offset_t len;
if( Cmd_Argc() != 2 )
{
Con_Printf( S_USAGE "exec <filename>\n" );
return;
}
Q_strncpy( cfgpath, Cmd_Argv( 1 ), sizeof( cfgpath ));
COM_DefaultExtension( cfgpath, ".cfg", sizeof( cfgpath )); // append as default
#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( cfgpath, unprivilegedWhitelist[i] ))
{
allow = true;
break;
}
}
if( !allow )
{
Con_Printf( "exec %s: not privileged or in whitelist\n", cfgpath );
return;
}
}
#endif // XASH_DEDICATED
// don't execute game.cfg in singleplayer
if( SV_GetMaxClients() == 1 && !Q_stricmp( "game.cfg", cfgpath ))
return;
f = FS_LoadFile( cfgpath, &len, false );
if( !f )
{
Con_Reportf( "couldn't exec %s\n", Cmd_Argv( 1 ));
return;
}
// len is fs_offset_t, which can be larger than size_t
if( len >= SIZE_MAX )
{
Con_Reportf( "%s: %s is too long\n", __func__, Cmd_Argv( 1 ));
return;
}
if( !Q_stricmp( "config.cfg", cfgpath ))
host.config_executed = true;
if( !host.apply_game_config )
Con_Printf( "execing %s\n", Cmd_Argv( 1 ));
// adds \n at end of the file
// FS_LoadFile always null terminates
if( f[len - 1] != '\n' )
{
Cbuf_InsertTextLen( f, len, len + 1 );
Cbuf_InsertTextLen( "\n", 1, 1 );
}
else Cbuf_InsertTextLen( f, len, len );
Mem_Free( f );
}
/*
===============
Host_MemStats_f
===============
*/
static 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;
}
}
/*
=================
Host_RegisterDecal
=================
*/
static 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
=================
*/
static 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( "%s: %i decals\n", __func__, num_decals );
}
/*
===================
Host_GetCommands
Add them exactly as if they had been typed at the console
===================
*/
static void Host_GetCommands( void )
{
char *cmd;
while( ( cmd = Platform_Input() ) )
{
Cbuf_AddText( cmd );
Cbuf_Execute();
}
}
/*
===================
Host_CalcFPS
compute actual FPS for various modes
===================
*/
static 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( !SV_Active() && CL_Protocol() == PROTO_GOLDSRC && cls.state != ca_disconnected && cls.state < ca_validate )
{
return 31.0;
}
else
{
if( !gl_vsync.value )
{
double max_fps = fps_override.value ? MAX_FPS_HARD : MAX_FPS_SOFT;
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_HARD );
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.000001;
if( dt < targetframetime * scale )
{
// if we have allocated time window, try to sleep
if( timewindow > realsleeptime )
{
// Platform_Sleep isn't guaranteed to sleep an exact amount of microseconds
// so we measure the real sleep time and use it to decrease the window
double t1 = Sys_DoubleTime(), t2;
Platform_NanoSleep( sleep * 1000 ); // in usec!
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
===================
*/
static qboolean Host_FilterTime( double 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( double time )
{
double t1;
// 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
host.framecount++;
host.pureframetime = Sys_DoubleTime() - t1;
}
/*
=================
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( "%sInit: %s", __func__, hosterror1 );
}
else if( host.framecount == host.errorframe )
{
Sys_Error( "%sMulti: %s", __func__, hosterror2 );
}
else
{
Con_Printf( "%s: %s", __func__, hosterror1 );
if( host.allow_console )
{
UI_SetActiveMenu( false );
Key_SetKeyDest( key_console );
}
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( "%sRecursive: %s", __func__, hosterror2 );
Sys_Error( "%s", hosterror1 );
}
recursive = true;
Q_strncpy( hosterror2, hosterror1, sizeof( hosterror2 ));
host.errorframe = host.framecount; // to avoid multply calls per frame
// clearing cmd buffer to prevent execute any commands
COM_InitHostState();
Cbuf_Clear();
SV_Shutdown( "Server was killed due to an error\n" );
CL_Drop(); // drop clients
// recreate world if needs
CL_ClearEdicts ();
// release all models
Mod_FreeAll();
recursive = false;
Host_AbortCurrentFrame();
}
static void Host_Error_f( void )
{
const char *error = Cmd_Argv( 1 );
if( !*error ) error = "Invoked host error";
Host_Error( "%s\n", error );
}
static 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
=================
*/
static 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 );
error_on_exit = tests_stats.failed > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
Sys_Quit( NULL );
}
}
#endif
static int Host_CheckBugcomp_splitstr_handler( char *prev, char *next, void *userdata )
{
size_t i;
uint32_t *flags = userdata;
*next = '\0';
if( !COM_CheckStringEmpty( prev ))
return 0;
for( i = 0; i < ARRAYSIZE( bugcomp_features ); i++ )
{
if( !Q_stricmp( bugcomp_features[i].arg, prev ))
{
SetBits( *flags, bugcomp_features[i].mask );
break;
}
}
if( i == ARRAYSIZE( bugcomp_features ))
{
Con_Printf( S_ERROR "Unknown bugcomp flag %s\n", prev );
Con_Printf( "Valid flags are:\n" );
for( i = 0; i < ARRAYSIZE( bugcomp_features ); i++ )
Con_Printf( "\t%s: %s\n", bugcomp_features[i].arg, bugcomp_features[i].msg );
}
return 0;
}
static uint32_t Host_CheckBugcomp( void )
{
uint32_t flags = 0;
string args;
if( !Sys_CheckParm( "-bugcomp" ))
return 0;
if( Sys_GetParmFromCmdLine( "-bugcomp", args ) && isalpha( args[0] ))
{
Q_splitstr( args, '+', &flags, Host_CheckBugcomp_splitstr_handler );
}
else
{
// no argument specified -bugcomp just enables everything
flags = -1;
}
Host_PrintFeatures( flags, "BUGCOMP", bugcomp_features, ARRAYSIZE( bugcomp_features ));
return flags;
}
static void Host_DetermineExecutableName( char *out, size_t size )
{
#if XASH_WIN32
char temp[MAX_SYSPATH];
if( GetModuleFileName( NULL, temp, sizeof( temp )))
COM_FileBase( temp, out, size );
#else
if( host.argc > 0 )
COM_FileBase( host.argv[0], out, size );
else
Q_strncpy( out, "xash", size );
#endif
}
/*
=================
Host_InitCommon
=================
*/
static void Host_InitCommon( int argc, char **argv, const char *progname, qboolean bChangeGame, char *exename, size_t exename_size )
{
const char *basedir = progname[0] == '#' ? progname + 1 : progname;
char dev_level[4], ticrate[16];
int developer = DEFAULT_DEV;
// 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 );
Host_DetermineExecutableName( exename, exename_size );
if( !Sys_CheckParm( "-disablehelp" ))
{
string arg;
if( Sys_CheckParm( "-help" ) || Sys_CheckParm( "-h" ) || Sys_CheckParm( "--help" ))
Sys_PrintUsage( exename );
if( Sys_GetParmFromCmdLine( "-bugcomp", arg ) && !Q_stricmp( arg, "help" ))
Sys_PrintBugcompUsage( exename );
}
if( !Sys_CheckParm( "-noch" ))
Sys_SetupCrashHandler( argv[0] );
#if XASH_DLL_LOADER
host.enabledll = !Sys_CheckParm( "-nodll" );
#endif
host.change_game = bChangeGame || Sys_CheckParm( "-changegame" );
host.config_executed = false;
host.status = HOST_INIT; // initialzation started
host.type = HOST_DEDICATED; // predict state
#ifndef XASH_DEDICATED
if( !Sys_CheckParm( "-dedicated" ))
host.type = HOST_NORMAL;
#endif
Memory_Init(); // init memory subsystem
host.mempool = Mem_AllocPool( "Zone Engine" );
host.allow_console = DEFAULT_ALLOWCONSOLE || DEFAULT_DEV > 0;
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
// always enable console for Quake and dedicated
if( !host.allow_console && ( Host_IsDedicated() || Sys_CheckParm( "-console" ) || !Q_strnicmp( exename, "quake", 5 )))
host.allow_console = true;
// member console allowing
host.allow_console_init = host.allow_console;
// 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_HARD );
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
#if XASH_DEDICATED
Platform_SetupSigtermHandling();
#endif
Platform_Init( Host_IsDedicated( ) || developer >= DEV_EXTENDED, basedir );
FS_Init( basedir );
Sys_InitLog();
// print current developer level to simplify processing users feedback
if( developer > 0 )
{
int i;
Con_Printf( "Program args: " S_YELLOW );
for( i = 0; i < host.argc; i++ )
Con_Printf( "%s ", host.argv[i] );
Con_Printf( S_DEFAULT "\n" );
Con_Printf( "Developer level: " S_YELLOW "%i" S_DEFAULT "\n", developer );
}
host.bugcomp = Host_CheckBugcomp();
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" );
#if !XASH_DEDICATED
Cmd_AddRestrictedCommand( "host_writeconfig", Host_WriteConfig, "save current configuration" );
#endif
Image_Init();
Sound_Init();
#if XASH_ENGINE_TESTS
if( Sys_CheckParm( "-runtests" ))
Host_RunTests( 1 );
#endif
FS_LoadGameInfo();
Cvar_PostFSInit();
Image_CheckPaletteQ1 ();
// NOTE: only once resource without which engine can't continue work
if( !FS_FileExists( "gfx/conchars", false ))
Sys_Error( "%s: couldn't load gfx.wad\n", __func__ );
Host_InitDecals (); // reload decals
HPAK_Init();
IN_Init();
Key_Init();
}
static void Host_FreeCommon( void )
{
Image_Shutdown();
Sound_Shutdown();
Netchan_Shutdown();
HPAK_FlushHostQueue();
FS_Shutdown();
}
static void Sys_Quit_f( void )
{
Sys_Quit( "command" );
}
static void Host_MainLoop( void *userdata )
{
double *poldtime = (double *)userdata;
double newtime = Sys_DoubleTime();
COM_Frame( newtime - *poldtime );
*poldtime = newtime;
}
/*
=================
Host_Main
=================
*/
int EXPORT Host_Main( int argc, char **argv, const char *progname, int bChangeGame, pfnChangeGame func )
{
static double oldtime;
string demoname, exename;
host.starttime = Sys_DoubleTime();
pChangeGame = func; // may be NULL
Host_InitCommon( argc, argv, progname, bChangeGame, exename, sizeof( exename ));
// 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_allow_materials );
Cvar_RegisterVariable( &host_serverstate );
Cvar_RegisterVariable( &host_maxfps );
Cvar_RegisterVariable( &fps_override );
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(), g_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 );
Cvar_Get( "host_hl25_extended_structs",
#if SUPPORT_HL25_EXTENDED_STRUCTS
"1",
#else
"0",
#endif
FCVAR_READ_ONLY, "indicates if engine was compiled with extended msurface_t struct" );
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();
SoundList_Init();
if( Host_IsDedicated( ))
{
#ifdef _WIN32
Wcon_InitConsoleCommands ();
#endif
// disable texture replacements for dedicated
Cvar_FullSet( "host_allow_materials", "0", FCVAR_READ_ONLY );
Cmd_AddRestrictedCommand( "quit", Sys_Quit_f, "quit the game" );
Cmd_AddRestrictedCommand( "exit", Sys_Quit_f, "quit the game" );
}
else Cmd_AddRestrictedCommand( "minimize", Platform_Minimize_f, "minimize main window to tray" );
host.errorframe = 0;
if( progname[0] == '#' )
progname++;
// post initializations
switch( host.type )
{
case HOST_NORMAL:
#ifdef _WIN32
Wcon_ShowConsole( false ); // hide console
#endif
// execute startup config and cmdline
if( FS_FileExists( va( "%s.rc", progname ), false )) // e.g. valve.rc
Cbuf_AddTextf( "exec %s.rc", progname );
else if( FS_FileExists( va( "%s.rc", exename ), false )) // e.g. quake.rc
Cbuf_AddTextf( "exec %s.rc", exename );
else if( FS_FileExists( va( "%s.rc", GI->gamefolder ), false )) // e.g. game.rc (ran from default launcher)
Cbuf_AddTextf( "exec %s.rc", GI->gamefolder );
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 )
#if XASH_WIN32 // FIXME: implement autocomplete on *nix
Con_Printf( "Type 'map <mapname>' to start game... (TAB-autocomplete is working too)\n" );
#else // !XASH_WIN32
Con_Printf( "Type 'map <mapname>' to start game...\n" );
#endif // !XASH_WIN32
// execute server.cfg after commandline
// so we have a chance to set servercfgfile
Cbuf_AddTextf( "exec %s\n", Cvar_VariableString( "servercfgfile" ));
Cbuf_Execute();
}
// check after all configs were executed
HPAK_CheckIntegrity( hpk_custom_file.string );
#if XASH_ANDROID
if( setjmp( return_from_main_buf ))
return error_on_exit;
#endif // XASH_ANDROID
#if !XASH_EMSCRIPTEN
// main window message loop
while( host.status != HOST_CRASHED )
Host_MainLoop( &oldtime );
#else // XASH_EMSCRIPTEN
emscripten_set_main_loop_arg( Host_MainLoop, &oldtime, 0, false );
#endif // XASH_EMSCRIPTEN
return 0;
}
void EXPORT Host_Shutdown( void );
void EXPORT Host_Shutdown( void )
{
Host_ShutdownWithReason( "launcher shutdown" );
}
/*
=================
Host_Shutdown
=================
*/
void Host_ShutdownWithReason( const char *reason )
{
qboolean error = host.status == HOST_ERR_FATAL;
if( host.shutdown_issued )
return;
host.shutdown_issued = true;
if( reason != NULL )
Con_Printf( S_NOTE "Issuing host shutdown due to reason \"%s\"\n", reason );
if( host.status != HOST_ERR_FATAL )
host.status = HOST_SHUTDOWN; // prepare host to normal shutdown
#if !XASH_DEDICATED
if( host.type == HOST_NORMAL && !error )
Host_WriteConfig();
#endif
SV_Shutdown( "Server shutdown\n" );
SV_UnloadProgs();
SV_ShutdownFilter();
CL_Shutdown();
SoundList_Shutdown();
Mod_Shutdown();
NET_Shutdown();
HTTP_Shutdown();
Host_FreeCommon();
Platform_Shutdown();
BaseCmd_Shutdown();
Cmd_Shutdown();
Cvar_Shutdown();
// must be last, console uses this
Mem_FreePool( &host.mempool );
// restore filter
Sys_RestoreCrashHandler();
Sys_CloseLog( reason );
}