mirror of
https://bitbucket.org/CPMADevs/cnq3
synced 2024-11-23 20:42:49 +00:00
a61c6f0e47
'h' indicates more help is available '?' indicates a cvar is user-created muted the Com_sprintf overflow warnings
3352 lines
81 KiB
C++
3352 lines
81 KiB
C++
/*
|
|
===========================================================================
|
|
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 <setjmp.h>
|
|
|
|
#if (_MSC_VER >= 1400) // Visual C++ 2005 or later
|
|
#define MSVC_CPUID 1
|
|
#include <intrin.h>
|
|
#elif (__GNUC__)
|
|
#define GCC_CPUID 1
|
|
#include <cpuid.h>
|
|
#endif
|
|
|
|
#ifndef _WIN32
|
|
#include <netinet/in.h>
|
|
#include <sys/stat.h> // 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 *com_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 <seconds>\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
|
|
{ &com_completionStyle, "com_completionStyle", "0", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, help_com_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 <filename>\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 ( com_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 && com_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");
|
|
}
|