Index: ps/trunk/binaries/data/mods/public/gui/pregame/SplashscreenHandler.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/pregame/SplashscreenHandler.js (revision 24109) +++ ps/trunk/binaries/data/mods/public/gui/pregame/SplashscreenHandler.js (revision 24110) @@ -1,67 +1,37 @@ class SplashScreenHandler { constructor(initData, hotloadData) { this.showSplashScreen = hotloadData ? hotloadData.showSplashScreen : initData && initData.isStartup; this.mainMenuPage = Engine.GetGUIObjectByName("mainMenuPage"); this.mainMenuPage.onTick = this.onFirstTick.bind(this); } getHotloadData() { // Only show splash screen(s) once at startup, but not again after hotloading return { "showSplashScreen": this.showSplashScreen }; } // Don't call this from the init function in order to not crash when opening the new page on init on hotloading // and not possibly crash when opening the new page on init and throwing a JS error. onFirstTick() { if (this.showSplashScreen) this.openPage(); delete this.mainMenuPage.onTick; } openPage() { this.showSplashScreen = false; if (Engine.ConfigDB_GetValue("user", "gui.splashscreen.enable") === "true" || Engine.ConfigDB_GetValue("user", "gui.splashscreen.version") < Engine.GetFileMTime("gui/splashscreen/splashscreen.txt")) - Engine.PushGuiPage("page_splashscreen.xml", {}, this.showRenderPathMessage); - else - this.showRenderPathMessage(); - } - - showRenderPathMessage() - { - // Warn about removing fixed render path - if (Engine.Renderer_GetRenderPath() != "fixed") - return; - - messageBox( - 600, 300, - "[font=\"sans-bold-16\"]" + - sprintf(translate("%(warning)s You appear to be using non-shader (fixed function) graphics. This option will be removed in a future 0 A.D. release, to allow for more advanced graphics features. We advise upgrading your graphics card to a more recent, shader-compatible model."), { - "warning": coloredText("Warning:", "200 20 20") - }) + - "\n\n" + - // Translation: This is the second paragraph of a warning. The - // warning explains that the user is using “non-shader“ graphics, - // and that in the future this will not be supported by the game, so - // the user will need a better graphics card. - translate("Please press \"Read More\" for more information or \"OK\" to continue."), - translate("WARNING!"), - [translate("OK"), translate("Read More")], - [ - null, - () => { - Engine.OpenURL("https://www.wildfiregames.com/forum/index.php?showtopic=16734"); - } - ]); + Engine.PushGuiPage("page_splashscreen.xml", {}); } } Index: ps/trunk/source/lib/debug.h =================================================================== --- ps/trunk/source/lib/debug.h (revision 24109) +++ ps/trunk/source/lib/debug.h (revision 24110) @@ -1,578 +1,582 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * platform-independent debug support code. */ #ifndef INCLUDED_DEBUG #define INCLUDED_DEBUG // this module provides platform-independent debug facilities, useful for // diagnosing and reporting program errors. // - a symbol engine provides access to compiler-generated debug information and // can also give a stack trace including local variables; // - our more powerful assert() replacement gives a stack trace so // that the underlying problem becomes apparent; // - the output routines make for platform-independent logging and // crashlogs with "last-known activity" reporting. #include "lib/lib_api.h" #include "lib/types.h" // intptr_t #include "lib/status.h" #include "lib/alignment.h" #include "lib/code_annotation.h" #include "lib/code_generation.h" /** * trigger a breakpoint when reached/"called". * if defined as a macro, the debugger can break directly into the * target function instead of one frame below it as with a conventional * call-based implementation. **/ #if MSC_VERSION # define debug_break __debugbreak // intrinsic "function" #else extern void debug_break(); #endif //----------------------------------------------------------------------------- // output //----------------------------------------------------------------------------- /** * write a formatted string to the debug channel, subject to filtering * (see below). implemented via debug_puts - see performance note there. * * @param fmt Format string and varargs; see printf. **/ LIB_API void debug_printf(const char* fmt, ...) PRINTF_ARGS(1); /** * translates and displays the given strings in a dialog. * this is typically only used when debug_DisplayError has failed or * is unavailable because that function is much more capable. * implemented via sys_display_msg; see documentation there. **/ LIB_API void debug_DisplayMessage(const wchar_t* caption, const wchar_t* msg); /// flags to customize debug_DisplayError behavior enum DebugDisplayErrorFlags { /** * disallow the Continue button. used e.g. if an exception is fatal. **/ DE_NO_CONTINUE = 1, /** * enable the Suppress button. set automatically by debug_DisplayError if * it receives a non-NULL suppress pointer. a flag is necessary because * the sys_display_error interface doesn't get that pointer. * rationale for automatic setting: this may prevent someone from * forgetting to specify it, and disabling Suppress despite having * passed a non-NULL pointer doesn't make much sense. **/ DE_ALLOW_SUPPRESS = 2, /** * do not trigger a breakpoint inside debug_DisplayError; caller * will take care of this if ER_BREAK is returned. this is so that the * debugger can jump directly into the offending function. **/ DE_MANUAL_BREAK = 4, /** * display just the given message; do not add any information about the * call stack, do not write crashlogs, etc. */ DE_NO_DEBUG_INFO = 8 }; /** * a bool that is reasonably certain to be set atomically. * we cannot assume support for OpenMP (requires GCC 4.2) or C++0x, * so we'll have to resort to intptr_t, cpu_CAS and COMPILER_FENCE. **/ typedef volatile intptr_t atomic_bool; /** * value for suppress flag once set by debug_DisplayError. * rationale: this value is fairly distinctive and helps when * debugging the symbol engine. * use 0 as the initial value to avoid allocating .rdata space. **/ static const atomic_bool DEBUG_SUPPRESS = 0xAB; /** * choices offered by the error dialog that are returned * by debug_DisplayError. **/ enum ErrorReaction { /** * ignore, continue as if nothing happened. * note: value doesn't start at 0 because that is interpreted as a * DialogBoxParam failure. **/ ER_CONTINUE = 1, /** * trigger breakpoint, i.e. enter debugger. * only returned if DE_MANUAL_BREAK was passed; otherwise, * debug_DisplayError will trigger a breakpoint itself. **/ ER_BREAK }; /** * all choices offered by the error dialog. those not defined in * ErrorReaction are acted upon by debug_DisplayError and * never returned to callers. * (this separation avoids enumerator-not-handled warnings) **/ enum ErrorReactionInternal { ERI_CONTINUE = ER_CONTINUE, ERI_BREAK = ER_BREAK, /** * ignore and do not report again. * note: non-persistent; only applicable during this program run. **/ ERI_SUPPRESS, /** * exit the program immediately. **/ ERI_EXIT, /** * special return value for the display_error app hook stub to indicate * that it has done nothing and that the normal sys_display_error * implementation should be called instead. **/ ERI_NOT_IMPLEMENTED }; /** * display an error dialog with a message and stack trace. * * @param description text to show. * @param flags: see DebugDisplayErrorFlags. * @param context, lastFuncToSkip: see debug_DumpStack. * @param file, line, func: location of the error (typically passed as * WIDEN(__FILE__), __LINE__, __func__ from a macro) * @param suppress pointer to a caller-allocated flag that can be used to * suppress this error. if NULL, this functionality is skipped and the * "Suppress" dialog button will be disabled. * note: this flag is read and written exclusively here; caller only * provides the storage. values: see DEBUG_SUPPRESS above. * @return ErrorReaction (user's choice: continue running or stop?) **/ LIB_API ErrorReaction debug_DisplayError(const wchar_t* description, size_t flags, void* context, const wchar_t* lastFuncToSkip, const wchar_t* file, int line, const char* func, atomic_bool* suppress); // simplified version for just displaying an error message -#define DEBUG_DISPLAY_ERROR(description)\ +#define DEBUG_DISPLAY_ERROR_IMPL(description, flags)\ do\ {\ CACHE_ALIGNED(u8) context[DEBUG_CONTEXT_SIZE];\ (void)debug_CaptureContext(context);\ - (void)debug_DisplayError(description, 0, context, L"debug_DisplayError", WIDEN(__FILE__), __LINE__, __func__, 0);\ + (void)debug_DisplayError(description, flags, context, L"debug_DisplayError", WIDEN(__FILE__), __LINE__, __func__, 0);\ }\ while(0) +#define DEBUG_DISPLAY_ERROR(description) DEBUG_DISPLAY_ERROR_IMPL(description, 0) +// disallow continue for the error. +#define DEBUG_DISPLAY_FATAL_ERROR(description) DEBUG_DISPLAY_ERROR_IMPL(description, DE_NO_CONTINUE) + // // filtering // /** * debug output is very useful, but "too much of a good thing can kill you". * we don't want to require different LOGn() macros that are enabled * depending on "debug level", because changing that entails lengthy * compiles and it's too coarse-grained. instead, we require all * strings to start with "tag_string|" (exact case and no quotes; * the alphanumeric-only \ identifies output type). * they are then subject to filtering: only if the tag has been * "added" via debug_filter_add is the appendant string displayed. * * this approach is easiest to implement and is fine because we control * all logging code. LIMODS falls from consideration since it's not * portable and too complex. * * notes: * - filter changes only affect subsequent debug_*printf calls; * output that didn't pass the filter is permanently discarded. * - strings not starting with a tag are always displayed. * - debug_filter_* can be called at any time and from the debugger, * but are not reentrant. * * in future, allow output with the given tag to proceed. * no effect if already added. **/ LIB_API void debug_filter_add(const char* tag); /** * in future, discard output with the given tag. * no effect if not currently added. **/ LIB_API void debug_filter_remove(const char* tag); /** * clear all filter state; equivalent to debug_filter_remove for * each tag that was debug_filter_add-ed. **/ LIB_API void debug_filter_clear(); /** * indicate if the given text would be printed. * useful for a series of debug_printfs - avoids needing to add a tag to * each of their format strings. **/ LIB_API bool debug_filter_allows(const char* text); /** * call debug_puts if debug_filter_allows allows the string. **/ LIB_API void debug_puts_filtered(const char* text); /** * write an error description and all logs into crashlog.txt * (in unicode format). * * @param text description of the error (including stack trace); * typically generated by debug_BuildErrorMessage. * * @return Status; ERR::REENTERED if reentered via recursion or * multithreading (not allowed since an infinite loop may result). **/ LIB_API Status debug_WriteCrashlog(const char* text); //----------------------------------------------------------------------------- // assertions //----------------------------------------------------------------------------- /** * ensure the expression \ evaluates to non-zero. used to validate * invariants in the program during development and thus gives a * very helpful warning if something isn't going as expected. * sprinkle these liberally throughout your code! * * to pass more information to users at runtime, you can write * ENSURE(expression && "descriptive string"). **/ #define ENSURE(expr)\ do\ {\ static atomic_bool suppress__;\ if(!(expr))\ {\ switch(debug_OnAssertionFailure(WIDEN(#expr), &suppress__, WIDEN(__FILE__), __LINE__, __func__))\ {\ case ER_CONTINUE:\ break;\ case ER_BREAK:\ default:\ debug_break();\ break;\ }\ }\ }\ while(0) /** * same as ENSURE in debug mode, does nothing in release mode. * (we don't override the `assert' macro because users may * inadvertently include \ afterwards) * (we do not provide an MFC-style VERIFY macro because the distinction * between ENSURE and VERIFY is unclear. to always run code but only * check for success in debug builds without raising unused-variable warnings, * use ASSERT + UNUSED2.) **/ #define ASSERT(expr) ENSURE(expr) #ifdef NDEBUG # undef ASSERT # define ASSERT(expr) #endif /** * display the error dialog with the given text. this is less error-prone than * ENSURE(0 && "text"). note that "conditional expression is constant" warnings * are disabled anyway. * * if being able to suppress the warning is desirable (e.g. for self-tests), * then use DEBUG_WARN_ERR instead. **/ #define debug_warn(expr) ENSURE(0 && (expr)) /** * display the error dialog with text corresponding to the given error code. * used by WARN_RETURN_STATUS_IF_ERR et al., which wrap function calls and automatically * raise warnings and return to the caller. **/ #define DEBUG_WARN_ERR(status)\ do\ {\ static atomic_bool suppress__;\ switch(debug_OnError(status, &suppress__, WIDEN(__FILE__), __LINE__, __func__))\ {\ case ER_CONTINUE:\ break;\ case ER_BREAK:\ default:\ debug_break();\ break;\ }\ }\ while(0) /** * called when a ENSURE/ASSERT fails; * notifies the user via debug_DisplayError. * * @param assert_expr the expression that failed; typically passed as * \#expr in the assert macro. * @param suppress see debug_DisplayError. * @param file, line source file name and line number of the spot that failed * @param func name of the function containing it * @return ErrorReaction (user's choice: continue running or stop?) **/ LIB_API ErrorReaction debug_OnAssertionFailure(const wchar_t* assert_expr, atomic_bool* suppress, const wchar_t* file, int line, const char* func) ANALYZER_NORETURN; /** * called when a DEBUG_WARN_ERR indicates an error occurred; * notifies the user via debug_DisplayError. * * @param err Status value indicating the error that occurred * @param suppress see debug_DisplayError. * @param file, line source file name and line number of the spot that failed * @param func name of the function containing it * @return ErrorReaction (user's choice: continue running or stop?) **/ LIB_API ErrorReaction debug_OnError(Status err, atomic_bool* suppress, const wchar_t* file, int line, const char* func) ANALYZER_NORETURN; /** * suppress (prevent from showing) the error dialog from subsequent * debug_OnError for the given Status. * * rationale: for edge cases in some functions, warnings are raised in * addition to returning an error code. self-tests deliberately trigger * these cases and check for the latter but shouldn't cause the former. * we therefore need to squelch them. * * @param err the Status to skip. * * note: only one concurrent skip request is allowed; call * debug_StopSkippingErrors before the next debug_SkipErrors. */ LIB_API void debug_SkipErrors(Status err); /** * @return how many errors were skipped since the call to debug_SkipErrors() **/ LIB_API size_t debug_StopSkippingErrors(); //----------------------------------------------------------------------------- // symbol access //----------------------------------------------------------------------------- namespace ERR { const Status SYM_NO_STACK_FRAMES_FOUND = -100400; const Status SYM_UNRETRIEVABLE_STATIC = -100401; const Status SYM_UNRETRIEVABLE = -100402; const Status SYM_TYPE_INFO_UNAVAILABLE = -100403; const Status SYM_INTERNAL_ERROR = -100404; const Status SYM_UNSUPPORTED = -100405; const Status SYM_CHILD_NOT_FOUND = -100406; // this limit is to prevent infinite recursion. const Status SYM_NESTING_LIMIT = -100407; // this limit is to prevent large symbols (e.g. arrays or linked lists) // from taking up all available output space. const Status SYM_SINGLE_SYMBOL_LIMIT = -100408; } namespace INFO { // one of the dump_sym* functions decided not to output anything at // all (e.g. for member functions in UDTs - we don't want those). // therefore, skip any post-symbol formatting (e.g. ) as well. const Status SYM_SUPPRESS_OUTPUT = +100409; } /** * Maximum number of characters (including null terminator) written to * user's buffers by debug_ResolveSymbol. **/ static const size_t DEBUG_SYMBOL_CHARS = 1000; static const size_t DEBUG_FILE_CHARS = 100; /** * read and return symbol information for the given address. * * NOTE: the PDB implementation is rather slow (~500 us). * * @param ptr_of_interest address of symbol (e.g. function, variable) * @param sym_name optional out; holds at least DEBUG_SYMBOL_CHARS; * receives symbol name returned via debug info. * @param file optional out; holds at least DEBUG_FILE_CHARS; * receives base name only (no path; see rationale in wdbg_sym) of * source file containing the symbol. * @param line optional out; receives source file line number of symbol. * * note: all of the output parameters are optional; we pass back as much * information as is available and desired. * @return Status; INFO::OK iff any information was successfully * retrieved and stored. **/ LIB_API Status debug_ResolveSymbol(void* ptr_of_interest, wchar_t* sym_name, wchar_t* file, int* line); static const size_t DEBUG_CONTEXT_SIZE = 2048; // Win32 CONTEXT is currently 1232 bytes /** * @param context must point to an instance of the platform-specific type * (e.g. CONTEXT) or CACHE_ALIGNED storage of DEBUG_CONTEXT_SIZE bytes. **/ LIB_API Status debug_CaptureContext(void* context); /** * write a complete stack trace (including values of local variables) into * the specified buffer. * * @param buf Target buffer. * @param maxChars Max chars of buffer (should be several thousand). * @param context Platform-specific representation of execution state * (e.g. Win32 CONTEXT). either specify an SEH exception's * context record or use debug_CaptureContext to retrieve the current state. * Rationale: intermediates such as debug_DisplayError change the * context, so it should be captured as soon as possible. * @param lastFuncToSkip Is used for omitting error-reporting functions like * debug_OnAssertionFailure from the stack trace. It is either 0 (skip nothing) or * a substring of a function's name (this allows platform-independent * matching of stdcall-decorated names). * Rationale: this is safer than specifying a fixed number of frames, * which can be incorrect due to inlining. * @return Status; ERR::REENTERED if reentered via recursion or * multithreading (not allowed since static data is used). **/ LIB_API Status debug_DumpStack(wchar_t* buf, size_t maxChars, void* context, const wchar_t* lastFuncToSkip); //----------------------------------------------------------------------------- // helper functions (used by implementation) //----------------------------------------------------------------------------- /** * [system-dependent] write a string to the debug channel. * this can be quite slow (~1 ms)! On Windows, it uses OutputDebugString * (entails context switch), otherwise stdout+fflush (waits for IO). **/ LIB_API void debug_puts(const char* text); /** * return the caller of a certain function on the call stack. * * this function is useful for recording (partial) stack traces for * memory allocation tracking, etc. * * @param context, lastFuncToSkip - see debug_DumpStack * @return address of the caller **/ LIB_API void* debug_GetCaller(void* context, const wchar_t* lastFuncToSkip); /** * check if a pointer appears to be totally invalid. * * this check is not authoritative (the pointer may be "valid" but incorrect) * but can be used to filter out obviously wrong values in a portable manner. * * @param p pointer * @return 1 if totally bogus, otherwise 0. **/ LIB_API int debug_IsPointerBogus(const void* p); /// does the given pointer appear to point to code? LIB_API bool debug_IsCodePointer(void* p); /// does the given pointer appear to point to the stack? LIB_API bool debug_IsStackPointer(void* p); /** * inform the debugger of the current thread's name. * * (threads are easier to keep apart when they are identified by * name rather than TID.) **/ LIB_API void debug_SetThreadName(const char* name); /** * holds memory for an error message. **/ struct ErrorMessageMem { // rationale: // - error messages with stack traces require a good deal of memory // (hundreds of KB). static buffers of that size are undesirable. // - the heap may be corrupted, so don't use malloc. // instead, "lib/sysdep/vm.h" functions should be safe. // - alloca is a bit iffy (the stack may be maxed out), non-portable and // complicates the code because it can't be allocated by a subroutine. // - this method is probably slow, but error messages aren't built often. // if necessary, first try malloc and use mmap if that fails. void* pa_mem; }; /** * free memory from the error message. * * @param emm ErrorMessageMem* **/ LIB_API void debug_FreeErrorMessage(ErrorMessageMem* emm); /** * build a string describing the given error. * * this is a helper function used by debug_DumpStack and is made available * so that the self-test doesn't have to display the error dialog. * * @param description: general description of the problem. * @param fn_only filename (no path) of source file that triggered the error. * @param line, func: exact position of the error. * @param context, lastFuncToSkip: see debug_DumpStack. * @param emm memory for the error message. caller should allocate * stack memory and set alloc_buf*; if not, there will be no * fallback in case heap alloc fails. should be freed via * debug_FreeErrorMessage when no longer needed. **/ LIB_API const wchar_t* debug_BuildErrorMessage(const wchar_t* description, const wchar_t* fn_only, int line, const char* func, void* context, const wchar_t* lastFuncToSkip, ErrorMessageMem* emm); #endif // #ifndef INCLUDED_DEBUG Index: ps/trunk/source/lib/sysdep/os/win/wsysdep.cpp =================================================================== --- ps/trunk/source/lib/sysdep/os/win/wsysdep.cpp (revision 24109) +++ ps/trunk/source/lib/sysdep/os/win/wsysdep.cpp (revision 24110) @@ -1,613 +1,636 @@ /* Copyright (C) 2020 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Windows backend of the sysdep interface */ #include "precompiled.h" #include "lib/sysdep/sysdep.h" #include "lib/alignment.h" #include "lib/sysdep/os/win/win.h" // includes windows.h; must come before shlobj #include // SDL_SetClipboardText #include // pick_dir #include // open_url #include #include // message crackers #include #include "lib/sysdep/os/win/error_dialog.h" #include "lib/sysdep/os/win/wutil.h" #if CONFIG_ENABLE_BOOST # include #endif #if MSC_VERSION #pragma comment(lib, "shell32.lib") // for sys_pick_directory SH* calls #pragma comment(lib, "winhttp.lib") #endif bool sys_IsDebuggerPresent() { return (IsDebuggerPresent() != 0); } std::wstring sys_WideFromArgv(const char* argv_i) { // NB: despite http://cbloomrants.blogspot.com/2008/06/06-14-08-1.html, // WinXP x64 EN cmd.exe (chcp reports 437) encodes argv u-umlaut // (entered manually or via auto-complete) via cp1252. the same applies // to WinXP SP2 DE (where chcp reports 850). const UINT cp = CP_ACP; const DWORD flags = MB_PRECOMPOSED|MB_ERR_INVALID_CHARS; const int inputSize = -1; // null-terminated std::vector buf(strlen(argv_i)+1); // (upper bound on number of characters) // NB: avoid mbstowcs because it may specify another locale const int ret = MultiByteToWideChar(cp, flags, argv_i, (int)inputSize, &buf[0], (int)buf.size()); ENSURE(ret != 0); return std::wstring(&buf[0]); } void sys_display_msg(const wchar_t* caption, const wchar_t* msg) { MessageBoxW(0, msg, caption, MB_ICONEXCLAMATION|MB_TASKMODAL|MB_SETFOREGROUND); } //----------------------------------------------------------------------------- // "program error" dialog (triggered by ENSURE and exception) //----------------------------------------------------------------------------- // support for resizing the dialog / its controls (must be done manually) static POINTS dlg_clientOrigin; static POINTS dlg_prevClientSize; static void dlg_OnMove(HWND UNUSED(hDlg), int x, int y) { dlg_clientOrigin.x = (short)x; dlg_clientOrigin.y = (short)y; } static const size_t ANCHOR_LEFT = 0x01; static const size_t ANCHOR_RIGHT = 0x02; static const size_t ANCHOR_TOP = 0x04; static const size_t ANCHOR_BOTTOM = 0x08; static const size_t ANCHOR_ALL = 0x0F; static void dlg_ResizeControl(HWND hDlg, int dlgItem, int dx, int dy, size_t anchors) { HWND hControl = GetDlgItem(hDlg, dlgItem); RECT r; GetWindowRect(hControl, &r); int w = r.right - r.left, h = r.bottom - r.top; int x = r.left - dlg_clientOrigin.x, y = r.top - dlg_clientOrigin.y; if(anchors & ANCHOR_RIGHT) { // right only if(!(anchors & ANCHOR_LEFT)) x += dx; // horizontal (stretch width) else w += dx; } if(anchors & ANCHOR_BOTTOM) { // bottom only if(!(anchors & ANCHOR_TOP)) y += dy; // vertical (stretch height) else h += dy; } SetWindowPos(hControl, 0, x,y, w,h, SWP_NOZORDER); } static void dlg_OnSize(HWND hDlg, UINT state, int clientSizeX, int clientSizeY) { // 'minimize' was clicked. we need to ignore this, otherwise // dx/dy would reduce some control positions to less than 0. // since Windows clips them, we wouldn't later be able to // reconstruct the previous values when 'restoring'. if(state == SIZE_MINIMIZED) return; // NB: origin might legitimately be 0, but we know it is invalid // on the first call to this function, where dlg_prevClientSize is 0. const bool isOriginValid = (dlg_prevClientSize.y != 0); const int dx = clientSizeX - dlg_prevClientSize.x; const int dy = clientSizeY - dlg_prevClientSize.y; dlg_prevClientSize.x = (short)clientSizeX; dlg_prevClientSize.y = (short)clientSizeY; if(!isOriginValid) // must not call dlg_ResizeControl return; dlg_ResizeControl(hDlg, IDC_CONTINUE, dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_SUPPRESS, dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_BREAK , dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_EXIT , dx,dy, ANCHOR_LEFT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_COPY , dx,dy, ANCHOR_RIGHT|ANCHOR_BOTTOM); dlg_ResizeControl(hDlg, IDC_EDIT1 , dx,dy, ANCHOR_ALL); } static void dlg_OnGetMinMaxInfo(HWND UNUSED(hDlg), LPMINMAXINFO mmi) { // we must make sure resize_control will never set negative coords - // Windows would clip them, and its real position would be lost. // restrict to a reasonable and good looking minimum size [pixels]. mmi->ptMinTrackSize.x = 407; mmi->ptMinTrackSize.y = 159; // determined experimentally } struct DialogParams { const wchar_t* text; size_t flags; }; static BOOL dlg_OnInitDialog(HWND hDlg, HWND UNUSED(hWndFocus), LPARAM lParam) { - const DialogParams* params = (const DialogParams*)lParam; + const DialogParams* params = reinterpret_cast(lParam); + SetWindowLongPtr(hDlg, DWLP_USER, lParam); HWND hWnd; // need to reset for new instance of dialog dlg_clientOrigin.x = dlg_clientOrigin.y = 0; dlg_prevClientSize.x = dlg_prevClientSize.y = 0; if(!(params->flags & DE_ALLOW_SUPPRESS)) { hWnd = GetDlgItem(hDlg, IDC_SUPPRESS); EnableWindow(hWnd, FALSE); } + if(params->flags & DE_NO_CONTINUE) + { + hWnd = GetDlgItem(hDlg, IDC_CONTINUE); + EnableWindow(hWnd, FALSE); + } + // set fixed font for readability hWnd = GetDlgItem(hDlg, IDC_EDIT1); HGDIOBJ hObj = (HGDIOBJ)GetStockObject(SYSTEM_FIXED_FONT); LPARAM redraw = FALSE; SendMessage(hWnd, WM_SETFONT, (WPARAM)hObj, redraw); SetDlgItemTextW(hDlg, IDC_EDIT1, params->text); return TRUE; // set default keyboard focus } static void dlg_OnCommand(HWND hDlg, int id, HWND UNUSED(hWndCtl), UINT UNUSED(codeNotify)) { switch(id) { case IDC_COPY: { std::vector buf(128*KiB); // (too big for stack) GetDlgItemTextW(hDlg, IDC_EDIT1, &buf[0], (int)buf.size()); std::string string = utf8_from_wstring(&buf[0]); SDL_SetClipboardText(string.c_str()); break; } case IDC_CONTINUE: EndDialog(hDlg, ERI_CONTINUE); break; case IDC_SUPPRESS: EndDialog(hDlg, ERI_SUPPRESS); break; case IDC_BREAK: EndDialog(hDlg, ERI_BREAK); break; case IDC_EXIT: EndDialog(hDlg, ERI_EXIT); break; default: break; } } +static void dlg_OnClose(HWND hDlg) +{ + const DialogParams* params = reinterpret_cast( + GetWindowLongPtr(hDlg, DWLP_USER)); + if (!params) + return; + + // Interpret close as exit in case we can't continue. + if(params->flags & DE_NO_CONTINUE) + EndDialog(hDlg, ERI_EXIT); +} + + static void dlg_OnSysCommand(HWND hDlg, UINT cmd, int UNUSED(x), int UNUSED(y)) { switch(cmd & 0xFFF0) // NB: lower 4 bits are reserved { // [X] clicked -> close dialog (doesn't happen automatically) case SC_CLOSE: EndDialog(hDlg, 0); break; default: break; } } static INT_PTR CALLBACK dlg_OnMessage(HWND hDlg, unsigned int msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_INITDIALOG: return HANDLE_WM_INITDIALOG(hDlg, wParam, lParam, dlg_OnInitDialog); case WM_SYSCOMMAND: return HANDLE_WM_SYSCOMMAND(hDlg, wParam, lParam, dlg_OnSysCommand); case WM_COMMAND: return HANDLE_WM_COMMAND(hDlg, wParam, lParam, dlg_OnCommand); case WM_MOVE: return HANDLE_WM_MOVE(hDlg, wParam, lParam, dlg_OnMove); case WM_GETMINMAXINFO: return HANDLE_WM_GETMINMAXINFO(hDlg, wParam, lParam, dlg_OnGetMinMaxInfo); case WM_SIZE: return HANDLE_WM_SIZE(hDlg, wParam, lParam, dlg_OnSize); + case WM_CLOSE: + return HANDLE_WM_CLOSE(hDlg, wParam, lParam, dlg_OnClose); + default: // we didn't process the message; caller will perform default action. return FALSE; } } ErrorReactionInternal sys_display_error(const wchar_t* text, size_t flags) { // note: other threads might still be running, crash and take down the // process before we have a chance to display this error message. // ideally we would suspend them all and resume when finished; however, // they may be holding system-wide locks (e.g. heap or loader) that // are potentially needed by DialogBoxParam. in that case, deadlock // would result; this is much worse than a crash because no error // at all is displayed to the end-user. therefore, do nothing here. // temporarily remove any pending quit message from the queue because // it would prevent the dialog from being displayed (DialogBoxParam // returns IDOK without doing anything). will be restored below. // notes: // - this isn't only relevant at exit - Windows also posts one if // window init fails. therefore, it is important that errors can be // displayed regardless. // - by passing hWnd=0, we check all windows belonging to the current // thread. there is no reason to use hWndParent below. MSG msg; const BOOL isQuitPending = PeekMessage(&msg, 0, WM_QUIT, WM_QUIT, PM_REMOVE); const HINSTANCE hInstance = wutil_LibModuleHandle(); LPCWSTR lpTemplateName = MAKEINTRESOURCEW(IDD_DIALOG1); const DialogParams params = { text, flags }; // get the enclosing app's window handle. we can't just pass 0 or // the desktop window because the dialog must be modal (if the app // continues running, it may crash and take down the process before // we've managed to show the dialog). const HWND hWndParent = wutil_AppWindow(); INT_PTR ret = DialogBoxParamW(hInstance, lpTemplateName, hWndParent, dlg_OnMessage, (LPARAM)¶ms); if(isQuitPending) PostQuitMessage((int)msg.wParam); // failed; warn user and make sure we return an ErrorReactionInternal. if(ret == 0 || ret == -1) { debug_DisplayMessage(L"Error", L"Unable to display detailed error dialog."); return ERI_CONTINUE; } return (ErrorReactionInternal)ret; } //----------------------------------------------------------------------------- // misc //----------------------------------------------------------------------------- Status sys_StatusDescription(int user_err, wchar_t* buf, size_t max_chars) { // validate user_err - Win32 doesn't have negative error numbers if(user_err < 0) return ERR::FAIL; // NOWARN const DWORD err = user_err? (DWORD)user_err : GetLastError(); // no one likes to see "The operation completed successfully" in // error messages, so return more descriptive text instead. if(err == 0) { wcscpy_s(buf, max_chars, L"0 (no error code was set)"); return INFO::OK; } wchar_t message[400]; { const LPCVOID source = 0; // ignored (we're not using FROM_HMODULE etc.) const DWORD lang_id = 0; // look for neutral, then current locale va_list* args = 0; // we don't care about "inserts" const DWORD charsWritten = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, source, err, lang_id, message, (DWORD)ARRAY_SIZE(message), args); if(!charsWritten) WARN_RETURN(ERR::FAIL); ENSURE(charsWritten < max_chars); if(message[charsWritten-1] == '\n') message[charsWritten-1] = '\0'; if(message[charsWritten-2] == '\r') message[charsWritten-2] = '\0'; } const int charsWritten = swprintf_s(buf, max_chars, L"%d (%ls)", err, message); ENSURE(charsWritten != -1); return INFO::OK; } static Status GetModulePathname(HMODULE hModule, OsPath& pathname) { wchar_t pathnameBuf[32768]; // NTFS limit const DWORD length = (DWORD)ARRAY_SIZE(pathnameBuf); const DWORD charsWritten = GetModuleFileNameW(hModule, pathnameBuf, length); if(charsWritten == 0) // failed WARN_RETURN(StatusFromWin()); ENSURE(charsWritten < length); // why would the above buffer ever be exceeded? pathname = pathnameBuf; return INFO::OK; } Status sys_get_module_filename(void* addr, OsPath& pathname) { MEMORY_BASIC_INFORMATION mbi; const SIZE_T bytesWritten = VirtualQuery(addr, &mbi, sizeof(mbi)); if(!bytesWritten) WARN_RETURN(StatusFromWin()); ENSURE(bytesWritten >= sizeof(mbi)); return GetModulePathname((HMODULE)mbi.AllocationBase, pathname); } OsPath sys_ExecutablePathname() { WinScopedPreserveLastError s; OsPath pathname; ENSURE(GetModulePathname(0, pathname) == INFO::OK); return pathname; } std::wstring sys_get_user_name() { wchar_t usernameBuf[256]; DWORD size = ARRAY_SIZE(usernameBuf); if(!GetUserNameW(usernameBuf, &size)) return L""; return usernameBuf; } // callback for shell directory picker: used to set starting directory // (for user convenience). static int CALLBACK BrowseCallback(HWND hWnd, unsigned int msg, LPARAM UNUSED(lParam), LPARAM lpData) { if(msg == BFFM_INITIALIZED) { const WPARAM wParam = TRUE; // lpData is a Unicode string, not PIDL. // (MSDN: the return values for both of these BFFM_ notifications are ignored) (void)SendMessage(hWnd, BFFM_SETSELECTIONW, wParam, lpData); } return 0; } Status sys_pick_directory(OsPath& path) { // (must not use multi-threaded apartment due to BIF_NEWDIALOGSTYLE) const HRESULT hr = CoInitialize(0); ENSURE(hr == S_OK || hr == S_FALSE); // S_FALSE == already initialized // note: bi.pszDisplayName isn't the full path, so it isn't of any use. BROWSEINFOW bi; memset(&bi, 0, sizeof(bi)); bi.ulFlags = BIF_RETURNONLYFSDIRS|BIF_NEWDIALOGSTYLE|BIF_NONEWFOLDERBUTTON; // for setting starting directory: bi.lpfn = (BFFCALLBACK)BrowseCallback; const Path::String initialPath = OsString(path); // NB: BFFM_SETSELECTIONW can't deal with '/' separators bi.lParam = (LPARAM)initialPath.c_str(); const LPITEMIDLIST pidl = SHBrowseForFolderW(&bi); if(!pidl) // user canceled return INFO::SKIPPED; // translate ITEMIDLIST to string wchar_t pathBuf[MAX_PATH]; // mandated by SHGetPathFromIDListW const BOOL ok = SHGetPathFromIDListW(pidl, pathBuf); // free the ITEMIDLIST CoTaskMemFree(pidl); if(ok == TRUE) { path = pathBuf; return INFO::OK; } // Balance call to CoInitialize, which must have been successful CoUninitialize(); WARN_RETURN(StatusFromWin()); } Status sys_open_url(const std::string& url) { HINSTANCE r = ShellExecuteA(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL); if ((int)(intptr_t)r > 32) return INFO::OK; WARN_RETURN(ERR::FAIL); } Status sys_generate_random_bytes(u8* buffer, size_t size) { HCRYPTPROV hCryptProv = 0; if(!CryptAcquireContext(&hCryptProv, 0, 0, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) WARN_RETURN(StatusFromWin()); memset(buffer, 0, size); if(!CryptGenRandom(hCryptProv, (DWORD)size, (BYTE*)buffer)) WARN_RETURN(StatusFromWin()); if(!CryptReleaseContext(hCryptProv, 0)) WARN_RETURN(StatusFromWin()); return INFO::OK; } #if CONFIG_ENABLE_BOOST /* * Given a string of the form * "example.com:80" * or * "ftp=ftp.example.com:80;http=example.com:80;https=example.com:80" * separated by semicolons or whitespace, * return the string "example.com:80". */ static std::wstring parse_proxy(const std::wstring& input) { if(input.find('=') == input.npos) return input; std::vector parts; split(parts, input, boost::algorithm::is_any_of("; \t\r\n"), boost::algorithm::token_compress_on); for(size_t i = 0; i < parts.size(); ++i) if(boost::algorithm::starts_with(parts[i], "http=")) return parts[i].substr(5); // If we got this far, proxies were only set for non-HTTP protocols return L""; } Status sys_get_proxy_config(const std::wstring& url, std::wstring& proxy) { WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions; memset(&autoProxyOptions, 0, sizeof(autoProxyOptions)); autoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT; autoProxyOptions.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A; autoProxyOptions.fAutoLogonIfChallenged = TRUE; WINHTTP_PROXY_INFO proxyInfo; memset(&proxyInfo, 0, sizeof(proxyInfo)); WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieConfig; memset(&ieConfig, 0, sizeof(ieConfig)); HINTERNET hSession = NULL; Status err = INFO::SKIPPED; bool useAutoDetect; if(WinHttpGetIEProxyConfigForCurrentUser(&ieConfig)) { if(ieConfig.lpszAutoConfigUrl) { // Use explicit auto-config script if specified useAutoDetect = true; autoProxyOptions.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL; autoProxyOptions.lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl; } else { // Use auto-discovery if enabled useAutoDetect = (ieConfig.fAutoDetect == TRUE); } } else { // Can't find IE config settings - fall back to auto-discovery useAutoDetect = true; } if(useAutoDetect) { hSession = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if(hSession && WinHttpGetProxyForUrl(hSession, url.c_str(), &autoProxyOptions, &proxyInfo) && proxyInfo.lpszProxy) { proxy = parse_proxy(proxyInfo.lpszProxy); if(!proxy.empty()) { err = INFO::OK; goto done; } } } // No valid auto-config; try explicit proxy instead if(ieConfig.lpszProxy) { proxy = parse_proxy(ieConfig.lpszProxy); if(!proxy.empty()) { err = INFO::OK; goto done; } } done: if(ieConfig.lpszProxy) GlobalFree(ieConfig.lpszProxy); if(ieConfig.lpszProxyBypass) GlobalFree(ieConfig.lpszProxyBypass); if(ieConfig.lpszAutoConfigUrl) GlobalFree(ieConfig.lpszAutoConfigUrl); if(proxyInfo.lpszProxy) GlobalFree(proxyInfo.lpszProxy); if(proxyInfo.lpszProxyBypass) GlobalFree(proxyInfo.lpszProxyBypass); if(hSession) WinHttpCloseHandle(hSession); return err; } #endif FILE* sys_OpenFile(const OsPath& pathname, const char* mode) { FILE* f = 0; const std::wstring wmode(mode, mode+strlen(mode)); (void)_wfopen_s(&f, OsString(pathname).c_str(), wmode.c_str()); return f; } Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24109) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24110) @@ -1,1638 +1,1642 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "graphics/CinemaManager.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptRuntime.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; shared_ptr g_ScriptRuntime; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code static const CStr g_EventNameGameLoadProgress = "GameLoadProgress"; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::AutoValueVector paramData(cx); paramData.append(JS::NumberValue(percent)); JS::RootedValue valPendingTask(cx); scriptInterface.ToJSVal(cx, &valPendingTask, pending_task); paramData.append(valPendingTask); g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData); } bool ShouldRender() { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void Render() { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; PROFILE3("render"); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); if (g_DoRenderGui) g_GUI->Draw(); ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0) LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName)); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (ThreadUtil::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } const std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) { // Add the user mod if it should be present if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) g_modsLoaded.push_back("user"); return g_modsLoaded; } g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); // Add the user mod if not explicitly disabled or we have a dev copy so // that saved files end up in version control and not in the user mod. if (add_user) g_modsLoaded.push_back("user"); return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; for (size_t i = 0; i < mods.size(); ++i) { size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0 size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; OsPath modName(mods[i]); if (InDevelopmentCopy()) { // We are running a dev copy, so only mount mods in the user mod path // if the mod does not exist in the data path. if (DirectoryExists(modPath / modName/"")) g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority); } else { g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); // Ensure that user modified files are loaded, if they are present g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1); } } } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; g_VFS->Mount(L"config/", readonlyConfig); // Engine localization files. g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH); // Mounting with highest priority, so that a mod supplied user.cfg is harmless g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1); if(readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitPsAutostart(bool networked, JS::HandleValue attrs) { // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue playerAssignments(cx); ScriptInterface::CreateObject(cx, &playerAssignments); if (!networked) { JS::RootedValue localPlayer(cx); ScriptInterface::CreateObject(cx, &localPlayer, "player", g_Game->GetPlayerID()); scriptInterface.SetProperty(playerAssignments, "local", localPlayer); } JS::RootedValue sessionInitData(cx); ScriptInterface::CreateObject( cx, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(conInputHandler); in_add_handler(HotkeyInputHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); in_add_handler(touch_input_handler); // must be registered after (called before) the GUI which relies on these globals in_add_handler(GlobalsInputHandler); // Should be called first, this updates our hotkey press state // so that js calls to HotkeyIsPressed are synched with events. in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false); } static void InitRenderer() { TIMER(L"InitRenderer"); // create renderer new CRenderer; g_RenderingOptions.ReadConfig(); // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_Profiler2.ShutdownGPU(); // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptRuntime.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); ThreadUtil::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_LatchStartTime(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the runtime is a workaround until Simulation and AI use // their own threads and also their own runtimes. const int runtimeSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptRuntime); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptRuntime, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual")) ISoundManager::CreateSoundManager(); #endif // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart RestartEngine(); return false; } } new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) - if (ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB + CStr8 renderPath = "default"; + CFG_GET_VAL("renderpath", renderPath); + if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL + || RenderPathEnum::FromString(renderPath) == FIXED) { - DEBUG_DISPLAY_ERROR( + // It doesn't make sense to continue working here, because we're not + // able to display anything. + DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." - L" In the future, the game will not support pre-shader graphics cards." + L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); - // TODO: actually quit once fixed function support is dropped } const char* missing = ogl_HaveExtensions(0, "GL_ARB_multitexture", "GL_EXT_draw_range_elements", "GL_ARB_texture_env_combine", "GL_ARB_texture_env_dot3", NULL); if(missing) { wchar_t buf[500]; swprintf_s(buf, ARRAY_SIZE(buf), L"The %hs extension doesn't appear to be available on your computer." L" The game may still work, though - you are welcome to try at your own risk." L" If not or it doesn't look right, upgrade your graphics card.", missing ); DEBUG_DISPLAY_ERROR(buf); // TODO: i18n } if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar")) { DEBUG_DISPLAY_ERROR( L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer." L" Shadows are not available and overall graphics quality might suffer." L" You are advised to try installing newer drivers and/or upgrade your graphics card."); g_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false); } ogl_WarnIfError(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue data(cx); if (g_GUI) { ScriptInterface::CreateObject(cx, &data, "isStartup", true); if (!installedMods.empty()) scriptInterface->SetProperty(data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ - * (default conquest). When the first given SCRIPTNAME is + * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(!args.Has("autostart-disable-replay")); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attrs(cx); JS::RootedValue settings(cx); JS::RootedValue playerData(cx); ScriptInterface::CreateObject(cx, &attrs); ScriptInterface::CreateObject(cx, &settings); ScriptInterface::CreateArray(cx, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway ScriptInterface::CreateObject(cx, &player, "Civ", "athen"); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName); scriptInterface.SetProperty(settings, "mapType", mapType); scriptInterface.SetProperty(settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(cx); if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } ScriptInterface::CreateObject(cx, &player); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; scriptInterface.SetProperty(player, "Team", teamID); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); scriptInterface.SetProperty(settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } ScriptInterface::CreateObject(cx, &player); } scriptInterface.SetProperty(player, "AI", aiArgs[i].AfterFirst(":")); scriptInterface.SetProperty(player, "AIDiff", 3); scriptInterface.SetProperty(player, "AIBehavior", "balanced"); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } ScriptInterface::CreateObject(cx, &player); } scriptInterface.SetProperty(player, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } ScriptInterface::CreateObject(cx, &player); } scriptInterface.SetProperty(player, "Civ", civArgs[i].AfterFirst(":")); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(cx); if (scriptInterface.HasProperty(settings, "TriggerScripts")) { scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts); FromJSVal_vector(cx, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions); for (const CStr& victory : victoryConditions) { JS::RootedValue scriptData(cx); JS::RootedValue data(cx); JS::RootedValue victoryScripts(cx); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json"; scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined() && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; FromJSVal_vector(cx, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } else { LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details."); } } ToJSVal_vector(cx, &triggerScripts, triggerScriptsVector); scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); scriptInterface.SetProperty(settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); scriptInterface.SetProperty(settings, "RelicCount", relicCount); if (args.Has("autostart-host")) { InitPsAutostart(true, attrs); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->UpdateGameAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game, true); g_NetClient->SetUserName(userName); g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT, nullptr); } else if (args.Has("autostart-client")) { InitPsAutostart(true, attrs); g_NetClient = new CNetClient(g_Game, false); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT, nullptr); ENSURE(ok); } else { g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1); g_Game->StartGame(&attrs, ""); if (CRenderer::IsInitialised()) { InitPsAutostart(false, attrs); } else { // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); } } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attrs(cx, g_Game->GetSimulation2()->GetInitAttributes()); InitPsAutostart(false, attrs); return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); JSContext* cx = pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, pScriptInterface->GetGlobalObject()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && pScriptInterface->HasProperty(global, "cancelOnLoadGameError")) pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; }