/* =========================================================================== Doom 3 GPL Source Code Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company. This file is part of the Doom 3 GPL Source Code (?Doom 3 Source Code?). Doom 3 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 3 of the License, or (at your option) any later version. Doom 3 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 Doom 3 Source Code. If not, see . In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ #include "../../idlib/precompiled.h" #include "../sys_local.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "posix_public.h" #define MAX_OSPATH 256 #define COMMAND_HISTORY 64 static int input_hide = 0; idEditField input_field; static char input_ret[256]; static idStr history[ COMMAND_HISTORY ]; // cycle buffer static int history_count = 0; // buffer fill up static int history_start = 0; // current history start static int history_current = 0; // goes back in history idEditField history_backup; // the base edit line // terminal support idCVar in_tty( "in_tty", "1", CVAR_BOOL | CVAR_INIT | CVAR_SYSTEM, "terminal tab-completion and history" ); static bool tty_enabled = false; static struct termios tty_tc; // pid - useful when you attach to gdb.. idCVar com_pid( "com_pid", "0", CVAR_INTEGER | CVAR_INIT | CVAR_SYSTEM, "process id" ); // exit - quit - error -------------------------------------------------------- static int set_exit = 0; static char exit_spawn[ 1024 ]; /* ================ Posix_Exit ================ */ void Posix_Exit(int ret) { if ( tty_enabled ) { Sys_Printf( "shutdown terminal support\n" ); if ( tcsetattr( 0, TCSADRAIN, &tty_tc ) == -1 ) { Sys_Printf( "tcsetattr failed: %s\n", strerror( errno ) ); } } // at this point, too late to catch signals Posix_ClearSigs(); if ( asyncThread.threadHandle ) { Sys_DestroyThread( asyncThread ); } // process spawning. it's best when it happens after everything has shut down if ( exit_spawn[0] ) { Sys_DoStartProcess( exit_spawn, false ); } // in case of signal, handler tries a common->Quit // we use set_exit to maintain a correct exit code if ( set_exit ) { exit( set_exit ); } exit( ret ); } /* ================ Posix_SetExit ================ */ void Posix_SetExit(int ret) { set_exit = 0; } /* =============== Posix_SetExitSpawn set the process to be spawned when we quit =============== */ void Posix_SetExitSpawn( const char *exeName ) { idStr::Copynz( exit_spawn, exeName, 1024 ); } /* ================== idSysLocal::StartProcess if !quit, start the process asap otherwise, push it for execution at exit (i.e. let complete shutdown of the game and freeing of resources happen) NOTE: might even want to add a small delay? ================== */ void idSysLocal::StartProcess( const char *exeName, bool quit ) { if ( quit ) { common->DPrintf( "Sys_StartProcess %s (delaying until final exit)\n", exeName ); Posix_SetExitSpawn( exeName ); cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "quit\n" ); return; } common->DPrintf( "Sys_StartProcess %s\n", exeName ); Sys_DoStartProcess( exeName ); } /* ================ Sys_Quit ================ */ void Sys_Quit(void) { Posix_Exit( EXIT_SUCCESS ); } /* ================ Sys_Milliseconds ================ */ /* base time in seconds, that's our origin timeval:tv_sec is an int: assuming this wraps every 0x7fffffff - ~68 years since the Epoch (1970) - we're safe till 2038 using unsigned long data type to work right with Sys_XTimeToSysTime */ unsigned long sys_timeBase = 0; /* current time in ms, using sys_timeBase as origin NOTE: sys_timeBase*1000 + curtime -> ms since the Epoch 0x7fffffff ms - ~24 days or is it 48 days? the specs say int, but maybe it's casted from unsigned int? */ int Sys_Milliseconds( void ) { int curtime; struct timeval tp; gettimeofday(&tp, NULL); if (!sys_timeBase) { sys_timeBase = tp.tv_sec; return tp.tv_usec / 1000; } curtime = (tp.tv_sec - sys_timeBase) * 1000 + tp.tv_usec / 1000; return curtime; } /* ================ Sys_Mkdir ================ */ void Sys_Mkdir( const char *path ) { mkdir(path, 0777); } /* ================ Sys_ListFiles ================ */ int Sys_ListFiles( const char *directory, const char *extension, idStrList &list ) { struct dirent *d; DIR *fdir; bool dironly = false; char search[MAX_OSPATH]; struct stat st; bool debug; list.Clear(); debug = cvarSystem->GetCVarBool( "fs_debug" ); if (!extension) extension = ""; // passing a slash as extension will find directories if (extension[0] == '/' && extension[1] == 0) { extension = ""; dironly = true; } // search // NOTE: case sensitivity of directory path can screw us up here if ((fdir = opendir(directory)) == NULL) { if (debug) { common->Printf("Sys_ListFiles: opendir %s failed\n", directory); } return -1; } while ((d = readdir(fdir)) != NULL) { idStr::snPrintf(search, sizeof(search), "%s/%s", directory, d->d_name); if (stat(search, &st) == -1) continue; if (!dironly) { idStr look(search); idStr ext; look.ExtractFileExtension(ext); if (extension[0] != '\0' && ext.Icmp(&extension[1]) != 0) { continue; } } if ((dironly && !(st.st_mode & S_IFDIR)) || (!dironly && (st.st_mode & S_IFDIR))) continue; list.Append(d->d_name); } closedir(fdir); if ( debug ) { common->Printf( "Sys_ListFiles: %d entries in %s\n", list.Num(), directory ); } return list.Num(); } /* ============================================================================ EVENT LOOP ============================================================================ */ #ifndef USE_SDL #define MAX_QUED_EVENTS 256 #define MASK_QUED_EVENTS ( MAX_QUED_EVENTS - 1 ) static sysEvent_t eventQue[MAX_QUED_EVENTS]; static int eventHead, eventTail; /* ================ Posix_QueEvent ptr should either be null, or point to a block of data that can be freed later ================ */ void Posix_QueEvent( sysEventType_t type, int value, int value2, int ptrLength, void *ptr ) { sysEvent_t *ev; ev = &eventQue[eventHead & MASK_QUED_EVENTS]; if (eventHead - eventTail >= MAX_QUED_EVENTS) { common->Printf( "Posix_QueEvent: overflow\n" ); // we are discarding an event, but don't leak memory // TTimo: verbose dropped event types? if (ev->evPtr) { Mem_Free(ev->evPtr); ev->evPtr = NULL; } eventTail++; } eventHead++; ev->evType = type; ev->evValue = value; ev->evValue2 = value2; ev->evPtrLength = ptrLength; ev->evPtr = ptr; #if 0 common->Printf( "Event %d: %d %d\n", ev->evType, ev->evValue, ev->evValue2 ); #endif } /* ================ Sys_GetEvent ================ */ sysEvent_t Sys_GetEvent(void) { static sysEvent_t ev; // return if we have data if (eventHead > eventTail) { eventTail++; return eventQue[(eventTail - 1) & MASK_QUED_EVENTS]; } // return the empty event with the current time memset(&ev, 0, sizeof(ev)); return ev; } /* ================ Sys_ClearEvents ================ */ void Sys_ClearEvents( void ) { eventHead = eventTail = 0; } #endif /* ================ Posix_Cwd ================ */ const char *Posix_Cwd( void ) { static char cwd[MAX_OSPATH]; getcwd( cwd, sizeof( cwd ) - 1 ); cwd[MAX_OSPATH-1] = 0; return cwd; } /* ================= Sys_GetMemoryStatus ================= */ void Sys_GetMemoryStatus( sysMemoryStats_t &stats ) { common->Printf( "FIXME: Sys_GetMemoryStatus stub\n" ); } void Sys_GetCurrentMemoryStatus( sysMemoryStats_t &stats ) { common->Printf( "FIXME: Sys_GetCurrentMemoryStatus\n" ); } void Sys_GetExeLaunchMemoryStatus( sysMemoryStats_t &stats ) { common->Printf( "FIXME: Sys_GetExeLaunchMemoryStatus\n" ); } /* ================= Sys_Init Posix_EarlyInit/Posix_LateInit is better ================= */ void Sys_Init( void ) { } /* ================= Posix_Shutdown ================= */ void Posix_Shutdown( void ) { for ( int i = 0; i < COMMAND_HISTORY; i++ ) { history[ i ].Clear(); } } // flibit: 64 bit fix, changed int to void* /* ================= Sys_DLL_Load TODO: OSX - use the native API instead? NSModule ================= */ void* Sys_DLL_Load( const char *path ) { int mode = RTLD_NOW; #ifdef __APPLE__ // NOTE: genenv starts crashing after I dlclose game.dylib >:-( mode |= RTLD_NODELETE; #endif void *handle = dlopen( path, mode ); if ( !handle ) { Sys_Printf( "dlopen '%s' failed: %s\n", path, dlerror() ); } return handle; } /* ================= Sys_DLL_GetProcAddress ================= */ void* Sys_DLL_GetProcAddress( void* handle, const char *sym ) { const char *error; void *ret = dlsym( handle, sym ); if ((error = dlerror()) != NULL) { Sys_Printf( "dlsym '%s' failed: %s\n", sym, error ); } return ret; } /* ================= Sys_DLL_Unload ================= */ void Sys_DLL_Unload( void* handle ) { dlclose( handle ); } // flibit end /* ================ Sys_ShowConsole ================ */ void Sys_ShowConsole( int visLevel, bool quitOnClose ) { } // --------------------------------------------------------------------------- // only relevant when specified on command line const char *Sys_DefaultCDPath( void ) { return ""; } long Sys_FileTimeStamp(FILE * fp) { struct stat st; fstat(fileno(fp), &st); return st.st_mtime; } void Sys_Sleep(int msec) { if ( msec < 20 ) { static int last = 0; int now = Sys_Milliseconds(); if ( now - last > 1000 ) { Sys_Printf("WARNING: Sys_Sleep - %d < 20 msec is not portable\n", msec); last = now; } // ignore that sleep call, keep going return; } // use nanosleep? keep sleeping if signal interrupt? if (usleep(msec * 1000) == -1) Sys_Printf("usleep: %s\n", strerror(errno)); } char *Sys_GetClipboardData(void) { Sys_Printf( "TODO: Sys_GetClipboardData\n" ); return NULL; } void Sys_SetClipboardData( const char *string ) { Sys_Printf( "TODO: Sys_SetClipboardData\n" ); } // stub pretty much everywhere - heavy calling void Sys_FlushCacheMemory(void *base, int bytes) { // Sys_Printf("Sys_FlushCacheMemory stub\n"); } bool Sys_FPU_StackIsEmpty( void ) { return true; } void Sys_FPU_ClearStack( void ) { } const char *Sys_FPU_GetState( void ) { return ""; } void Sys_FPU_SetPrecision( int precision ) { } /* ================ Sys_LockMemory ================ */ bool Sys_LockMemory( void *ptr, int bytes ) { return true; } /* ================ Sys_UnlockMemory ================ */ bool Sys_UnlockMemory( void *ptr, int bytes ) { return true; } /* ================ Sys_SetPhysicalWorkMemory ================ */ void Sys_SetPhysicalWorkMemory( int minBytes, int maxBytes ) { common->DPrintf( "TODO: Sys_SetPhysicalWorkMemory\n" ); } /* =========== Sys_GetDriveFreeSpace return in MegaBytes =========== */ int Sys_GetDriveFreeSpace( const char *path ) { common->DPrintf( "TODO: Sys_GetDriveFreeSpace\n" ); return 1000 * 1024; } /* ================ Sys_AlreadyRunning return true if there is a copy of D3 running already ================ */ bool Sys_AlreadyRunning( void ) { return false; } /* =============== Posix_EarlyInit =============== */ void Posix_EarlyInit( void ) { memset( &asyncThread, 0, sizeof( asyncThread ) ); exit_spawn[0] = '\0'; Posix_InitSigs(); // set the base time Sys_Milliseconds(); Posix_InitPThreads(); } /* =============== Posix_LateInit =============== */ void Posix_LateInit( void ) { Posix_InitConsoleInput(); com_pid.SetInteger( getpid() ); common->Printf( "pid: %d\n", com_pid.GetInteger() ); common->Printf( "%d MB System Memory\n", Sys_GetSystemRam() ); #ifndef ID_DEDICATED common->Printf( "%d MB Video Memory\n", Sys_GetVideoRam() ); #endif Posix_StartAsyncThread( ); } /* =============== Posix_InitConsoleInput =============== */ void Posix_InitConsoleInput( void ) { struct termios tc; if ( in_tty.GetBool() ) { if ( isatty( STDIN_FILENO ) != 1 ) { Sys_Printf( "terminal support disabled: stdin is not a tty\n" ); in_tty.SetBool( false ); return; } if ( tcgetattr( 0, &tty_tc ) == -1 ) { Sys_Printf( "tcgetattr failed. disabling terminal support: %s\n", strerror( errno ) ); in_tty.SetBool( false ); return; } // make the input non blocking if ( fcntl( STDIN_FILENO, F_SETFL, fcntl( STDIN_FILENO, F_GETFL, 0 ) | O_NONBLOCK ) == -1 ) { Sys_Printf( "fcntl STDIN non blocking failed. disabling terminal support: %s\n", strerror( errno ) ); in_tty.SetBool( false ); return; } tc = tty_tc; /* ECHO: don't echo input characters ICANON: enable canonical mode. This enables the special characters EOF, EOL, EOL2, ERASE, KILL, REPRINT, STATUS, and WERASE, and buffers by lines. ISIG: when any of the characters INTR, QUIT, SUSP, or DSUSP are received, generate the corresponding signal */ tc.c_lflag &= ~(ECHO | ICANON); /* ISTRIP strip off bit 8 INPCK enable input parity checking */ tc.c_iflag &= ~(ISTRIP | INPCK); tc.c_cc[VMIN] = 1; tc.c_cc[VTIME] = 0; if ( tcsetattr( 0, TCSADRAIN, &tc ) == -1 ) { Sys_Printf( "tcsetattr failed: %s\n", strerror( errno ) ); Sys_Printf( "terminal support may not work correctly. Use +set in_tty 0 to disable it\n" ); } #if 0 // make the output non blocking if ( fcntl( STDOUT_FILENO, F_SETFL, fcntl( STDOUT_FILENO, F_GETFL, 0 ) | O_NONBLOCK ) == -1 ) { Sys_Printf( "fcntl STDOUT non blocking failed: %s\n", strerror( errno ) ); } #endif tty_enabled = true; // check the terminal type for the supported ones char *term = getenv( "TERM" ); if ( term ) { if ( strcmp( term, "linux" ) && strcmp( term, "xterm" ) && strcmp( term, "xterm-color" ) && strcmp( term, "screen" ) ) { Sys_Printf( "WARNING: terminal type '%s' is unknown. terminal support may not work correctly\n", term ); } } Sys_Printf( "terminal support enabled ( use +set in_tty 0 to disabled )\n" ); } else { Sys_Printf( "terminal support disabled\n" ); } } /* ================ terminal support utilities ================ */ void tty_Del() { char key; key = '\b'; write( STDOUT_FILENO, &key, 1 ); key = ' '; write( STDOUT_FILENO, &key, 1 ); key = '\b'; write( STDOUT_FILENO, &key, 1 ); } void tty_Left() { char key = '\b'; write( STDOUT_FILENO, &key, 1 ); } void tty_Right() { char key = 27; write( STDOUT_FILENO, &key, 1 ); write( STDOUT_FILENO, "[C", 2 ); } // clear the display of the line currently edited // bring cursor back to beginning of line void tty_Hide() { int len, buf_len; if ( !tty_enabled ) { return; } if ( input_hide ) { input_hide++; return; } // clear after cursor len = strlen( input_field.GetBuffer() ) - input_field.GetCursor(); while ( len > 0 ) { tty_Right(); len--; } buf_len = strlen( input_field.GetBuffer() ); while ( buf_len > 0 ) { tty_Del(); buf_len--; } input_hide++; } // show the current line void tty_Show() { // int i; if ( !tty_enabled ) { return; } assert( input_hide > 0 ); input_hide--; if ( input_hide == 0 ) { char *buf = input_field.GetBuffer(); if ( buf[0] ) { write( STDOUT_FILENO, buf, strlen( buf ) ); int back = strlen( buf ) - input_field.GetCursor(); while ( back > 0 ) { tty_Left(); back--; } } } } void tty_FlushIn() { char key; while ( read(0, &key, 1) != -1 ) { Sys_Printf( "'%d' ", key ); } Sys_Printf( "\n" ); } /* ================ Sys_ConsoleInput Checks for a complete line of text typed in at the console. Return NULL if a complete line is not ready. ================ */ char *Sys_ConsoleInput( void ) { if ( tty_enabled ) { int ret; char key; bool hidden = false; while ( ( ret = read( STDIN_FILENO, &key, 1 ) ) > 0 ) { if ( !hidden ) { tty_Hide(); hidden = true; } switch ( key ) { case 1: input_field.SetCursor( 0 ); break; case 5: input_field.SetCursor( strlen( input_field.GetBuffer() ) ); break; case 127: case 8: input_field.CharEvent( K_BACKSPACE ); break; case '\n': idStr::Copynz( input_ret, input_field.GetBuffer(), sizeof( input_ret ) ); assert( hidden ); tty_Show(); write( STDOUT_FILENO, &key, 1 ); input_field.Clear(); if ( history_count < COMMAND_HISTORY ) { history[ history_count ] = input_ret; history_count++; } else { history[ history_start ] = input_ret; history_start++; history_start %= COMMAND_HISTORY; } history_current = 0; return input_ret; case '\t': input_field.AutoComplete(); break; case 27: { // enter escape sequence mode ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 ) { Sys_Printf( "dropping sequence: '27' " ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } switch ( key ) { case 79: ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 ) { Sys_Printf( "dropping sequence: '27' '79' " ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } switch ( key ) { case 72: // xterm only input_field.SetCursor( 0 ); break; case 70: // xterm only input_field.SetCursor( strlen( input_field.GetBuffer() ) ); break; default: Sys_Printf( "dropping sequence: '27' '79' '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } break; case 91: { ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 ) { Sys_Printf( "dropping sequence: '27' '91' " ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } switch ( key ) { case 49: { ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 || key != 126 ) { Sys_Printf( "dropping sequence: '27' '91' '49' '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } // only screen and linux terms input_field.SetCursor( 0 ); break; } case 50: { ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 || key != 126 ) { Sys_Printf( "dropping sequence: '27' '91' '50' '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } // all terms input_field.KeyDownEvent( K_INS ); break; } case 52: { ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 || key != 126 ) { Sys_Printf( "dropping sequence: '27' '91' '52' '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } // only screen and linux terms input_field.SetCursor( strlen( input_field.GetBuffer() ) ); break; } case 51: { ret = read( STDIN_FILENO, &key, 1 ); if ( ret <= 0 ) { Sys_Printf( "dropping sequence: '27' '91' '51' " ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } if ( key == 126 ) { input_field.KeyDownEvent( K_DEL ); break; } Sys_Printf( "dropping sequence: '27' '91' '51' '%d'", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } case 65: case 66: { // history if ( history_current == 0 ) { history_backup = input_field; } if ( key == 65 ) { // up history_current++; } else { // down history_current--; } // history_current cycle: // 0: current edit // 1 .. Min( COMMAND_HISTORY, history_count ): back in history if ( history_current < 0 ) { history_current = Min( COMMAND_HISTORY, history_count ); } else { history_current %= Min( COMMAND_HISTORY, history_count ) + 1; } int index = -1; if ( history_current == 0 ) { input_field = history_backup; } else { index = history_start + Min( COMMAND_HISTORY, history_count ) - history_current; index %= COMMAND_HISTORY; assert( index >= 0 && index < COMMAND_HISTORY ); input_field.SetBuffer( history[ index ] ); } assert( hidden ); tty_Show(); return NULL; } case 67: input_field.KeyDownEvent( K_RIGHTARROW ); break; case 68: input_field.KeyDownEvent( K_LEFTARROW ); break; default: Sys_Printf( "dropping sequence: '27' '91' '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } break; } default: Sys_Printf( "dropping sequence: '27' '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } break; } default: if ( key >= ' ' ) { input_field.CharEvent( key ); break; } Sys_Printf( "dropping sequence: '%d' ", key ); tty_FlushIn(); assert( hidden ); tty_Show(); return NULL; } } if ( hidden ) { tty_Show(); } return NULL; } else { // disabled on OSX. works fine from a terminal, but launching from Finder is causing trouble // I'm pretty sure it could be re-enabled if needed, and just handling the Finder failure case right (TTimo) #ifndef MACOS_X // no terminal support - read only complete lines int len; fd_set fdset; struct timeval timeout; FD_ZERO( &fdset ); FD_SET( STDIN_FILENO, &fdset ); timeout.tv_sec = 0; timeout.tv_usec = 0; if ( select( 1, &fdset, NULL, NULL, &timeout ) == -1 || !FD_ISSET( 0, &fdset ) ) { return NULL; } len = read( 0, input_ret, sizeof( input_ret ) ); if ( len == 0 ) { // EOF return NULL; } if ( len < 1 ) { Sys_Printf( "read failed: %s\n", strerror( errno ) ); // something bad happened, cancel this line and print an error return NULL; } if ( len == sizeof( input_ret ) ) { Sys_Printf( "read overflow\n" ); // things are likely to break, as input will be cut into pieces } input_ret[ len-1 ] = '\0'; // rip off the \n and terminate return input_ret; #endif } return NULL; } #ifndef USE_SDL /* called during frame loops, pacifier updates etc. this is only for console input polling and misc mouse grab tasks the actual mouse and keyboard input is in the Sys_Poll logic */ void Sys_GenerateEvents( void ) { char *s; if ( ( s = Sys_ConsoleInput() ) ) { char *b; int len; len = strlen( s ) + 1; b = (char *)Mem_Alloc( len ); strcpy( b, s ); Posix_QueEvent( SE_CONSOLE, 0, 0, len, b ); } } #endif /* =============== low level output =============== */ void Sys_DebugPrintf( const char *fmt, ... ) { va_list argptr; tty_Hide(); va_start( argptr, fmt ); vprintf( fmt, argptr ); va_end( argptr ); tty_Show(); } void Sys_DebugVPrintf( const char *fmt, va_list arg ) { tty_Hide(); vprintf( fmt, arg ); tty_Show(); } void Sys_Printf(const char *msg, ...) { va_list argptr; tty_Hide(); va_start( argptr, msg ); vprintf( msg, argptr ); va_end( argptr ); tty_Show(); } void Sys_VPrintf(const char *msg, va_list arg) { tty_Hide(); vprintf(msg, arg); tty_Show(); } /* ================ Sys_Error ================ */ void Sys_Error(const char *error, ...) { va_list argptr; Sys_Printf( "Sys_Error: " ); va_start( argptr, error ); Sys_DebugVPrintf( error, argptr ); va_end( argptr ); Sys_Printf( "\n" ); Posix_Exit( EXIT_FAILURE ); } /* =============== Sys_FreeOpenAL =============== */ void Sys_FreeOpenAL( void ) { }