1 /* Copyright (C) 2023 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/>.
20 This module drives the game when running without Atlas (our integrated
21 map editor). It receives input and OS messages via SDL and feeds them
22 into the input dispatcher, where they are passed on to the game GUI and
24 It also contains main(), which either runs the above controller or
25 that of Atlas depending on commandline parameters.
29 // not for any PCH effort, but instead for the (common) definitions
32 #include "lib/precompiled.h"
34 #include "lib/debug.h"
35 #include "lib/status.h"
36 #include "lib/secure_crt.h"
37 #include "lib/frequency_filter.h"
38 #include "lib/input.h"
39 #include "lib/timer.h"
40 #include "lib/external_libraries/libsdl.h"
42 #include "ps/ArchiveBuilder.h"
43 #include "ps/CConsole.h"
44 #include "ps/CLogger.h"
45 #include "ps/ConfigDB.h"
46 #include "ps/Filesystem.h"
48 #include "ps/Globals.h"
49 #include "ps/Hotkey.h"
50 #include "ps/Loader.h"
52 #include "ps/ModInstaller.h"
53 #include "ps/Profile.h"
54 #include "ps/Profiler2.h"
55 #include "ps/Pyrogenesis.h"
56 #include "ps/Replay.h"
57 #include "ps/TouchInput.h"
58 #include "ps/UserReport.h"
60 #include "ps/VideoMode.h"
61 #include "ps/TaskManager.h"
63 #include "ps/GameSetup/GameSetup.h"
64 #include "ps/GameSetup/Atlas.h"
65 #include "ps/GameSetup/Config.h"
66 #include "ps/GameSetup/CmdLineArgs.h"
67 #include "ps/GameSetup/Paths.h"
68 #include "ps/XML/Xeromyces.h"
69 #include "network/NetClient.h"
70 #include "network/NetServer.h"
71 #include "network/NetSession.h"
72 #include "lobby/IXmppClient.h"
73 #include "graphics/Camera.h"
74 #include "graphics/GameView.h"
75 #include "graphics/TextureManager.h"
76 #include "gui/GUIManager.h"
77 #include "renderer/backend/IDevice.h"
78 #include "renderer/Renderer.h"
79 #include "rlinterface/RLInterface.h"
80 #include "scriptinterface/ScriptContext.h"
81 #include "scriptinterface/ScriptEngine.h"
82 #include "scriptinterface/ScriptInterface.h"
83 #include "scriptinterface/JSON.h"
84 #include "simulation2/Simulation2.h"
85 #include "simulation2/system/TurnManager.h"
86 #include "soundmanager/ISoundManager.h"
90 #include <unistd.h> // geteuid
94 #include "lib/sysdep/os/osx/osx_atlas.h"
99 #define getpid _getpid // Use the non-deprecated function name
103 // Forward declarations to avoid including Windows dependent headers.
104 Status
waio_Shutdown();
105 Status
wdir_watch_Init();
106 Status
wdir_watch_Shutdown();
108 Status
wutil_Shutdown();
110 // We don't want to include Windows.h as it might mess up the rest
111 // of the file so we just define DWORD as done in Windef.h.
113 typedef unsigned long DWORD
;
115 // Request the high performance GPU on Windows by default if no system override is specified.
117 // - https://github.com/supertuxkart/stk-code/pull/4693/commits/0a99c667ef513b2ce0f5755729a6e05df8aac48a
118 // - https://docs.nvidia.com/gameworks/content/technologies/desktop/optimus.htm
119 // - https://gpuopen.com/learn/amdpowerxpressrequesthighperformance/
122 __declspec(dllexport
) DWORD NvOptimusEnablement
= 0x00000001;
123 __declspec(dllexport
) DWORD AmdPowerXpressRequestHighPerformance
= 0x00000001;
129 extern CStrW g_UniqueLogPostfix
;
131 // Determines the lifetime of the mainloop
134 // The application shall continue the main loop.
137 // The process shall terminate as soon as possible.
140 // The engine should be restarted in the same process, for instance to activate different mods.
143 // Atlas should be started in the same process.
147 static ShutdownType g_Shutdown
= ShutdownType::None
;
149 // to avoid redundant and/or recursive resizing, we save the new
150 // size after VIDEORESIZE messages and only update the video mode
152 // these values are the latest resize message, and reset to 0 once we've
153 // updated the video mode
154 static int g_ResizedW
;
155 static int g_ResizedH
;
157 static std::chrono::high_resolution_clock::time_point lastFrameTime
;
159 bool IsQuitRequested()
161 return g_Shutdown
== ShutdownType::Quit
;
166 g_Shutdown
= ShutdownType::Quit
;
171 g_Shutdown
= ShutdownType::Restart
;
176 g_Shutdown
= ShutdownType::RestartAsAtlas
;
179 // main app message handler
180 static InReaction
MainInputHandler(const SDL_Event_
* ev
)
184 case SDL_WINDOWEVENT
:
185 switch(ev
->ev
.window
.event
)
187 case SDL_WINDOWEVENT_RESIZED
:
188 g_ResizedW
= ev
->ev
.window
.data1
;
189 g_ResizedH
= ev
->ev
.window
.data2
;
191 case SDL_WINDOWEVENT_MOVED
:
192 g_VideoMode
.UpdatePosition(ev
->ev
.window
.data1
, ev
->ev
.window
.data2
);
202 char* dropped_filedir
= ev
->ev
.drop
.file
;
203 const Paths
paths(g_CmdLineArgs
);
204 CModInstaller
installer(paths
.UserData() / "mods", paths
.Cache());
205 installer
.Install(std::string(dropped_filedir
), g_ScriptContext
, true);
206 SDL_free(dropped_filedir
);
207 if (installer
.GetInstalledMods().empty())
208 LOGERROR("Failed to install mod %s", dropped_filedir
);
211 LOGMESSAGE("Installed mod %s", installer
.GetInstalledMods().front());
212 ScriptInterface
modInterface("Engine", "Mod", g_ScriptContext
);
213 g_Mods
.UpdateAvailableMods(modInterface
);
219 case SDL_HOTKEYPRESS
:
220 std::string hotkey
= static_cast<const char*>(ev
->ev
.user
.data1
);
221 if (hotkey
== "exit")
226 else if (hotkey
== "screenshot")
228 g_Renderer
.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::DEFAULT
);
231 else if (hotkey
== "bigscreenshot")
233 g_Renderer
.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::BIG
);
236 else if (hotkey
== "togglefullscreen")
238 g_VideoMode
.ToggleFullscreen();
241 else if (hotkey
== "profile2.toggle")
243 g_Profiler2
.Toggle();
253 // dispatch all pending events to the various receivers.
254 static void PumpEvents()
256 ScriptRequest
rq(g_GUI
->GetScriptInterface());
258 PROFILE3("dispatch events");
261 while (in_poll_event(&ev
))
266 JS::RootedValue
tmpVal(rq
.cx
);
267 Script::ToJSVal(rq
, &tmpVal
, ev
);
268 std::string data
= Script::StringifyJSON(rq
, &tmpVal
);
269 PROFILE2_ATTR("%s", data
.c_str());
271 in_dispatch_event(&ev
);
274 g_TouchInput
.Frame();
278 * Optionally throttle the render frequency in order to
279 * prevent 100% workload of the currently used CPU core.
281 inline static void LimitFPS()
283 if (g_VideoMode
.IsVSyncEnabled())
286 double fpsLimit
= 0.0;
287 CFG_GET_VAL(g_Game
&& g_Game
->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit
);
289 // Keep in sync with options.json
290 if (fpsLimit
< 20.0 || fpsLimit
>= 360.0)
293 double wait
= 1000.0 / fpsLimit
-
294 std::chrono::duration_cast
<std::chrono::microseconds
>(
295 std::chrono::high_resolution_clock::now() - lastFrameTime
).count() / 1000.0;
300 lastFrameTime
= std::chrono::high_resolution_clock::now();
303 static int ProgressiveLoad()
305 PROFILE3("progressive load");
307 wchar_t description
[100];
308 int progress_percent
;
311 Status ret
= LDR_ProgressiveLoad(10e-3, description
, ARRAY_SIZE(description
), &progress_percent
);
314 // no load active => no-op (skip code below)
317 // current task didn't complete. we only care about this insofar as the
318 // load process is therefore not yet finished.
321 // just finished loading
322 case INFO::ALL_COMPLETE
:
323 g_Game
->ReallyStartGame();
324 wcscpy_s(description
, ARRAY_SIZE(description
), L
"Game is starting..");
325 // LDR_ProgressiveLoad returns L""; set to valid text to
326 // avoid problems in converting to JSString
330 WARN_RETURN_STATUS_IF_ERR(ret
);
331 // can't do this above due to legit ERR::TIMED_OUT
335 catch (PSERROR_Game_World_MapLoadFailed
& e
)
337 // Map loading failed
339 // Call script function to do the actual work
340 // (delete game data, switch GUI page, show error, etc.)
341 CancelLoad(CStr(e
.what()).FromUTF8());
344 g_GUI
->DisplayLoadProgress(progress_percent
, description
);
349 static void RendererIncrementalLoad()
351 PROFILE3("renderer incremental load");
353 const double maxTime
= 0.1f
;
355 double startTime
= timer_Time();
358 more
= g_Renderer
.GetTextureManager().MakeProgress();
360 while (more
&& timer_Time() - startTime
< maxTime
);
363 static void Frame(RL::Interface
* rlInterface
)
365 g_Profiler2
.RecordFrameStart();
367 g_Profiler2
.IncrementFrameNumber();
368 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
371 const double time
= timer_Time();
372 g_frequencyFilter
->Update(time
);
373 // .. old method - "exact" but contains jumps
375 static double last_time
;
376 const double time
= timer_Time();
377 const float TimeSinceLastFrame
= (float)(time
-last_time
);
379 ONCE(return); // first call: set last_time and return
381 // .. new method - filtered and more smooth, but errors may accumulate
383 const float realTimeSinceLastFrame
= 1.0 / g_frequencyFilter
->SmoothedFrequency();
385 ENSURE(realTimeSinceLastFrame
> 0.0f
);
387 // Decide if update is necessary
388 const bool needUpdate
{g_app_has_focus
|| g_NetClient
|| !g_PauseOnFocusLoss
};
390 // If we are not running a multiplayer game, disable updates when the game is
391 // minimized or out of focus and relinquish the CPU a bit, in order to make
395 PROFILE3("non-focus delay");
396 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
400 // this scans for changed files/directories and reloads them, thus
401 // allowing hotloading (changes are immediately assimilated in-game).
402 ReloadChangedFiles();
406 RendererIncrementalLoad();
410 // if the user quit by closing the window, the GL context will be broken and
411 // may crash when we call Render() on some drivers, so leave this loop
413 if (g_Shutdown
!= ShutdownType::None
)
416 // respond to pumped resize events
417 if (g_ResizedW
|| g_ResizedH
)
419 g_VideoMode
.ResizeWindow(g_ResizedW
, g_ResizedH
);
420 g_ResizedW
= g_ResizedH
= 0;
426 g_GUI
->TickObjects();
429 rlInterface
->TryApplyMessage();
431 if (g_Game
&& g_Game
->IsGameStarted() && needUpdate
)
434 g_Game
->Update(realTimeSinceLastFrame
);
436 g_Game
->GetView()->Update(float(realTimeSinceLastFrame
));
439 // Keep us connected to any XMPP servers
441 g_XmppClient
->recv();
443 g_UserReporter
.Update();
445 g_Console
->Update(realTimeSinceLastFrame
);
448 g_SoundManager
->IdleTask();
450 g_Renderer
.RenderFrame(true);
457 static void NonVisualFrame()
459 g_Profiler2
.RecordFrameStart();
461 g_Profiler2
.IncrementFrameNumber();
462 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
468 if (g_Game
&& g_Game
->IsGameStarted() && g_Game
->GetTurnManager())
469 if (g_Game
->GetTurnManager()->Update(DEFAULT_TURN_LENGTH
, 1))
470 debug_printf("Turn %u (%u)...\n", turn
++, DEFAULT_TURN_LENGTH
);
474 if (g_Game
->IsGameFinished())
478 static void MainControllerInit()
480 // add additional input handlers only needed by this controller:
482 // must be registered after gui_handler. Should mayhap even be last.
483 in_add_handler(MainInputHandler
);
486 static void MainControllerShutdown()
491 static std::optional
<RL::Interface
> CreateRLInterface(const CmdLineArgs
& args
)
493 if (!args
.Has("rl-interface"))
496 std::string server_address
;
497 CFG_GET_VAL("rlinterface.address", server_address
);
499 if (!args
.Get("rl-interface").empty())
500 server_address
= args
.Get("rl-interface");
502 debug_printf("RL interface listening on %s\n", server_address
.c_str());
503 return std::make_optional
<RL::Interface
>(server_address
.c_str());
506 // moved into a helper function to ensure args is destroyed before
507 // exit(), which may result in a memory leak.
508 static void RunGameOrAtlas(const PS::span
<const char* const> argv
)
510 const CmdLineArgs
args(argv
);
512 g_CmdLineArgs
= args
;
514 if (args
.Has("version"))
516 debug_printf("Pyrogenesis %s\n", engine_version
);
520 if (args
.Has("autostart-nonvisual") && args
.Get("autostart").empty() && !args
.Has("rl-interface") && !args
.Has("autostart-client"))
522 LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
526 if (args
.Has("unique-logs"))
527 g_UniqueLogPostfix
= L
"_" + std::to_wstring(std::time(nullptr)) + L
"_" + std::to_wstring(getpid());
529 const bool isVisualReplay
= args
.Has("replay-visual");
530 const bool isNonVisualReplay
= args
.Has("replay");
531 const bool isVisual
= !args
.Has("autostart-nonvisual");
533 const OsPath
replayFile(
534 isVisualReplay
? args
.Get("replay-visual") :
535 isNonVisualReplay
? args
.Get("replay") : "");
537 if (isVisualReplay
|| isNonVisualReplay
)
539 if (!FileExists(replayFile
))
541 debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile
.string8().c_str());
544 if (DirectoryExists(replayFile
))
546 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile
.string8().c_str());
551 std::vector
<OsPath
> modsToInstall
;
552 for (const CStr
& arg
: args
.GetArgsWithoutName())
554 const OsPath
modPath(arg
);
555 if (!CModInstaller::IsDefaultModExtension(modPath
.Extension()))
557 debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath
.string8().c_str());
560 if (!FileExists(modPath
))
562 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath
.string8().c_str());
565 if (DirectoryExists(modPath
))
567 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath
.string8().c_str());
570 modsToInstall
.emplace_back(std::move(modPath
));
573 // We need to initialize SpiderMonkey and libxml2 in the main thread before
574 // any thread uses them. So initialize them here before we might run Atlas.
575 ScriptEngine scriptEngine
;
576 CXeromyces::Startup();
578 // Initialise the global task manager at this point (JS & Profiler2 are set up).
579 Threading::TaskManager::Initialise();
581 if (ATLAS_RunIfOnCmdLine(args
, false))
583 CXeromyces::Terminate();
587 if (isNonVisualReplay
)
591 // Mount with highest priority, we don't want mods overwriting this.
592 g_VFS
->Mount(L
"cache/", paths
.Cache(), VFS_MOUNT_ARCHIVABLE
, VFS_MAX_PRIORITY
);
595 CReplayPlayer replay
;
596 replay
.Load(replayFile
);
598 args
.Has("serializationtest"),
599 args
.Has("rejointest") ? args
.Get("rejointest").ToInt() : -1,
601 !args
.Has("hashtest-full") || args
.Get("hashtest-full") == "true",
602 args
.Has("hashtest-quick") && args
.Get("hashtest-quick") == "true");
607 CXeromyces::Terminate();
611 // run in archive-building mode if requested
612 if (args
.Has("archivebuild"))
616 OsPath
mod(args
.Get("archivebuild"));
618 if (args
.Has("archivebuild-output"))
619 zip
= args
.Get("archivebuild-output");
621 zip
= mod
.Filename().ChangeExtension(L
".zip");
623 CArchiveBuilder
builder(mod
, paths
.Cache());
625 // Add mods provided on the command line
626 // NOTE: We do not handle mods in the user mod path here
627 std::vector
<CStr
> mods
= args
.GetMultiple("mod");
628 for (size_t i
= 0; i
< mods
.size(); ++i
)
629 builder
.AddBaseMod(paths
.RData()/"mods"/mods
[i
]);
631 builder
.Build(zip
, args
.Has("archivebuild-compress"));
633 CXeromyces::Terminate();
637 const double res
= timer_Resolution();
638 g_frequencyFilter
= CreateFrequencyFilter(res
, 30.0);
641 int flags
= INIT_MODS
;
644 g_Shutdown
= ShutdownType::None
;
646 // Do this as soon as possible, because it chdirs and will mess up the error reporting if
647 // anything crashes before the working directory is set.
650 // This must come after VFS init, which sets the current directory (required for finding our
651 // output log files).
654 if (!Init(args
, flags
))
657 Shutdown(SHUTDOWN_FROM_CONFIG
);
661 std::vector
<CStr
> installedMods
;
662 if (!modsToInstall
.empty())
665 CModInstaller
installer(paths
.UserData() / "mods", paths
.Cache());
667 // Install the mods without deleting the pyromod files
668 for (const OsPath
& modPath
: modsToInstall
)
670 CModInstaller::ModInstallationResult result
= installer
.Install(modPath
, g_ScriptContext
, true);
671 if (result
!= CModInstaller::ModInstallationResult::SUCCESS
)
672 LOGERROR("Failed to install '%s'", modPath
.string8().c_str());
675 installedMods
= installer
.GetInstalledMods();
677 ScriptInterface
modInterface("Engine", "Mod", g_ScriptContext
);
678 g_Mods
.UpdateAvailableMods(modInterface
);
683 InitGraphics(args
, 0, installedMods
);
684 MainControllerInit();
686 else if (!InitNonVisual(args
))
687 g_Shutdown
= ShutdownType::Quit
;
689 // MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead.
690 std::optional
<RL::Interface
> rlInterface
{[&]() -> std::optional
<RL::Interface
>
692 if (g_Shutdown
== ShutdownType::None
)
693 return CreateRLInterface(args
);
698 while (g_Shutdown
== ShutdownType::None
)
701 Frame(rlInterface
? &*rlInterface
: nullptr);
703 rlInterface
->TryApplyMessage();
708 // Do not install mods again in case of restart (typically from the mod selector)
709 modsToInstall
.clear();
712 MainControllerShutdown();
715 } while (g_Shutdown
== ShutdownType::Restart
);
718 if (g_Shutdown
== ShutdownType::RestartAsAtlas
)
719 startNewAtlasProcess(g_Mods
.GetEnabledMods());
721 if (g_Shutdown
== ShutdownType::RestartAsAtlas
)
722 ATLAS_RunIfOnCmdLine(args
, true);
725 Threading::TaskManager::Instance().ClearQueue();
726 CXeromyces::Terminate();
730 // In Android we compile the engine as a shared library, not an executable,
731 // so rename main() to a different symbol that the wrapper library can load
733 #define main pyrogenesis_main
734 extern "C" __attribute__((visibility ("default"))) int main(int argc
, char* argv
[]);
737 extern "C" int main(int argc
, char* argv
[])
740 // Don't allow people to run the game with root permissions,
741 // because bad things can happen, check before we do anything
744 std::cerr
<< "********************************************************\n"
745 << "WARNING: Attempted to run the game with root permission!\n"
746 << "This is not allowed because it can alter home directory \n"
747 << "permissions and opens your system to vulnerabilities. \n"
748 << "(You received this message because you were either \n"
749 <<" logged in as root or used e.g. the 'sudo' command.) \n"
750 << "********************************************************\n\n";
760 EarlyInit(); // must come at beginning of main
762 // static_cast is ok, argc is never negative.
763 RunGameOrAtlas({argv
, static_cast<std::size_t>(argc
)});
765 // Shut down profiler initialised by EarlyInit
766 g_Profiler2
.Shutdown();
769 // All calls to Windows specific functions have to happen before the following
771 wdir_watch_Shutdown();