1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
4 * 0 A.D. is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 of the License, or
7 * (at your option) any later version.
9 * 0 A.D. is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
18 #include "precompiled.h"
20 #include "ps/GameSetup/GameSetup.h"
22 #include "graphics/GameView.h"
23 #include "graphics/MapReader.h"
24 #include "graphics/TerrainTextureManager.h"
26 #include "gui/GUIManager.h"
27 #include "i18n/L10n.h"
28 #include "lib/app_hooks.h"
29 #include "lib/config2.h"
30 #include "lib/external_libraries/libsdl.h"
31 #include "lib/file/common/file_stats.h"
32 #include "lib/input.h"
33 #include "lib/timer.h"
34 #include "lobby/IXmppClient.h"
35 #include "network/NetServer.h"
36 #include "network/NetClient.h"
37 #include "network/NetMessage.h"
38 #include "network/NetMessages.h"
39 #include "ps/CConsole.h"
40 #include "ps/CLogger.h"
41 #include "ps/ConfigDB.h"
42 #include "ps/Filesystem.h"
44 #include "ps/GameSetup/Atlas.h"
45 #include "ps/GameSetup/Paths.h"
46 #include "ps/GameSetup/Config.h"
47 #include "ps/GameSetup/CmdLineArgs.h"
48 #include "ps/GameSetup/HWDetect.h"
49 #include "ps/Globals.h"
51 #include "ps/Hotkey.h"
52 #include "ps/Joystick.h"
53 #include "ps/Loader.h"
56 #include "ps/Profile.h"
57 #include "ps/ProfileViewer.h"
58 #include "ps/Profiler2.h"
59 #include "ps/Pyrogenesis.h" // psSetLogDir
60 #include "ps/scripting/JSInterface_Console.h"
61 #include "ps/TouchInput.h"
62 #include "ps/UserReport.h"
64 #include "ps/VideoMode.h"
65 #include "ps/VisualReplay.h"
67 #include "renderer/Renderer.h"
68 #include "renderer/SceneRenderer.h"
69 #include "renderer/VertexBufferManager.h"
70 #include "scriptinterface/FunctionWrapper.h"
71 #include "scriptinterface/JSON.h"
72 #include "scriptinterface/ScriptInterface.h"
73 #include "scriptinterface/ScriptStats.h"
74 #include "scriptinterface/ScriptContext.h"
75 #include "scriptinterface/ScriptConversions.h"
76 #include "simulation2/Simulation2.h"
77 #include "soundmanager/scripting/JSInterface_Sound.h"
78 #include "soundmanager/ISoundManager.h"
79 #include "tools/atlas/GameInterface/GameLoop.h"
81 #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets
82 #define MUST_INIT_X11 1
85 #define MUST_INIT_X11 0
88 extern void RestartEngine();
93 #include <boost/algorithm/string/classification.hpp>
94 #include <boost/algorithm/string/join.hpp>
95 #include <boost/algorithm/string/split.hpp>
98 ERROR_TYPE(System
, SDLInitFailed
);
99 ERROR_TYPE(System
, VmodeFailed
);
100 ERROR_TYPE(System
, RequiredExtensionsMissing
);
102 thread_local
std::shared_ptr
<ScriptContext
> g_ScriptContext
;
104 bool g_InDevelopmentCopy
;
105 bool g_CheckedIfInDevelopmentCopy
= false;
107 ErrorReactionInternal
psDisplayError(const wchar_t* UNUSED(text
), size_t UNUSED(flags
))
109 // If we're fullscreen, then sometimes (at least on some particular drivers on Linux)
110 // displaying the error dialog hangs the desktop since the dialog box is behind the
111 // fullscreen window. So we just force the game to windowed mode before displaying the dialog.
112 // (But only if we're in the main thread, and not if we're being reentrant.)
113 if (Threading::IsMainThread())
115 static bool reentering
= false;
119 g_VideoMode
.SetFullscreen(false);
124 // We don't actually implement the error display here, so return appropriately
125 return ERI_NOT_IMPLEMENTED
;
128 void MountMods(const Paths
& paths
, const std::vector
<CStr
>& mods
)
130 OsPath modPath
= paths
.RData()/"mods";
131 OsPath modUserPath
= paths
.UserData()/"mods";
133 size_t userFlags
= VFS_MOUNT_WATCH
|VFS_MOUNT_ARCHIVABLE
;
134 size_t baseFlags
= userFlags
|VFS_MOUNT_MUST_EXIST
;
136 for (size_t i
= 0; i
< mods
.size(); ++i
)
138 priority
= i
+ 1; // Mods are higher priority than regular mountings, which default to priority 0
140 OsPath
modName(mods
[i
]);
141 // Only mount mods from the user path if they don't exist in the 'rdata' path.
142 if (DirectoryExists(modPath
/ modName
/ ""))
143 g_VFS
->Mount(L
"", modPath
/ modName
/ "", baseFlags
, priority
);
145 g_VFS
->Mount(L
"", modUserPath
/ modName
/ "", userFlags
, priority
);
148 // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable.
149 g_VFS
->Mount(L
"", modUserPath
/ "user" / "", userFlags
, InDevelopmentCopy() ? 0 : priority
+ 1);
152 static void InitVfs(const CmdLineArgs
& args
, int flags
)
156 const bool setup_error
= (flags
& INIT_HAVE_DISPLAY_ERROR
) == 0;
158 const Paths
paths(args
);
160 OsPath
logs(paths
.Logs());
161 CreateDirectories(logs
, 0700);
164 // desired location for crashlog is now known. update AppHooks ASAP
165 // (particularly before the following error-prone operations):
166 AppHooks hooks
= {0};
167 hooks
.bundle_logs
= psBundleLogs
;
168 hooks
.get_log_dir
= psLogDir
;
170 hooks
.display_error
= psDisplayError
;
171 app_hooks_update(&hooks
);
175 const OsPath readonlyConfig
= paths
.RData()/"config"/"";
177 // Mount these dirs with highest priority so that mods can't overwrite them.
178 g_VFS
->Mount(L
"cache/", paths
.Cache(), VFS_MOUNT_ARCHIVABLE
, VFS_MAX_PRIORITY
); // (adding XMBs to archive speeds up subsequent reads)
179 if (readonlyConfig
!= paths
.Config())
180 g_VFS
->Mount(L
"config/", readonlyConfig
, 0, VFS_MAX_PRIORITY
-1);
181 g_VFS
->Mount(L
"config/", paths
.Config(), 0, VFS_MAX_PRIORITY
);
182 g_VFS
->Mount(L
"screenshots/", paths
.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY
);
183 g_VFS
->Mount(L
"saves/", paths
.UserData()/"saves"/"", VFS_MOUNT_WATCH
, VFS_MAX_PRIORITY
);
185 // Engine localization files (regular priority, these can be overwritten).
186 g_VFS
->Mount(L
"l10n/", paths
.RData()/"l10n"/"");
188 // Mods will be mounted later.
190 // note: don't bother with g_VFS->TextRepresentation - directories
191 // haven't yet been populated and are empty.
195 static void InitPs(bool setup_gui
, const CStrW
& gui_page
, ScriptInterface
* srcScriptInterface
, JS::HandleValue initData
)
199 TIMER(L
"ps_console");
206 TIMER(L
"ps_lang_hotkeys");
207 LoadHotkeys(g_ConfigDB
);
212 // We do actually need *some* kind of GUI loaded, so use the
213 // (currently empty) Atlas one
214 g_GUI
->SwitchPage(L
"page_atlas.xml", srcScriptInterface
, initData
);
218 // GUI uses VFS, so this must come after VFS init.
219 g_GUI
->SwitchPage(gui_page
, srcScriptInterface
, initData
);
222 void InitPsAutostart(bool networked
, JS::HandleValue attrs
)
224 // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable
225 ScriptInterface
& scriptInterface
= g_Game
->GetSimulation2()->GetScriptInterface();
226 ScriptRequest
rq(scriptInterface
);
228 JS::RootedValue
playerAssignments(rq
.cx
);
229 Script::CreateObject(rq
, &playerAssignments
);
233 JS::RootedValue
localPlayer(rq
.cx
);
234 Script::CreateObject(rq
, &localPlayer
, "player", g_Game
->GetPlayerID());
235 Script::SetProperty(rq
, playerAssignments
, "local", localPlayer
);
238 JS::RootedValue
sessionInitData(rq
.cx
);
240 Script::CreateObject(
244 "playerAssignments", playerAssignments
);
246 InitPs(true, L
"page_loading.xml", &scriptInterface
, sessionInitData
);
252 g_Joystick
.Initialise();
254 // register input handlers
255 // This stack is constructed so the first added, will be the last
256 // one called. This is important, because each of the handlers
257 // has the potential to block events to go further down
258 // in the chain. I.e. the last one in the list added, is the
259 // only handler that can block all messages before they are
261 in_add_handler(game_view_handler
);
263 in_add_handler(CProfileViewer::InputThunk
);
265 in_add_handler(HotkeyInputActualHandler
);
267 // gui_handler needs to be registered after (i.e. called before!) the
268 // hotkey handler so that input boxes can be typed in without
269 // setting off hotkeys.
270 in_add_handler(gui_handler
);
271 // Likewise for the console.
272 in_add_handler(conInputHandler
);
274 in_add_handler(touch_input_handler
);
276 // Should be called after scancode map update (i.e. after the global input, but before UI).
277 // This never blocks the event, but it does some processing necessary for hotkeys,
278 // which are triggered later down the input chain.
279 // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI).
280 in_add_handler(HotkeyInputPrepHandler
);
282 // These two must be called first (i.e. pushed last)
283 // GlobalsInputHandler deals with some important global state,
284 // such as which scancodes are being pressed, mouse buttons pressed, etc.
285 // while HotkeyStateChange updates the map of active hotkeys.
286 in_add_handler(GlobalsInputHandler
);
287 in_add_handler(HotkeyStateChange
);
291 static void ShutdownPs()
298 static void InitSDL()
301 // In fullscreen mode when SDL is compiled with DGA support, the mouse
302 // sensitivity often appears to be unusably wrong (typically too low).
303 // (This seems to be reported almost exclusively on Ubuntu, but can be
304 // reproduced on Gentoo after explicitly enabling DGA.)
305 // Disabling the DGA mouse appears to fix that problem, and doesn't
306 // have any obvious negative effects.
307 setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0);
310 if(SDL_Init(SDL_INIT_VIDEO
|SDL_INIT_TIMER
|SDL_INIT_NOPARACHUTE
) < 0)
312 LOGERROR("SDL library initialization failed: %s", SDL_GetError());
313 throw PSERROR_System_SDLInitFailed();
317 // Text input is active by default, disable it until it is actually needed.
320 #if SDL_VERSION_ATLEAST(2, 0, 9)
321 // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking.
322 SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS
, "1");
325 #if SDL_VERSION_ATLEAST(2, 0, 14) && OS_WIN
326 // SDL2 >= 2.0.14 Before SDL 2.0.14, this defaulted to true. In 2.0.14 they switched to false
327 // breaking the behavior on Windows.
328 // https://github.com/libsdl-org/SDL/commit/1947ca7028ab165cc3e6cbdb0b4b7c4db68d1710
329 // https://github.com/libsdl-org/SDL/issues/5033
330 SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS
, "1");
334 // Some Mac mice only have one button, so they can't right-click
335 // but SDL2 can emulate that with Ctrl+Click
336 bool macMouse
= false;
337 CFG_GET_VAL("macmouse", macMouse
);
338 SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK
, macMouse
? "1" : "0");
342 static void ShutdownSDL()
350 SAFE_DELETE(g_NetClient
);
351 SAFE_DELETE(g_NetServer
);
354 if (CRenderer::IsInitialised())
356 ISoundManager::CloseGame();
357 g_Renderer
.GetSceneRenderer().ResetState();
361 void Shutdown(int flags
)
363 const bool hasRenderer
= CRenderer::IsInitialised();
365 if ((flags
& SHUTDOWN_FROM_CONFIG
))
370 SAFE_DELETE(g_XmppClient
);
372 SAFE_DELETE(g_ModIo
);
378 TIMER_BEGIN(L
"shutdown Renderer");
379 g_Renderer
.~CRenderer();
381 TIMER_END(L
"shutdown Renderer");
384 g_RenderingOptions
.ClearHooks();
386 g_Profiler2
.ShutdownGPU();
388 TIMER_BEGIN(L
"shutdown SDL");
390 TIMER_END(L
"shutdown SDL");
393 g_VideoMode
.Shutdown();
395 TIMER_BEGIN(L
"shutdown UserReporter");
396 g_UserReporter
.Deinitialize();
397 TIMER_END(L
"shutdown UserReporter");
399 // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown.
400 curl_global_cleanup();
405 TIMER_BEGIN(L
"shutdown ConfigDB");
406 CConfigDB::Shutdown();
407 TIMER_END(L
"shutdown ConfigDB");
409 SAFE_DELETE(g_Console
);
411 // This is needed to ensure that no callbacks from the JSAPI try to use
412 // the profiler when it's already destructed
413 g_ScriptContext
.reset();
416 // first shut down all resource owners, and then the handle manager.
417 TIMER_BEGIN(L
"resource modules");
419 ISoundManager::SetEnabled(false);
425 TIMER_END(L
"resource modules");
427 TIMER_BEGIN(L
"shutdown misc");
428 timer_DisplayClientTotals();
430 CNetHost::Deinitialize();
432 // should be last, since the above use them
433 SAFE_DELETE(g_Logger
);
435 delete &g_ProfileViewer
;
437 SAFE_DELETE(g_ScriptStatsTable
);
438 TIMER_END(L
"shutdown misc");
442 static void FixLocales()
444 #if OS_MACOSX || OS_BSD
445 // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle
446 // wide characters. Peculiarly the string "UTF-8" seems to be acceptable
447 // despite not being a real locale, and it's conveniently language-agnostic,
449 setlocale(LC_CTYPE
, "UTF-8");
453 // On misconfigured systems with incorrect locale settings, we'll die
454 // with a C++ exception when some code (e.g. Boost) tries to use locales.
455 // To avoid death, we'll detect the problem here and warn the user and
456 // reset to the default C locale.
459 // For informing the user of the problem, use the list of env vars that
460 // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.)
461 const char* const LocaleEnvVars
[] = {
474 // this constructor is similar to setlocale(LC_ALL, ""),
475 // but instead of returning NULL, it throws runtime_error
476 // when the first locale env variable found contains an invalid value
479 catch (std::runtime_error
&)
481 LOGWARNING("Invalid locale settings");
483 for (size_t i
= 0; i
< ARRAY_SIZE(LocaleEnvVars
); i
++)
485 if (char* envval
= getenv(LocaleEnvVars
[i
]))
486 LOGWARNING(" %s=\"%s\"", LocaleEnvVars
[i
], envval
);
488 LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars
[i
]);
491 // We should set LC_ALL since it overrides LANG
492 if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1))
493 debug_warn(L
"Invalid locale settings, and unable to set LC_ALL env variable.");
495 LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL"));
499 static void FixLocales()
501 // Do nothing on Windows
507 // If you ever want to catch a particular allocation:
508 //_CrtSetBreakAlloc(232647);
510 Threading::SetMainThread();
512 debug_SetThreadName("main");
513 // add all debug_printf "tags" that we are interested in:
514 debug_filter_add("TIMER");
515 debug_filter_add("FILES");
519 // initialise profiler early so it can profile startup,
520 // but only after LatchStartTime
521 g_Profiler2
.Initialise();
525 // Because we do GL calls from a secondary thread, Xlib needs to
526 // be told to support multiple threads safely.
527 // This is needed for Atlas, but we have to call it before any other
528 // Xlib functions (e.g. the ones used when drawing the main menu
529 // before launching Atlas)
531 int status
= XInitThreads();
533 debug_printf("Error enabling thread-safety via XInitThreads\n");
536 // Initialise the low-quality rand function
537 srand(time(NULL
)); // NOTE: this rand should *not* be used for simulation!
540 bool Autostart(const CmdLineArgs
& args
);
543 * Returns true if the user has intended to start a visual replay from command line.
545 bool AutostartVisualReplay(const std::string
& replayFile
);
547 bool Init(const CmdLineArgs
& args
, int flags
)
549 // Do this as soon as possible, because it chdirs
550 // and will mess up the error reporting if anything
551 // crashes before the working directory is set.
552 InitVfs(args
, flags
);
554 // This must come after VFS init, which sets the current directory
555 // (required for finding our output log files).
556 g_Logger
= new CLogger
;
559 new CProfileManager
; // before any script code
561 g_ScriptStatsTable
= new CScriptStatsTable
;
562 g_ProfileViewer
.AddRootTable(g_ScriptStatsTable
);
564 // Set up the console early, so that debugging
565 // messages can be logged to it. (The console's size
566 // and fonts are set later in InitPs())
567 g_Console
= new CConsole();
569 // g_ConfigDB, command line args, globals
572 // Using a global object for the context is a workaround until Simulation and AI use
573 // their own threads and also their own contexts.
574 const int contextSize
= 384 * 1024 * 1024;
575 const int heapGrowthBytesGCTrigger
= 20 * 1024 * 1024;
576 g_ScriptContext
= ScriptContext::CreateContext(contextSize
, heapGrowthBytesGCTrigger
);
578 // On the first Init (INIT_MODS), check for command-line arguments
579 // or use the default mods from the config and enable those.
580 // On later engine restarts (e.g. the mod selector), we will skip this path,
581 // to avoid overwriting the newly selected mods.
582 if (flags
& INIT_MODS
)
584 ScriptInterface
modInterface("Engine", "Mod", g_ScriptContext
);
585 g_Mods
.UpdateAvailableMods(modInterface
);
586 std::vector
<CStr
> mods
;
588 mods
= args
.GetMultiple("mod");
592 CFG_GET_VAL("mod.enabledmods", modsStr
);
593 boost::split(mods
, modsStr
, boost::algorithm::is_space(), boost::token_compress_on
);
596 if (!g_Mods
.EnableMods(mods
, flags
& INIT_MODS_PUBLIC
))
598 // In non-visual mode, fail entirely.
599 if (args
.Has("autostart-nonvisual"))
601 LOGERROR("Trying to start with incompatible mods: %s.", boost::algorithm::join(g_Mods
.GetIncompatibleMods(), ", "));
606 // If there are incompatible mods, switch to the mod selector so players can resolve the problem.
607 if (g_Mods
.GetIncompatibleMods().empty())
608 MountMods(Paths(args
), g_Mods
.GetEnabledMods());
610 MountMods(Paths(args
), { "mod" });
612 // Special command-line mode to dump the entity schemas instead of running the game.
613 // (This must be done after loading VFS etc, but should be done before wasting time
614 // on anything else.)
615 if (args
.Has("dumpSchema"))
617 CSimulation2
sim(NULL
, g_ScriptContext
, NULL
);
618 sim
.LoadDefaultScripts();
619 std::ofstream
f("entity.rng", std::ios_base::out
| std::ios_base::trunc
);
620 f
<< sim
.GenerateSchema();
621 std::cout
<< "Generated entity.rng\n";
625 CNetHost::Initialize();
628 if (!args
.Has("autostart-nonvisual") && !g_DisableAudio
)
629 ISoundManager::CreateSoundManager();
634 // Optionally start profiler HTTP output automatically
635 // (By default it's only enabled by a hotkey, for security/performance)
636 bool profilerHTTPEnable
= false;
637 CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable
);
638 if (profilerHTTPEnable
)
639 g_Profiler2
.EnableHTTP();
641 // Initialise everything except Win32 sockets (because our networking
642 // system already inits those)
643 curl_global_init(CURL_GLOBAL_ALL
& ~CURL_GLOBAL_WIN32
);
646 g_UserReporter
.Initialize(); // after config
648 PROFILE2_EVENT("Init finished");
652 void InitGraphics(const CmdLineArgs
& args
, int flags
, const std::vector
<CStr
>& installedMods
)
654 const bool setup_vmode
= (flags
& INIT_HAVE_VMODE
) == 0;
660 if (!g_VideoMode
.InitSDL())
661 throw PSERROR_System_VmodeFailed(); // abort startup
664 RunHardwareDetection();
668 // Optionally start profiler GPU timings automatically
669 // (By default it's only enabled by a hotkey, for performance/compatibility)
670 bool profilerGPUEnable
= false;
671 CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable
);
672 if (profilerGPUEnable
)
673 g_Profiler2
.EnableGPU();
678 // note: no longer vfs_display here. it's dog-slow due to unbuffered
679 // file output and very rarely needed.
683 ISoundManager::SetEnabled(false);
685 g_GUI
= new CGUIManager();
687 CStr8 renderPath
= "default";
688 CFG_GET_VAL("renderpath", renderPath
);
689 if (RenderPathEnum::FromString(renderPath
) == FIXED
)
691 // It doesn't make sense to continue working here, because we're not
692 // able to display anything.
693 DEBUG_DISPLAY_FATAL_ERROR(
694 L
"Your graphics card doesn't appear to be fully compatible with OpenGL shaders."
695 L
" The game does not support pre-shader graphics cards."
696 L
" You are advised to try installing newer drivers and/or upgrade your graphics card."
697 L
" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734"
703 g_RenderingOptions
.ReadConfigAndSetupHooks();
712 // TODO: Is this the best place for this?
713 if (VfsDirectoryExists(L
"maps/"))
714 CXeromyces::AddValidator(g_VFS
, "map", "maps/scenario.rng");
718 if (!AutostartVisualReplay(args
.Get("replay-visual")) && !Autostart(args
))
720 const bool setup_gui
= ((flags
& INIT_NO_GUI
) == 0);
721 // We only want to display the splash screen at startup
722 std::shared_ptr
<ScriptInterface
> scriptInterface
= g_GUI
->GetScriptInterface();
723 ScriptRequest
rq(scriptInterface
);
724 JS::RootedValue
data(rq
.cx
);
727 Script::CreateObject(rq
, &data
, "isStartup", true);
728 if (!installedMods
.empty())
729 Script::SetProperty(rq
, data
, "installedMods", installedMods
);
731 InitPs(setup_gui
, installedMods
.empty() ? L
"page_pregame.xml" : L
"page_modmod.xml", g_GUI
->GetScriptInterface().get(), data
);
734 catch (PSERROR_Game_World_MapLoadFailed
& e
)
736 // Map Loading failed
738 // Start the engine so we have a GUI
739 InitPs(true, L
"page_pregame.xml", NULL
, JS::UndefinedHandleValue
);
741 // Call script function to do the actual work
742 // (delete game data, switch GUI page, show error, etc.)
743 CancelLoad(CStr(e
.what()).FromUTF8());
747 void InitNonVisual(const CmdLineArgs
& args
)
753 * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON
755 * The scenario map format is used for scenario and skirmish map types (random
756 * games do not use a "map" (format) but a small JavaScript program which
757 * creates a map on the fly). It contains a section to initialize the game
759 * @param mapPath Absolute path (from VFS root) to the map file to peek in.
760 * @return ScriptSettings in JSON format extracted from the map.
762 CStr8
LoadSettingsOfScenarioMap(const VfsPath
&mapPath
)
765 const char *pathToSettings
[] =
767 "Scenario", "ScriptSettings", "" // Path to JSON data in map
770 Status loadResult
= mapFile
.Load(g_VFS
, mapPath
);
772 if (INFO::OK
!= loadResult
)
774 LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath
.string8());
775 throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos.");
777 XMBElement mapElement
= mapFile
.GetRoot();
779 // Select the ScriptSettings node in the map file...
780 for (int i
= 0; pathToSettings
[i
][0]; ++i
)
782 int childId
= mapFile
.GetElementID(pathToSettings
[i
]);
784 XMBElementList nodes
= mapElement
.GetChildNodes();
785 auto it
= std::find_if(nodes
.begin(), nodes
.end(), [&childId
](const XMBElement
& child
) {
786 return child
.GetNodeName() == childId
;
789 if (it
!= nodes
.end())
792 // ... they contain a JSON document to initialize the game setup
794 return mapElement
.GetText();
798 * Command line options for autostart
799 * (keep synchronized with binaries/system/readme.txt):
801 * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME;
802 * TYPEDIR is skirmishes, scenarios, or random
803 * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random)
804 * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra)
805 * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI
806 * (0: sandbox, 5: very hard)
807 * -autostart-aiseed=AISEED sets the seed used for the AI random
808 * generator (default 0, use -1 for random)
809 * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer)
810 * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV
811 * (skirmish and random maps only)
812 * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2).
813 * -autostart-ceasefire=NUM sets a ceasefire duration NUM
814 * (default 0 minutes)
815 * -autostart-nonvisual disable any graphics and sounds
816 * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME
817 * located in simulation/data/settings/victory_conditions/
818 * (default conquest). When the first given SCRIPTNAME is
819 * "endless", no victory conditions will apply.
820 * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition
821 * (default 10 minutes)
822 * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition
823 * (default 10 minutes)
824 * -autostart-reliccount=NUM sets the number of relics for relic victory condition
826 * -autostart-disable-replay disable saving of replays
829 * -autostart-playername=NAME sets local player NAME (default 'anonymous')
830 * -autostart-host sets multiplayer host mode
831 * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer
833 * -autostart-client=IP sets multiplayer client to join host at
836 * -autostart-size=TILES sets random map size in TILES (default 192)
837 * -autostart-players=NUMBER sets NUMBER of players on random map
841 * 1) "Bob" will host a 2 player game on the Arcadia map:
842 * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob"
843 * "Alice" joins the match as player 2:
844 * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice"
845 * The players use the developer overlay to control players.
847 * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot:
848 * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra
850 * 3) Observe the PetraBot on a triggerscript map:
851 * -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
853 bool Autostart(const CmdLineArgs
& args
)
855 CStr autoStartName
= args
.Get("autostart");
857 if (autoStartName
.empty())
860 g_Game
= new CGame(!args
.Has("autostart-disable-replay"));
862 ScriptInterface
& scriptInterface
= g_Game
->GetSimulation2()->GetScriptInterface();
863 ScriptRequest
rq(scriptInterface
);
865 JS::RootedValue
attrs(rq
.cx
);
866 JS::RootedValue
settings(rq
.cx
);
867 JS::RootedValue
playerData(rq
.cx
);
869 Script::CreateObject(rq
, &attrs
);
870 Script::CreateObject(rq
, &settings
);
871 Script::CreateArray(rq
, &playerData
);
873 // The directory in front of the actual map name indicates which type
874 // of map is being loaded. Drawback of this approach is the association
875 // of map types and folders is hard-coded, but benefits are:
876 // - No need to pass the map type via command line separately
877 // - Prevents mixing up of scenarios and skirmish maps to some degree
878 Path mapPath
= Path(autoStartName
);
879 std::wstring mapDirectory
= mapPath
.Parent().Filename().string();
882 if (mapDirectory
== L
"random")
884 // Random map definition will be loaded from JSON file, so we need to parse it
885 std::wstring scriptPath
= L
"maps/" + autoStartName
.FromUTF8() + L
".json";
886 JS::RootedValue
scriptData(rq
.cx
);
887 Script::ReadJSONFile(rq
, scriptPath
, &scriptData
);
888 if (!scriptData
.isUndefined() && Script::GetProperty(rq
, scriptData
, "settings", &settings
))
890 // JSON loaded ok - copy script name over to game attributes
891 std::wstring scriptFile
;
892 if (!Script::GetProperty(rq
, settings
, "Script", scriptFile
))
894 LOGERROR("Autostart: random map '%s' data has no 'Script' property.", utf8_from_wstring(scriptPath
));
895 throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
897 Script::SetProperty(rq
, attrs
, "script", scriptFile
); // RMS filename
901 // Problem with JSON file
902 LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath
));
903 throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details.");
906 // Get optional map size argument (default 192)
908 if (args
.Has("autostart-size"))
910 CStr size
= args
.Get("autostart-size");
911 mapSize
= size
.ToUInt();
914 Script::SetProperty(rq
, settings
, "Size", mapSize
); // Random map size (in patches)
916 // Get optional number of players (default 2)
917 size_t numPlayers
= 2;
918 if (args
.Has("autostart-players"))
920 CStr num
= args
.Get("autostart-players");
921 numPlayers
= num
.ToUInt();
924 // Set up player data
925 for (size_t i
= 0; i
< numPlayers
; ++i
)
927 JS::RootedValue
player(rq
.cx
);
929 // We could load player_defaults.json here, but that would complicate the logic
930 // even more and autostart is only intended for developers anyway
931 Script::CreateObject(rq
, &player
, "Civ", "athen");
933 Script::SetPropertyInt(rq
, playerData
, i
, player
);
937 else if (mapDirectory
== L
"scenarios" || mapDirectory
== L
"skirmishes")
939 // Initialize general settings from the map data so some values
940 // (e.g. name of map) are always present, even when autostart is
941 // partially configured
942 CStr8 mapSettingsJSON
= LoadSettingsOfScenarioMap("maps/" + autoStartName
+ ".xml");
943 Script::ParseJSON(rq
, mapSettingsJSON
, &settings
);
945 // Initialize the playerData array being modified by autostart
946 // with the real map data, so sensible values are present:
947 Script::GetProperty(rq
, settings
, "PlayerData", &playerData
);
949 if (mapDirectory
== L
"scenarios")
950 mapType
= "scenario";
952 mapType
= "skirmish";
956 LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory
));
957 throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types.");
960 Script::SetProperty(rq
, attrs
, "mapType", mapType
);
961 Script::SetProperty(rq
, attrs
, "map", "maps/" + autoStartName
);
962 Script::SetProperty(rq
, settings
, "mapType", mapType
);
963 Script::SetProperty(rq
, settings
, "CheatsEnabled", true);
965 // The seed is used for both random map generation and simulation
967 if (args
.Has("autostart-seed"))
969 CStr seedArg
= args
.Get("autostart-seed");
973 seed
= seedArg
.ToULong();
975 Script::SetProperty(rq
, settings
, "Seed", seed
);
979 if (args
.Has("autostart-aiseed"))
981 CStr seedArg
= args
.Get("autostart-aiseed");
985 aiseed
= seedArg
.ToULong();
987 Script::SetProperty(rq
, settings
, "AISeed", aiseed
);
989 // Set player data for AIs
990 // attrs.settings = { PlayerData: [ { AI: ... }, ... ] }
991 // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set
993 JS::RootedValue
player(rq
.cx
);
994 if (Script::GetPropertyInt(rq
, playerData
, 0, &player
) && player
.isNull())
998 if (args
.Has("autostart-team"))
1000 std::vector
<CStr
> civArgs
= args
.GetMultiple("autostart-team");
1001 for (size_t i
= 0; i
< civArgs
.size(); ++i
)
1003 int playerID
= civArgs
[i
].BeforeFirst(":").ToInt();
1005 // Instead of overwriting existing player data, modify the array
1006 JS::RootedValue
currentPlayer(rq
.cx
);
1007 if (!Script::GetPropertyInt(rq
, playerData
, playerID
-offset
, ¤tPlayer
) || currentPlayer
.isUndefined())
1009 if (mapDirectory
== L
"skirmishes")
1011 // playerID is certainly bigger than this map player number
1012 LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID
);
1015 Script::CreateObject(rq
, ¤tPlayer
);
1018 int teamID
= civArgs
[i
].AfterFirst(":").ToInt() - 1;
1019 Script::SetProperty(rq
, currentPlayer
, "Team", teamID
);
1020 Script::SetPropertyInt(rq
, playerData
, playerID
-offset
, currentPlayer
);
1025 if (args
.Has("autostart-ceasefire"))
1026 ceasefire
= args
.Get("autostart-ceasefire").ToInt();
1027 Script::SetProperty(rq
, settings
, "Ceasefire", ceasefire
);
1029 if (args
.Has("autostart-ai"))
1031 std::vector
<CStr
> aiArgs
= args
.GetMultiple("autostart-ai");
1032 for (size_t i
= 0; i
< aiArgs
.size(); ++i
)
1034 int playerID
= aiArgs
[i
].BeforeFirst(":").ToInt();
1036 // Instead of overwriting existing player data, modify the array
1037 JS::RootedValue
currentPlayer(rq
.cx
);
1038 if (!Script::GetPropertyInt(rq
, playerData
, playerID
-offset
, ¤tPlayer
) || currentPlayer
.isUndefined())
1040 if (mapDirectory
== L
"scenarios" || mapDirectory
== L
"skirmishes")
1042 // playerID is certainly bigger than this map player number
1043 LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID
);
1046 Script::CreateObject(rq
, ¤tPlayer
);
1049 Script::SetProperty(rq
, currentPlayer
, "AI", aiArgs
[i
].AfterFirst(":"));
1050 Script::SetProperty(rq
, currentPlayer
, "AIDiff", 3);
1051 Script::SetProperty(rq
, currentPlayer
, "AIBehavior", "balanced");
1052 Script::SetPropertyInt(rq
, playerData
, playerID
-offset
, currentPlayer
);
1055 // Set AI difficulty
1056 if (args
.Has("autostart-aidiff"))
1058 std::vector
<CStr
> civArgs
= args
.GetMultiple("autostart-aidiff");
1059 for (size_t i
= 0; i
< civArgs
.size(); ++i
)
1061 int playerID
= civArgs
[i
].BeforeFirst(":").ToInt();
1063 // Instead of overwriting existing player data, modify the array
1064 JS::RootedValue
currentPlayer(rq
.cx
);
1065 if (!Script::GetPropertyInt(rq
, playerData
, playerID
-offset
, ¤tPlayer
) || currentPlayer
.isUndefined())
1067 if (mapDirectory
== L
"scenarios" || mapDirectory
== L
"skirmishes")
1069 // playerID is certainly bigger than this map player number
1070 LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID
);
1073 Script::CreateObject(rq
, ¤tPlayer
);
1076 Script::SetProperty(rq
, currentPlayer
, "AIDiff", civArgs
[i
].AfterFirst(":").ToInt());
1077 Script::SetPropertyInt(rq
, playerData
, playerID
-offset
, currentPlayer
);
1080 // Set player data for Civs
1081 if (args
.Has("autostart-civ"))
1083 if (mapDirectory
!= L
"scenarios")
1085 std::vector
<CStr
> civArgs
= args
.GetMultiple("autostart-civ");
1086 for (size_t i
= 0; i
< civArgs
.size(); ++i
)
1088 int playerID
= civArgs
[i
].BeforeFirst(":").ToInt();
1090 // Instead of overwriting existing player data, modify the array
1091 JS::RootedValue
currentPlayer(rq
.cx
);
1092 if (!Script::GetPropertyInt(rq
, playerData
, playerID
-offset
, ¤tPlayer
) || currentPlayer
.isUndefined())
1094 if (mapDirectory
== L
"skirmishes")
1096 // playerID is certainly bigger than this map player number
1097 LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID
);
1100 Script::CreateObject(rq
, ¤tPlayer
);
1103 Script::SetProperty(rq
, currentPlayer
, "Civ", civArgs
[i
].AfterFirst(":"));
1104 Script::SetPropertyInt(rq
, playerData
, playerID
-offset
, currentPlayer
);
1108 LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios");
1111 // Add player data to map settings
1112 Script::SetProperty(rq
, settings
, "PlayerData", playerData
);
1114 // Add map settings to game attributes
1115 Script::SetProperty(rq
, attrs
, "settings", settings
);
1117 // Get optional playername
1118 CStrW userName
= L
"anonymous";
1119 if (args
.Has("autostart-playername"))
1120 userName
= args
.Get("autostart-playername").FromUTF8();
1122 // Add additional scripts to the TriggerScripts property
1123 std::vector
<CStrW
> triggerScriptsVector
;
1124 JS::RootedValue
triggerScripts(rq
.cx
);
1126 if (Script::HasProperty(rq
, settings
, "TriggerScripts"))
1128 Script::GetProperty(rq
, settings
, "TriggerScripts", &triggerScripts
);
1129 Script::FromJSVal(rq
, triggerScripts
, triggerScriptsVector
);
1132 if (!CRenderer::IsInitialised())
1134 CStr nonVisualScript
= "scripts/NonVisualTrigger.js";
1135 triggerScriptsVector
.push_back(nonVisualScript
.FromUTF8());
1138 std::vector
<CStr
> victoryConditions(1, "conquest");
1139 if (args
.Has("autostart-victory"))
1140 victoryConditions
= args
.GetMultiple("autostart-victory");
1142 if (victoryConditions
.size() == 1 && victoryConditions
[0] == "endless")
1143 victoryConditions
.clear();
1145 Script::SetProperty(rq
, settings
, "VictoryConditions", victoryConditions
);
1147 for (const CStr
& victory
: victoryConditions
)
1149 JS::RootedValue
scriptData(rq
.cx
);
1150 JS::RootedValue
data(rq
.cx
);
1151 JS::RootedValue
victoryScripts(rq
.cx
);
1153 CStrW scriptPath
= L
"simulation/data/settings/victory_conditions/" + victory
.FromUTF8() + L
".json";
1154 Script::ReadJSONFile(rq
, scriptPath
, &scriptData
);
1155 if (!scriptData
.isUndefined() && Script::GetProperty(rq
, scriptData
, "Data", &data
) && !data
.isUndefined()
1156 && Script::GetProperty(rq
, data
, "Scripts", &victoryScripts
) && !victoryScripts
.isUndefined())
1158 std::vector
<CStrW
> victoryScriptsVector
;
1159 Script::FromJSVal(rq
, victoryScripts
, victoryScriptsVector
);
1160 triggerScriptsVector
.insert(triggerScriptsVector
.end(), victoryScriptsVector
.begin(), victoryScriptsVector
.end());
1164 LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath
));
1165 throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details.");
1169 Script::ToJSVal(rq
, &triggerScripts
, triggerScriptsVector
);
1170 Script::SetProperty(rq
, settings
, "TriggerScripts", triggerScripts
);
1172 int wonderDuration
= 10;
1173 if (args
.Has("autostart-wonderduration"))
1174 wonderDuration
= args
.Get("autostart-wonderduration").ToInt();
1175 Script::SetProperty(rq
, settings
, "WonderDuration", wonderDuration
);
1177 int relicDuration
= 10;
1178 if (args
.Has("autostart-relicduration"))
1179 relicDuration
= args
.Get("autostart-relicduration").ToInt();
1180 Script::SetProperty(rq
, settings
, "RelicDuration", relicDuration
);
1183 if (args
.Has("autostart-reliccount"))
1184 relicCount
= args
.Get("autostart-reliccount").ToInt();
1185 Script::SetProperty(rq
, settings
, "RelicCount", relicCount
);
1187 if (args
.Has("autostart-host"))
1189 InitPsAutostart(true, attrs
);
1191 size_t maxPlayers
= 2;
1192 if (args
.Has("autostart-host-players"))
1193 maxPlayers
= args
.Get("autostart-host-players").ToUInt();
1195 // Generate a secret to identify the host client.
1196 std::string secret
= ps_generate_guid();
1198 g_NetServer
= new CNetServer(false, maxPlayers
);
1199 g_NetServer
->SetControllerSecret(secret
);
1200 g_NetServer
->UpdateInitAttributes(&attrs
, scriptInterface
);
1202 bool ok
= g_NetServer
->SetupConnection(PS_DEFAULT_PORT
);
1205 g_NetClient
= new CNetClient(g_Game
);
1206 g_NetClient
->SetUserName(userName
);
1207 g_NetClient
->SetupServerData("127.0.0.1", PS_DEFAULT_PORT
, false);
1208 g_NetClient
->SetControllerSecret(secret
);
1209 g_NetClient
->SetupConnection(nullptr);
1211 else if (args
.Has("autostart-client"))
1213 InitPsAutostart(true, attrs
);
1215 g_NetClient
= new CNetClient(g_Game
);
1216 g_NetClient
->SetUserName(userName
);
1218 CStr ip
= args
.Get("autostart-client");
1222 g_NetClient
->SetupServerData(ip
, PS_DEFAULT_PORT
, false);
1223 ENSURE(g_NetClient
->SetupConnection(nullptr));
1227 g_Game
->SetPlayerID(args
.Has("autostart-player") ? args
.Get("autostart-player").ToInt() : 1);
1229 g_Game
->StartGame(&attrs
, "");
1231 if (CRenderer::IsInitialised())
1233 InitPsAutostart(false, attrs
);
1237 // TODO: Non progressive load can fail - need a decent way to handle this
1238 LDR_NonprogressiveLoad();
1239 ENSURE(g_Game
->ReallyStartGame() == PSRETURN_OK
);
1246 bool AutostartVisualReplay(const std::string
& replayFile
)
1248 if (!FileExists(OsPath(replayFile
)))
1251 g_Game
= new CGame(false);
1252 g_Game
->SetPlayerID(-1);
1253 g_Game
->StartVisualReplay(replayFile
);
1255 ScriptInterface
& scriptInterface
= g_Game
->GetSimulation2()->GetScriptInterface();
1256 ScriptRequest
rq(scriptInterface
);
1257 JS::RootedValue
attrs(rq
.cx
, g_Game
->GetSimulation2()->GetInitAttributes());
1259 InitPsAutostart(false, attrs
);
1264 void CancelLoad(const CStrW
& message
)
1266 std::shared_ptr
<ScriptInterface
> pScriptInterface
= g_GUI
->GetActiveGUI()->GetScriptInterface();
1267 ScriptRequest
rq(pScriptInterface
);
1269 JS::RootedValue
global(rq
.cx
, rq
.globalValue());
1274 g_GUI
->GetPageCount() &&
1275 Script::HasProperty(rq
, global
, "cancelOnLoadGameError"))
1276 ScriptFunction::CallVoid(rq
, global
, "cancelOnLoadGameError", message
);
1279 bool InDevelopmentCopy()
1281 if (!g_CheckedIfInDevelopmentCopy
)
1283 g_InDevelopmentCopy
= (g_VFS
->GetFileInfo(L
"config/dev.cfg", NULL
) == INFO::OK
);
1284 g_CheckedIfInDevelopmentCopy
= true;
1286 return g_InDevelopmentCopy
;