diff options
Diffstat (limited to 'src/common.c')
-rw-r--r-- | src/common.c | 1962 |
1 files changed, 1962 insertions, 0 deletions
diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..0bbd30e --- /dev/null +++ b/src/common.c @@ -0,0 +1,1962 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 2 +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. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ +// common.c -- misc functions used in client and server +#include "com_local.h" +#include "files.h" +#include "protocol.h" +#include "q_msg.h" +#include "q_fifo.h" +#include "net_sock.h" +#include "net_chan.h" +#include "sys_public.h" +#include "cl_public.h" +#include "sv_public.h" +#include "q_list.h" +#include "bsp.h" +#include "cmodel.h" +#include "q_field.h" +#include "prompt.h" +#include "io_sleep.h" +#include <setjmp.h> +#if USE_ZLIB +#include <zlib.h> +#endif + +static jmp_buf abortframe; // an ERR_DROP occured, exit the entire frame + +static char com_errorMsg[MAXPRINTMSG]; + +static char **com_argv; +static int com_argc; + +#ifdef _DEBUG +cvar_t *developer; +#endif +cvar_t *timescale; +cvar_t *fixedtime; +cvar_t *dedicated; +cvar_t *com_version; + +cvar_t *logfile_enable; // 1 = create new, 2 = append to existing +cvar_t *logfile_flush; // 1 = flush after each print +cvar_t *logfile_name; +cvar_t *logfile_prefix; + +#if USE_CLIENT +cvar_t *cl_running; +cvar_t *cl_paused; +#endif +cvar_t *sv_running; +cvar_t *sv_paused; +cvar_t *com_timedemo; +cvar_t *com_date_format; +cvar_t *com_time_format; +#ifdef _DEBUG +cvar_t *com_debug_break; +#endif +cvar_t *com_fatal_error; + +cvar_t *allow_download; +cvar_t *allow_download_players; +cvar_t *allow_download_models; +cvar_t *allow_download_sounds; +cvar_t *allow_download_maps; +cvar_t *allow_download_textures; +cvar_t *allow_download_pics; +cvar_t *allow_download_others; + +cvar_t *rcon_password; + +qhandle_t com_logFile; +qboolean com_logNewline; +unsigned com_framenum; +unsigned com_eventTime; +unsigned com_localTime; +qboolean com_initialized; +time_t com_startTime; + +#if USE_CLIENT +cvar_t *host_speeds; + +// host_speeds times +unsigned time_before_game; +unsigned time_after_game; +unsigned time_before_ref; +unsigned time_after_ref; +#endif + +/* +============================================================================ + +CLIENT / SERVER interactions + +============================================================================ +*/ + +static int rd_target; +static char *rd_buffer; +static size_t rd_buffersize; +static size_t rd_length; +static rdflush_t rd_flush; + +void Com_BeginRedirect( int target, char *buffer, size_t buffersize, rdflush_t flush ) { + if( rd_target || !target || !buffer || buffersize < 1 || !flush ) { + return; + } + rd_target = target; + rd_buffer = buffer; + rd_buffersize = buffersize; + rd_flush = flush; + rd_length = 0; +} + +static void Com_AbortRedirect( void ) { + rd_target = 0; + rd_buffer = NULL; + rd_buffersize = 0; + rd_flush = NULL; + rd_length = 0; +} + +void Com_EndRedirect( void ) { + if( !rd_target ) { + return; + } + rd_flush( rd_target, rd_buffer, rd_length ); + rd_target = 0; + rd_buffer = NULL; + rd_buffersize = 0; + rd_flush = NULL; + rd_length = 0; +} + +static void Com_Redirect( const char *msg, size_t total ) { + size_t length; + + while( total ) { + length = total; + if( length > rd_buffersize ) { + length = rd_buffersize; + } + if( rd_length + length > rd_buffersize ) { + rd_flush( rd_target, rd_buffer, rd_length ); + rd_length = 0; + } + memcpy( rd_buffer + rd_length, msg, length ); + rd_length += length; + total -= length; + } +} + +static void logfile_close( void ) { + if( !com_logFile ) { + return; + } + + Com_Printf( "Closing console log.\n" ); + + FS_FCloseFile( com_logFile ); + com_logFile = 0; +} + +static void logfile_open( void ) { + char buffer[MAX_OSPATH]; + unsigned mode; + qhandle_t f; + + mode = logfile_enable->integer > 1 ? FS_MODE_APPEND : FS_MODE_WRITE; + if( logfile_flush->integer ) { + mode |= FS_FLUSH_SYNC; + } + + f = FS_EasyOpenFile( buffer, sizeof( buffer ), mode, + "logs/", logfile_name->string, ".log" ); + if( !f ) { + Cvar_Set( "logfile", "0" ); + return; + } + + com_logFile = f; + com_logNewline = qtrue; + Com_Printf( "Logging console to %s\n", buffer ); +} + +static void logfile_enable_changed( cvar_t *self ) { + logfile_close(); + if( self->integer ) { + logfile_open(); + } +} + +static void logfile_param_changed( cvar_t *self ) { + if( logfile_enable->integer ) { + logfile_close(); + logfile_open(); + } +} + +static size_t format_local_time( char *buffer, size_t size, const char *fmt ) { + time_t t; + struct tm *tm; + + if( !size ) { + return 0; + } + + buffer[0] = 0; + + t = time( NULL ); + tm = localtime( &t ); + if( !tm ) { + return 0; + } + + return strftime( buffer, size, fmt, tm ); +} + +static void logfile_write( print_type_t type, const char *string ) { + char text[MAXPRINTMSG]; + char buf[MAX_QPATH]; + char *p, *maxp; + size_t len; + int c; + + if( logfile_prefix->string[0] ) { + p = strchr( logfile_prefix->string, '@' ); + if( p ) { + // expand it in place, hacky + switch( type ) { + case PRINT_TALK: *p = 'T'; break; + case PRINT_DEVELOPER: *p = 'D'; break; + case PRINT_WARNING: *p = 'W'; break; + case PRINT_ERROR: *p = 'E'; break; + default: *p = 'A'; break; + } + } + len = format_local_time( buf, sizeof( buf ), logfile_prefix->string ); + if( p ) { + *p = '@'; + } + } else { + len = 0; + } + + p = text; + maxp = text + sizeof( text ) - 1; + while( *string ) { + if( com_logNewline ) { + if( len > 0 && p + len < maxp ) { + memcpy( p, buf, len ); + p += len; + } + com_logNewline = qfalse; + } + + if( p == maxp ) { + break; + } + + c = *string++; + if( c == '\n' ) { + com_logNewline = qtrue; + } else { + c = Q_charascii( c ); + } + + *p++ = c; + } + *p = 0; + + len = p - text; + FS_Write( text, len, com_logFile ); +} + +#ifdef __unix__ +/* +============= +Com_FlushLogs + +When called from SIGHUP handler on UNIX-like systems, +will close and reopen logfile handle for rotation. +============= +*/ +void Com_FlushLogs( void ) { + if( logfile_enable ) { + logfile_enable_changed( logfile_enable ); + } +} +#endif + +void Com_SetColor( color_index_t color ) { + if( rd_target ) { + return; + } +#if USE_CLIENT + // graphical console + Con_SetColor( color ); +#endif +#if USE_SYSCON + // debugging console + Sys_SetConsoleColor( color ); +#endif +} + +/* +============= +Com_Printf + +Both client and server can use this, and it will output +to the apropriate place. +============= +*/ +void Com_LPrintf( print_type_t type, const char *fmt, ... ) { + va_list argptr; + char msg[MAXPRINTMSG]; + static int recursive; + size_t len; + + if( recursive == 2 ) { + return; + } + + recursive++; + + va_start( argptr, fmt ); + len = Q_vscnprintf( msg, sizeof( msg ), fmt, argptr ); + va_end( argptr ); + + if( rd_target ) { + Com_Redirect( msg, len ); + } else { + switch( type ) { + case PRINT_TALK: + Com_SetColor( COLOR_ALT ); + break; + case PRINT_DEVELOPER: + Com_SetColor( COLOR_BLUE ); + break; + case PRINT_WARNING: + Com_SetColor( COLOR_YELLOW ); + break; + case PRINT_ERROR: + Com_SetColor( COLOR_RED ); + break; + default: + break; + } + +#if USE_CLIENT + // graphical console + Con_Print( msg ); +#endif + +#if USE_SYSCON + // debugging console + Sys_ConsoleOutput( msg ); +#endif + + // remote console + //SV_ConsoleOutput( msg ); + + // logfile + if( com_logFile ) { + logfile_write( type, msg ); + } + + if( type ) { + Com_SetColor( COLOR_NONE ); + } + } + + recursive--; +} + + +/* +============= +Com_Error + +Both client and server can use this, and it will +do the apropriate things. +============= +*/ +void Com_Error( error_type_t code, const char *fmt, ... ) { + va_list argptr; + static qboolean recursive; + + if( recursive ) { +#ifdef _DEBUG + Sys_DebugBreak(); +#endif + Sys_Error( "recursive error after: %s", com_errorMsg ); + } + recursive = qtrue; + + va_start( argptr, fmt ); + Q_vsnprintf( com_errorMsg, sizeof( com_errorMsg ), fmt, argptr ); + va_end( argptr ); + + // fix up drity message buffers + MSG_Init(); + + // abort any console redirects + Com_AbortRedirect(); + + if( code == ERR_DISCONNECT || code == ERR_SILENT ) { + Com_WPrintf( "%s\n", com_errorMsg ); + SV_Shutdown( va( "Server was killed: %s", com_errorMsg ), + KILL_DISCONNECT ); +#if USE_CLIENT + CL_Disconnect( code, com_errorMsg ); +#endif + goto abort; + } + +#ifdef _DEBUG + if( com_debug_break && com_debug_break->integer ) { + Sys_DebugBreak(); + } +#endif + + // make otherwise non-fatal errors fatal + if( com_fatal_error && com_fatal_error->integer ) { + code = ERR_FATAL; + } + + if( code == ERR_DROP ) { + Com_EPrintf( "********************\n" + "ERROR: %s\n" + "********************\n", com_errorMsg ); + SV_Shutdown( va( "Server crashed: %s\n", com_errorMsg ), KILL_DROP ); +#if USE_CLIENT + CL_Disconnect( ERR_DROP, com_errorMsg ); +#endif + goto abort; + } + + if( com_logFile ) { + FS_FPrintf( com_logFile, "FATAL: %s\n", com_errorMsg ); + } + + SV_Shutdown( va( "Server fatal crashed: %s\n", com_errorMsg ), KILL_DROP ); +#if USE_CLIENT + CL_Shutdown(); +#endif + Qcommon_Shutdown(); + + Sys_Error( "%s", com_errorMsg ); + // doesn't get there + +abort: + if( com_logFile ) { + FS_Flush( com_logFile ); + } + recursive = qfalse; + longjmp( abortframe, -1 ); +} + +#ifdef _WIN32 +void Com_AbortFrame( void ) { + longjmp( abortframe, -1 ); +} +#endif + +/* +============= +Com_Quit + +Both client and server can use this, and it will +do the apropriate things. This function never returns. +============= +*/ +void Com_Quit( const char *reason, killtype_t type ) { + char buffer[MAX_STRING_CHARS]; + char *what = type == KILL_RESTART ? "restarted" : "quit"; + + if( reason && *reason ) { + Q_snprintf( buffer, sizeof( buffer ), + "Server %s: %s\n", what, reason ); + } else { + Q_snprintf( buffer, sizeof( buffer ), + "Server %s\n", what ); + } + SV_Shutdown( buffer, type ); + +#if USE_CLIENT + CL_Shutdown(); +#endif + Qcommon_Shutdown(); + + Sys_Quit(); +} + +static void Com_Quit_f( void ) { + Com_Quit( Cmd_Args(), KILL_DROP ); +} + +#if !USE_CLIENT +static void Com_Recycle_f( void ) { + Com_Quit( Cmd_Args(), KILL_RESTART ); +} +#endif + + +/* +============================================================================== + + ZONE MEMORY ALLOCATION + +just cleared malloc with counters now... + +============================================================================== +*/ + +#define Z_MAGIC 0x1d0d +#define Z_TAIL 0x5b7b + +#define Z_TAIL_F( z ) \ + *( uint16_t * )( ( byte * )(z) + (z)->size - sizeof( uint16_t ) ) + +#define Z_FOR_EACH( z ) \ + for( (z) = z_chain.next; (z) != &z_chain; (z) = (z)->next ) + +#define Z_FOR_EACH_SAFE( z, n ) \ + for( (z) = z_chain.next; (z) != &z_chain; (z) = (n) ) + +typedef struct zhead_s { + uint16_t magic; + uint16_t tag; // for group free + size_t size; +#ifdef _DEBUG + void *addr; +#endif + struct zhead_s *prev, *next; +} zhead_t; + +// number of overhead bytes +#define Z_EXTRA ( sizeof( zhead_t ) + sizeof( uint16_t ) ) + +static zhead_t z_chain; + +static cvar_t *z_perturb; + +typedef struct { + zhead_t z; + char data[2]; + uint16_t tail; +} zstatic_t; + +static const zstatic_t z_static[] = { +#define Z_STATIC( x ) \ + { { Z_MAGIC, TAG_STATIC, q_offsetof( zstatic_t, tail ) + \ + sizeof( uint16_t ) }, x, Z_TAIL } + + Z_STATIC( "0" ), + Z_STATIC( "1" ), + Z_STATIC( "2" ), + Z_STATIC( "3" ), + Z_STATIC( "4" ), + Z_STATIC( "5" ), + Z_STATIC( "6" ), + Z_STATIC( "7" ), + Z_STATIC( "8" ), + Z_STATIC( "9" ), + Z_STATIC( "" ) + +#undef Z_STATIC +}; + +typedef struct { + size_t count; + size_t bytes; +} zstats_t; + +static zstats_t z_stats[TAG_MAX]; + +static const char z_tagnames[TAG_MAX][8] = { + "game", + "static", + "generic", + "cmd", + "cvar", + "fs", + "refresh", + "ui", + "server", + "mvd", + "sound", + "cmodel" +}; + +static inline void Z_Validate( zhead_t *z, const char *func ) { + if( z->magic != Z_MAGIC ) { + Com_Error( ERR_FATAL, "%s: bad magic", func ); + } + if( Z_TAIL_F( z ) != Z_TAIL ) { + Com_Error( ERR_FATAL, "%s: bad tail", func ); + } + if( z->tag == TAG_FREE ) { + Com_Error( ERR_FATAL, "%s: bad tag", func ); + } +} + +void Z_Check( void ) { + zhead_t *z; + + Z_FOR_EACH( z ) { + Z_Validate( z, __func__ ); + } +} + +void Z_LeakTest( memtag_t tag ) { + zhead_t *z; + size_t numLeaks = 0, numBytes = 0; + + Z_FOR_EACH( z ) { + Z_Validate( z, __func__ ); + if( z->tag == tag ) { + numLeaks++; + numBytes += z->size; + } + } + + if( numLeaks ) { + Com_WPrintf( "************* Z_LeakTest *************\n" + "%s leaked %"PRIz" bytes of memory (%"PRIz" object%s)\n" + "**************************************\n", + z_tagnames[tag < TAG_MAX ? tag : TAG_FREE], + numBytes, numLeaks, numLeaks == 1 ? "" : "s" ); + } +} + +/* +======================== +Z_Free +======================== +*/ +void Z_Free( void *ptr ) { + zhead_t *z; + zstats_t *s; + + if( !ptr ) { + return; + } + + z = ( zhead_t * )ptr - 1; + + Z_Validate( z, __func__ ); + + s = &z_stats[z->tag < TAG_MAX ? z->tag : TAG_FREE]; + s->count--; + s->bytes -= z->size; + + if( z->tag != TAG_STATIC ) { + z->prev->next = z->next; + z->next->prev = z->prev; + free( z ); + } +} + +#if 0 +/* +======================== +Z_Realloc +======================== +*/ +void *Z_Realloc( void *ptr, size_t size ) { + zhead_t *z; + zstats_t *s; + + if( !ptr ) { + return Z_Malloc( size ); + } + + if( !size ) { + Z_Free( ptr ); + return NULL; + } + + z = ( zhead_t * )ptr - 1; + + Z_Validate( z, __func__ ); + + if( z->tag == TAG_STATIC ) { + Com_Error( ERR_FATAL, "%s: couldn't realloc static memory", __func__ ); + } + + s = &z_stats[z->tag < TAG_MAX ? z->tag : TAG_FREE]; + s->bytes -= z->size; + + if( size > SIZE_MAX - Z_EXTRA - 3 ) { + Com_Error( ERR_FATAL, "%s: bad size", __func__ ); + } + + size = ( size + Z_EXTRA + 3 ) & ~3; + z = realloc( z, size ); + if( !z ) { + Com_Error( ERR_FATAL, "%s: couldn't realloc %"PRIz" bytes", __func__, size ); + } + + z->size = size; + z->prev->next = z; + z->next->prev = z; + + s->bytes += size; + + Z_TAIL_F( z ) = Z_TAIL; + + return z + 1; +} +#endif + +/* +======================== +Z_Stats_f +======================== +*/ +void Z_Stats_f( void ) { + size_t bytes = 0, count = 0; + zstats_t *s; + int i; + + Com_Printf( " bytes blocks name\n" + "--------- ------ -------\n" ); + + for( i = 0, s = z_stats; i < TAG_MAX; i++, s++ ) { + if( !s->count ) { + continue; + } + Com_Printf( "%9"PRIz" %6"PRIz" %s\n", s->bytes, s->count, z_tagnames[i] ); + bytes += s->bytes; + count += s->count; + } + + Com_Printf( "--------- ------ -------\n" + "%9"PRIz" %6"PRIz" total\n", + bytes, count ); +} + +/* +======================== +Z_FreeTags +======================== +*/ +void Z_FreeTags( memtag_t tag ) { + zhead_t *z, *n; + + Z_FOR_EACH_SAFE( z, n ) { + Z_Validate( z, __func__ ); + n = z->next; + if( z->tag == tag ) { + Z_Free( z + 1 ); + } + } +} + +/* +======================== +Z_TagMalloc +======================== +*/ +void *Z_TagMalloc( size_t size, memtag_t tag ) { + zhead_t *z; + zstats_t *s; + + if( !size ) { + return NULL; + } + + if( tag == TAG_FREE ) { + Com_Error( ERR_FATAL, "%s: bad tag", __func__ ); + } + + if( size > SIZE_MAX - Z_EXTRA - 3 ) { + Com_Error( ERR_FATAL, "%s: bad size", __func__ ); + } + + size = ( size + Z_EXTRA + 3 ) & ~3; + z = malloc( size ); + if( !z ) { + Com_Error( ERR_FATAL, "%s: couldn't allocate %"PRIz" bytes", __func__, size ); + } + z->magic = Z_MAGIC; + z->tag = tag; + z->size = size; + +#ifdef _DEBUG +#if( defined __GNUC__ ) + z->addr = __builtin_return_address( 0 ); +#elif( defined _MSC_VER ) + z->addr = _ReturnAddress(); +#else + z->addr = NULL; +#endif +#endif + + z->next = z_chain.next; + z->prev = &z_chain; + z_chain.next->prev = z; + z_chain.next = z; + + if( z_perturb && z_perturb->integer ) { + memset( z + 1, z_perturb->integer, size - + sizeof( zhead_t ) - sizeof( uint16_t ) ); + } + + Z_TAIL_F( z ) = Z_TAIL; + + s = &z_stats[tag < TAG_MAX ? tag : TAG_FREE]; + s->count++; + s->bytes += size; + + return z + 1; +} + +void *Z_TagMallocz( size_t size, memtag_t tag ) { + if( !size ) { + return NULL; + } + return memset( Z_TagMalloc( size, tag ), 0, size ); +} + +static byte *z_reserved_data; +static size_t z_reserved_inuse; +static size_t z_reserved_total; + +void Z_TagReserve( size_t size, memtag_t tag ) { + z_reserved_data = Z_TagMalloc( size, tag ); + z_reserved_total = size; + z_reserved_inuse = 0; +} + +void *Z_ReservedAlloc( size_t size ) { + void *ptr; + + if( !size ) { + return NULL; + } + + if( size > z_reserved_total - z_reserved_inuse ) { + Com_Error( ERR_FATAL, "%s: couldn't allocate %"PRIz" bytes", __func__, size ); + } + + ptr = z_reserved_data + z_reserved_inuse; + z_reserved_inuse += size; + + return ptr; +} + +void *Z_ReservedAllocz( size_t size ) { + if( !size ) { + return NULL; + } + return memset( Z_ReservedAlloc( size ), 0, size ); +} + +char *Z_ReservedCopyString( const char *in ) { + size_t len; + + if( !in ) { + return NULL; + } + + len = strlen( in ) + 1; + return memcpy( Z_ReservedAlloc( len ), in, len ); +} + +/* +======================== +Z_Init +======================== +*/ +static void Z_Init( void ) { + z_chain.next = z_chain.prev = &z_chain; +} + +/* +================ +Z_TagCopyString +================ +*/ +char *Z_TagCopyString( const char *in, memtag_t tag ) { + size_t len; + + if( !in ) { + return NULL; + } + + len = strlen( in ) + 1; + return memcpy( Z_TagMalloc( len, tag ), in, len ); +} + +/* +================ +Cvar_CopyString +================ +*/ +char *Cvar_CopyString( const char *in ) { + size_t len; + zstatic_t *z; + zstats_t *s; + int i; + + if( !in ) { + return NULL; + } + + if( !in[0] ) { + i = 10; + } else if( !in[1] && Q_isdigit( in[0] ) ) { + i = in[0] - '0'; + } else { + len = strlen( in ) + 1; + return memcpy( Z_TagMalloc( len, TAG_CVAR ), in, len ); + } + + // return static storage + z = ( zstatic_t * )&z_static[i]; + s = &z_stats[TAG_STATIC]; + s->count++; + s->bytes += z->z.size; + return z->data; +} + +/* +============================================================================== + + FIFO + +============================================================================== +*/ + +size_t FIFO_Read( fifo_t *fifo, void *buffer, size_t len ) { + size_t wrapped, head = fifo->ay - fifo->ax; + + if( head > len ) { + if( buffer ) { + memcpy( buffer, fifo->data + fifo->ax, len ); + fifo->ax += len; + } + return len; + } + + wrapped = len - head; + if( wrapped > fifo->bs ) { + wrapped = fifo->bs; + } + if( buffer ) { + memcpy( buffer, fifo->data + fifo->ax, head ); + memcpy( ( byte * )buffer + head, fifo->data, wrapped ); + fifo->ax = wrapped; + fifo->ay = fifo->bs; + fifo->bs = 0; + } + + return head + wrapped; +} + +size_t FIFO_Write( fifo_t *fifo, const void *buffer, size_t len ) { + size_t tail, wrapped, remaining; + + if( fifo->bs ) { + remaining = fifo->ax - fifo->bs; + if( len > remaining ) { + len = remaining; + } + if( buffer ) { + memcpy( fifo->data + fifo->bs, buffer, len ); + fifo->bs += len; + } + return len; + } + + tail = fifo->size - fifo->ay; + if( tail > len ) { + if( buffer ) { + memcpy( fifo->data + fifo->ay, buffer, len ); + fifo->ay += len; + } + return len; + } + + wrapped = len - tail; + if( wrapped > fifo->ax ) { + wrapped = fifo->ax; + } + if( buffer ) { + memcpy( fifo->data + fifo->ay, buffer, tail ); + memcpy( fifo->data, ( byte * )buffer + tail, wrapped ); + fifo->ay = fifo->size; + fifo->bs = wrapped; + } + + return tail + wrapped; +} + +qboolean FIFO_ReadMessage( fifo_t *fifo, size_t msglen ) { + size_t len; + byte *data; + + data = FIFO_Peek( fifo, &len ); + if( len < msglen ) { + // read in two chunks into message buffer + if( !FIFO_TryRead( fifo, msg_read_buffer, msglen ) ) { + return qfalse; // not yet available + } + SZ_Init( &msg_read, msg_read_buffer, sizeof( msg_read_buffer ) ); + } else { + // read in a single block without copying any memory + SZ_Init( &msg_read, data, msglen ); + FIFO_Decommit( fifo, msglen ); + } + + msg_read.cursize = msglen; + return qtrue; +} + +/* +============================================================================== + + MATH + +============================================================================== +*/ + +const vec3_t bytedirs[NUMVERTEXNORMALS] = { +#include "anorms.h" +}; + +int DirToByte( const vec3_t dir ) { + int i, best; + float d, bestd; + + if( !dir ) { + return 0; + } + + bestd = 0; + best = 0; + for( i = 0; i < NUMVERTEXNORMALS; i++ ) { + d = DotProduct( dir, bytedirs[i] ); + if( d > bestd ) { + bestd = d; + best = i; + } + } + + return best; +} + +void ByteToDir( int index, vec3_t dir ) { + if( index < 0 || index >= NUMVERTEXNORMALS ) { + Com_Error( ERR_FATAL, "ByteToDir: illegal index" ); + } + + VectorCopy( bytedirs[index], dir ); +} + +void SetPlaneType( cplane_t *plane ) { + vec_t *normal = plane->normal; + + if( normal[0] == 1 ) { + plane->type = PLANE_X; + return; + } + if( normal[1] == 1 ) { + plane->type = PLANE_Y; + return; + } + if( normal[2] == 1 ) { + plane->type = PLANE_Z; + return; + } + + plane->type = PLANE_NON_AXIAL; +} + +void SetPlaneSignbits( cplane_t *plane ) { + int bits = 0; + + if( plane->normal[0] < 0 ) { + bits |= 1; + } + if( plane->normal[1] < 0 ) { + bits |= 2; + } + if( plane->normal[2] < 0 ) { + bits |= 4; + } + plane->signbits = bits; +} + +/* +================== +BoxOnPlaneSide + +Returns 1, 2, or 1 + 2 +================== +*/ +#if !USE_ASM +int BoxOnPlaneSide( vec3_t emins, vec3_t emaxs, cplane_t *p ) { + vec_t *bounds[2] = { emins, emaxs }; + int i = p->signbits & 1; + int j = ( p->signbits >> 1 ) & 1; + int k = ( p->signbits >> 2 ) & 1; + +#define P(i,j,k) \ + p->normal[0]*bounds[i][0]+ \ + p->normal[1]*bounds[j][1]+ \ + p->normal[2]*bounds[k][2] + + vec_t dist1 = P( i ^ 1, j ^ 1, k ^ 1 ); + vec_t dist2 = P( i, j, k ); + int sides = 0; + +#undef P + + if (dist1 >= p->dist) + sides = 1; + if (dist2 < p->dist) + sides |= 2; + + return sides; +} +#endif // USE_ASM + + +/* +============================================================================== + + MISC + +============================================================================== +*/ + +#if USE_CLIENT || USE_MVD_CLIENT || USE_MVD_SERVER +const cmd_option_t o_record[] = { + { "h", "help", "display this message" }, + { "z", "gzip", "compress file with gzip" }, + { NULL } +}; +#endif + +const char colorNames[10][8] = { + "black", "red", "green", "yellow", + "blue", "cyan", "magenta", "white", + "alt", "none" +}; + +/* +================ +Com_ParseColor + +Parses color name or index up to the maximum allowed index. +Returns COLOR_NONE in case of error. +================ +*/ +color_index_t Com_ParseColor( const char *s, color_index_t last ) { + color_index_t i; + + if( COM_IsUint( s ) ) { + i = strtoul( s, NULL, 10 ); + return i > last ? COLOR_NONE : i; + } + + for( i = 0; i <= last; i++ ) { + if( !strcmp( colorNames[i], s ) ) { + return i; + } + } + return COLOR_NONE; +} + +/* +================ +Com_PlayerToEntityState + +Restores entity origin and angles from player state +================ +*/ +void Com_PlayerToEntityState( const player_state_t *ps, entity_state_t *es ) { + vec_t pitch; + + VectorScale( ps->pmove.origin, 0.125f, es->origin ); + + pitch = ps->viewangles[PITCH]; + if( pitch > 180 ) { + pitch -= 360; + } + es->angles[PITCH] = pitch / 3; + es->angles[YAW] = ps->viewangles[YAW]; + es->angles[ROLL] = 0; +} + +/* +================= +Com_WildCmp + +Wildcard compare. +Returns non-zero if matches, zero otherwise. +================= +*/ +int Com_WildCmp( const char *filter, const char *string, qboolean ignoreCase ) { + switch( *filter ) { + case '\0': + return !*string; + + case '*': + return Com_WildCmp( filter + 1, string, ignoreCase ) || (*string && Com_WildCmp( filter, string + 1, ignoreCase )); + + case '?': + return *string && Com_WildCmp( filter + 1, string + 1, ignoreCase ); + + default: + return ((*filter == *string) || (ignoreCase && (Q_toupper( *filter ) == Q_toupper( *string )))) && Com_WildCmp( filter + 1, string + 1, ignoreCase ); + } +} + +/* +================ +Com_HashString +================ +*/ +unsigned Com_HashString( const char *string, int hashSize ) { + unsigned hash, c; + + hash = 0; + while( *string ) { + c = *string++; + hash = 127 * hash + c; + } + + hash = ( hash >> 20 ) ^ ( hash >> 10 ) ^ hash; + return hash & ( hashSize - 1 ); +} + +/* +================ +Com_HashPath +================ +*/ +unsigned Com_HashPath( const char *string, int hashSize ) { + unsigned hash, c; + + hash = 0; + while( *string ) { + c = *string++; + if( c == '\\' ) { + c = '/'; + } else { + c = Q_tolower( c ); + } + hash = 127 * hash + c; + } + + hash = ( hash >> 20 ) ^ ( hash >> 10 ) ^ hash; + return hash & ( hashSize - 1 ); +} + + +/* +=============== +Com_PageInMemory + +=============== +*/ +int paged_total; + +void Com_PageInMemory( void *buffer, size_t size ) { + int i; + + for( i = size - 1; i > 0; i -= 4096 ) + paged_total += (( byte * )buffer)[i]; +} + +size_t Com_FormatTime( char *buffer, size_t size, time_t t ) { + int sec, min, hour, day; + + min = t / 60; sec = t % 60; + hour = min / 60; min %= 60; + day = hour / 24; hour %= 24; + + if( day ) { + return Q_scnprintf( buffer, size, "%d+%d:%02d.%02d", day, hour, min, sec ); + } + if( hour ) { + return Q_scnprintf( buffer, size, "%d:%02d.%02d", hour, min, sec ); + } + return Q_scnprintf( buffer, size, "%02d.%02d", min, sec ); +} + +size_t Com_FormatTimeLong( char *buffer, size_t size, time_t t ) { + int sec, min, hour, day; + size_t len; + + if( !t ) { + return Q_scnprintf( buffer, size, "0 secs" ); + } + + min = t / 60; sec = t % 60; + hour = min / 60; min %= 60; + day = hour / 24; hour %= 24; + + len = 0; + + if( day ) { + len += Q_scnprintf( buffer + len, size - len, + "%d day%s%s", day, day == 1 ? "" : "s", ( hour || min || sec ) ? ", " : "" ); + } + if( hour ) { + len += Q_scnprintf( buffer + len, size - len, + "%d hour%s%s", hour, hour == 1 ? "" : "s", ( min || sec ) ? ", " : "" ); + } + if( min ) { + len += Q_scnprintf( buffer + len, size - len, + "%d min%s%s", min, min == 1 ? "" : "s", sec ? ", " : "" ); + } + if( sec ) { + len += Q_scnprintf( buffer + len, size - len, + "%d sec%s", sec, sec == 1 ? "" : "s" ); + } + + return len; +} + +size_t Com_TimeDiff( char *buffer, size_t size, time_t start, time_t end ) { + time_t diff; + + if( start > end ) { + start = end; + } + diff = end - start; + return Com_FormatTime( buffer, size, diff ); +} + +size_t Com_TimeDiffLong( char *buffer, size_t size, time_t start, time_t end ) { + time_t diff; + + if( start > end ) { + start = end; + } + diff = end - start; + return Com_FormatTimeLong( buffer, size, diff ); +} + + +/* +============================================================================== + + INIT / SHUTDOWN + +============================================================================== +*/ + +/* +============= +Com_Time_m +============= +*/ +size_t Com_Time_m( char *buffer, size_t size ) { + return format_local_time( buffer, size, com_time_format->string ); +} + +/* +============= +Com_Date_m +============= +*/ +static size_t Com_Date_m( char *buffer, size_t size ) { + return format_local_time( buffer, size, com_date_format->string ); +} + +size_t Com_Uptime_m( char *buffer, size_t size ) { + return Com_TimeDiff( buffer, size, com_startTime, time( NULL ) ); +} + +size_t Com_UptimeLong_m( char *buffer, size_t size ) { + return Com_TimeDiffLong( buffer, size, com_startTime, time( NULL ) ); +} + +size_t Com_Random_m( char *buffer, size_t size ) { + return Q_scnprintf( buffer, size, "%d", rand_byte() % 10 ); +} + +static size_t Com_MapList_m( char *buffer, size_t size ) { + int i, numFiles; + void **list; + char *s, *p; + size_t len, total = 0; + + list = FS_ListFiles( "maps", ".bsp", 0, &numFiles ); + for( i = 0; i < numFiles; i++ ) { + s = list[i]; + p = COM_FileExtension( list[i] ); + *p = 0; + len = strlen( s ); + if( total + len + 1 < size ) { + memcpy( buffer + total, s, len ); + buffer[total + len] = ' '; + total += len + 1; + } + Z_Free( s ); + } + buffer[total] = 0; + + Z_Free( list ); + return total; +} + +static void Com_LastError_f( void ) { + if( com_errorMsg[0] ) { + Com_Printf( "%s\n", com_errorMsg ); + } else { + Com_Printf( "No error.\n" ); + } +} + +#ifndef __COREDLL__ +static void Com_Setenv_f( void ) { + int argc = Cmd_Argc(); + + if( argc > 2 ) { + Q_setenv( Cmd_Argv( 1 ), Cmd_ArgsFrom( 2 ) ); + } else if( argc == 2 ) { + char *env = getenv( Cmd_Argv( 1 ) ); + + if( env ) { + Com_Printf( "%s=%s\n", Cmd_Argv( 1 ), env ); + } else { + Com_Printf( "%s undefined\n", Cmd_Argv( 1 ) ); + } + } else { + Com_Printf( "Usage: %s <name> [value]\n", Cmd_Argv( 0 ) ); + } +} +#endif + +#ifdef _DEBUG + +/* +============= +Com_Error_f + +Just throw a fatal error to +test error shutdown procedures +============= +*/ +static void Com_Error_f( void ) { + Com_Error( ERR_FATAL, "%s", Cmd_Argv( 1 ) ); +} + +static void Com_ErrorDrop_f( void ) { + Com_Error( ERR_DROP, "%s", Cmd_Argv( 1 ) ); +} + +static void Com_Freeze_f( void ) { + unsigned time, msec; + float seconds; + + if( Cmd_Argc() < 2 ) { + Com_Printf( "Usage: %s <seconds>\n", Cmd_Argv( 0 ) ); + return; + } + + seconds = atof( Cmd_Argv( 1 ) ); + if( seconds < 0 ) { + return; + } + + time = Sys_Milliseconds(); + msec = seconds * 1000; + while( Sys_Milliseconds() - time < msec ) + ; +} + +static void Com_Crash_f( void ) { + *( uint32_t * )0 = 0x123456; +} + +#endif + +void Com_Address_g( genctx_t *ctx ) { + int i; + cvar_t *var; + + for( i = 0; i < 1024; i++ ) { + var = Cvar_FindVar( va( "adr%d", i ) ); + if( !var ) { + break; + } + if( !var->string[0] ) { + continue; + } + if( !Prompt_AddMatch( ctx, var->string ) ) { + break; + } + } +} + +void Com_Generic_c( genctx_t *ctx, int argnum ) { + xcompleter_t c; + xgenerator_t g; + cvar_t *var; + char *s; + + // complete command, alias or cvar name + if( !argnum ) { + Cmd_Command_g( ctx ); + Cvar_Variable_g( ctx ); + Cmd_Alias_g( ctx ); + return; + } + + s = Cmd_Argv( ctx->argnum - argnum ); + + // complete command argument or cvar value + if( ( c = Cmd_FindCompleter( s ) ) != NULL ) { + c( ctx, argnum ); + } else if( argnum == 1 && ( var = Cvar_FindVar( s ) ) != NULL ) { + g = var->generator; + if( g ) { + ctx->data = var; + g( ctx ); + } + } +} + +#if USE_CLIENT +void Com_Color_g( genctx_t *ctx ) { + int color; + + for( color = 0; color < 8; color++ ) { + if( !Prompt_AddMatch( ctx, colorNames[color] ) ) { + break; + } + } +} +#endif + +/* +=============== +Com_AddEarlyCommands + +Adds command line parameters as script statements. +Commands lead with a +, and continue until another + + +Set commands are added early, so they are guaranteed to be set before +the client and server initialize for the first time. + +Other commands are added late, after all initialization is complete. +=============== +*/ +static void Com_AddEarlyCommands( qboolean clear ) { + int i; + char *s; + + for( i = 1; i < com_argc; i++ ) { + s = com_argv[i]; + if( !s ) { + continue; + } + if( strcmp( s, "+set" ) ) { + continue; + } + if( i + 2 >= com_argc ) { + Com_Printf( "Usage: +set <variable> <value>\n" ); + com_argc = i; + break; + } + Cvar_SetEx( com_argv[ i + 1 ], com_argv[ i + 2 ], FROM_CMDLINE ); + if( clear ) { + com_argv[i] = com_argv[ i + 1 ] = com_argv[ i + 2 ] = NULL; + } + i += 2; + } +} + +/* +================= +Com_AddLateCommands + +Adds command line parameters as script statements +Commands lead with a + and continue until another + + +Returns qtrue if any late commands were added, which +will keep the demoloop from immediately starting + +Assumes +set commands are already filtered out +================= +*/ +static qboolean Com_AddLateCommands( void ) { + int i; + char *s; + qboolean ret = qfalse; + + for( i = 1; i < com_argc; i++ ) { + s = com_argv[i]; + if( !s ) { + continue; + } + if( *s == '+' ) { + if( ret ) { + Cbuf_AddText( &cmd_buffer, "\n" ); + } + s++; + } else if( ret ) { + Cbuf_AddText( &cmd_buffer, " " ); + } + Cbuf_AddText( &cmd_buffer, s ); + ret = qtrue; + } + + if( ret ) { + Cbuf_AddText( &cmd_buffer, "\n" ); + Cbuf_Execute( &cmd_buffer ); + } + + return ret; +} + + +/* +================= +Qcommon_Init +================= +*/ +void Qcommon_Init( int argc, char **argv ) { + static const char version[] = APPLICATION " " VERSION " " __DATE__ " " BUILDSTRING " " CPUSTRING; + + if( setjmp( abortframe ) ) + Sys_Error( "Error during initialization: %s", com_errorMsg ); + + com_argc = argc; + com_argv = argv; + + // prepare enough of the subsystems to handle + // cvar and command buffer management + Z_Init(); + MSG_Init(); + Cbuf_Init(); + Cmd_Init(); + Cvar_Init(); + Key_Init(); + Prompt_Init(); +#if USE_CLIENT + Con_Init(); +#endif + + // + // init commands and vars + // + z_perturb = Cvar_Get( "z_perturb", "0", 0 ); +#if USE_CLIENT + host_speeds = Cvar_Get ("host_speeds", "0", 0); +#endif +#ifdef _DEBUG + developer = Cvar_Get ("developer", "0", 0); +#endif + timescale = Cvar_Get ("timescale", "1", CVAR_CHEAT ); + fixedtime = Cvar_Get ("fixedtime", "0", CVAR_CHEAT ); + logfile_enable = Cvar_Get( "logfile", "0", 0 ); + logfile_flush = Cvar_Get( "logfile_flush", "0", 0 ); + logfile_name = Cvar_Get( "logfile_name", "console", 0 ); + logfile_prefix = Cvar_Get( "logfile_prefix", "[%Y-%m-%d %H:%M] ", 0 ); +#if USE_CLIENT + dedicated = Cvar_Get ("dedicated", "0", CVAR_NOSET); + cl_running = Cvar_Get( "cl_running", "0", CVAR_ROM ); + cl_paused = Cvar_Get( "cl_paused", "0", CVAR_ROM ); +#else + dedicated = Cvar_Get ("dedicated", "1", CVAR_ROM); +#endif + sv_running = Cvar_Get( "sv_running", "0", CVAR_ROM ); + sv_paused = Cvar_Get( "sv_paused", "0", CVAR_ROM ); + com_timedemo = Cvar_Get( "timedemo", "0", CVAR_CHEAT ); + com_date_format = Cvar_Get( "com_date_format", "%Y-%m-%d", 0 ); +#ifdef _WIN32 + com_time_format = Cvar_Get( "com_time_format", "%H.%M", 0 ); +#else + com_time_format = Cvar_Get( "com_time_format", "%H:%M", 0 ); +#endif +#ifdef _DEBUG + com_debug_break = Cvar_Get( "com_debug_break", "0", 0 ); +#endif + com_fatal_error = Cvar_Get( "com_fatal_error", "0", 0 ); + com_version = Cvar_Get( "version", version, CVAR_SERVERINFO|CVAR_ROM ); + + allow_download = Cvar_Get( "allow_download", "0", CVAR_ARCHIVE ); + allow_download_players = Cvar_Get( "allow_download_players", "1", CVAR_ARCHIVE ); + allow_download_models = Cvar_Get( "allow_download_models", "1", CVAR_ARCHIVE ); + allow_download_sounds = Cvar_Get( "allow_download_sounds", "1", CVAR_ARCHIVE ); + allow_download_maps = Cvar_Get( "allow_download_maps", "1", CVAR_ARCHIVE ); + allow_download_textures = Cvar_Get( "allow_download_textures", "1", CVAR_ARCHIVE ); + allow_download_pics = Cvar_Get( "allow_download_pics", "1", CVAR_ARCHIVE ); + allow_download_others = Cvar_Get( "allow_download_others", "0", 0 ); + + rcon_password = Cvar_Get( "rcon_password", "", CVAR_PRIVATE ); + + Cmd_AddCommand ("z_stats", Z_Stats_f); + +#ifndef __COREDLL__ + Cmd_AddCommand( "setenv", Com_Setenv_f ); +#endif + + Cmd_AddMacro( "com_date", Com_Date_m ); + Cmd_AddMacro( "com_time", Com_Time_m ); + Cmd_AddMacro( "com_uptime", Com_Uptime_m ); + Cmd_AddMacro( "com_uptime_long", Com_UptimeLong_m ); + Cmd_AddMacro( "random", Com_Random_m ); + Cmd_AddMacro( "com_maplist", Com_MapList_m ); + + // add any system-wide configuration files + Sys_AddDefaultConfig(); + Cbuf_Execute( &cmd_buffer ); + + // we need to add the early commands twice, because + // a basedir or cddir needs to be set before execing + // config files, but we want other parms to override + // the settings of the config files + Com_AddEarlyCommands( qfalse ); + + Sys_Init(); + +#if USE_SYSCON + Sys_RunConsole(); +#endif + + // print version + Com_SetColor( COLOR_CYAN ); + Com_Printf( "%s\n", version ); + Com_SetColor( COLOR_NONE ); + + FS_Init(); + +#if USE_SYSCON + Sys_RunConsole(); +#endif + + // no longer allow CVAR_NOSET modifications + com_initialized = qtrue; + + // after FS is initialized, open logfile + logfile_enable->changed = logfile_enable_changed; + logfile_flush->changed = logfile_param_changed; + logfile_name->changed = logfile_param_changed; + logfile_enable_changed( logfile_enable ); + + Cbuf_AddText( &cmd_buffer, "exec "COM_DEFAULTCFG_NAME"\n" ); + Cbuf_Execute( &cmd_buffer ); + + Cbuf_AddText( &cmd_buffer, "exec "COM_CONFIG_NAME"\n" ); + Cbuf_Execute( &cmd_buffer ); + + Cbuf_AddText( &cmd_buffer, "exec "COM_AUTOEXECCFG_NAME"\n" ); + Cbuf_Execute( &cmd_buffer ); + + Com_AddEarlyCommands( qtrue ); + +#ifdef _DEBUG + Cmd_AddCommand( "error", Com_Error_f ); + Cmd_AddCommand( "errordrop", Com_ErrorDrop_f ); + Cmd_AddCommand( "freeze", Com_Freeze_f ); + Cmd_AddCommand( "crash", Com_Crash_f ); +#endif + + Cmd_AddCommand( "lasterror", Com_LastError_f ); + + Cmd_AddCommand( "quit", Com_Quit_f ); +#if !USE_CLIENT + Cmd_AddCommand( "recycle", Com_Recycle_f ); +#endif + + srand( Sys_Milliseconds() ); + + Netchan_Init(); + NET_Init(); + BSP_Init(); + CM_Init(); + SV_Init(); +#if USE_CLIENT + CL_Init(); +#endif + +#if USE_SYSCON + Sys_RunConsole(); +#endif + + // add + commands from command line + if( !Com_AddLateCommands() ) { + // if the user didn't give any commands, run default action + if( Com_IsDedicated() ) { + Cbuf_AddText( &cmd_buffer, "dedicated_start\n" ); + } else { + // TODO + //Cbuf_AddText( "d1\n" ); + } + Cbuf_Execute( &cmd_buffer ); + } +#if USE_CLIENT + else { + // the user asked for something explicit + // so drop the loading plaque + SCR_EndLoadingPlaque(); + } +#endif + + // even not given a starting map, dedicated server starts + // listening for rcon commands (create socket after all configs + // are executed to make sure port number is properly set) + if( Com_IsDedicated() ) { + NET_Config( NET_SERVER ); + } + + Com_Printf( "====== " APPLICATION " initialized ======\n\n" ); + Com_SetColor( COLOR_CYAN ); + Com_Printf( APPLICATION " " VERSION ", " __DATE__ "\n" ); + Com_SetColor( COLOR_NONE ); + Com_Printf( "http://skuller.net/q2pro/\n\n" ); + + time( &com_startTime ); + + com_eventTime = Sys_Milliseconds(); +} + +/* +================= +Qcommon_Frame +================= +*/ +void Qcommon_Frame( void ) { +#if USE_CLIENT + unsigned time_before, time_event, time_between, time_after; +#endif + unsigned oldtime, msec; + static unsigned remaining; + static float frac; + + if( setjmp( abortframe ) ) { + return; // an ERR_DROP was thrown + } + +#if USE_CLIENT + time_before = time_event = time_between = time_after = 0; + + if( host_speeds->integer ) + time_before = Sys_Milliseconds(); +#endif + + // sleep on network sockets when running a dedicated server + // still do a select(), but don't sleep when running a client! + if( Com_IsDedicated() ) { + IO_Sleep( remaining ); + } else { + IO_Sleep( 0 ); + } + + // calculate time spent running last frame and sleeping + oldtime = com_eventTime; + com_eventTime = Sys_Milliseconds(); + if( oldtime > com_eventTime ) { + oldtime = com_eventTime; + } + msec = com_eventTime - oldtime; + +#if USE_CLIENT + // spin until msec is non-zero if running a client + if( !dedicated->integer && !com_timedemo->integer ) { + while( msec < 1 ) { + CL_ProcessEvents(); + com_eventTime = Sys_Milliseconds(); + msec = com_eventTime - oldtime; + } + } +#endif + + if( msec > 250 ) { + Com_DPrintf( "Hitch warning: %u msec frame time\n", msec ); + msec = 100; // time was unreasonable, + // host OS was hibernated or something + } + + if( fixedtime->integer ) { + Cvar_ClampInteger( fixedtime, 1, 1000 ); + msec = fixedtime->integer; + } else if( timescale->value > 0 ) { + frac += msec * timescale->value; + msec = frac; + frac -= msec; + } + + // run local time + com_localTime += msec; + com_framenum++; + +#if USE_CLIENT + if( host_speeds->integer ) + time_event = Sys_Milliseconds(); +#endif + +#if USE_SYSCON + // run system console + Sys_RunConsole(); +#endif + + remaining = SV_Frame( msec ); + +#if USE_CLIENT + if( host_speeds->integer ) + time_between = Sys_Milliseconds(); + + CL_Frame( msec ); + + if( host_speeds->integer ) + time_after = Sys_Milliseconds(); + + if( host_speeds->integer ) { + int all, ev, sv, gm, cl, rf; + + all = time_after - time_before; + ev = time_event - time_before; + sv = time_between - time_event; + cl = time_after - time_between; + gm = time_after_game - time_before_game; + rf = time_after_ref - time_before_ref; + sv -= gm; + cl -= rf; + + Com_Printf( "all:%3i ev:%3i sv:%3i gm:%3i cl:%3i rf:%3i\n", + all, ev, sv, gm, cl, rf ); + } +#endif + + // this is the only place where console commands are processed. + Cbuf_Execute( &cmd_buffer ); +} + +/* +================= +Qcommon_Shutdown +================= +*/ +void Qcommon_Shutdown( void ) { + NET_Shutdown(); + logfile_close(); + FS_Shutdown(); +} + |