/* Copyright (C) 2025 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 "ps/GameSetup/GameSetup.h" #include "graphics/GameView.h" #include "graphics/MapReader.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/Scripting/JSInterface_GUIManager.h" #include "i18n/L10n.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/input.h" #include "lib/timer.h" #include "lobby/IXmppClient.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "network/scripting/JSInterface_Network.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/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.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/scripting/JSInterface_Game.h" #include "ps/scripting/JSInterface_Main.h" #include "ps/scripting/JSInterface_VFS.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/SceneRenderer.h" #include "renderer/VertexBufferManager.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/JSON.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "simulation2/scripting/JSInterface_Simulation.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.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 #include #include using namespace std::literals; ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); thread_local std::shared_ptr g_ScriptContext; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; 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 (Threading::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; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; size_t priority = 0; for (size_t i = 0; i < mods.size(); ++i) { priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); // Only mount mods from the user path if they don't exist in the 'rdata' path. if (DirectoryExists(modPath / modName / "")) g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } void InitVfs(const CmdLineArgs& args) { TIMER(L"InitVfs"); 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; hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; // Mount these dirs with highest priority so that mods can't overwrite them. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); // Mods will be mounted later. // 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->Init(); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } 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 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(HotkeyInputActualHandler); // 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); // Likewise for the console. in_add_handler(conInputHandler); in_add_handler(touch_input_handler); // Should be called after scancode map update (i.e. after the global input, but before UI). // This never blocks the event, but it does some processing necessary for hotkeys, // which are triggered later down the input chain. // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). in_add_handler(HotkeyInputPrepHandler); // These two must be called first (i.e. pushed last) // GlobalsInputHandler deals with some important global state, // such as which scancodes are being pressed, mouse buttons pressed, etc. // while HotkeyStateChange updates the map of active hotkeys. in_add_handler(GlobalsInputHandler); in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); } 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 SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if SDL_VERSION_ATLEAST(2, 0, 14) && OS_WIN // SDL2 >= 2.0.14 Before SDL 2.0.14, this defaulted to true. In 2.0.14 they switched to false // breaking the behavior on Windows. // https://github.com/libsdl-org/SDL/commit/1947ca7028ab165cc3e6cbdb0b4b7c4db68d1710 // https://github.com/libsdl-org/SDL/issues/5033 SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, g_ConfigDB.Get("macmouse", false) ? "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.GetSceneRenderer().ResetState(); } } void ShutdownNetworkAndUI() { const bool hasRenderer = CRenderer::IsInitialised(); EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); delete &g_Renderer; TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); 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; } void ShutdownConfigAndSubsequent() { TIMER_BEGIN(L"shutdown ConfigDB"); CConfigDB::Shutdown(); 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_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); 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 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); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); debug_filter_add("FILES"); timer_Init(); // 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) { 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 context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 12 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); // On the first Init (INIT_MODS), check for command-line arguments // or use the default mods from the config and enable those. // On later engine restarts (e.g. the mod selector), we will skip this path, // to avoid overwriting the newly selected mods. if (flags & INIT_MODS) { ScriptInterface modInterface("Engine", "Mod", g_ScriptContext); g_Mods.UpdateAvailableMods(modInterface); std::vector mods; if (args.Has("mod")) mods = args.GetMultiple("mod"); else { boost::split(mods, g_ConfigDB.Get("mod.enabledmods", std::string{}), boost::algorithm::is_space(), boost::token_compress_on); } if (!g_Mods.EnableMods(mods, flags & INIT_MODS_PUBLIC)) { // In non-visual mode, fail entirely. if (args.Has("autostart-nonvisual")) { LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(g_Mods.GetIncompatibleMods(), ", ")); return false; } } } // If there are incompatible mods, switch to the mod selector so players can resolve the problem. if (g_Mods.GetIncompatibleMods().empty()) MountMods(Paths(args), g_Mods.GetEnabledMods()); else MountMods(Paths(args), { "mod" }); // 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_ScriptContext, NULL}; sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); debug_printf("Generated entity.rng\n"); return false; } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) if (g_ConfigDB.Get("profiler2.autoenable", false)) 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, ScriptContext& scriptContext, ScriptInterface& scriptInterface) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(!g_Quickstart, g_VideoMode.GetBackendDevice()); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) if (g_ConfigDB.Get("profiler2.autoenable", false)) g_Profiler2.EnableGPU(); if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager{scriptContext, scriptInterface}; if (RenderPathEnum::FromString(g_ConfigDB.Get("renderpath", "default"s)) == FIXED) { // 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" 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" ); } g_RenderingOptions.ReadConfigAndSetupHooks(); // create renderer new CRenderer(g_VideoMode.GetBackendDevice()); InitInput(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) g_Xeromyces.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); ScriptRequest rq{g_GUI->GetScriptInterface()}; JS::RootedValue data(rq.cx); Script::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) Script::SetProperty(rq, data, "installedMods", installedMods); InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", &g_GUI->GetScriptInterface(), 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()); } } bool InitNonVisual(const CmdLineArgs& args) { return Autostart(args); } /** * 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(); } /** * Autostart arguments are parsed in javascript for convenience and moddability. * This C++ part only handles the following arguments: * -autostart=MAP to enable regular autostart / host mode autostart * -autostart-client=IP to enable 'MP client' autostart. * -autostart-host to enable 'MP host' autostart. * -autostart-nonvisual to start in non-visual mode. * TODO: it might be nice to move these to JS too. */ bool Autostart(const CmdLineArgs& args) { if (!args.Has("autostart-client") && !args.Has("autostart")) return false; // Create some scriptinterface to store the js values for the settings. ScriptInterface scriptInterface("Engine", "Game Setup", g_ScriptContext); ScriptRequest rq(scriptInterface); // We use the javascript gameSettings to handle options, but that requires running JS. // Since we don't want to use the full Gui manager, we load an entrypoint script // that can run the priviledged "LoadScript" function, and then call the appropriate function. // TODO: this essentially duplicates the CGUI logic to load directory or scripts. std::unordered_set templateCache; const auto autostartLoadScript = [&templateCache](const ScriptInterface& scriptInterface, const VfsPath& path) { if (!std::get<1>(templateCache.insert(path))) return; if (path.IsDirectory()) { VfsPaths pathnames; vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); for (const VfsPath& file : pathnames) scriptInterface.LoadGlobalScriptFile(file); } else scriptInterface.LoadGlobalScriptFile(path); }; const auto loadScriptCallback = ScriptFunction::Register(rq, "LoadScript", autostartLoadScript); // Load the entire folder to allow mods to extend the entrypoint without copying the whole file. autostartLoadScript(scriptInterface, VfsPath(L"autostart/")); // Provide some required functions to the script. struct GetTemplate { CTemplateLoader templateLoader; CParamNode operator()(const std::string& templateName){ // TODO: this essentially duplicates the CGUI function const CParamNode& templateRoot{ templateLoader.GetTemplateFileData(templateName).GetOnlyChild()}; if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } }; std::optional> getTemplateCallback; if (args.Has("autostart-nonvisual")) getTemplateCallback.emplace(rq, "GetTemplate", GetTemplate{}); else { JSI_GUIManager::RegisterScriptFunctions(rq); // TODO: this loads pregame, which is hardcoded to exist by various code paths. That ought be changed. InitPs(false, L"page_pregame.xml", &g_GUI->GetScriptInterface(), JS::UndefinedHandleValue); } JSI_Game::RegisterScriptFunctions(rq); JSI_Main::RegisterScriptFunctions(rq); JSI_Simulation::RegisterScriptFunctions(rq); JSI_VFS::RegisterScriptFunctions_ReadWriteAnywhere(rq); JSI_Network::RegisterScriptFunctions(rq); JS::RootedValue cmdLineArgs(rq.cx); Script::ToJSVal(rq, &cmdLineArgs, args); if (args.Has("autostart-client") || args.Has("autostart-host")) { // Pass the default port if undefined, to avoid duplicating it in JS. if (!Script::HasProperty(rq, cmdLineArgs, "autostart-port")) Script::SetProperty(rq, cmdLineArgs, "autostart-port", PS_DEFAULT_PORT); JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, args.Has("autostart-client") ? "autostartClient" : "autostartHost", cmdLineArgs, true)) return false; bool shouldQuit = false; while (!shouldQuit) { g_NetClient->Poll(); ScriptFunction::Call(rq, global, "onTick", shouldQuit); std::this_thread::sleep_for(std::chrono::microseconds(200)); } } else { JS::RootedValue global(rq.cx, rq.globalValue()); if (!ScriptFunction::CallVoid(rq, global, "autostartHost", cmdLineArgs, false)) return false; } if (args.Has("autostart-nonvisual")) { LDR_NonprogressiveLoad(); g_Game->ReallyStartGame(); } 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(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); JS::RootedValue playerAssignments(rq.cx); Script::CreateObject(rq, &playerAssignments); JS::RootedValue localPlayer(rq.cx); Script::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); Script::SetProperty(rq, playerAssignments, "local", localPlayer); JS::RootedValue sessionInitData(rq.cx); Script::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); return true; } void CancelLoad(const CStrW& message) { std::shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && Script::HasProperty(rq, global, "cancelOnLoadGameError")) ScriptFunction::CallVoid(rq, 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; }