mirror of
https://github.com/dhewm/dhewm3.git
synced 2024-11-30 08:01:02 +00:00
1190 lines
30 KiB
C++
1190 lines
30 KiB
C++
/*
|
|
===========================================================================
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
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 "sys/platform.h"
|
|
#include "idlib/CmdArgs.h"
|
|
#include "framework/async/AsyncNetwork.h"
|
|
#include "framework/Licensee.h"
|
|
#include "framework/UsercmdGen.h"
|
|
#include "renderer/tr_local.h"
|
|
#include "sys/win32/rc/CreateResourceIDs.h"
|
|
#include "sys/sys_local.h"
|
|
|
|
#include "sys/win32/win_local.h"
|
|
|
|
#include <errno.h>
|
|
#include <float.h>
|
|
#include <fcntl.h>
|
|
#include <direct.h>
|
|
#include <io.h>
|
|
#include <conio.h>
|
|
#include <shellapi.h>
|
|
#include <shlobj.h>
|
|
|
|
#ifndef __MRC__
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#endif
|
|
|
|
#include "tools/edit_public.h"
|
|
|
|
#include <SDL_main.h>
|
|
|
|
idCVar Win32Vars_t::win_outputDebugString( "win_outputDebugString", "0", CVAR_SYSTEM | CVAR_BOOL, "" );
|
|
idCVar Win32Vars_t::win_outputEditString( "win_outputEditString", "1", CVAR_SYSTEM | CVAR_BOOL, "" );
|
|
idCVar Win32Vars_t::win_viewlog( "win_viewlog", "0", CVAR_SYSTEM | CVAR_INTEGER, "" );
|
|
|
|
Win32Vars_t win32;
|
|
|
|
#ifdef ID_ALLOW_TOOLS
|
|
/* These are required for tools (DG: taken from SteelStorm2) */
|
|
|
|
static HMODULE hOpenGL_DLL;
|
|
|
|
typedef BOOL(WINAPI * PWGLSWAPBUFFERS)(HDC);
|
|
|
|
PWGLSWAPBUFFERS qwglSwapBuffers;
|
|
|
|
typedef BOOL(WINAPI * PWGLCOPYCONTEXT)(HGLRC, HGLRC, UINT);
|
|
typedef HGLRC(WINAPI * PWGLCREATECONTEXT)(HDC);
|
|
typedef HGLRC(WINAPI * PWGLCREATELAYERCONTEXT)(HDC, int);
|
|
typedef BOOL(WINAPI * PWGLDELETECONTEXT)(HGLRC);
|
|
typedef HGLRC(WINAPI * PWGLGETCURRENTCONTEXT)(VOID);
|
|
typedef HDC(WINAPI * PWGLGETCURRENTDC)(VOID);
|
|
typedef PROC(WINAPI * PWGLGETPROCADDRESS)(LPCSTR);
|
|
typedef BOOL(WINAPI * PWGLMAKECURRENT)(HDC, HGLRC);
|
|
typedef BOOL(WINAPI * PWGLSHARELISTS)(HGLRC, HGLRC);
|
|
typedef BOOL(WINAPI * PWGLUSEFONTBITMAPS)(HDC, DWORD, DWORD, DWORD);
|
|
|
|
|
|
PWGLCOPYCONTEXT qwglCopyContext;
|
|
PWGLCREATECONTEXT qwglCreateContext;
|
|
PWGLCREATELAYERCONTEXT qwglCreateLayerContext;
|
|
PWGLDELETECONTEXT qwglDeleteContext;
|
|
PWGLGETCURRENTCONTEXT qwglGetCurrentContext;
|
|
PWGLGETCURRENTDC qwglGetCurrentDC;
|
|
PWGLGETPROCADDRESS qwglGetProcAddress;
|
|
PWGLMAKECURRENT qwglMakeCurrent;
|
|
PWGLSHARELISTS qwglShareLists;
|
|
PWGLUSEFONTBITMAPS qwglUseFontBitmaps;
|
|
|
|
typedef BOOL(WINAPI * PWGLUSEFONTOUTLINES)(HDC, DWORD, DWORD, DWORD, FLOAT,
|
|
FLOAT, int, LPGLYPHMETRICSFLOAT);
|
|
typedef BOOL(WINAPI * PWGLDESCRIBELAYERPLANE)(HDC, int, int, UINT,
|
|
LPLAYERPLANEDESCRIPTOR);
|
|
typedef int (WINAPI * PWGLSETLAYERPALETTEENTRIES)(HDC, int, int, int,
|
|
CONST COLORREF *);
|
|
typedef int (WINAPI * PWGLGETLAYERPALETTEENTRIES)(HDC, int, int, int,
|
|
COLORREF *);
|
|
typedef BOOL(WINAPI * PWGLREALIZELAYERPALETTE)(HDC, int, BOOL);
|
|
typedef BOOL(WINAPI * PWGLSWAPLAYERBUFFERS)(HDC, UINT);
|
|
|
|
PWGLUSEFONTOUTLINES qwglUseFontOutlines;
|
|
PWGLDESCRIBELAYERPLANE qwglDescribeLayerPlane;
|
|
PWGLSETLAYERPALETTEENTRIES qwglSetLayerPaletteEntries;
|
|
PWGLGETLAYERPALETTEENTRIES qwglGetLayerPaletteEntries;
|
|
PWGLREALIZELAYERPALETTE qwglRealizeLayerPalette;
|
|
PWGLSWAPLAYERBUFFERS qwglSwapLayerBuffers;
|
|
|
|
#endif /* End stuff required for tools */
|
|
|
|
static bool hadError = false;
|
|
static char errorText[4096];
|
|
|
|
/*
|
|
=============
|
|
Sys_Error
|
|
|
|
Show the early console as an error dialog
|
|
=============
|
|
*/
|
|
void Sys_Error( const char *error, ... ) {
|
|
va_list argptr;
|
|
char text[4096];
|
|
MSG msg;
|
|
|
|
if ( !Sys_IsMainThread() ) {
|
|
// to avoid deadlocks we mustn't call Conbuf_AppendText() etc if not in main thread!
|
|
va_start(argptr, error);
|
|
vsprintf(errorText, error, argptr);
|
|
va_end(argptr);
|
|
|
|
printf("%s", errorText);
|
|
OutputDebugString( errorText );
|
|
|
|
hadError = true;
|
|
return;
|
|
}
|
|
|
|
va_start( argptr, error );
|
|
vsprintf( text, error, argptr );
|
|
va_end( argptr);
|
|
|
|
if ( !hadError ) {
|
|
// if we had an error in another thread, printf() and OutputDebugString() has already been called for this
|
|
printf( "%s", text );
|
|
OutputDebugString( text );
|
|
}
|
|
|
|
Conbuf_AppendText( text );
|
|
Conbuf_AppendText( "\n" );
|
|
|
|
Win_SetErrorText( text );
|
|
Sys_ShowConsole( 1, true );
|
|
|
|
timeEndPeriod( 1 );
|
|
|
|
Sys_ShutdownInput();
|
|
|
|
GLimp_Shutdown();
|
|
|
|
// wait for the user to quit
|
|
while ( 1 ) {
|
|
if ( !GetMessage( &msg, NULL, 0, 0 ) ) {
|
|
common->Quit();
|
|
}
|
|
TranslateMessage( &msg );
|
|
DispatchMessage( &msg );
|
|
}
|
|
|
|
Sys_DestroyConsole();
|
|
|
|
exit (1);
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_Quit
|
|
==============
|
|
*/
|
|
void Sys_Quit( void ) {
|
|
#ifdef ID_ALLOW_TOOLS
|
|
// Free OpenGL DLL.
|
|
if (hOpenGL_DLL)
|
|
{
|
|
FreeLibrary(hOpenGL_DLL);
|
|
}
|
|
#endif
|
|
|
|
timeEndPeriod( 1 );
|
|
Sys_ShutdownInput();
|
|
Sys_DestroyConsole();
|
|
ExitProcess( 0 );
|
|
}
|
|
|
|
|
|
/*
|
|
==============
|
|
Sys_Printf
|
|
==============
|
|
*/
|
|
|
|
enum {
|
|
MAXPRINTMSG = 4096,
|
|
MAXNUMBUFFEREDLINES = 16
|
|
};
|
|
|
|
static char bufferedPrintfLines[MAXNUMBUFFEREDLINES][MAXPRINTMSG];
|
|
static int curNumBufferedPrintfLines = 0;
|
|
static CRITICAL_SECTION printfCritSect;
|
|
|
|
void Sys_Printf( const char *fmt, ... ) {
|
|
char msg[MAXPRINTMSG];
|
|
|
|
va_list argptr;
|
|
va_start(argptr, fmt);
|
|
int len = idStr::vsnPrintf( msg, MAXPRINTMSG-1, fmt, argptr );
|
|
va_end(argptr);
|
|
msg[sizeof(msg)-1] = '\0';
|
|
|
|
printf("%s", msg);
|
|
|
|
if ( win32.win_outputDebugString.GetBool() ) {
|
|
OutputDebugString( msg );
|
|
}
|
|
if ( win32.win_outputEditString.GetBool() ) {
|
|
if ( Sys_IsMainThread() ) {
|
|
Conbuf_AppendText( msg );
|
|
} else {
|
|
EnterCriticalSection( &printfCritSect );
|
|
int idx = curNumBufferedPrintfLines++;
|
|
if ( idx < MAXNUMBUFFEREDLINES ) {
|
|
if ( len >= MAXPRINTMSG )
|
|
len = MAXPRINTMSG - 1;
|
|
memcpy( bufferedPrintfLines[idx], msg, len + 1 );
|
|
}
|
|
LeaveCriticalSection( &printfCritSect );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_DebugPrintf
|
|
==============
|
|
*/
|
|
#define MAXPRINTMSG 4096
|
|
void Sys_DebugPrintf( const char *fmt, ... ) {
|
|
char msg[MAXPRINTMSG];
|
|
|
|
va_list argptr;
|
|
va_start( argptr, fmt );
|
|
idStr::vsnPrintf( msg, MAXPRINTMSG-1, fmt, argptr );
|
|
msg[ sizeof(msg)-1 ] = '\0';
|
|
va_end( argptr );
|
|
|
|
printf("%s", msg);
|
|
|
|
OutputDebugString( msg );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_DebugVPrintf
|
|
==============
|
|
*/
|
|
void Sys_DebugVPrintf( const char *fmt, va_list arg ) {
|
|
char msg[MAXPRINTMSG];
|
|
|
|
idStr::vsnPrintf( msg, MAXPRINTMSG-1, fmt, arg );
|
|
msg[ sizeof(msg)-1 ] = '\0';
|
|
|
|
printf("%s", msg);
|
|
|
|
OutputDebugString( msg );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_ShowWindow
|
|
==============
|
|
*/
|
|
void Sys_ShowWindow( bool show ) {
|
|
::ShowWindow( win32.hWnd, show ? SW_SHOW : SW_HIDE );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_IsWindowVisible
|
|
==============
|
|
*/
|
|
bool Sys_IsWindowVisible( void ) {
|
|
return ( ::IsWindowVisible( win32.hWnd ) != 0 );
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_Mkdir
|
|
==============
|
|
*/
|
|
void Sys_Mkdir( const char *path ) {
|
|
_mkdir (path);
|
|
}
|
|
|
|
/*
|
|
=================
|
|
Sys_FileTimeStamp
|
|
=================
|
|
*/
|
|
ID_TIME_T Sys_FileTimeStamp( FILE *fp ) {
|
|
struct _stat st;
|
|
_fstat( _fileno( fp ), &st );
|
|
return (long) st.st_mtime;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_Cwd
|
|
==============
|
|
*/
|
|
const char *Sys_Cwd( void ) {
|
|
static char cwd[MAX_OSPATH];
|
|
|
|
_getcwd( cwd, sizeof( cwd ) - 1 );
|
|
cwd[MAX_OSPATH-1] = 0;
|
|
|
|
return cwd;
|
|
}
|
|
|
|
static int WPath2A(char *dst, size_t size, const WCHAR *src) {
|
|
int len;
|
|
BOOL default_char = FALSE;
|
|
|
|
// test if we can convert lossless
|
|
len = WideCharToMultiByte(CP_ACP, 0, src, -1, dst, size, NULL, &default_char);
|
|
|
|
if (default_char) {
|
|
/* The following lines implement a horrible
|
|
hack to connect the UTF-16 WinAPI to the
|
|
ASCII doom3 strings. While this should work in
|
|
most cases, it'll fail if the "Windows to
|
|
DOS filename translation" is switched off.
|
|
In that case the function will return NULL
|
|
and no homedir is used. */
|
|
WCHAR w[MAX_OSPATH];
|
|
len = GetShortPathNameW(src, w, sizeof(w));
|
|
|
|
if (len == 0)
|
|
return 0;
|
|
|
|
/* Since the DOS path contains no UTF-16 characters, convert it to the system's default code page */
|
|
len = WideCharToMultiByte(CP_ACP, 0, w, len, dst, size - 1, NULL, NULL);
|
|
}
|
|
|
|
if (len == 0)
|
|
return 0;
|
|
|
|
dst[len] = 0;
|
|
/* Replace backslashes by slashes */
|
|
for (int i = 0; i < len; ++i)
|
|
if (dst[i] == '\\')
|
|
dst[i] = '/';
|
|
|
|
// cut trailing slash
|
|
if (dst[len - 1] == '/') {
|
|
dst[len - 1] = 0;
|
|
len--;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Returns "My Documents"/My Games/dhewm3 directory (or equivalent - "CSIDL_PERSONAL").
|
|
To be used with Sys_DefaultSavePath(), so savegames, screenshots etc will be
|
|
saved to the users files instead of systemwide.
|
|
|
|
Based on (with kind permission) Yamagi Quake II's Sys_GetHomeDir()
|
|
|
|
Returns the number of characters written to dst
|
|
==============
|
|
*/
|
|
extern "C" { // DG: I need this in SDL_win32_main.c
|
|
int Sys_GetHomeDir(char *dst, size_t size)
|
|
{
|
|
int len;
|
|
WCHAR profile[MAX_OSPATH];
|
|
|
|
/* Get the path to "My Documents" directory */
|
|
SHGetFolderPathW(NULL, CSIDL_PERSONAL, NULL, 0, profile);
|
|
|
|
len = WPath2A(dst, size, profile);
|
|
if (len == 0)
|
|
return 0;
|
|
|
|
idStr::Append(dst, size, "/My Games/dhewm3");
|
|
|
|
return len;
|
|
}
|
|
}
|
|
|
|
static int GetRegistryPath(char *dst, size_t size, const WCHAR *subkey, const WCHAR *name) {
|
|
WCHAR w[MAX_OSPATH];
|
|
DWORD len = sizeof(w);
|
|
HKEY res;
|
|
DWORD sam = KEY_QUERY_VALUE
|
|
#ifdef _WIN64
|
|
| KEY_WOW64_32KEY
|
|
#endif
|
|
;
|
|
DWORD type;
|
|
|
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, subkey, 0, sam, &res) != ERROR_SUCCESS)
|
|
return 0;
|
|
|
|
if (RegQueryValueExW(res, name, NULL, &type, (LPBYTE)w, &len) != ERROR_SUCCESS) {
|
|
RegCloseKey(res);
|
|
return 0;
|
|
}
|
|
|
|
RegCloseKey(res);
|
|
|
|
if (type != REG_SZ)
|
|
return 0;
|
|
|
|
return WPath2A(dst, size, w);
|
|
}
|
|
|
|
bool Sys_GetPath(sysPath_t type, idStr &path) {
|
|
char buf[MAX_OSPATH];
|
|
struct _stat st;
|
|
idStr s;
|
|
|
|
switch(type) {
|
|
case PATH_BASE:
|
|
// try <path to exe>/base first
|
|
if (Sys_GetPath(PATH_EXE, path)) {
|
|
path.StripFilename();
|
|
|
|
s = path;
|
|
s.AppendPath(BASE_GAMEDIR);
|
|
if (_stat(s.c_str(), &st) != -1 && (st.st_mode & _S_IFDIR)) {
|
|
common->Warning("using path of executable: %s", path.c_str());
|
|
return true;
|
|
} else {
|
|
s = path + "/demo/demo00.pk4";
|
|
if (_stat(s.c_str(), &st) != -1 && (st.st_mode & _S_IFREG)) {
|
|
common->Warning("using path of executable (seems to contain demo game data): %s ", path.c_str());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
common->Warning("base path '%s' does not exist", s.c_str());
|
|
}
|
|
|
|
// Note: apparently there is no registry entry for the Doom 3 Demo
|
|
|
|
// fallback to vanilla doom3 cd install
|
|
if (GetRegistryPath(buf, sizeof(buf), L"SOFTWARE\\id\\Doom 3", L"InstallPath") > 0) {
|
|
path = buf;
|
|
return true;
|
|
}
|
|
|
|
// fallback to steam doom3 install
|
|
if (GetRegistryPath(buf, sizeof(buf), L"SOFTWARE\\Valve\\Steam", L"InstallPath") > 0) {
|
|
path = buf;
|
|
path.AppendPath("steamapps\\common\\doom 3");
|
|
|
|
if (_stat(path.c_str(), &st) != -1 && st.st_mode & _S_IFDIR)
|
|
return true;
|
|
}
|
|
|
|
common->Warning("vanilla doom3 path not found either");
|
|
|
|
return false;
|
|
|
|
case PATH_CONFIG:
|
|
case PATH_SAVE:
|
|
if (Sys_GetHomeDir(buf, sizeof(buf)) < 1) {
|
|
Sys_Error("ERROR: Couldn't get dir to home path");
|
|
return false;
|
|
}
|
|
|
|
path = buf;
|
|
return true;
|
|
|
|
case PATH_EXE:
|
|
GetModuleFileName(NULL, buf, sizeof(buf) - 1);
|
|
path = buf;
|
|
path.BackSlashesToSlashes();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
Sys_ListFiles
|
|
==============
|
|
*/
|
|
int Sys_ListFiles( const char *directory, const char *extension, idStrList &list ) {
|
|
idStr search;
|
|
struct _finddata_t findinfo;
|
|
intptr_t findhandle;
|
|
int flag;
|
|
|
|
if ( !extension) {
|
|
extension = "";
|
|
}
|
|
|
|
// passing a slash as extension will find directories
|
|
if ( extension[0] == '/' && extension[1] == 0 ) {
|
|
extension = "";
|
|
flag = 0;
|
|
} else {
|
|
flag = _A_SUBDIR;
|
|
}
|
|
|
|
sprintf( search, "%s\\*%s", directory, extension );
|
|
|
|
// search
|
|
list.Clear();
|
|
|
|
findhandle = _findfirst( search, &findinfo );
|
|
if ( findhandle == -1 ) {
|
|
return -1;
|
|
}
|
|
|
|
do {
|
|
if ( flag ^ ( findinfo.attrib & _A_SUBDIR ) ) {
|
|
list.Append( findinfo.name );
|
|
}
|
|
} while ( _findnext( findhandle, &findinfo ) != -1 );
|
|
|
|
_findclose( findhandle );
|
|
|
|
return list.Num();
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
Sys_GetClipboardData
|
|
================
|
|
*/
|
|
char *Sys_GetClipboardData( void ) {
|
|
char *data = NULL;
|
|
char *cliptext;
|
|
|
|
if ( OpenClipboard( NULL ) != 0 ) {
|
|
HANDLE hClipboardData;
|
|
|
|
if ( ( hClipboardData = GetClipboardData( CF_TEXT ) ) != 0 ) {
|
|
if ( ( cliptext = (char *)GlobalLock( hClipboardData ) ) != 0 ) {
|
|
data = (char *)Mem_Alloc( GlobalSize( hClipboardData ) + 1 );
|
|
strcpy( data, cliptext );
|
|
GlobalUnlock( hClipboardData );
|
|
|
|
strtok( data, "\n\r\b" );
|
|
}
|
|
}
|
|
CloseClipboard();
|
|
}
|
|
return data;
|
|
}
|
|
|
|
void Sys_FreeClipboardData( char* data ) {
|
|
Mem_Free( data );
|
|
}
|
|
|
|
/*
|
|
================
|
|
Sys_SetClipboardData
|
|
================
|
|
*/
|
|
void Sys_SetClipboardData( const char *string ) {
|
|
HGLOBAL HMem;
|
|
char *PMem;
|
|
|
|
// allocate memory block
|
|
HMem = (char *)::GlobalAlloc( GMEM_MOVEABLE | GMEM_DDESHARE, strlen( string ) + 1 );
|
|
if ( HMem == NULL ) {
|
|
return;
|
|
}
|
|
// lock allocated memory and obtain a pointer
|
|
PMem = (char *)::GlobalLock( HMem );
|
|
if ( PMem == NULL ) {
|
|
return;
|
|
}
|
|
// copy text into allocated memory block
|
|
lstrcpy( PMem, string );
|
|
// unlock allocated memory
|
|
::GlobalUnlock( HMem );
|
|
// open Clipboard
|
|
if ( !OpenClipboard( 0 ) ) {
|
|
::GlobalFree( HMem );
|
|
return;
|
|
}
|
|
// remove current Clipboard contents
|
|
EmptyClipboard();
|
|
// supply the memory handle to the Clipboard
|
|
SetClipboardData( CF_TEXT, HMem );
|
|
HMem = 0;
|
|
// close Clipboard
|
|
CloseClipboard();
|
|
}
|
|
|
|
/*
|
|
========================================================================
|
|
|
|
DLL Loading
|
|
|
|
========================================================================
|
|
*/
|
|
|
|
/*
|
|
=====================
|
|
Sys_DLL_Load
|
|
=====================
|
|
*/
|
|
uintptr_t Sys_DLL_Load( const char *dllName ) {
|
|
HINSTANCE libHandle;
|
|
libHandle = LoadLibrary( dllName );
|
|
if ( libHandle ) {
|
|
// since we can't have LoadLibrary load only from the specified path, check it did the right thing
|
|
char loadedPath[ MAX_OSPATH ];
|
|
GetModuleFileName( libHandle, loadedPath, sizeof( loadedPath ) - 1 );
|
|
if ( idStr::IcmpPath( dllName, loadedPath ) ) {
|
|
Sys_Printf( "ERROR: LoadLibrary '%s' wants to load '%s'\n", dllName, loadedPath );
|
|
Sys_DLL_Unload( (uintptr_t)libHandle );
|
|
return 0;
|
|
}
|
|
} else {
|
|
DWORD e = GetLastError();
|
|
LPVOID msgBuf = NULL;
|
|
|
|
FormatMessage(
|
|
FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
|
FORMAT_MESSAGE_FROM_SYSTEM |
|
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
NULL,
|
|
e,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
(LPTSTR)&msgBuf,
|
|
0, NULL);
|
|
|
|
idStr errorStr = va( "[%i (0x%X)]\t%s", e, e, msgBuf );
|
|
|
|
// common, skipped.
|
|
if ( e == 0x7E ) // [126 (0x7E)] The specified module could not be found.
|
|
errorStr = "";
|
|
// probably going to be common. Lets try to be less cryptic.
|
|
else if ( e == 0xC1 ) // [193 (0xC1)] is not a valid Win32 application.
|
|
errorStr = va( "[%i (0x%X)]\t%s", e, e, "probably the DLL is of the wrong architecture, like x64 instead of x86" );
|
|
|
|
if ( errorStr.Length() )
|
|
common->Warning( "LoadLibrary(%s) Failed ! %s", dllName, errorStr.c_str() );
|
|
|
|
::LocalFree( msgBuf );
|
|
}
|
|
return (uintptr_t)libHandle;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
Sys_DLL_GetProcAddress
|
|
=====================
|
|
*/
|
|
void *Sys_DLL_GetProcAddress( uintptr_t dllHandle, const char *procName ) {
|
|
void * adr = (void*)GetProcAddress((HINSTANCE)dllHandle, procName);
|
|
if (!adr)
|
|
{
|
|
DWORD e = GetLastError();
|
|
LPVOID msgBuf = nullptr;
|
|
|
|
FormatMessage(
|
|
FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
|
FORMAT_MESSAGE_FROM_SYSTEM |
|
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
NULL,
|
|
e,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
(LPTSTR)&msgBuf,
|
|
0, NULL);
|
|
|
|
idStr errorStr = va("[%i (0x%X)]\t%s", e, e, msgBuf);
|
|
|
|
if (errorStr.Length())
|
|
common->Warning("GetProcAddress( %i %s) Failed ! %s", dllHandle, procName, errorStr.c_str());
|
|
|
|
::LocalFree(msgBuf);
|
|
}
|
|
return adr;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
Sys_DLL_Unload
|
|
=====================
|
|
*/
|
|
void Sys_DLL_Unload( uintptr_t dllHandle ) {
|
|
if ( !dllHandle ) {
|
|
return;
|
|
}
|
|
if ( FreeLibrary( (HINSTANCE)dllHandle ) == 0 ) {
|
|
int lastError = GetLastError();
|
|
LPVOID lpMsgBuf;
|
|
FormatMessage(
|
|
FORMAT_MESSAGE_ALLOCATE_BUFFER,
|
|
NULL,
|
|
lastError,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
|
|
(LPTSTR) &lpMsgBuf,
|
|
0,
|
|
NULL
|
|
);
|
|
Sys_Error( "Sys_DLL_Unload: FreeLibrary failed - %s (%d)", lpMsgBuf, lastError );
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
Sys_Init
|
|
|
|
The cvar system must already be setup
|
|
================
|
|
*/
|
|
void Sys_Init( void ) {
|
|
|
|
CoInitialize( NULL );
|
|
|
|
// make sure the timer is high precision, otherwise
|
|
// NT gets 18ms resolution
|
|
timeBeginPeriod( 1 );
|
|
|
|
// get WM_TIMER messages pumped every millisecond
|
|
// SetTimer( NULL, 0, 100, NULL );
|
|
|
|
#ifdef DEBUG
|
|
cmdSystem->AddCommand( "createResourceIDs", CreateResourceIDs_f, CMD_FL_TOOL, "assigns resource IDs in _resouce.h files" );
|
|
#endif
|
|
#if 0
|
|
cmdSystem->AddCommand( "setAsyncSound", Sys_SetAsyncSound_f, CMD_FL_SYSTEM, "set the async sound option" );
|
|
#endif
|
|
|
|
//
|
|
// Windows version
|
|
//
|
|
win32.osversion.dwOSVersionInfoSize = sizeof( win32.osversion );
|
|
|
|
if ( !GetVersionEx( (LPOSVERSIONINFO)&win32.osversion ) )
|
|
Sys_Error( "Couldn't get OS info" );
|
|
|
|
if ( win32.osversion.dwMajorVersion < 4 ) {
|
|
Sys_Error( GAME_NAME " requires Windows version 4 (NT) or greater" );
|
|
}
|
|
if ( win32.osversion.dwPlatformId == VER_PLATFORM_WIN32s ) {
|
|
Sys_Error( GAME_NAME " doesn't run on Win32s" );
|
|
}
|
|
|
|
common->Printf( "%d MB System Memory\n", Sys_GetSystemRam() );
|
|
}
|
|
|
|
/*
|
|
================
|
|
Sys_Shutdown
|
|
================
|
|
*/
|
|
void Sys_Shutdown( void ) {
|
|
#ifdef ID_ALLOW_TOOLS
|
|
qwglCopyContext = NULL;
|
|
qwglCreateContext = NULL;
|
|
qwglCreateLayerContext = NULL;
|
|
qwglDeleteContext = NULL;
|
|
qwglDescribeLayerPlane = NULL;
|
|
qwglGetCurrentContext = NULL;
|
|
qwglGetCurrentDC = NULL;
|
|
qwglGetLayerPaletteEntries = NULL;
|
|
qwglGetProcAddress = NULL;
|
|
qwglMakeCurrent = NULL;
|
|
qwglRealizeLayerPalette = NULL;
|
|
qwglSetLayerPaletteEntries = NULL;
|
|
qwglShareLists = NULL;
|
|
qwglSwapLayerBuffers = NULL;
|
|
qwglUseFontBitmaps = NULL;
|
|
qwglUseFontOutlines = NULL;
|
|
qwglSwapBuffers = NULL;
|
|
#endif // ID_ALLOW_TOOLS
|
|
|
|
CoUninitialize();
|
|
}
|
|
|
|
//=======================================================================
|
|
|
|
//#define SET_THREAD_AFFINITY
|
|
|
|
|
|
/*
|
|
====================
|
|
Win_Frame
|
|
====================
|
|
*/
|
|
void Win_Frame( void ) {
|
|
// if "viewlog" has been modified, show or hide the log console
|
|
if ( win32.win_viewlog.IsModified() ) {
|
|
if ( !com_skipRenderer.GetBool() && idAsyncNetwork::serverDedicated.GetInteger() != 1 ) {
|
|
Sys_ShowConsole( win32.win_viewlog.GetInteger(), false );
|
|
}
|
|
win32.win_viewlog.ClearModified();
|
|
}
|
|
|
|
if ( curNumBufferedPrintfLines > 0 ) {
|
|
// if Sys_Printf() had been called in another thread, add those lines to the windows console now
|
|
EnterCriticalSection( &printfCritSect );
|
|
int n = Min( curNumBufferedPrintfLines, (int)MAXNUMBUFFEREDLINES );
|
|
for ( int i = 0; i < n; ++i ) {
|
|
Conbuf_AppendText( bufferedPrintfLines[i] );
|
|
}
|
|
curNumBufferedPrintfLines = 0;
|
|
LeaveCriticalSection( &printfCritSect );
|
|
}
|
|
|
|
if ( hadError ) {
|
|
// if Sys_Error() had been called in another thread, handle it now
|
|
Sys_Error( "%s", errorText );
|
|
}
|
|
}
|
|
|
|
// the MFC tools use Win_GetWindowScalingFactor() for High-DPI support
|
|
#ifdef ID_ALLOW_TOOLS
|
|
|
|
typedef enum D3_MONITOR_DPI_TYPE {
|
|
D3_MDT_EFFECTIVE_DPI = 0,
|
|
D3_MDT_ANGULAR_DPI = 1,
|
|
D3_MDT_RAW_DPI = 2,
|
|
D3_MDT_DEFAULT = D3_MDT_EFFECTIVE_DPI
|
|
} D3_MONITOR_DPI_TYPE;
|
|
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-getdpiformonitor
|
|
// GetDpiForMonitor() - Win8.1+, shellscalingapi.h, Shcore.dll
|
|
static HRESULT (STDAPICALLTYPE *D3_GetDpiForMonitor)(HMONITOR hmonitor, D3_MONITOR_DPI_TYPE dpiType, UINT *dpiX, UINT *dpiY) = NULL;
|
|
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdpiforwindow
|
|
// GetDpiForWindow() - Win10 1607+, winuser.h/Windows.h, User32.dll
|
|
static UINT(WINAPI *D3_GetDpiForWindow)(HWND hwnd) = NULL;
|
|
|
|
float Win_GetWindowScalingFactor(HWND window)
|
|
{
|
|
// the best way - supported by Win10 1607 and newer
|
|
if ( D3_GetDpiForWindow != NULL ) {
|
|
UINT dpi = D3_GetDpiForWindow(window);
|
|
return static_cast<float>(dpi) / 96.0f;
|
|
}
|
|
|
|
// probably second best, supported by Win8.1 and newer
|
|
if ( D3_GetDpiForMonitor != NULL ) {
|
|
HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
|
|
UINT dpiX = 96, dpiY;
|
|
D3_GetDpiForMonitor(monitor, D3_MDT_EFFECTIVE_DPI, &dpiX, &dpiY);
|
|
return static_cast<float>(dpiX) / 96.0f;
|
|
}
|
|
|
|
// on older versions of windows, DPI was system-wide (not per monitor)
|
|
// and changing DPI required logging out and in again (AFAIK), so we only need to get it once
|
|
static float scaling_factor = -1.0f;
|
|
if ( scaling_factor == -1.0f ) {
|
|
HDC hdc = GetDC(window);
|
|
if (hdc == NULL) {
|
|
return 1.0f;
|
|
}
|
|
// "Number of pixels per logical inch along the screen width. In a system with multiple display monitors, this value is the same for all monitors."
|
|
int ppi = GetDeviceCaps(hdc, LOGPIXELSX);
|
|
scaling_factor = static_cast<float>(ppi) / 96.0f;
|
|
}
|
|
return scaling_factor;
|
|
}
|
|
|
|
#endif // ID_ALLOW_TOOLS
|
|
|
|
// code that tells windows we're High DPI aware so it doesn't scale our windows
|
|
// taken from Yamagi Quake II
|
|
|
|
typedef enum D3_PROCESS_DPI_AWARENESS {
|
|
D3_PROCESS_DPI_UNAWARE = 0,
|
|
D3_PROCESS_SYSTEM_DPI_AWARE = 1,
|
|
D3_PROCESS_PER_MONITOR_DPI_AWARE = 2
|
|
} D3_PROCESS_DPI_AWARENESS;
|
|
|
|
static void setHighDPIMode(void)
|
|
{
|
|
/* For Vista, Win7 and Win8 */
|
|
BOOL(WINAPI *SetProcessDPIAware)(void) = NULL;
|
|
|
|
/* Win8.1 and later */
|
|
HRESULT(WINAPI *SetProcessDpiAwareness)(D3_PROCESS_DPI_AWARENESS dpiAwareness) = NULL;
|
|
|
|
HINSTANCE userDLL = LoadLibrary("USER32.DLL");
|
|
|
|
if (userDLL)
|
|
{
|
|
SetProcessDPIAware = (BOOL(WINAPI *)(void)) GetProcAddress(userDLL, "SetProcessDPIAware");
|
|
}
|
|
|
|
HINSTANCE shcoreDLL = LoadLibrary("SHCORE.DLL");
|
|
|
|
if (shcoreDLL)
|
|
{
|
|
SetProcessDpiAwareness = (HRESULT(WINAPI *)(D3_PROCESS_DPI_AWARENESS))
|
|
GetProcAddress(shcoreDLL, "SetProcessDpiAwareness");
|
|
}
|
|
|
|
if (SetProcessDpiAwareness) {
|
|
SetProcessDpiAwareness(D3_PROCESS_PER_MONITOR_DPI_AWARE);
|
|
}
|
|
else if (SetProcessDPIAware) {
|
|
SetProcessDPIAware();
|
|
}
|
|
|
|
#ifdef ID_ALLOW_TOOLS // also init function pointers for Win_GetWindowScalingFactor() here
|
|
if (userDLL) {
|
|
D3_GetDpiForWindow = (UINT(WINAPI *)(HWND))GetProcAddress(userDLL, "GetDpiForWindow");
|
|
}
|
|
if (shcoreDLL) {
|
|
D3_GetDpiForMonitor = (HRESULT (STDAPICALLTYPE *)(HMONITOR, D3_MONITOR_DPI_TYPE, UINT *, UINT *))
|
|
GetProcAddress(shcoreDLL, "GetDpiForMonitor");
|
|
}
|
|
#endif // ID_ALLOW_TOOLS
|
|
}
|
|
|
|
#ifdef ID_ALLOW_TOOLS
|
|
static void loadWGLpointers() {
|
|
if (hOpenGL_DLL == NULL)
|
|
{
|
|
// Load OpenGL DLL.
|
|
hOpenGL_DLL = LoadLibrary("opengl32.dll");
|
|
if (hOpenGL_DLL == NULL) {
|
|
Sys_Error(GAME_NAME " Cannot Load opengl32.dll - Disabling TOOLS");
|
|
return;
|
|
}
|
|
}
|
|
// opengl32.dll found... grab the addresses.
|
|
|
|
qwglGetProcAddress = (PWGLGETPROCADDRESS)GetProcAddress(hOpenGL_DLL, "wglGetProcAddress");
|
|
|
|
// Context controls
|
|
qwglCopyContext = (PWGLCOPYCONTEXT)GetProcAddress(hOpenGL_DLL, "wglCopyContext");
|
|
qwglCreateContext = (PWGLCREATECONTEXT)GetProcAddress(hOpenGL_DLL, "wglCreateContext");
|
|
qwglCreateLayerContext = (PWGLCREATELAYERCONTEXT)GetProcAddress(hOpenGL_DLL, "wglCreateLayerContext");
|
|
qwglDeleteContext = (PWGLDELETECONTEXT)GetProcAddress(hOpenGL_DLL, "wglDeleteContext");
|
|
qwglGetCurrentContext = (PWGLGETCURRENTCONTEXT)GetProcAddress(hOpenGL_DLL, "wglGetCurrentContext");
|
|
qwglGetCurrentDC = (PWGLGETCURRENTDC)GetProcAddress(hOpenGL_DLL, "wglGetCurrentDC");
|
|
qwglMakeCurrent = (PWGLMAKECURRENT)GetProcAddress(hOpenGL_DLL, "wglMakeCurrent");
|
|
qwglShareLists = (PWGLSHARELISTS)GetProcAddress(hOpenGL_DLL, "wglShareLists");
|
|
|
|
// Fonts
|
|
qwglUseFontBitmaps = (PWGLUSEFONTBITMAPS)GetProcAddress(hOpenGL_DLL, "wglUseFontBitmapsA");
|
|
qwglUseFontOutlines = (PWGLUSEFONTOUTLINES)GetProcAddress(hOpenGL_DLL, "wglUseFontOutlinesA");
|
|
|
|
// Layers.
|
|
qwglDescribeLayerPlane = (PWGLDESCRIBELAYERPLANE)GetProcAddress(hOpenGL_DLL, "wglDescribeLayerPlane");
|
|
qwglSwapLayerBuffers = (PWGLSWAPLAYERBUFFERS)GetProcAddress(hOpenGL_DLL, "wglSwapLayerBuffers");
|
|
|
|
// Palette controls
|
|
qwglGetLayerPaletteEntries = (PWGLGETLAYERPALETTEENTRIES)GetProcAddress(hOpenGL_DLL, "wglGetLayerPaletteEntries");
|
|
qwglRealizeLayerPalette = (PWGLREALIZELAYERPALETTE)GetProcAddress(hOpenGL_DLL, "wglRealizeLayerPalette");
|
|
qwglSetLayerPaletteEntries = (PWGLSETLAYERPALETTEENTRIES)GetProcAddress(hOpenGL_DLL, "wglSetLayerPaletteEntries");
|
|
|
|
|
|
// These by default exist in windows
|
|
qwglSwapBuffers = SwapBuffers;
|
|
}
|
|
|
|
// calls wglChoosePixelFormatARB() or ChoosePixelFormat() matching the main window from SDL
|
|
int Win_ChoosePixelFormat(HDC hdc)
|
|
{
|
|
if (win32.wglChoosePixelFormatARB != NULL && win32.piAttribIList != NULL) {
|
|
int formats[4];
|
|
UINT numFormats = 0;
|
|
if (win32.wglChoosePixelFormatARB(hdc, win32.piAttribIList, NULL, 4, formats, &numFormats) && numFormats > 0) {
|
|
return formats[0];
|
|
}
|
|
static bool haveWarned = false;
|
|
if(!haveWarned) {
|
|
common->Warning("wglChoosePixelFormatARB() failed, falling back to ChoosePixelFormat()!\n");
|
|
haveWarned = true;
|
|
}
|
|
}
|
|
// fallback to normal ChoosePixelFormats() - doesn't support MSAA!
|
|
return ChoosePixelFormat(hdc, &win32.pfd);
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
==================
|
|
WinMain
|
|
==================
|
|
*/
|
|
int main(int argc, char *argv[]) {
|
|
const HCURSOR hcurSave = ::SetCursor( LoadCursor( 0, IDC_WAIT ) );
|
|
|
|
InitializeCriticalSection( &printfCritSect );
|
|
|
|
#ifdef ID_DEDICATED
|
|
MSG msg;
|
|
#else
|
|
// tell windows we're high dpi aware, otherwise display scaling screws up the game
|
|
setHighDPIMode();
|
|
#endif
|
|
|
|
Sys_SetPhysicalWorkMemory( 192 << 20, 1024 << 20 );
|
|
|
|
win32.hInstance = GetModuleHandle(NULL);
|
|
|
|
// done before Com/Sys_Init since we need this for error output
|
|
Sys_CreateConsole();
|
|
|
|
// no abort/retry/fail errors
|
|
SetErrorMode( SEM_FAILCRITICALERRORS );
|
|
|
|
#ifdef DEBUG
|
|
// disable the painfully slow MS heap check every 1024 allocs
|
|
_CrtSetDbgFlag( 0 );
|
|
#endif
|
|
|
|
#ifdef ID_ALLOW_TOOLS
|
|
loadWGLpointers();
|
|
#endif
|
|
|
|
if ( argc > 1 ) {
|
|
common->Init( argc-1, &argv[1] );
|
|
} else {
|
|
common->Init( 0, NULL );
|
|
}
|
|
|
|
// hide or show the early console as necessary
|
|
if ( win32.win_viewlog.GetInteger() || com_skipRenderer.GetBool() || idAsyncNetwork::serverDedicated.GetInteger() ) {
|
|
Sys_ShowConsole( 1, true );
|
|
} else {
|
|
Sys_ShowConsole( 0, false );
|
|
}
|
|
|
|
#ifdef SET_THREAD_AFFINITY
|
|
// give the main thread an affinity for the first cpu
|
|
SetThreadAffinityMask( GetCurrentThread(), 1 );
|
|
#endif
|
|
|
|
// ::SetCursor( hcurSave ); // DG: I think SDL handles the cursor fine..
|
|
|
|
// Launch the script debugger
|
|
if ( strstr( GetCommandLine(), "+debugger" ) ) {
|
|
|
|
#ifdef ID_ALLOW_TOOLS
|
|
DebuggerClientInit(GetCommandLine());
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
// ::SetFocus( win32.hWnd ); // DG: let SDL handle focus, otherwise input is fucked up! (#100)
|
|
|
|
// main game loop
|
|
while( 1 ) {
|
|
#if ID_DEDICATED
|
|
// Since this is a Dedicated Server, process all Windowing Messages
|
|
// Now.
|
|
while(PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)){
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
}
|
|
|
|
// Give the OS a little time to recuperate.
|
|
Sleep(10);
|
|
#endif
|
|
|
|
Win_Frame();
|
|
|
|
#ifdef ID_ALLOW_TOOLS
|
|
if ( com_editors ) {
|
|
if ( com_editors & EDITOR_GUI ) {
|
|
// GUI editor
|
|
GUIEditorRun();
|
|
} else if ( com_editors & EDITOR_RADIANT ) {
|
|
// Level Editor
|
|
RadiantRun();
|
|
}
|
|
else if (com_editors & EDITOR_MATERIAL ) {
|
|
//BSM Nerve: Add support for the material editor
|
|
MaterialEditorRun();
|
|
}
|
|
else {
|
|
if ( com_editors & EDITOR_LIGHT ) {
|
|
// in-game Light Editor
|
|
LightEditorRun();
|
|
}
|
|
if ( com_editors & EDITOR_SOUND ) {
|
|
// in-game Sound Editor
|
|
SoundEditorRun();
|
|
}
|
|
if ( com_editors & EDITOR_DECL ) {
|
|
// in-game Declaration Browser
|
|
DeclBrowserRun();
|
|
}
|
|
if ( com_editors & EDITOR_AF ) {
|
|
// in-game Articulated Figure Editor
|
|
AFEditorRun();
|
|
}
|
|
if ( com_editors & EDITOR_PARTICLE ) {
|
|
// in-game Particle Editor
|
|
ParticleEditorRun();
|
|
}
|
|
if ( com_editors & EDITOR_SCRIPT ) {
|
|
// in-game Script Editor
|
|
ScriptEditorRun();
|
|
}
|
|
if ( com_editors & EDITOR_PDA ) {
|
|
// in-game PDA Editor
|
|
PDAEditorRun();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
// run the game
|
|
common->Frame();
|
|
}
|
|
|
|
// never gets here
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idSysLocal::OpenURL
|
|
==================
|
|
*/
|
|
void idSysLocal::OpenURL( const char *url, bool doexit ) {
|
|
static bool doexit_spamguard = false;
|
|
HWND wnd;
|
|
|
|
if (doexit_spamguard) {
|
|
common->DPrintf( "OpenURL: already in an exit sequence, ignoring %s\n", url );
|
|
return;
|
|
}
|
|
|
|
common->Printf("Open URL: %s\n", url);
|
|
|
|
if ( !ShellExecute( NULL, "open", url, NULL, NULL, SW_RESTORE ) ) {
|
|
common->Error( "Could not open url: '%s' ", url );
|
|
return;
|
|
}
|
|
|
|
wnd = GetForegroundWindow();
|
|
if ( wnd ) {
|
|
ShowWindow( wnd, SW_MAXIMIZE );
|
|
}
|
|
|
|
if ( doexit ) {
|
|
doexit_spamguard = true;
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "quit\n" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
idSysLocal::StartProcess
|
|
==================
|
|
*/
|
|
void idSysLocal::StartProcess( const char *exePath, bool doexit ) {
|
|
TCHAR szPathOrig[_MAX_PATH];
|
|
STARTUPINFO si;
|
|
PROCESS_INFORMATION pi;
|
|
|
|
ZeroMemory( &si, sizeof(si) );
|
|
si.cb = sizeof(si);
|
|
|
|
strncpy( szPathOrig, exePath, _MAX_PATH );
|
|
|
|
if( !CreateProcess( NULL, szPathOrig, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi ) ) {
|
|
common->Error( "Could not start process: '%s' ", szPathOrig );
|
|
return;
|
|
}
|
|
|
|
if ( doexit ) {
|
|
cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "quit\n" );
|
|
}
|
|
}
|