/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. This file is part of Quake III Arena source code. Quake III Arena source code 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. Quake III Arena source code 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 Quake III Arena source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // common.c -- misc functions used in client and server #include "q_shared.h" #include "qcommon.h" #include "common_help.h" #include #if (_MSC_VER >= 1400) // Visual C++ 2005 or later #define MSVC_CPUID 1 #include #elif (__GNUC__) #define GCC_CPUID 1 #include #endif #ifndef _WIN32 #include #include // umask #endif #if defined(_MSC_VER) #include "../win32/windows.h" #endif #define MIN_COMHUNKMEGS_DED 8 // for the dedicated server #define MIN_COMHUNKMEGS 56 #define DEF_COMHUNKMEGS 64 #define DEF_COMZONEMEGS 32 static jmp_buf abortframe; // an ERR_DROP occured, exit the entire frame static fileHandle_t logfile = 0; static fileHandle_t com_journalFile = 0; // events are written here fileHandle_t com_journalDataFile = 0; // config files are written here cvar_t *com_viewlog = 0; cvar_t *com_speeds = 0; cvar_t *com_developer = 0; cvar_t *com_dedicated = 0; cvar_t *com_timescale = 0; cvar_t *com_fixedtime = 0; cvar_t *com_journal = 0; cvar_t *com_maxfps = 0; cvar_t *com_timedemo = 0; cvar_t *com_sv_running = 0; cvar_t *com_cl_running = 0; cvar_t *com_logfile = 0; // 1 = buffer log, 2 = flush after each print cvar_t *com_showtrace = 0; cvar_t *com_version = 0; cvar_t *cl_paused = 0; cvar_t *sv_paused = 0; cvar_t *cl_packetdelay = 0; cvar_t *sv_packetdelay = 0; #if defined(_WIN32) && defined(_DEBUG) cvar_t *com_noErrorInterrupt; #endif static cvar_t *con_completionStyle; // 0 = legacy, 1 = ET-style static cvar_t *con_history; // com_speeds times int time_game; int time_frontend; // renderer frontend time int time_backend; // renderer backend time int com_frameTime; int com_frameNumber; qbool com_errorEntered; qbool com_fullyInitialized; static char com_errorMessage[MAXPRINTMSG]; static void Com_WriteConfig_f(); static void Com_CompleteWriteConfig_f( int startArg, int compArg ); extern void CIN_CloseAllVideos( void ); //============================================================================ static char *rd_buffer; static int rd_buffersize; static void (*rd_flush)( char *buffer ); void Com_BeginRedirect (char *buffer, int buffersize, void (*flush)( char *) ) { if (!buffer || !buffersize || !flush) return; rd_buffer = buffer; rd_buffersize = buffersize; rd_flush = flush; *rd_buffer = 0; } void Com_EndRedirect (void) { if ( rd_flush ) { rd_flush(rd_buffer); } rd_buffer = NULL; rd_buffersize = 0; rd_flush = NULL; } /////////////////////////////////////////////////////////////// // client and server can both use these, and will do the appropriate things // a raw string should NEVER be passed as fmt, because of "%f" type crashers void QDECL Com_Printf( const char *fmt, ... ) { char msg[MAXPRINTMSG]; va_list argptr; va_start( argptr, fmt ); Q_vsnprintf( msg, sizeof(msg), fmt, argptr ); va_end( argptr ); if ( rd_buffer ) { if ((strlen (msg) + strlen(rd_buffer)) > (rd_buffersize - 1)) { rd_flush(rd_buffer); *rd_buffer = 0; } Q_strcat(rd_buffer, rd_buffersize, msg); return; } #ifndef DEDICATED // echo to console if we're not a dedicated server if ( com_dedicated && !com_dedicated->integer ) { CL_ConsolePrint( msg ); } #endif // echo to dedicated console and early console Sys_Print( msg ); if ( !com_logfile || !com_logfile->integer ) return; static qbool opening_qconsole = qfalse; // TTimo: only open the qconsole.log if the filesystem is in an initialized state // also, avoid recursing in the qconsole.log opening (i.e. if fs_debug is on) if ( !logfile && FS_Initialized() && !opening_qconsole) { opening_qconsole = qtrue; time_t aclock; time( &aclock ); struct tm* newtime = localtime( &aclock ); logfile = FS_FOpenFileWrite( "qconsole.log" ); if (logfile) { Com_Printf( "logfile opened on %s\n", asctime( newtime ) ); if ( com_logfile->integer > 1 ) { // force it to not buffer so we get valid // data even if we are crashing FS_ForceFlush(logfile); } } else { Com_Printf("Opening qconsole.log failed!\n"); Cvar_SetValue("logfile", 0); } opening_qconsole = qfalse; } if ( logfile && FS_Initialized() ) { FS_Write( msg, strlen(msg), logfile ); } } // a Com_Printf that only shows up if the "developer" cvar is set void QDECL Com_DPrintf( const char *fmt, ...) { // don't confuse non-developers with techie stuff... if ( !com_developer || !com_developer->integer ) return; char msg[MAXPRINTMSG]; va_list argptr; va_start( argptr, fmt ); Q_vsnprintf( msg, sizeof(msg), fmt, argptr ); Com_Printf( "%s", msg ); va_end( argptr ); } void QDECL Com_Error( int level, const char* fmt, ... ) { static char msg[MAXPRINTMSG]; va_list argptr; va_start( argptr, fmt ); Q_vsnprintf( msg, sizeof(msg), fmt, argptr ); Com_ErrorExt( level, EXT_ERRMOD_ENGINE, qtrue, "%s", msg ); va_end(argptr); } void QDECL Com_ErrorExt( int code, int module, qbool realError, const char *fmt, ... ) { static int lastErrorTime; static int errorCount; #ifndef DEDICATED void CL_ForwardUIError( int level, int module, const char* error ); // client.h #endif // make sure we can get at our local stuff FS_PureServerSetLoadedPaks( "" ); // if we are getting a solid stream of ERR_DROP, do an ERR_FATAL int currentTime = Sys_Milliseconds(); if ( currentTime - lastErrorTime < 100 ) { if ( ++errorCount > 3 ) { code = ERR_FATAL; } } else { errorCount = 0; } lastErrorTime = currentTime; if ( com_errorEntered ) { Sys_Error( "recursive error after: %s", com_errorMessage ); } com_errorEntered = qtrue; va_list argptr; va_start( argptr, fmt ); vsprintf( com_errorMessage, fmt, argptr ); va_end( argptr ); #if defined(_WIN32) && defined(_DEBUG) if ( code != ERR_DISCONNECT && code != ERR_NEED_CD ) { if ((!com_noErrorInterrupt || !com_noErrorInterrupt->integer) && IsDebuggerPresent()) { __debugbreak(); } } #endif if ( code != ERR_DISCONNECT && code != ERR_NEED_CD ) { Cvar_Set("com_errorMessage", com_errorMessage); } if ( code == ERR_SERVERDISCONNECT ) { #ifndef DEDICATED CL_Disconnect( qtrue ); CL_FlushMemory(); if ( realError ) CL_ForwardUIError( EXT_ERRLEV_SVDISC, module, com_errorMessage ); #endif com_errorEntered = qfalse; longjmp (abortframe, -1); } else if ( code == ERR_DROP || code == ERR_DISCONNECT ) { if (realError) Com_Printf( "********************\nERROR: %s\n********************\n", com_errorMessage ); else Com_Printf( "ERROR: %s\n", com_errorMessage ); SV_Shutdown( va("Server crashed: %s", com_errorMessage) ); #ifndef DEDICATED CL_Disconnect( qtrue ); CL_FlushMemory(); // shuts down the VMs and starts them back up if ( realError ) CL_ForwardUIError( code == ERR_DROP ? EXT_ERRLEV_DROP : EXT_ERRLEV_DISC, module, com_errorMessage ); #endif com_errorEntered = qfalse; longjmp (abortframe, -1); } else if ( code == ERR_NEED_CD ) { SV_Shutdown( "Server didn't have CD" ); #ifndef DEDICATED if ( com_cl_running && com_cl_running->integer ) { CL_Disconnect( qtrue ); CL_FlushMemory(); com_errorEntered = qfalse; CL_CDDialog(); } else { Com_Printf("Server didn't have CD\n" ); } #endif longjmp (abortframe, -1); } else { #ifndef DEDICATED CL_Shutdown(); #endif SV_Shutdown( va("Server fatal crashed: %s", com_errorMessage) ); } Com_Shutdown(); Sys_Error( "%s", com_errorMessage ); } void Com_Quit( int status ) { Sys_SaveHistory(); // don't try to shutdown if we are in a recursive error if ( !com_errorEntered ) { SV_Shutdown( "Server quit" ); #ifndef DEDICATED CL_Shutdown(); #endif Com_Shutdown(); FS_Shutdown( qtrue ); } Sys_Quit( status ); } static void Com_Quit_f( void ) { Com_Quit( 0 ); } /////////////////////////////////////////////////////////////// /* COMMAND LINE FUNCTIONS + characters seperate the commandLine string into multiple console command lines. All of these are valid: quake3 +set test blah +map test quake3 set test blah+map test quake3 set test blah + map test */ #define MAX_CONSOLE_LINES 32 static int com_numConsoleLines; static char* com_consoleLines[MAX_CONSOLE_LINES]; // break the process args up into multiple console lines static void Com_ParseCommandLine( char* commandLine ) { int inq = 0; com_consoleLines[0] = commandLine; com_numConsoleLines = 1; while ( *commandLine ) { if (*commandLine == '"') { inq = !inq; } // look for a + separating character // if commandLine came from a file, we might have real line separators if ( (*commandLine == '+' && !inq) || *commandLine == '\n' || *commandLine == '\r' ) { if ( com_numConsoleLines == MAX_CONSOLE_LINES ) { return; } *commandLine++ = 0; // terminate the PREVIOUS command com_consoleLines[com_numConsoleLines++] = commandLine; } ++commandLine; } } // check for "+safe" on the command line, which will skip loading of q3config.cfg qbool Com_SafeMode() { for (int i = 0; i < com_numConsoleLines; ++i) { Cmd_TokenizeString( com_consoleLines[i] ); if ( !Q_stricmp( Cmd_Argv(0), "safe" ) || !Q_stricmp( Cmd_Argv(0), "cvar_restart" ) ) { com_consoleLines[i][0] = 0; return qtrue; } } return qfalse; } /* Searches for command line parameters that are set commands. If match is not NULL, only that cvar will be looked for. That is necessary because cddir and basedir need to be set before the filesystem is started, but all other sets should be after execing the config and default. */ void Com_StartupVariable( const char *match ) { for (int i = 0; i < com_numConsoleLines; ++i) { Cmd_TokenizeString( com_consoleLines[i] ); if ( strcmp( Cmd_Argv(0), "set" ) ) continue; const char* s = Cmd_Argv(1); if ( !match || !strcmp( s, match ) ) { Cvar_Set( s, Cmd_Argv(2) ); cvar_t* cv = Cvar_Get( s, "", 0 ); cv->flags |= CVAR_USER_CREATED; } } } /* Adds command line parameters as script statements Commands are separated by + signs Returns qtrue if any late commands were added which will keep the fucking annoying cins from playing */ static qbool Com_AddStartupCommands() { qbool added = qfalse; // quote every token, so args with semicolons can work for (int i = 0; i < com_numConsoleLines; ++i) { if ( !com_consoleLines[i] || !com_consoleLines[i][0] ) { continue; } // set commands won't override menu startup if ( Q_stricmpn( com_consoleLines[i], "set", 3 ) ) { added = qtrue; } Cbuf_AddText( com_consoleLines[i] ); Cbuf_AddText( "\n" ); } return added; } //============================================================================ void Info_Print( const char *s ) { char key[512]; char value[512]; char *o; int l; if (*s == '\\') s++; while (*s) { o = key; while (*s && *s != '\\') *o++ = *s++; l = o - key; if (l < 20) { Com_Memset (o, ' ', 20-l); key[20] = 0; } else *o = 0; Com_Printf ("%s", key); if (!*s) { Com_Printf ("MISSING VALUE\n"); return; } o = value; s++; while (*s && *s != '\\') *o++ = *s++; *o = 0; if (*s) s++; Com_Printf ("%s\n", value); } } static const char* Com_StringContains( const char* str1, const char* str2) { int i, j; int len = strlen(str1) - strlen(str2); for (i = 0; i <= len; i++, str1++) { for (j = 0; str2[j]; j++) { if (toupper(str1[j]) != toupper(str2[j])) { break; } } if (!str2[j]) { return str1; } } return NULL; } int Com_Filter( const char* filter, const char* name ) { char buf[MAX_TOKEN_CHARS]; const char* ptr; int i, found; while (*filter) { if (*filter == '*') { filter++; for (i = 0; *filter; i++) { if (*filter == '*' || *filter == '?') break; buf[i] = *filter; filter++; } buf[i] = '\0'; if (buf[0]) { ptr = Com_StringContains(name, buf); if (!ptr) return qfalse; name = ptr + strlen(buf); } } else if (*filter == '?') { filter++; name++; } else if (*filter == '[' && *(filter+1) == '[') { filter++; } else if (*filter == '[') { filter++; found = qfalse; while(*filter && !found) { if (*filter == ']' && *(filter+1) != ']') break; if (*(filter+1) == '-' && *(filter+2) && (*(filter+2) != ']' || *(filter+3) == ']')) { if (toupper(*name) >= toupper(*filter) && toupper(*name) <= toupper(*(filter+2))) found = qtrue; filter += 3; } else { if (toupper(*filter) == toupper(*name)) found = qtrue; filter++; } } if (!found) return qfalse; while(*filter) { if (*filter == ']' && *(filter+1) != ']') break; filter++; } filter++; name++; } else { if (toupper(*filter) != toupper(*name)) return qfalse; filter++; name++; } } return qtrue; } int Com_FilterPath( const char* filter, const char* name ) { int i; char new_filter[MAX_QPATH]; char new_name[MAX_QPATH]; for (i = 0; i < MAX_QPATH-1 && filter[i]; i++) { if ( filter[i] == '\\' || filter[i] == ':' ) { new_filter[i] = '/'; } else { new_filter[i] = filter[i]; } } new_filter[i] = '\0'; for (i = 0; i < MAX_QPATH-1 && name[i]; i++) { if ( name[i] == '\\' || name[i] == ':' ) { new_name[i] = '/'; } else { new_name[i] = name[i]; } } new_name[i] = '\0'; return Com_Filter( new_filter, new_name ); } /* ============ Com_HashKey ============ */ int Com_HashKey(char *string, int maxlen) { int register hash, i; hash = 0; for (i = 0; i < maxlen && string[i] != '\0'; i++) { hash += string[i] * (119 + i); } hash = (hash ^ (hash >> 10) ^ (hash >> 20)); return hash; } int Com_RealTime( qtime_t* qtime ) { time_t t = time(NULL); if (!qtime) return t; const struct tm* tms = localtime(&t); if (tms) { qtime->tm_sec = tms->tm_sec; qtime->tm_min = tms->tm_min; qtime->tm_hour = tms->tm_hour; qtime->tm_mday = tms->tm_mday; qtime->tm_mon = tms->tm_mon; qtime->tm_year = tms->tm_year; qtime->tm_wday = tms->tm_wday; qtime->tm_yday = tms->tm_yday; qtime->tm_isdst = tms->tm_isdst; } return t; } /* ============================================================================== ZONE MEMORY ALLOCATION There is never any space between memblocks, and there will never be two contiguous free memblocks. The rover can be left pointing at a non-empty block The zone calls are pretty much only used for small strings and structures, all big things are allocated on the hunk. ============================================================================== */ #define ZONEID 0x1d4a11 #define MINFRAGMENT 64 typedef struct zonedebug_s { char *label; char *file; int line; int allocSize; } zonedebug_t; typedef struct memblock_s { int size; // including the header and possibly tiny fragments int tag; // a tag of 0 is a free block struct memblock_s *next, *prev; int id; // should be ZONEID #ifdef ZONE_DEBUG zonedebug_t d; #endif } memblock_t; typedef struct { int size; // total bytes malloced, including header int used; // total bytes used memblock_t blocklist; // start / end cap for linked list memblock_t *rover; } memzone_t; // main zone for all "dynamic" memory allocation static memzone_t* mainzone = NULL; // we also have a small zone for small allocations that would only // fragment the main zone (think of cvar and cmd strings) static memzone_t* smallzone = NULL; static void Z_ClearZone( memzone_t* zone, int size ) { memblock_t* block; // set the entire zone to one free block zone->blocklist.next = zone->blocklist.prev = block = (memblock_t *)( (byte *)zone + sizeof(memzone_t) ); zone->blocklist.tag = 1; // in use block zone->blocklist.id = 0; zone->blocklist.size = 0; zone->rover = block; zone->size = size; zone->used = 0; block->prev = block->next = &zone->blocklist; block->tag = 0; // free block block->id = ZONEID; block->size = size - sizeof(memzone_t); } static int Z_AvailableZoneMemory( const memzone_t* zone ) { return zone->size - zone->used; } int Z_AvailableMemory( void ) { return Z_AvailableZoneMemory( mainzone ); } void Z_Free( void* ptr ) { if (!ptr) { Com_Error( ERR_DROP, "Z_Free: NULL pointer" ); } memblock_t* block = (memblock_t*)((byte*)ptr - sizeof(memblock_t)); if (block->id != ZONEID) { Com_Error( ERR_FATAL, "Z_Free: freed a pointer without ZONEID" ); } if (block->tag == 0) { Com_Error( ERR_FATAL, "Z_Free: freed a freed pointer" ); } // if static memory if (block->tag == TAG_STATIC) { return; } // check the memory trash tester if ( *(int *)((byte *)block + block->size - 4 ) != ZONEID ) { Com_Error( ERR_FATAL, "Z_Free: memory block wrote past end" ); } memzone_t* zone = (block->tag == TAG_SMALL) ? smallzone : mainzone; zone->used -= block->size; // set the block to something that should cause problems // if it is referenced... Com_Memset( ptr, 0xaa, block->size - sizeof( *block ) ); block->tag = 0; // mark as free memblock_t* other = block->prev; if (!other->tag) { // merge with previous free block other->size += block->size; other->next = block->next; other->next->prev = other; if (block == zone->rover) { zone->rover = other; } block = other; } zone->rover = block; other = block->next; if ( !other->tag ) { // merge the next free block onto the end block->size += other->size; block->next = other->next; block->next->prev = block; if (other == zone->rover) { zone->rover = block; } } } #ifdef ZONE_DEBUG static void Z_LogZoneHeap( const memzone_t* zone, const char* name ) { char dump[32], *ptr; int i, j; memblock_t *block; char buf[4096]; int size, allocSize, numBlocks; if (!logfile || !FS_Initialized()) return; size = allocSize = numBlocks = 0; Com_sprintf(buf, sizeof(buf), "\r\n================\r\n%s log\r\n================\r\n", name); FS_Write(buf, strlen(buf), logfile); for (block = zone->blocklist.next ; block->next != &zone->blocklist; block = block->next) { if (block->tag) { ptr = ((char *) block) + sizeof(memblock_t); j = 0; for (i = 0; i < 20 && i < block->d.allocSize; i++) { if (ptr[i] >= 32 && ptr[i] < 127) { dump[j++] = ptr[i]; } else { dump[j++] = '_'; } } dump[j] = '\0'; Com_sprintf(buf, sizeof(buf), "size = %8d: %s, line: %d (%s) [%s]\r\n", block->d.allocSize, block->d.file, block->d.line, block->d.label, dump); FS_Write(buf, strlen(buf), logfile); allocSize += block->d.allocSize; size += block->size; numBlocks++; } } // subtract debug memory size -= numBlocks * sizeof(zonedebug_t); allocSize = numBlocks * sizeof(memblock_t); // + 32 bit alignment Com_sprintf(buf, sizeof(buf), "%d %s memory in %d blocks\r\n", size, name, numBlocks); FS_Write(buf, strlen(buf), logfile); Com_sprintf(buf, sizeof(buf), "%d %s memory overhead\r\n", size - allocSize, name); FS_Write(buf, strlen(buf), logfile); } static void Z_LogHeap() { Z_LogZoneHeap( mainzone, "MAIN" ); Z_LogZoneHeap( smallzone, "SMALL" ); } #endif /* ================ Z_TagMalloc ================ */ #ifdef ZONE_DEBUG void *Z_TagMallocDebug( int size, int tag, char *label, char *file, int line ) { #else void *Z_TagMalloc( int size, int tag ) { #endif int extra, allocSize; memblock_t *start, *rover, *base; memzone_t *zone; if (!tag) { Com_Error( ERR_FATAL, "Z_TagMalloc: tried to use a 0 tag" ); } if ( tag == TAG_SMALL ) { zone = smallzone; } else { zone = mainzone; } allocSize = size; // // scan through the block list looking for the first free block // of sufficient size // size += sizeof(memblock_t); // account for size of block header size += 4; // space for memory trash tester size = PAD(size, sizeof(intptr_t)); // align to 32/64 bit boundary base = rover = zone->rover; start = base->prev; do { if (rover == start) { #ifdef ZONE_DEBUG Z_LogHeap(); #endif // scaned all the way around the list Com_Error( ERR_FATAL, "Z_Malloc: failed on allocation of %i bytes from the %s zone", size, zone == smallzone ? "small" : "main"); return NULL; } if (rover->tag) { base = rover = rover->next; } else { rover = rover->next; } } while (base->tag || base->size < size); // // found a block big enough // extra = base->size - size; if (extra > MINFRAGMENT) { // there will be a free fragment after the allocated block memblock_t* p = (memblock_t *) ((byte *)base + size ); p->size = extra; p->tag = 0; // free block p->prev = base; p->id = ZONEID; p->next = base->next; p->next->prev = p; base->next = p; base->size = size; } base->tag = tag; // no longer a free block zone->rover = base->next; // next allocation will start looking here zone->used += base->size; // base->id = ZONEID; #ifdef ZONE_DEBUG base->d.label = label; base->d.file = file; base->d.line = line; base->d.allocSize = allocSize; #endif // marker for memory trash testing *(int *)((byte *)base + base->size - 4) = ZONEID; return (void *) ((byte *)base + sizeof(memblock_t)); } static void Z_CheckHeap() { const memblock_t* block; for (block = mainzone->blocklist.next ; ; block = block->next) { if (block->next == &mainzone->blocklist) { break; // all blocks have been hit } if ( (byte *)block + block->size != (byte *)block->next) Com_Error( ERR_FATAL, "Z_CheckHeap: block size does not touch the next block\n" ); if ( block->next->prev != block) { Com_Error( ERR_FATAL, "Z_CheckHeap: next block doesn't have proper back link\n" ); } if ( !block->tag && !block->next->tag ) { Com_Error( ERR_FATAL, "Z_CheckHeap: two consecutive free blocks\n" ); } } } #ifdef ZONE_DEBUG void *Z_MallocDebug( int size, char *label, char *file, int line ) { Z_CheckHeap(); void* buf = Z_TagMallocDebug( size, TAG_GENERAL, label, file, line ); #else void *Z_Malloc( int size ) { void* buf = Z_TagMalloc( size, TAG_GENERAL ); #endif Com_Memset( buf, 0, size ); return buf; } #ifdef ZONE_DEBUG void *S_MallocDebug( int size, char *label, char *file, int line ) { return Z_TagMallocDebug( size, TAG_SMALL, label, file, line ); } #else void *S_Malloc( int size ) { return Z_TagMalloc( size, TAG_SMALL ); } #endif // static mem blocks to reduce a lot of small zone overhead typedef struct memstatic_s { memblock_t b; byte mem[2]; } memstatic_t; static memstatic_t emptystring = { {(sizeof(memblock_t)+2 + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'\0', '\0'} }; static memstatic_t numberstring[] = { { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'0', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'1', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'2', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'3', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'4', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'5', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'6', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'7', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'8', '\0'} }, { {(sizeof(memstatic_t) + 3) & ~3, TAG_STATIC, NULL, NULL, ZONEID}, {'9', '\0'} } }; /* ======================== CopyString NOTE: never write over the memory CopyString returns because memory from a memstatic_t might be returned ======================== */ char* CopyString( const char *in ) { if (!in[0]) { return ((char *)&emptystring) + sizeof(memblock_t); } else if (!in[1]) { if (in[0] >= '0' && in[0] <= '9') { return ((char *)&numberstring[in[0]-'0']) + sizeof(memblock_t); } } char* out = (char*)S_Malloc(strlen(in)+1); strcpy(out, in); return out; } /* ============================================================================== Goals: reproducable without history effects -- no out of memory errors on weird map to map changes allow restarting of the client without fragmentation minimize total pages in use at run time minimize total pages needed during load time Single block of memory with stack allocators coming from both ends towards the middle. One side is designated the temporary memory allocator. Temporary memory can be allocated and freed in any order. A highwater mark is kept of the most in use at any time. When there is no temporary memory allocated, the permanent and temp sides can be switched, allowing the already touched temp memory to be used for permanent storage. Temp memory must never be allocated on two ends at once, or fragmentation could occur. If we have any in-use temp memory, additional temp allocations must come from that side. If not, we can choose to make either side the new temp side and push future permanent allocations to the other side. Permanent allocations should be kept on the side that has the current greatest wasted highwater mark. ============================================================================== */ typedef unsigned int hmagic; static const hmagic HUNK_MAGIC_INUSE = 0x89537892; static const hmagic HUNK_MAGIC_FREED = 0x89537893; typedef struct { hmagic magic; int size; } hunkHeader_t; typedef struct { int mark; int permanent; int temp; int tempHighwater; } hunkUsed_t; static hunkUsed_t hunk_low = {0}, hunk_high = {0}; static hunkUsed_t *hunk_permanent = &hunk_low, *hunk_temp = &hunk_high; static byte *s_hunkData = NULL; static int s_hunkTotal = 0; static int s_zoneTotal = 0; #ifdef HUNK_DEBUG typedef struct hunkblock_s { int size; byte printed; struct hunkblock_s *next; char *label; char *file; int line; } hunkblock_t; static hunkblock_t* hunkblocks = NULL; #endif static void Com_Meminfo_f( void ) { const memblock_t* block; int zoneBytes = 0; int zoneBlocks = 0; int botlibBytes = 0; int rendererBytes = 0; for (block = mainzone->blocklist.next ; ; block = block->next) { if ( Cmd_Argc() != 1 ) { Com_Printf ("block:%p size:%7i tag:%3i\n", block, block->size, block->tag); } if ( block->tag ) { zoneBytes += block->size; zoneBlocks++; if ( block->tag == TAG_BOTLIB ) { botlibBytes += block->size; } else if ( block->tag == TAG_RENDERER ) { rendererBytes += block->size; } } if (block->next == &mainzone->blocklist) { break; // all blocks have been hit } if ( (byte *)block + block->size != (byte *)block->next) { Com_Printf ("ERROR: block size does not touch the next block\n"); } if ( block->next->prev != block) { Com_Printf ("ERROR: next block doesn't have proper back link\n"); } if ( !block->tag && !block->next->tag ) { Com_Printf ("ERROR: two consecutive free blocks\n"); } } int smallZoneBytes = 0; int smallZoneBlocks = 0; for (block = smallzone->blocklist.next ; ; block = block->next) { if ( block->tag ) { smallZoneBytes += block->size; smallZoneBlocks++; } if (block->next == &smallzone->blocklist) { break; // all blocks have been hit } } Com_Printf( "%8i bytes total hunk\n", s_hunkTotal ); Com_Printf( "%8i bytes total zone\n", s_zoneTotal ); Com_Printf( "\n" ); Com_Printf( "%8i low mark\n", hunk_low.mark ); Com_Printf( "%8i low permanent\n", hunk_low.permanent ); if ( hunk_low.temp != hunk_low.permanent ) { Com_Printf( "%8i low temp\n", hunk_low.temp ); } Com_Printf( "%8i low tempHighwater\n", hunk_low.tempHighwater ); Com_Printf( "\n" ); Com_Printf( "%8i high mark\n", hunk_high.mark ); Com_Printf( "%8i high permanent\n", hunk_high.permanent ); if ( hunk_high.temp != hunk_high.permanent ) { Com_Printf( "%8i high temp\n", hunk_high.temp ); } Com_Printf( "%8i high tempHighwater\n", hunk_high.tempHighwater ); Com_Printf( "\n" ); Com_Printf( "%8i total hunk in use\n", hunk_low.permanent + hunk_high.permanent ); int unused = 0; if ( hunk_low.tempHighwater > hunk_low.permanent ) { unused += hunk_low.tempHighwater - hunk_low.permanent; } if ( hunk_high.tempHighwater > hunk_high.permanent ) { unused += hunk_high.tempHighwater - hunk_high.permanent; } Com_Printf( "%8i unused highwater\n", unused ); Com_Printf( "\n" ); Com_Printf( "%8i bytes in %i zone blocks\n", zoneBytes, zoneBlocks ); Com_Printf( " %8i bytes in dynamic botlib\n", botlibBytes ); Com_Printf( " %8i bytes in dynamic renderer\n", rendererBytes ); Com_Printf( " %8i bytes in dynamic other\n", zoneBytes - ( botlibBytes + rendererBytes ) ); Com_Printf( " %8i bytes in small Zone memory\n", smallZoneBytes ); } // touch all known used data to make sure it is paged in void Com_TouchMemory() { int i, j; int sum = 0; const memblock_t* block; Z_CheckHeap(); int start = Sys_Milliseconds(); j = hunk_low.permanent >> 2; for ( i = 0 ; i < j ; i+=64 ) { // only need to touch each page sum += ((int *)s_hunkData)[i]; } i = ( s_hunkTotal - hunk_high.permanent ) >> 2; j = hunk_high.permanent >> 2; for ( ; i < j ; i+=64 ) { // only need to touch each page sum += ((int *)s_hunkData)[i]; } for (block = mainzone->blocklist.next ; ; block = block->next) { if ( block->tag ) { j = block->size >> 2; for ( i = 0 ; i < j ; i+=64 ) { // only need to touch each page sum += ((int *)block)[i]; } } if ( block->next == &mainzone->blocklist ) { break; // all blocks have been hit } } int end = Sys_Milliseconds(); Com_Printf( "Com_TouchMemory: %i msec\n", end - start ); } static void Com_InitSmallZoneMemory() { const int s_smallZoneTotal = 512 * 1024; smallzone = (memzone_t*)calloc( s_smallZoneTotal, 1 ); if ( !smallzone ) Com_Error( ERR_FATAL, "Small zone data failed to allocate %1.1f megs", (float)s_smallZoneTotal / (1024*1024) ); Z_ClearZone( smallzone, s_smallZoneTotal ); } static void Com_InitZoneMemory() { //FIXME: 05/01/06 com_zoneMegs is useless right now as neither q3config.cfg nor // Com_StartupVariable have been executed by this point. The net result is that // s_zoneTotal will always be set to the default value. // myT: removed com_zoneMegs for now // allocate the random block zone s_zoneTotal = 1024 * 1024 * DEF_COMZONEMEGS; mainzone = (memzone_t*)calloc( s_zoneTotal, 1 ); if ( !mainzone ) Com_Error( ERR_FATAL, "Zone data failed to allocate %i megs", s_zoneTotal / (1024*1024) ); Z_ClearZone( mainzone, s_zoneTotal ); } #ifdef HUNK_DEBUG static void Hunk_Log( void ) { char buf[4096]; const hunkblock_t* block; int size = 0, numBlocks = 0; if (!logfile || !FS_Initialized()) return; Com_sprintf(buf, sizeof(buf), "\r\n================\r\nHunk log\r\n================\r\n"); FS_Write(buf, strlen(buf), logfile); for (block = hunkblocks ; block; block = block->next) { Com_sprintf(buf, sizeof(buf), "size = %8d: %s, line: %d (%s)\r\n", block->size, block->file, block->line, block->label); FS_Write(buf, strlen(buf), logfile); size += block->size; numBlocks++; } Com_sprintf(buf, sizeof(buf), "%d Hunk memory\r\n", size); FS_Write(buf, strlen(buf), logfile); Com_sprintf(buf, sizeof(buf), "%d hunk blocks\r\n", numBlocks); FS_Write(buf, strlen(buf), logfile); } void Hunk_SmallLog( void ) { hunkblock_t *block, *block2; char buf[4096]; int size, locsize, numBlocks; if (!logfile || !FS_Initialized()) return; for (block = hunkblocks ; block; block = block->next) { block->printed = qfalse; } size = 0; numBlocks = 0; Com_sprintf(buf, sizeof(buf), "\r\n================\r\nHunk Small log\r\n================\r\n"); FS_Write(buf, strlen(buf), logfile); for (block = hunkblocks; block; block = block->next) { if (block->printed) { continue; } locsize = block->size; for (block2 = block->next; block2; block2 = block2->next) { if (block->line != block2->line) { continue; } if (Q_stricmp(block->file, block2->file)) { continue; } size += block2->size; locsize += block2->size; block2->printed = qtrue; } Com_sprintf(buf, sizeof(buf), "size = %8d: %s, line: %d (%s)\r\n", locsize, block->file, block->line, block->label); FS_Write(buf, strlen(buf), logfile); size += block->size; numBlocks++; } Com_sprintf(buf, sizeof(buf), "%d Hunk memory\r\n", size); FS_Write(buf, strlen(buf), logfile); Com_sprintf(buf, sizeof(buf), "%d hunk blocks\r\n", numBlocks); FS_Write(buf, strlen(buf), logfile); } #endif static const cmdTableItem_t hunk_cmds[] = { { "meminfo", Com_Meminfo_f, NULL, "prints memory allocation info" }, #ifdef ZONE_DEBUG { "zonelog", Z_LogHeap }, #endif #ifdef HUNK_DEBUG { "hunklog", Hunk_Log }, { "hunksmalllog", Hunk_SmallLog } #endif }; static void Com_InitHunkMemory() { // make sure the file system has allocated and "not" freed any temp blocks // this allows the config and product id files ( journal files too ) to be loaded // by the file system without redundant routines in the FS utilizing different memory systems if (FS_LoadStack() != 0) { Com_Error( ERR_FATAL, "Hunk initialization failed. File system load stack not zero" ); } // allocate the stack based hunk allocator const cvar_t* cv = Cvar_Get( "com_hunkMegs", XSTRING(DEF_COMHUNKMEGS), CVAR_LATCH | CVAR_ARCHIVE ); if (com_dedicated && com_dedicated->integer) Cvar_SetRange( "com_hunkMegs", CVART_INTEGER, XSTRING(MIN_COMHUNKMEGS_DED), "256" ); else Cvar_SetRange( "com_hunkMegs", CVART_INTEGER, XSTRING(MIN_COMHUNKMEGS), "256" ); int nMinAlloc; const char* s; if (com_dedicated && com_dedicated->integer) { nMinAlloc = MIN_COMHUNKMEGS_DED; s = "Minimum com_hunkMegs for a dedicated server is %i, allocating %i megs.\n"; } else { nMinAlloc = MIN_COMHUNKMEGS; s = "Minimum com_hunkMegs is %i, allocating %i megs.\n"; } if ( cv->integer < nMinAlloc ) { s_hunkTotal = 1024 * 1024 * nMinAlloc; Com_Printf( s, nMinAlloc, s_hunkTotal / (1024 * 1024) ); } else { s_hunkTotal = cv->integer * 1024 * 1024; } #if defined( _MSC_VER ) && defined( _DEBUG ) && defined( idx64 ) // try to allocate at the highest possible address range to help detect errors during development s_hunkData = (byte*)VirtualAlloc( NULL, ( s_hunkTotal + 4095 ) & ( ~4095 ), MEM_COMMIT | MEM_TOP_DOWN, PAGE_READWRITE ); Cvar_Get( "sys_hunkBaseAddress", va( "%p", s_hunkData ), CVAR_ROM ); #else s_hunkData = (byte*)calloc( s_hunkTotal + 63, 1 ); #endif if ( !s_hunkData ) { Com_Error( ERR_FATAL, "Hunk data failed to allocate %i megs", s_hunkTotal / (1024*1024) ); } // cacheline align s_hunkData = (byte *) ( ( (intptr_t)s_hunkData + 63 ) & ( ~63 ) ); Hunk_Clear(); Cmd_RegisterArray( hunk_cmds, MODULE_COMMON ); } int Hunk_MemoryRemaining() { int low = hunk_low.permanent > hunk_low.temp ? hunk_low.permanent : hunk_low.temp; int high = hunk_high.permanent > hunk_high.temp ? hunk_high.permanent : hunk_high.temp; return s_hunkTotal - ( low + high ); } // the server calls this after the level and game VM have been loaded void Hunk_SetMark() { hunk_low.mark = hunk_low.permanent; hunk_high.mark = hunk_high.permanent; } // the client calls this before starting a vid_restart or snd_restart void Hunk_ClearToMark() { hunk_low.permanent = hunk_low.temp = hunk_low.mark; hunk_high.permanent = hunk_high.temp = hunk_high.mark; } // the bot code uses this for no good reason FAICT qbool Hunk_CheckMark() { return ( hunk_low.mark || hunk_high.mark ); } // the server calls this before shutting down or loading a new map void Hunk_Clear() { #ifndef DEDICATED extern void CL_ShutdownCGame(); extern void CL_ShutdownUI(); #endif extern void SV_ShutdownGameProgs(); #ifndef DEDICATED CL_ShutdownCGame(); CL_ShutdownUI(); #endif SV_ShutdownGameProgs(); #ifndef DEDICATED CIN_CloseAllVideos(); #endif hunk_low.mark = 0; hunk_low.permanent = 0; hunk_low.temp = 0; hunk_low.tempHighwater = 0; hunk_high.mark = 0; hunk_high.permanent = 0; hunk_high.temp = 0; hunk_high.tempHighwater = 0; hunk_permanent = &hunk_low; hunk_temp = &hunk_high; VM_Clear(); #ifdef HUNK_DEBUG hunkblocks = NULL; #endif } static void Hunk_SwapBanks() { // can't swap banks if there is any temp already allocated if ( hunk_temp->temp != hunk_temp->permanent ) { return; } // if we have a larger highwater mark on this side, start making // our permanent allocations here and use the other side for temp if ( hunk_temp->tempHighwater - hunk_temp->permanent > hunk_permanent->tempHighwater - hunk_permanent->permanent ) { hunkUsed_t* swap = hunk_temp; hunk_temp = hunk_permanent; hunk_permanent = swap; } } // allocate permanent (until the hunk is cleared) memory #ifdef HUNK_DEBUG void *Hunk_AllocDebug( int size, ha_pref preference, char *label, char *file, int line ) #else void *Hunk_Alloc( int size, ha_pref preference ) #endif { if (!s_hunkData) { Com_Error( ERR_FATAL, "Hunk_Alloc: Hunk memory system not initialized" ); } // can't do preference if there is any temp allocated if (preference == h_dontcare || hunk_temp->temp != hunk_temp->permanent) { Hunk_SwapBanks(); } else { if (preference == h_low && hunk_permanent != &hunk_low) { Hunk_SwapBanks(); } else if (preference == h_high && hunk_permanent != &hunk_high) { Hunk_SwapBanks(); } } #ifdef HUNK_DEBUG size += sizeof(hunkblock_t); #endif // round to cacheline size = ( size + 63 ) & ( ~63 ); if ( hunk_low.temp + hunk_high.temp + size > s_hunkTotal ) { #ifdef HUNK_DEBUG Hunk_Log(); Hunk_SmallLog(); #endif Com_Error( ERR_DROP, "Hunk_Alloc failed on %i", size ); } void* buf; if ( hunk_permanent == &hunk_low ) { buf = (void *)(s_hunkData + hunk_permanent->permanent); hunk_permanent->permanent += size; } else { hunk_permanent->permanent += size; buf = (void *)(s_hunkData + s_hunkTotal - hunk_permanent->permanent ); } hunk_permanent->temp = hunk_permanent->permanent; Com_Memset( buf, 0, size ); #ifdef HUNK_DEBUG { hunkblock_t* block = (hunkblock_t*)buf; block->size = size - sizeof(hunkblock_t); block->file = file; block->label = label; block->line = line; block->next = hunkblocks; hunkblocks = block; buf = ((byte *) buf) + sizeof(hunkblock_t); } #endif return buf; } /* This is used by the file loading system. Multiple files can be loaded in temporary memory. When the files-in-use count reaches zero, all temp memory will be deleted */ void* Hunk_AllocateTempMemory( int size ) { // return a Z_Malloc'd block if the hunk has not been initialized // this allows the config and product id files ( journal files too ) to be loaded // by the file system without redundant routines in the FS utilizing different memory systems if (!s_hunkData) { return Z_Malloc(size); } Hunk_SwapBanks(); size = PAD(size, sizeof(intptr_t)) + sizeof( hunkHeader_t ); if ( hunk_temp->temp + hunk_permanent->permanent + size > s_hunkTotal ) { Com_Error( ERR_DROP, "Hunk_AllocateTempMemory: failed on %i", size ); } void* buf; if ( hunk_temp == &hunk_low ) { buf = (void *)(s_hunkData + hunk_temp->temp); hunk_temp->temp += size; } else { hunk_temp->temp += size; buf = (void *)(s_hunkData + s_hunkTotal - hunk_temp->temp ); } if ( hunk_temp->temp > hunk_temp->tempHighwater ) { hunk_temp->tempHighwater = hunk_temp->temp; } hunkHeader_t* hdr = (hunkHeader_t*)buf; buf = (void*)(hdr+1); hdr->magic = HUNK_MAGIC_INUSE; hdr->size = size; // don't bother clearing, because we are going to load a file over it return buf; } void Hunk_FreeTempMemory( void* buf ) { // free with Z_Free if the hunk has not been initialized // this allows the config and product id files ( journal files too ) to be loaded // by the file system without redundant routines in the FS utilizing different memory systems if (!s_hunkData) { Z_Free(buf); return; } hunkHeader_t* hdr = ( (hunkHeader_t *)buf ) - 1; if ( hdr->magic != HUNK_MAGIC_INUSE ) { Com_Error( ERR_FATAL, "Hunk_FreeTempMemory: bad magic" ); } hdr->magic = HUNK_MAGIC_FREED; // this only works if the files are freed in stack order, // otherwise the memory will stay around until Hunk_ClearTempMemory if ( hunk_temp == &hunk_low ) { if ( hdr == (void *)(s_hunkData + hunk_temp->temp - hdr->size ) ) { hunk_temp->temp -= hdr->size; } else { Com_Printf( "Hunk_FreeTempMemory: not the final block\n" ); } } else { if ( hdr == (void *)(s_hunkData + s_hunkTotal - hunk_temp->temp ) ) { hunk_temp->temp -= hdr->size; } else { Com_Printf( "Hunk_FreeTempMemory: not the final block\n" ); } } } /* The temp space is no longer needed. If we have left more touched but unused memory on this side, have future permanent allocs use this side. */ void Hunk_ClearTempMemory() { if ( s_hunkData ) { hunk_temp->temp = hunk_temp->permanent; } } /* =================================================================== EVENTS AND JOURNALING In addition to these events, .cfg files are also copied to the journaled file =================================================================== */ // FIXME TTimo blunt upping from 256 to 1024 // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=5 #define MAX_PUSHED_EVENTS 1024 static sysEvent_t com_pushedEvents[MAX_PUSHED_EVENTS]; static int com_pushedEventsHead; static int com_pushedEventsTail; static void Com_InitJournaling() { Com_StartupVariable( "journal" ); com_journal = Cvar_Get ("journal", "0", CVAR_INIT); if ( !com_journal->integer ) { return; } if ( com_journal->integer == 1 ) { Com_Printf( "Journaling events\n"); com_journalFile = FS_FOpenFileWrite( "journal.dat" ); com_journalDataFile = FS_FOpenFileWrite( "journaldata.dat" ); } else if ( com_journal->integer == 2 ) { Com_Printf( "Replaying journaled events\n"); FS_FOpenFileRead( "journal.dat", &com_journalFile, qtrue ); FS_FOpenFileRead( "journaldata.dat", &com_journalDataFile, qtrue ); } if ( !com_journalFile || !com_journalDataFile ) { Cvar_Set( "com_journal", "0" ); com_journalFile = 0; com_journalDataFile = 0; Com_Printf( "Couldn't open journal files\n" ); } } static sysEvent_t Com_GetRealEvent() { int r; sysEvent_t ev; // get an event from either the system or the journal file if ( com_journal->integer == 2 ) { r = FS_Read( &ev, sizeof(ev), com_journalFile ); if ( r != sizeof(ev) ) { Com_Error( ERR_FATAL, "Error reading from journal file" ); } if ( ev.evPtrLength ) { ev.evPtr = Z_Malloc( ev.evPtrLength ); r = FS_Read( ev.evPtr, ev.evPtrLength, com_journalFile ); if ( r != ev.evPtrLength ) { Com_Error( ERR_FATAL, "Error reading from journal file" ); } } } else { ev = Sys_GetEvent(); // write the journal value out if needed if ( com_journal->integer == 1 ) { r = FS_Write( &ev, sizeof(ev), com_journalFile ); if ( r != sizeof(ev) ) { Com_Error( ERR_FATAL, "Error writing to journal file" ); } if ( ev.evPtrLength ) { r = FS_Write( ev.evPtr, ev.evPtrLength, com_journalFile ); if ( r != ev.evPtrLength ) { Com_Error( ERR_FATAL, "Error writing to journal file" ); } } } } return ev; } static void Com_PushEvent( const sysEvent_t* event ) { static qbool printedWarning = qfalse; sysEvent_t* ev = &com_pushedEvents[ com_pushedEventsHead & (MAX_PUSHED_EVENTS-1) ]; if ( com_pushedEventsHead - com_pushedEventsTail >= MAX_PUSHED_EVENTS ) { // don't print the warning constantly, or it can give time for more... if ( !printedWarning ) { printedWarning = qtrue; Com_Printf( "WARNING: Com_PushEvent overflow\n" ); } if ( ev->evPtr ) { Z_Free( ev->evPtr ); } com_pushedEventsTail++; } else { printedWarning = qfalse; } *ev = *event; com_pushedEventsHead++; } static sysEvent_t Com_GetEvent() { if ( com_pushedEventsHead > com_pushedEventsTail ) { com_pushedEventsTail++; return com_pushedEvents[ (com_pushedEventsTail-1) & (MAX_PUSHED_EVENTS-1) ]; } return Com_GetRealEvent(); } static void Com_RunAndTimeServerPacket( const netadr_t& from, msg_t* msg ) { int t1 = (com_speeds->integer == 3) ? Sys_Milliseconds() : 0; SV_PacketEvent( from, msg ); if (com_speeds->integer == 3) { int ms = Sys_Milliseconds() - t1; Com_Printf( "SV_PacketEvent time: %i\n", ms ); } } // returns last event time int Com_EventLoop() { sysEvent_t ev; netadr_t evFrom; byte bufData[MAX_MSGLEN]; msg_t buf; MSG_Init( &buf, bufData, sizeof( bufData ) ); while ( 1 ) { NET_FlushPacketQueue(); ev = Com_GetEvent(); // if no more events are available if ( ev.evType == SE_NONE ) { // manually send packet events for the loopback channel #ifndef DEDICATED while ( NET_GetLoopPacket( NS_CLIENT, &evFrom, &buf ) ) { CL_PacketEvent( evFrom, &buf ); } #endif while ( NET_GetLoopPacket( NS_SERVER, &evFrom, &buf ) ) { // if the server just shut down, flush the events if ( com_sv_running->integer ) { Com_RunAndTimeServerPacket( evFrom, &buf ); } } return ev.evTime; } switch ( ev.evType ) { default: Com_Error( ERR_FATAL, "Com_EventLoop: bad event type %i", ev.evType ); break; case SE_NONE: break; #ifndef DEDICATED case SE_KEY: CL_KeyEvent( ev.evValue, (ev.evValue2 != 0), ev.evTime ); break; case SE_CHAR: CL_CharEvent( ev.evValue ); break; case SE_MOUSE: CL_MouseEvent( ev.evValue, ev.evValue2, ev.evTime ); break; case SE_JOYSTICK_AXIS: CL_JoystickEvent( ev.evValue, ev.evValue2, ev.evTime ); break; #endif case SE_CONSOLE: Cbuf_AddText( (char *)ev.evPtr ); Cbuf_AddText( "\n" ); break; case SE_PACKET: evFrom = *(netadr_t *)ev.evPtr; buf.cursize = ev.evPtrLength - sizeof( evFrom ); // we must copy the contents of the message out, because // the event buffers are only large enough to hold the // exact payload, but channel messages need to be large // enough to hold fragment reassembly if ( (unsigned)buf.cursize > buf.maxsize ) { Com_Printf("Com_EventLoop: oversize packet\n"); continue; } Com_Memcpy( buf.data, (byte *)((netadr_t *)ev.evPtr + 1), buf.cursize ); if ( com_sv_running->integer ) { Com_RunAndTimeServerPacket( evFrom, &buf ); } else { #ifndef DEDICATED CL_PacketEvent( evFrom, &buf ); #endif } break; } // free any block data if ( ev.evPtr ) { Z_Free( ev.evPtr ); } } return 0; // never reached } // can be used for profiling, but will be journaled accurately int Com_Milliseconds() { sysEvent_t ev; // get events and push them until we get a null event with the current time do { ev = Com_GetRealEvent(); if ( ev.evType != SE_NONE ) { Com_PushEvent( &ev ); } } while ( ev.evType != SE_NONE ); return ev.evTime; } /////////////////////////////////////////////////////////////// #if defined(DEBUG) || defined(CNQ3_DEV) // throw a fatal error to test error shutdown procedures static void Com_Error_f( void ) { if ( Cmd_Argc() > 1 ) { Com_Error( ERR_DROP, "Testing drop error" ); } else { Com_Error( ERR_FATAL, "Testing fatal error" ); } } // freeze in place for a given number of seconds to test error recovery static void Com_Freeze_f( void ) { if ( Cmd_Argc() != 2 ) { Com_Printf( "freeze \n" ); return; } float s = atof( Cmd_Argv(1) ); int start = Com_Milliseconds(); while ( ( Com_Milliseconds() - start ) / 1000.0f < s ) ; } static void Com_Exit_f( void ) { exit( 666 ); } // force a bus error for development reasons static void Com_Crash_f( void ) { *(int*)0 = 0x12345678; } #endif // TTimo: centralizing the cl_cdkey stuff after I discovered a buffer overflow problem with the dedicated server version // not sure it's necessary to have different defaults for regular and dedicated, but I don't want to risk it // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=470 #ifndef DEDICATED char cl_cdkey[34] = " "; #else char cl_cdkey[34] = "123456789"; #endif #ifndef DEDICATED /* ================= Com_ReadCDKey ================= */ qbool CL_CDKeyValidate( const char *key, const char *checksum ); void Com_ReadCDKey( const char *filename ) { fileHandle_t f; char buffer[33]; char fbuffer[MAX_OSPATH]; sprintf(fbuffer, "%s/q3key", filename); FS_SV_FOpenFileRead( fbuffer, &f ); if ( !f ) { Q_strncpyz( cl_cdkey, " ", 17 ); return; } Com_Memset( buffer, 0, sizeof(buffer) ); FS_Read( buffer, 16, f ); FS_FCloseFile( f ); if (CL_CDKeyValidate(buffer, NULL)) { Q_strncpyz( cl_cdkey, buffer, 17 ); } else { Q_strncpyz( cl_cdkey, " ", 17 ); } } /* ================= Com_AppendCDKey ================= */ void Com_AppendCDKey( const char *filename ) { fileHandle_t f; char buffer[33]; char fbuffer[MAX_OSPATH]; sprintf(fbuffer, "%s/q3key", filename); FS_SV_FOpenFileRead( fbuffer, &f ); if (!f) { Q_strncpyz( &cl_cdkey[16], " ", 17 ); return; } Com_Memset( buffer, 0, sizeof(buffer) ); FS_Read( buffer, 16, f ); FS_FCloseFile( f ); if (CL_CDKeyValidate(buffer, NULL)) { strcat( &cl_cdkey[16], buffer ); } else { Q_strncpyz( &cl_cdkey[16], " ", 17 ); } } #else void Com_AppendCDKey( const char *filename ) { } void Com_ReadCDKey( const char *filename ) { } #endif // 0=eax 1=ebx 2=ecx 3=edx static qbool Com_CPUID( int function, int registers[4] ) { #if MSVC_CPUID __cpuid( registers, function ); return qtrue; #elif GCC_CPUID if( __get_cpuid( (unsigned int)function, (unsigned int*)®isters[0], (unsigned int*)®isters[1], (unsigned int*)®isters[2], (unsigned int*)®isters[3] ) != 1 ) return qfalse; return qtrue; #else return qfalse; #endif } static const char* Com_ProcessorName() { static int regs[4]; if( !Com_CPUID( 0, regs) ) { return NULL; } regs[0] = regs[1]; regs[1] = regs[3]; regs[3] = 0; return (const char*)regs; } typedef struct { const char* s; int reg; int bit; int flag; qbool noTest; } cpuFeatureBit_t; #if idx64 #define IS_X64 qtrue #define BASIC_CPU_FEATURES (CPU_MMX | CPU_SSE | CPU_SSE2) #else #define IS_X64 qfalse #define BASIC_CPU_FEATURES 0 #endif static const cpuFeatureBit_t cpu_featureBits[] = { { " MMX", 3, 23, CPU_MMX, IS_X64 }, { " SSE", 3, 25, CPU_SSE, IS_X64 }, { " SSE2", 3, 26, CPU_SSE2, IS_X64 }, // the following aren't used anywhere for now: // { " SSE3", 2, 0, CPU_SSE3, qfalse }, // { " SSSE3", 2, 9, CPU_SSSE3, qfalse }, // { " SSE4.1", 2, 19, CPU_SSE41, qfalse }, // { " SSE4.2", 2, 20, CPU_SSE42, qfalse }, // { " AVX", 2, 28, CPU_AVX, qfalse } // for AVX2 and later, you'd need to call cpuid with eax=7 and ecx=0 ("extended features") }; int cpu_features = BASIC_CPU_FEATURES; static qbool Com_GetProcessorInfo() { Cvar_Get( "sys_cpustring", "unknown", 0 ); int regs[4]; const char* name = Com_ProcessorName(); if ( name == NULL || !Com_CPUID( 1, regs ) ) { cpu_features = BASIC_CPU_FEATURES; return qfalse; } char s[256] = ""; Q_strcat( s, sizeof(s), name ); int features = BASIC_CPU_FEATURES; for (int i = 0; i < ARRAY_LEN(cpu_featureBits); i++) { const cpuFeatureBit_t* f = cpu_featureBits + i; if ( f->noTest || (regs[f->reg] & (1 << f->bit)) ) { Q_strcat( s, sizeof(s), f->s ); features |= f->flag; } } cpu_features = features; Cvar_Set( "sys_cpustring", s ); return qtrue; } #undef BASIC_CPU_FEATURES #undef IS_X64 static const cmdTableItem_t com_cmds[] = { #if defined(DEBUG) || defined(CNQ3_DEV) { "error", Com_Error_f }, { "crash", Com_Crash_f }, { "freeze", Com_Freeze_f }, { "exit", Com_Exit_f }, #endif { "quit", Com_Quit_f, NULL, "closes the application" }, { "writeconfig", Com_WriteConfig_f, Com_CompleteWriteConfig_f, "write the cvars and key binds to a file" } }; static const cvarTableItem_t com_cvars[] = { { &com_maxfps, "com_maxfps", "125", CVAR_ARCHIVE, CVART_INTEGER, "60", "250", help_com_maxfps }, { &com_developer, "developer", "0", CVAR_TEMP, CVART_BOOL, NULL, NULL, "enables detailed logging" }, { &com_logfile, "logfile", "0", CVAR_TEMP, CVART_INTEGER, "0", "2", help_com_logfile }, { &com_timescale, "timescale", "1", CVAR_CHEAT | CVAR_SYSTEMINFO, CVART_FLOAT, "0.1", "100", "game time to real time ratio" }, { &com_fixedtime, "fixedtime", "0", CVAR_CHEAT, CVART_INTEGER, "1", "64", "fixed number of ms per simulation tick" }, { &com_showtrace, "com_showtrace", "0", CVAR_CHEAT, CVART_BOOL, NULL, NULL, "prints trace optimization info" }, { &com_viewlog, "viewlog", "0", CVAR_CHEAT, CVART_INTEGER, "0", "2", help_com_viewlog }, { &com_speeds, "com_speeds", "0", 0, CVART_BOOL, NULL, NULL, "prints timing info" }, { &com_timedemo, "timedemo", "0", CVAR_CHEAT, CVART_BOOL, NULL, NULL, "benchmarking mode for demo playback" }, { &cl_paused, "cl_paused", "0", CVAR_ROM, CVART_BOOL }, { &sv_paused, "sv_paused", "0", CVAR_ROM, CVART_BOOL }, { &cl_packetdelay, "cl_packetdelay", "0", CVAR_CHEAT, CVART_INTEGER, "0", NULL }, { &sv_packetdelay, "sv_packetdelay", "0", CVAR_CHEAT, CVART_INTEGER, "0", NULL }, { &com_sv_running, "sv_running", "0", CVAR_ROM, CVART_BOOL }, { &com_cl_running, "cl_running", "0", CVAR_ROM, CVART_BOOL }, #if defined(_WIN32) && defined(_DEBUG) { &com_noErrorInterrupt, "com_noErrorInterrupt", "0", 0, CVART_BOOL }, #endif { &con_completionStyle, "con_completionStyle", "0", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, help_con_completionStyle }, { &con_history, "con_history", "1", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, "writes the command history to a file on exit" } }; #if defined(_MSC_VER) #pragma warning (disable: 4611) // setjmp + destructors = bad. which it is, but... #endif void Com_Init( char *commandLine ) { Com_Printf( "%s %s %s\n", Q3_VERSION, PLATFORM_STRING, __DATE__ ); if ( setjmp(abortframe) ) { Sys_Error ("Error during initialization"); } memset( com_pushedEvents, 0, sizeof(com_pushedEvents) ); com_pushedEventsHead = 0; com_pushedEventsTail = 0; Com_InitSmallZoneMemory(); Cvar_Init(); // prepare enough of the subsystems to handle // cvar and command buffer management Com_ParseCommandLine( commandLine ); Cbuf_Init(); Com_InitZoneMemory(); Cmd_Init(); // override anything from the config files with command line args Com_StartupVariable( NULL ); // get the developer cvar set as early as possible Com_StartupVariable( "developer" ); // done early so bind command exists #ifndef DEDICATED CL_InitKeyCommands(); #endif FS_InitFilesystem(); Com_InitJournaling(); Cbuf_AddText("exec default.cfg\n"); // skip the q3config.cfg if "safe" is on the command line if ( !Com_SafeMode() ) Cbuf_AddText("exec q3config.cfg\n"); Cbuf_AddText("exec autoexec.cfg\n"); Cbuf_Execute(); // override anything from the config files with command line args Com_StartupVariable( NULL ); // get dedicated here for proper hunk megs initialization #ifdef DEDICATED com_dedicated = Cvar_Get("dedicated", "1", CVAR_ROM); #else com_dedicated = Cvar_Get("dedicated", "0", CVAR_LATCH); #endif // allocate the stack based hunk allocator Com_InitHunkMemory(); // if any archived cvars are modified after this, we will trigger a writing // of the config file cvar_modifiedFlags &= ~CVAR_ARCHIVE; // // init commands and vars // Cvar_RegisterArray( com_cvars, MODULE_COMMON ); if ( com_dedicated->integer ) { if ( !com_viewlog->integer ) { Cvar_Set( "viewlog", "1" ); } } Cmd_RegisterArray( com_cmds, MODULE_COMMON ); const char* s = Q3_VERSION" "PLATFORM_STRING" "__DATE__; com_version = Cvar_Get( "version", s, CVAR_ROM | CVAR_SERVERINFO ); Cvar_Get( "sys_cpustring", "detect", 0 ); if ( Com_GetProcessorInfo() ) { Com_Printf( "CPU: %s\n", Cvar_VariableString( "sys_cpustring" ) ); } Sys_Init(); Netchan_Init( Com_Milliseconds() & 0xffff ); // pick a port value that should be nice and random VM_Init(); SV_Init(); com_dedicated->modified = qfalse; if ( !com_dedicated->integer ) { #ifndef DEDICATED CL_Init(); #endif } Sys_LoadHistory(); // set com_frameTime so that if a map is started on the // command line it will still be able to count on com_frameTime // being random enough for a serverid com_frameTime = Com_Milliseconds(); // add + commands from command line Com_AddStartupCommands(); // start in full screen ui mode Cvar_Set( "r_uiFullScreen", "1" ); #ifndef DEDICATED CL_StartHunkUsers(); #endif // moved to fix the console window staying visible when starting the game in full-screen mode if ( !com_dedicated->integer ) Sys_ShowConsole( com_viewlog->integer, qfalse ); // make sure single player is off by default Cvar_Set( "sv_singlePlayer", "0" ); com_fullyInitialized = qtrue; Com_Printf ("--- Common Initialization Complete ---\n"); } //================================================================== static void Com_WriteConfigToFile( const char* filename ) { fileHandle_t f = FS_FOpenFileWrite( filename ); if ( !f ) { Com_Printf ("Couldn't write %s.\n", filename ); return; } FS_Printf(f, "// generated by quake, do not modify\n"); #ifndef DEDICATED Key_WriteBindings(f); #endif Cvar_WriteVariables(f); FS_FCloseFile( f ); } // write the config file to a specific name static void Com_WriteConfig_f() { if ( Cmd_Argc() != 2 ) { Com_Printf( "Usage: writeconfig \n" ); return; } char filename[MAX_QPATH]; Q_strncpyz( filename, Cmd_Argv(1), sizeof( filename ) ); COM_DefaultExtension( filename, sizeof( filename ), ".cfg" ); Com_Printf( "Writing %s.\n", filename ); Com_WriteConfigToFile( filename ); } static void Com_CompleteWriteConfig_f( int startArg, int compArg ) { if ( startArg + 1 == compArg ) Field_AutoCompleteCustom( startArg, compArg, &Field_AutoCompleteConfigName ); } static int Com_ModifyMsec( int msec ) { // modify time for debugging values // if ( com_fixedtime->integer ) { msec = com_fixedtime->integer; } else if ( com_timescale->value ) { msec *= com_timescale->value; } // don't let it scale below 1 msec if ( msec < 1 && com_timescale->value) { msec = 1; } int clampTime; if ( com_dedicated->integer ) { // dedicated servers don't want to clamp for too long a period // because it would mess up all the client's views of time if ( msec > 500 ) { Com_Printf( "Hitch warning: %i msec frame time\n", msec ); } clampTime = 5000; } else if ( !com_sv_running->integer ) { // clients of remote servers do not want to clamp time, because // it would skew their view of the server's time temporarily clampTime = 5000; } else { // for local single player gaming // we may want to clamp the time to prevent players from // flying off edges when something hitches. clampTime = 200; } if ( msec > clampTime ) { msec = clampTime; } return msec; } void Com_Frame() { if ( setjmp(abortframe) ) { return; // an ERR_DROP was thrown } // bk001204 - init to zero. // also: might be clobbered by `longjmp' or `vfork' int timeBeforeFirstEvents =0; int timeBeforeServer =0; int timeBeforeEvents =0; int timeBeforeClient = 0; int timeAfter = 0; // if "viewlog" has been modified, show or hide the log console if ( com_viewlog->modified ) { if ( !com_dedicated->value ) { Sys_ShowConsole( com_viewlog->integer, qfalse ); } com_viewlog->modified = qfalse; } // // main event loop // if ( com_speeds->integer ) { timeBeforeFirstEvents = Sys_Milliseconds(); } // we may want to spin here if things are going too fast int minMsec = 1; if ( !com_dedicated->integer && com_maxfps->integer > 0 && !com_timedemo->integer ) minMsec = 1000 / com_maxfps->integer; #ifndef DEDICATED // let's not limit the download speed by sleeping too much qbool CL_MapDownload_Active(); // in client.h if ( CL_MapDownload_Active() ) minMsec = 1; #endif static int lastTime = 0; int msec; do { com_frameTime = Com_EventLoop(); msec = com_frameTime - lastTime; } while ( msec < minMsec ); lastTime = com_frameTime; Cbuf_Execute(); // mess with msec if needed msec = Com_ModifyMsec( msec ); // // server side // if ( com_speeds->integer ) { timeBeforeServer = Sys_Milliseconds(); } SV_Frame( msec ); // if "dedicated" has been modified, start up // or shut down the client system. // Do this after the server may have started, // but before the client tries to auto-connect if ( com_dedicated->modified ) { // get the latched value Cvar_Get( "dedicated", "0", 0 ); com_dedicated->modified = qfalse; if ( !com_dedicated->integer ) { #ifndef DEDICATED CL_Init(); #endif Sys_ShowConsole( com_viewlog->integer, qfalse ); } else { #ifndef DEDICATED CL_Shutdown(); #endif Sys_ShowConsole( 1, qtrue ); } } #ifdef DEDICATED if ( com_speeds->integer ) { timeAfter = Sys_Milliseconds(); timeBeforeEvents = timeAfter; timeBeforeClient = timeAfter; } #else // // client system // if ( !com_dedicated->integer ) { // // run event loop a second time to get server to client packets // without a frame of latency // if ( com_speeds->integer ) { timeBeforeEvents = Sys_Milliseconds(); } Com_EventLoop(); Cbuf_Execute(); // // client side // if ( com_speeds->integer ) { timeBeforeClient = Sys_Milliseconds(); } CL_Frame( msec ); if ( com_speeds->integer ) { timeAfter = Sys_Milliseconds(); } } #endif // // report timing information // if ( com_speeds->integer ) { int all = timeAfter - timeBeforeServer; int sv = timeBeforeEvents - timeBeforeServer - time_game; int ev = timeBeforeServer - timeBeforeFirstEvents + timeBeforeClient - timeBeforeEvents; int cl = timeAfter - timeBeforeClient - (time_frontend + time_backend); Com_Printf( "frame:%i all:%3i sv:%3i ev:%3i cl:%3i gm:%3i rf:%3i bk:%3i\n", com_frameNumber, all, sv, ev, cl, time_game, time_frontend, time_backend ); } // // trace optimization tracking // if ( com_showtrace->integer ) { extern int c_traces, c_brush_traces, c_patch_traces, c_pointcontents; Com_Printf( "%4i traces (%ib %ip) %4i points\n", c_traces, c_brush_traces, c_patch_traces, c_pointcontents ); c_traces = 0; c_brush_traces = 0; c_patch_traces = 0; c_pointcontents = 0; } com_frameNumber++; } void Com_Shutdown() { if (logfile) { FS_FCloseFile( logfile ); logfile = 0; } if ( com_journalFile ) { FS_FCloseFile( com_journalFile ); com_journalFile = 0; } } /////////////////////////////////////////////////////////////// /* ===================== KHB !!! see if this is still true - even vc might actually have a bug fixed in 8 years :P the msvc acos doesn't always return a value between -PI and PI: int i; i = 1065353246; acos(*(float*) &i) == -1.#IND0 This should go in q_math but it is too late to add new traps to game and ui ===================== */ float Q_acos(float c) { float angle = acos(c); if (angle > M_PI) { return (float)M_PI; } if (angle < -M_PI) { return (float)M_PI; } return angle; } static unsigned int CRC32_table[256]; static qbool CRC32_tableCreated = qfalse; void CRC32_Begin( unsigned int* crc ) { if ( !CRC32_tableCreated ) { for ( int i = 0; i < 256; i++ ) { unsigned int c = i; for ( int j = 0; j < 8; j++ ) c = c & 1 ? (c >> 1) ^ 0xEDB88320UL : c >> 1; CRC32_table[i] = c; } CRC32_tableCreated = qtrue; } *crc = 0xFFFFFFFFUL; } void CRC32_ProcessBlock( unsigned int* crc, const void* buffer, unsigned int length ) { unsigned int hash = *crc; const unsigned char* buf = (const unsigned char*)buffer; while ( length-- ) { hash = CRC32_table[(hash ^ *buf++) & 0xFF] ^ (hash >> 8); } *crc = hash; } void CRC32_End( unsigned int* crc ) { *crc ^= 0xFFFFFFFFUL; } /* =========================================== command line completion =========================================== */ void Field_Clear( field_t* edit ) { memset(edit->buffer, 0, MAX_EDIT_LINE); edit->cursor = 0; edit->scroll = 0; } static const char* completionString; static char shortestMatch[MAX_TOKEN_CHARS]; static char fullMatch[MAX_TOKEN_CHARS]; static int matchCount; static int matchIndex; static qbool findIndexOnly; // for ET-style completion of command arguments static field_t* completionField; // field we are working on, passed to Field_AutoComplete static void FindMatches( const char *s ) { int i; if ( Q_stricmpn( s, completionString, strlen( completionString ) ) ) { return; } matchCount++; if ( matchCount == 1 ) { Q_strncpyz( shortestMatch, s, sizeof( shortestMatch ) ); return; } // cut shortestMatch to the amount common with s for ( i = 0 ; shortestMatch[i] ; i++ ) { if ( i >= strlen( s ) ) { shortestMatch[i] = 0; break; } if ( tolower(shortestMatch[i]) != tolower(s[i]) ) { shortestMatch[i] = 0; } } } static int findMatchIndex; static void FindIndexMatch( const char *s ) { if ( Q_stricmpn( s, completionString, strlen( completionString ) ) ) { return; } if ( findMatchIndex == matchIndex ) { Q_strncpyz( fullMatch, s, sizeof( fullMatch ) ); } findMatchIndex++; } static void PrintMatches( const char *s ) { if ( !Q_stricmpn( s, shortestMatch, strlen( shortestMatch ) ) ) { Com_Printf( " %s\n", s ); } } static void PrintCmdMatches( const char *s ) { if ( Q_stricmpn( s, shortestMatch, strlen( shortestMatch ) ) ) return; char msg[CONSOLE_WIDTH * 2]; // account for lots of color codes const char* desc; const char* help; Cmd_GetHelp( &desc, &help, s ); const char h = help != NULL ? 'h' : ' '; if ( desc ) Com_sprintf( msg, sizeof(msg), " %c "COLOR_CMD"%s - "COLOR_HELP"%s\n", h, s, desc ); else Com_sprintf( msg, sizeof(msg), " %c "COLOR_CMD"%s\n", h, s ); Com_TruncatePrintString( msg, sizeof(msg), CONSOLE_WIDTH ); Com_Printf( msg ); } static void PrintCvarMatches( const char *s ) { if ( Q_stricmpn( s, shortestMatch, strlen( shortestMatch ) ) ) return; char msg[CONSOLE_WIDTH * 2]; // account for lots of color codes const char* desc; const char* help; Cvar_GetHelp( &desc, &help, s ); const char h = help != NULL ? 'h' : ' '; const char u = ( Cvar_Flags(s) & CVAR_USER_CREATED ) != 0 ? '?' : h; if ( desc ) Com_sprintf( msg, sizeof(msg), " %c "COLOR_CVAR"%s^7 = \""COLOR_VAL"%s^7\" - "COLOR_HELP"%s\n", u, s, Cvar_VariableString( s ), desc ); else Com_sprintf( msg, sizeof(msg), " %c "COLOR_CVAR"%s^7 = \""COLOR_VAL"%s^7\"\n", u, s, Cvar_VariableString( s ) ); Com_TruncatePrintString( msg, sizeof(msg), CONSOLE_WIDTH ); Com_Printf( msg ); } static void Field_AppendArgString( field_t *field, int arg, const char *s ) { const qbool quoted = Cmd_ArgQuoted( arg ); if ( quoted ) Q_strcat( field->buffer, sizeof( field->buffer ), "\"" ); Q_strcat( field->buffer, sizeof( field->buffer ), s ); if ( quoted ) Q_strcat( field->buffer, sizeof( field->buffer ), "\"" ); } static void Field_AppendArg( field_t *field, int arg ) { Field_AppendArgString( field, arg, Cmd_Argv( arg ) ); } static void Field_AppendLastArgs( field_t *field, int startArg ) { const int argc = Cmd_Argc(); for ( int i = startArg; i < argc; ++i ) { Q_strcat( field->buffer, sizeof( field->buffer ), " " ); Field_AppendArg( field, i ); } } static void Field_AppendFirstArgs( field_t *field, int count ) { if ( count <= 0 ) return; Field_AppendArg( field, 0 ); for ( int i = 1; i < count; ++i ) { Q_strcat( field->buffer, sizeof( field->buffer ), " " ); Field_AppendArg( field, i ); } } static qbool String_HasLeadingSlash( const char *arg ) { return ((*arg == '\\') || (*arg == '/')); } // returns qtrue if the match list should be printed static qboolean Field_CompleteShortestMatch( int startArg, int compArg ) { if ( matchCount == 0 ) return qfalse; field_t *const field = completionField; // write the first part of the field *field->buffer = '\0'; if ( compArg > 0 ) { Field_AppendFirstArgs( field, compArg ); Q_strcat( field->buffer, sizeof( field->buffer ), " " ); } Field_AppendArgString( field, compArg, shortestMatch ); if ( matchCount == 1 ) { // finish the field with the only match if ( compArg == Cmd_Argc() - 1 ) Q_strcat( field->buffer, sizeof( field->buffer ), " " ); field->cursor = strlen( field->buffer ); Field_AppendLastArgs( field, compArg + 1 ); return qfalse; } // finish the field with the shortest match and echo the command field->cursor = strlen( field->buffer ); if ( Cmd_ArgQuoted( compArg ) ) field->cursor--; Field_AppendLastArgs( field, compArg + 1 ); Com_Printf( "]%s\n", Cmd_Cmd() ); return qtrue; } static void Field_AutoCompleteCmdOrVarName( int startArg, int compArg, qbool searchCmds, qbool searchVars ) { if ( !searchCmds && !searchVars ) return; field_t* const field = completionField; if ( field->acOffset > 0 ) { if ( matchCount > 1 ) { // find the next match completionString = shortestMatch; findMatchIndex = 0; if ( searchCmds ) Cmd_CommandCompletion( FindIndexMatch ); if ( searchVars ) Cvar_CommandCompletion( FindIndexMatch ); matchIndex = ( matchIndex + 1 ) % matchCount; // insert it in the edit field if ( compArg == 0 ) { Q_strncpyz( field->buffer, fullMatch, sizeof(field->buffer) ); field->cursor = strlen( field->buffer ); Field_AppendLastArgs( field, 1 ); } else { field->buffer[0] = '\0'; Field_AppendFirstArgs( field, compArg ); Q_strcat( field->buffer, sizeof(field->buffer), " " ); Q_strcat( field->buffer, sizeof(field->buffer), fullMatch ); field->cursor = strlen( field->buffer ); Field_AppendLastArgs( field, compArg + 1 ); } const int delta = String_HasLeadingSlash( field->buffer ) ? 0 : 1; field->acLength = field->cursor + delta - field->acOffset; } return; } *shortestMatch = '\0'; matchCount = 0; matchIndex = 0; if ( searchCmds ) Cmd_CommandCompletion( FindMatches ); if ( searchVars ) Cvar_CommandCompletion( FindMatches ); if ( !Field_CompleteShortestMatch( startArg, compArg ) ) return; // we found 2+ matches if ( con_completionStyle->integer ) { const int delta = String_HasLeadingSlash( field->buffer ) ? 0 : 1; field->acStartArg = startArg; field->acCompArg = compArg; field->acOffset = field->cursor + delta; field->acLength = 0; } if ( searchCmds ) Cmd_CommandCompletion( PrintCmdMatches ); if ( searchVars ) Cvar_CommandCompletion( PrintCvarMatches ); } static void Field_AutoCompleteCommandArgument( int startArg, int compArg ) { field_t* const field = completionField; if ( field->acOffset == 0 ) { *shortestMatch = '\0'; matchCount = 0; matchIndex = 0; } const char* cmdName = Cmd_Argv( startArg ); if ( String_HasLeadingSlash( cmdName ) ) cmdName++; if ( *cmdName == '\0' ) return; if ( field->acStartArg == startArg && field->acOffset > 0 ) { if ( matchCount > 1 ) { // find the next match completionString = shortestMatch; findMatchIndex = 0; findIndexOnly = qtrue; Cmd_AutoCompleteArgument( cmdName, startArg, compArg ); findIndexOnly = qfalse; matchIndex = ( matchIndex + 1 ) % matchCount; // insert it in the edit field field->buffer[0] = '\0'; Field_AppendFirstArgs( field, compArg ); Q_strcat( field->buffer, sizeof(field->buffer), " " ); Q_strcat( field->buffer, sizeof(field->buffer), fullMatch ); field->cursor = strlen( field->buffer ); Field_AppendLastArgs( field, compArg + 1 ); const int delta = String_HasLeadingSlash( field->buffer ) ? 0 : 1; field->acLength = field->cursor + delta - field->acOffset; } return; } Cmd_AutoCompleteArgument( cmdName, startArg, compArg ); // we found 2+ matches if ( field->acOffset == 0 && matchCount >= 2 && con_completionStyle->integer ) { const int delta = String_HasLeadingSlash( field->buffer ) ? 0 : 1; field->acStartArg = startArg; field->acCompArg = compArg; field->acOffset = field->cursor + delta; field->acLength = 0; } } void Field_AutoCompleteFrom( int startArg, int compArg, qbool searchCmds, qbool searchVars ) { // For the first argument, we always check both variables and commands. // For other arguments, we run a custom auto-completion handler // registered by the command if one was provided. if ( compArg == startArg ) { Field_AutoCompleteCmdOrVarName( startArg, compArg, searchCmds, searchVars ); } else { Field_AutoCompleteCommandArgument( startArg, compArg ); } } static void Field_AddLeadingSlash( field_t *field ) { const size_t length = strlen( field->buffer ); if ( length + 1 < sizeof( field->buffer ) ) { memmove( field->buffer + 1, field->buffer, length + 1 ); *field->buffer = '\\'; field->cursor++; } } // runs the auto-completion but doesn't do the final leading slash and cursor position fix-ups // returns qtrue if auto-completion was actually run on an argument static qbool Field_AutoCompleteNoLeadingSlash( field_t *field ) { completionField = field; // first, decide which argument we're going to run completion on Cmd_TokenizeString( field->buffer ); const int compArg = Cmd_Argc() == 1 ? 0 : Cmd_ArgIndexFromOffset( field->cursor ); if ( compArg < 0 || compArg >= Cmd_Argc() ) return qfalse; // now select the actual string that needs completing completionString = Cmd_Argv( compArg ); #ifndef DEDICATED if ( compArg == 0 && String_HasLeadingSlash( completionString ) ) completionString++; #endif if ( *completionString == '\0' ) return qfalse; Field_AutoCompleteFrom( 0, compArg, qtrue, qtrue ); // get rid of any superfluous space between arguments if ( matchCount == 0 ) { *field->buffer = '\0'; Field_AppendFirstArgs( field, Cmd_Argc() ); field->cursor = strlen( field->buffer ); } return qtrue; } void Field_AutoComplete( field_t *field, qbool insertBackslash ) { const qbool ranComp = Field_AutoCompleteNoLeadingSlash( field ); const qbool hadSlash = String_HasLeadingSlash( field->buffer ); if ( !hadSlash && insertBackslash ) Field_AddLeadingSlash( field ); if ( ranComp ) return; const int argc = Cmd_Argc(); if ( argc > 0 ) { // keep the whitespace and clamp the cursor to 1 past the last argument const int offset = Cmd_ArgOffset( argc - 1 ); const int length = strlen( Cmd_Argv( argc - 1 ) ); const int max = offset + length + 1 + ( hadSlash ? 0 : 1 ); if ( field->cursor > max ) field->cursor = max; } else { // the input line is pure whitespace so we rewrite it Q_strncpyz ( field->buffer, insertBackslash ? "\\" : "", sizeof( field->buffer ) ); field->cursor = strlen( field->buffer ); } } void Field_AutoCompleteCustom( int startArg, int compArg, fieldCompletionHandler_t callback ) { if ( findIndexOnly ) { ( *callback )( FindIndexMatch ); return; } ( *callback )( FindMatches ); if ( Field_CompleteShortestMatch( startArg, compArg ) ) ( *callback )( PrintMatches ); } void Field_AutoCompleteMapName( fieldCallback_t callback ) { FS_FilenameCompletion( "maps", "bsp", qtrue, callback, 0 ); } void Field_AutoCompleteConfigName( fieldCallback_t callback ) { FS_FilenameCompletion( "", "cfg", qtrue, callback, FS_FILTER_INPAK ); } void Field_AutoCompleteDemoNameRead( fieldCallback_t callback ) { FS_FilenameCompletion( "demos", "dm_66", qtrue, callback, 0 ); FS_FilenameCompletion( "demos", "dm_67", qtrue, callback, 0 ); FS_FilenameCompletion( "demos", "dm_68", qtrue, callback, 0 ); } void Field_AutoCompleteDemoNameWrite( fieldCallback_t callback ) { FS_FilenameCompletion( "demos", "dm_68", qtrue, callback, FS_FILTER_INPAK ); } #ifndef DEDICATED void Field_AutoCompleteKeyName( fieldCallback_t callback ) { Key_KeyNameCompletion( callback ); } #endif void History_Clear( history_t* history, int width ) { for ( int i = 0; i < COMMAND_HISTORY; ++i ) { Field_Clear( &history->commands[i] ); history->commands[i].widthInChars = width; } } static int LengthWithoutTrailingWhitespace( const char* s ) { int i = (int)strlen(s); while ( i-- ) { if ( s[i] != ' ' && s[i] != '\t' ) return i + 1; } return 0; } void History_SaveCommand( history_t* history, const field_t* edit ) { // Avoid having the same command twice in a row. // Unfortunately, this has to be case sensitive since case might matter for some commands. if ( history->next > 0 ) { // The real proper way to ignore whitespace is to tokenize both strings and compare the // argument count and then each argument with a case sensitive comparison, // but there's only one tokenizer data instance... // Instead, we only ignore the trailing whitespace. const int lengthCur = LengthWithoutTrailingWhitespace( edit->buffer ); if ( lengthCur == 0 ) { history->display = history->next; return; } const int prevLine = (history->next - 1) % COMMAND_HISTORY; const int lengthPrev = LengthWithoutTrailingWhitespace( history->commands[prevLine].buffer ); if ( lengthCur == lengthPrev && strncmp(edit->buffer, history->commands[prevLine].buffer, lengthCur) == 0 ) { history->display = history->next; return; } } // copy the line history->commands[history->next % COMMAND_HISTORY] = *edit; ++history->next; history->display = history->next; } void History_GetPreviousCommand( field_t* edit, history_t* history ) { if ( history->next - history->display < COMMAND_HISTORY && history->display > 0 ) --history->display; *edit = history->commands[history->display % COMMAND_HISTORY]; } void History_GetNextCommand( field_t* edit, history_t* history, int width ) { ++history->display; if ( history->display < history->next ) { *edit = history->commands[history->display % COMMAND_HISTORY]; return; } history->display = history->next; Field_Clear( edit ); edit->widthInChars = width; } // It makes no sense for both executables to share the same command history. #if defined(DEDICATED) #define HISTORY_PATH "cnq3svcmdhistory" #else #define HISTORY_PATH "cnq3cmdhistory" #endif void History_LoadFromFile( history_t* history ) { fileHandle_t f; FS_FOpenFileRead( HISTORY_PATH, &f, qfalse ); if ( f == 0 ) return; int count; if ( FS_Read( &count, sizeof(int), f ) != sizeof(int) || count <= 0 || count > COMMAND_HISTORY ) { FS_FCloseFile( f ); return; } int lengths[COMMAND_HISTORY]; const int lengthBytes = sizeof(int) * count; if ( FS_Read( lengths, lengthBytes, f ) != lengthBytes ) { FS_FCloseFile( f ); return; } for ( int i = 0; i < count; ++i ) { const int l = lengths[i]; if ( l <= 0 || FS_Read( history->commands[i].buffer, l, f ) != l ) { FS_FCloseFile( f ); return; } history->commands[i].buffer[l] = '\0'; history->commands[i].cursor = l; } history->next = count; history->display = count; const int totalCount = ARRAY_LEN( history->commands ); for ( int i = count; i < totalCount; ++i ) { history->commands[i].buffer[0] = '\0'; } FS_FCloseFile( f ); } void History_SaveToFile( const history_t* history ) { if ( con_history->integer == 0 ) return; const fileHandle_t f = FS_FOpenFileWrite( HISTORY_PATH ); if ( f == 0 ) return; int count = 0; int lengths[COMMAND_HISTORY]; const int totalCount = ARRAY_LEN( history->commands ); for ( int i = 0; i < totalCount; ++i ) { const char* const s = history->commands[(history->display + i) % COMMAND_HISTORY].buffer; if ( *s == '\0' ) continue; lengths[count++] = strlen( s ); } FS_Write( &count, sizeof(count), f ); FS_Write( lengths, sizeof(int) * count, f ); for ( int i = 0, j = 0; i < totalCount; ++i ) { const char* const s = history->commands[(history->display + i) % COMMAND_HISTORY].buffer; if ( *s == '\0' ) continue; FS_Write( s, lengths[j++], f ); } FS_FCloseFile( f ); } const char* Q_itohex( uint64_t number, qbool uppercase, qbool prefix ) { static const char* luts[2] = { "0123456789abcdef", "0123456789ABCDEF" }; static char buffer[19]; const int maxLength = 16; const char* const lut = luts[uppercase == 0 ? 0 : 1]; uint64_t x = number; int i = maxLength + 2; buffer[i] = '\0'; while ( i-- ) { buffer[i] = lut[x & 15]; x >>= 4; } int startOffset = 2; for ( i = 2; i < maxLength + 1; i++, startOffset++ ) { if ( buffer[i] != '0' ) break; } if ( prefix ) { startOffset -= 2; buffer[startOffset + 0] = '0'; buffer[startOffset + 1] = 'x'; } return buffer + startOffset; } void Help_AllocSplitText( char** desc, char** help, const char* combined ) { if ( *desc != NULL || *help != NULL ) { // break here for some debugging fun return; } const char* const newLine = strchr( combined, '\n' ); if ( !newLine ) { *desc = CopyString( combined ); return; } const int srcLen = strlen( combined ); const int descLen = newLine - combined; const int helpLen = srcLen - descLen - 1; *desc = (char*)S_Malloc( descLen + 1 ); *help = (char*)S_Malloc( helpLen + 1 ); memcpy( *desc, combined, descLen ); memcpy( *help, combined + descLen + 1, helpLen ); (*desc)[descLen] = '\0'; (*help)[helpLen] = '\0'; } void Com_TruncatePrintString( char* buffer, int size, int maxLength ) { if ( Q_PrintStrlen( buffer ) <= maxLength ) return; int byteIndex = Q_PrintStroff( buffer, maxLength ); if ( byteIndex < 0 || byteIndex >= size ) byteIndex = size - 1; buffer[byteIndex - 4] = '.'; buffer[byteIndex - 3] = '.'; buffer[byteIndex - 2] = '.'; buffer[byteIndex - 1] = '\n'; buffer[byteIndex - 0] = '\0'; } void Com_PrintModules( module_t firstModule, int moduleMask ) { #define MODULE_ITEM(Enum, Desc) Desc, static const char* ModuleNames[MODULE_COUNT + 1] = { MODULE_LIST(MODULE_ITEM) "" }; #undef MODULE_ITEM if ( firstModule == MODULE_NONE || moduleMask == 0 ) return; const int otherModules = moduleMask & (~(1 << firstModule)); if ( otherModules ) Com_Printf( "Modules: " ); else Com_Printf( "Module: " ); Com_Printf( "%s", ModuleNames[firstModule] ); for ( int i = 0; i < 32; ++i ) { if ( (otherModules >> i) & 1 ) Com_Printf( ", %s", ModuleNames[i] ); } Com_Printf("\n"); }