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/>.
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 // We don't want to include Windows.h as it might mess up the rest
104 // of the file so we just define DWORD as done in Windef.h.
106 typedef unsigned long DWORD
;
108 // Request the high performance GPU on Windows by default if no system override is specified.
110 // - https://github.com/supertuxkart/stk-code/pull/4693/commits/0a99c667ef513b2ce0f5755729a6e05df8aac48a
111 // - https://docs.nvidia.com/gameworks/content/technologies/desktop/optimus.htm
112 // - https://gpuopen.com/learn/amdpowerxpressrequesthighperformance/
115 __declspec(dllexport
) DWORD NvOptimusEnablement
= 0x00000001;
116 __declspec(dllexport
) DWORD AmdPowerXpressRequestHighPerformance
= 0x00000001;
122 extern CStrW g_UniqueLogPostfix
;
124 // Determines the lifetime of the mainloop
127 // The application shall continue the main loop.
130 // The process shall terminate as soon as possible.
133 // The engine should be restarted in the same process, for instance to activate different mods.
136 // Atlas should be started in the same process.
140 static ShutdownType g_Shutdown
= ShutdownType::None
;
142 // to avoid redundant and/or recursive resizing, we save the new
143 // size after VIDEORESIZE messages and only update the video mode
145 // these values are the latest resize message, and reset to 0 once we've
146 // updated the video mode
147 static int g_ResizedW
;
148 static int g_ResizedH
;
150 static std::chrono::high_resolution_clock::time_point lastFrameTime
;
152 bool IsQuitRequested()
154 return g_Shutdown
== ShutdownType::Quit
;
159 g_Shutdown
= ShutdownType::Quit
;
164 g_Shutdown
= ShutdownType::Restart
;
169 g_Shutdown
= ShutdownType::RestartAsAtlas
;
172 // main app message handler
173 static InReaction
MainInputHandler(const SDL_Event_
* ev
)
177 case SDL_WINDOWEVENT
:
178 switch(ev
->ev
.window
.event
)
180 case SDL_WINDOWEVENT_RESIZED
:
181 g_ResizedW
= ev
->ev
.window
.data1
;
182 g_ResizedH
= ev
->ev
.window
.data2
;
184 case SDL_WINDOWEVENT_MOVED
:
185 g_VideoMode
.UpdatePosition(ev
->ev
.window
.data1
, ev
->ev
.window
.data2
);
195 char* dropped_filedir
= ev
->ev
.drop
.file
;
196 const Paths
paths(g_CmdLineArgs
);
197 CModInstaller
installer(paths
.UserData() / "mods", paths
.Cache());
198 installer
.Install(std::string(dropped_filedir
), g_ScriptContext
, true);
199 SDL_free(dropped_filedir
);
200 if (installer
.GetInstalledMods().empty())
201 LOGERROR("Failed to install mod %s", dropped_filedir
);
204 LOGMESSAGE("Installed mod %s", installer
.GetInstalledMods().front());
205 ScriptInterface
modInterface("Engine", "Mod", g_ScriptContext
);
206 g_Mods
.UpdateAvailableMods(modInterface
);
212 case SDL_HOTKEYPRESS
:
213 std::string hotkey
= static_cast<const char*>(ev
->ev
.user
.data1
);
214 if (hotkey
== "exit")
219 else if (hotkey
== "screenshot")
221 g_Renderer
.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::DEFAULT
);
224 else if (hotkey
== "bigscreenshot")
226 g_Renderer
.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::BIG
);
229 else if (hotkey
== "togglefullscreen")
231 g_VideoMode
.ToggleFullscreen();
234 else if (hotkey
== "profile2.toggle")
236 g_Profiler2
.Toggle();
246 // dispatch all pending events to the various receivers.
247 static void PumpEvents()
249 ScriptRequest
rq(g_GUI
->GetScriptInterface());
251 PROFILE3("dispatch events");
254 while (in_poll_event(&ev
))
259 JS::RootedValue
tmpVal(rq
.cx
);
260 Script::ToJSVal(rq
, &tmpVal
, ev
);
261 std::string data
= Script::StringifyJSON(rq
, &tmpVal
);
262 PROFILE2_ATTR("%s", data
.c_str());
264 in_dispatch_event(&ev
);
267 g_TouchInput
.Frame();
271 * Optionally throttle the render frequency in order to
272 * prevent 100% workload of the currently used CPU core.
274 inline static void LimitFPS()
276 if (g_VideoMode
.IsVSyncEnabled())
279 double fpsLimit
= 0.0;
280 CFG_GET_VAL(g_Game
&& g_Game
->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit
);
282 // Keep in sync with options.json
283 if (fpsLimit
< 20.0 || fpsLimit
>= 360.0)
286 double wait
= 1000.0 / fpsLimit
-
287 std::chrono::duration_cast
<std::chrono::microseconds
>(
288 std::chrono::high_resolution_clock::now() - lastFrameTime
).count() / 1000.0;
293 lastFrameTime
= std::chrono::high_resolution_clock::now();
296 static int ProgressiveLoad()
298 PROFILE3("progressive load");
300 wchar_t description
[100];
301 int progress_percent
;
304 Status ret
= LDR_ProgressiveLoad(10e-3, description
, ARRAY_SIZE(description
), &progress_percent
);
307 // no load active => no-op (skip code below)
310 // current task didn't complete. we only care about this insofar as the
311 // load process is therefore not yet finished.
314 // just finished loading
315 case INFO::ALL_COMPLETE
:
316 g_Game
->ReallyStartGame();
317 wcscpy_s(description
, ARRAY_SIZE(description
), L
"Game is starting..");
318 // LDR_ProgressiveLoad returns L""; set to valid text to
319 // avoid problems in converting to JSString
323 WARN_RETURN_STATUS_IF_ERR(ret
);
324 // can't do this above due to legit ERR::TIMED_OUT
328 catch (PSERROR_Game_World_MapLoadFailed
& e
)
330 // Map loading failed
332 // Call script function to do the actual work
333 // (delete game data, switch GUI page, show error, etc.)
334 CancelLoad(CStr(e
.what()).FromUTF8());
337 g_GUI
->DisplayLoadProgress(progress_percent
, description
);
342 static void RendererIncrementalLoad()
344 PROFILE3("renderer incremental load");
346 const double maxTime
= 0.1f
;
348 double startTime
= timer_Time();
351 more
= g_Renderer
.GetTextureManager().MakeProgress();
353 while (more
&& timer_Time() - startTime
< maxTime
);
358 g_Profiler2
.RecordFrameStart();
360 g_Profiler2
.IncrementFrameNumber();
361 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
364 const double time
= timer_Time();
365 g_frequencyFilter
->Update(time
);
366 // .. old method - "exact" but contains jumps
368 static double last_time
;
369 const double time
= timer_Time();
370 const float TimeSinceLastFrame
= (float)(time
-last_time
);
372 ONCE(return); // first call: set last_time and return
374 // .. new method - filtered and more smooth, but errors may accumulate
376 const float realTimeSinceLastFrame
= 1.0 / g_frequencyFilter
->SmoothedFrequency();
378 ENSURE(realTimeSinceLastFrame
> 0.0f
);
380 // Decide if update is necessary
381 bool need_update
= true;
383 // If we are not running a multiplayer game, disable updates when the game is
384 // minimized or out of focus and relinquish the CPU a bit, in order to make
386 if (g_PauseOnFocusLoss
&& !g_NetClient
&& !g_app_has_focus
)
388 PROFILE3("non-focus delay");
390 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
394 // this scans for changed files/directories and reloads them, thus
395 // allowing hotloading (changes are immediately assimilated in-game).
396 ReloadChangedFiles();
400 RendererIncrementalLoad();
404 // if the user quit by closing the window, the GL context will be broken and
405 // may crash when we call Render() on some drivers, so leave this loop
407 if (g_Shutdown
!= ShutdownType::None
)
410 // respond to pumped resize events
411 if (g_ResizedW
|| g_ResizedH
)
413 g_VideoMode
.ResizeWindow(g_ResizedW
, g_ResizedH
);
414 g_ResizedW
= g_ResizedH
= 0;
420 g_GUI
->TickObjects();
423 g_RLInterface
->TryApplyMessage();
425 if (g_Game
&& g_Game
->IsGameStarted() && need_update
)
428 g_Game
->Update(realTimeSinceLastFrame
);
430 g_Game
->GetView()->Update(float(realTimeSinceLastFrame
));
433 // Keep us connected to any XMPP servers
435 g_XmppClient
->recv();
437 g_UserReporter
.Update();
439 g_Console
->Update(realTimeSinceLastFrame
);
442 g_SoundManager
->IdleTask();
444 g_Renderer
.RenderFrame(true);
451 static void NonVisualFrame()
453 g_Profiler2
.RecordFrameStart();
455 g_Profiler2
.IncrementFrameNumber();
456 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
462 if (g_Game
&& g_Game
->IsGameStarted() && g_Game
->GetTurnManager())
463 if (g_Game
->GetTurnManager()->Update(DEFAULT_TURN_LENGTH
, 1))
464 debug_printf("Turn %u (%u)...\n", turn
++, DEFAULT_TURN_LENGTH
);
468 if (g_Game
->IsGameFinished())
472 static void MainControllerInit()
474 // add additional input handlers only needed by this controller:
476 // must be registered after gui_handler. Should mayhap even be last.
477 in_add_handler(MainInputHandler
);
480 static void MainControllerShutdown()
485 static void StartRLInterface(CmdLineArgs args
)
487 std::string server_address
;
488 CFG_GET_VAL("rlinterface.address", server_address
);
490 if (!args
.Get("rl-interface").empty())
491 server_address
= args
.Get("rl-interface");
493 g_RLInterface
= std::make_unique
<RL::Interface
>(server_address
.c_str());
494 debug_printf("RL interface listening on %s\n", server_address
.c_str());
497 // moved into a helper function to ensure args is destroyed before
498 // exit(), which may result in a memory leak.
499 static void RunGameOrAtlas(const PS::span
<const char* const> argv
)
501 const CmdLineArgs
args(argv
);
503 g_CmdLineArgs
= args
;
505 if (args
.Has("version"))
507 debug_printf("Pyrogenesis %s\n", engine_version
);
511 if (args
.Has("autostart-nonvisual") && args
.Get("autostart").empty() && !args
.Has("rl-interface") && !args
.Has("autostart-client"))
513 LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
517 if (args
.Has("unique-logs"))
518 g_UniqueLogPostfix
= L
"_" + std::to_wstring(std::time(nullptr)) + L
"_" + std::to_wstring(getpid());
520 const bool isVisualReplay
= args
.Has("replay-visual");
521 const bool isNonVisualReplay
= args
.Has("replay");
522 const bool isNonVisual
= args
.Has("autostart-nonvisual");
523 const bool isUsingRLInterface
= args
.Has("rl-interface");
525 const OsPath
replayFile(
526 isVisualReplay
? args
.Get("replay-visual") :
527 isNonVisualReplay
? args
.Get("replay") : "");
529 if (isVisualReplay
|| isNonVisualReplay
)
531 if (!FileExists(replayFile
))
533 debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile
.string8().c_str());
536 if (DirectoryExists(replayFile
))
538 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile
.string8().c_str());
543 std::vector
<OsPath
> modsToInstall
;
544 for (const CStr
& arg
: args
.GetArgsWithoutName())
546 const OsPath
modPath(arg
);
547 if (!CModInstaller::IsDefaultModExtension(modPath
.Extension()))
549 debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath
.string8().c_str());
552 if (!FileExists(modPath
))
554 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath
.string8().c_str());
557 if (DirectoryExists(modPath
))
559 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath
.string8().c_str());
562 modsToInstall
.emplace_back(std::move(modPath
));
565 // We need to initialize SpiderMonkey and libxml2 in the main thread before
566 // any thread uses them. So initialize them here before we might run Atlas.
567 ScriptEngine scriptEngine
;
568 CXeromyces::Startup();
570 // Initialise the global task manager at this point (JS & Profiler2 are set up).
571 Threading::TaskManager::Initialise();
573 if (ATLAS_RunIfOnCmdLine(args
, false))
575 CXeromyces::Terminate();
579 if (isNonVisualReplay
)
583 // Mount with highest priority, we don't want mods overwriting this.
584 g_VFS
->Mount(L
"cache/", paths
.Cache(), VFS_MOUNT_ARCHIVABLE
, VFS_MAX_PRIORITY
);
587 CReplayPlayer replay
;
588 replay
.Load(replayFile
);
590 args
.Has("serializationtest"),
591 args
.Has("rejointest") ? args
.Get("rejointest").ToInt() : -1,
593 !args
.Has("hashtest-full") || args
.Get("hashtest-full") == "true",
594 args
.Has("hashtest-quick") && args
.Get("hashtest-quick") == "true");
599 CXeromyces::Terminate();
603 // run in archive-building mode if requested
604 if (args
.Has("archivebuild"))
608 OsPath
mod(args
.Get("archivebuild"));
610 if (args
.Has("archivebuild-output"))
611 zip
= args
.Get("archivebuild-output");
613 zip
= mod
.Filename().ChangeExtension(L
".zip");
615 CArchiveBuilder
builder(mod
, paths
.Cache());
617 // Add mods provided on the command line
618 // NOTE: We do not handle mods in the user mod path here
619 std::vector
<CStr
> mods
= args
.GetMultiple("mod");
620 for (size_t i
= 0; i
< mods
.size(); ++i
)
621 builder
.AddBaseMod(paths
.RData()/"mods"/mods
[i
]);
623 builder
.Build(zip
, args
.Has("archivebuild-compress"));
625 CXeromyces::Terminate();
629 const double res
= timer_Resolution();
630 g_frequencyFilter
= CreateFrequencyFilter(res
, 30.0);
633 int flags
= INIT_MODS
;
636 g_Shutdown
= ShutdownType::None
;
638 if (!Init(args
, flags
))
641 Shutdown(SHUTDOWN_FROM_CONFIG
);
645 std::vector
<CStr
> installedMods
;
646 if (!modsToInstall
.empty())
649 CModInstaller
installer(paths
.UserData() / "mods", paths
.Cache());
651 // Install the mods without deleting the pyromod files
652 for (const OsPath
& modPath
: modsToInstall
)
654 CModInstaller::ModInstallationResult result
= installer
.Install(modPath
, g_ScriptContext
, true);
655 if (result
!= CModInstaller::ModInstallationResult::SUCCESS
)
656 LOGERROR("Failed to install '%s'", modPath
.string8().c_str());
659 installedMods
= installer
.GetInstalledMods();
661 ScriptInterface
modInterface("Engine", "Mod", g_ScriptContext
);
662 g_Mods
.UpdateAvailableMods(modInterface
);
667 if (!InitNonVisual(args
))
668 g_Shutdown
= ShutdownType::Quit
;
669 else if (isUsingRLInterface
)
670 StartRLInterface(args
);
672 while (g_Shutdown
== ShutdownType::None
)
674 if (isUsingRLInterface
)
675 g_RLInterface
->TryApplyMessage();
682 InitGraphics(args
, 0, installedMods
);
683 MainControllerInit();
684 if (isUsingRLInterface
)
685 StartRLInterface(args
);
686 while (g_Shutdown
== ShutdownType::None
)
690 // Do not install mods again in case of restart (typically from the mod selector)
691 modsToInstall
.clear();
694 MainControllerShutdown();
697 } while (g_Shutdown
== ShutdownType::Restart
);
700 if (g_Shutdown
== ShutdownType::RestartAsAtlas
)
701 startNewAtlasProcess(g_Mods
.GetEnabledMods());
703 if (g_Shutdown
== ShutdownType::RestartAsAtlas
)
704 ATLAS_RunIfOnCmdLine(args
, true);
707 Threading::TaskManager::Instance().ClearQueue();
708 CXeromyces::Terminate();
712 // In Android we compile the engine as a shared library, not an executable,
713 // so rename main() to a different symbol that the wrapper library can load
715 #define main pyrogenesis_main
716 extern "C" __attribute__((visibility ("default"))) int main(int argc
, char* argv
[]);
719 extern "C" int main(int argc
, char* argv
[])
722 // Don't allow people to run the game with root permissions,
723 // because bad things can happen, check before we do anything
726 std::cerr
<< "********************************************************\n"
727 << "WARNING: Attempted to run the game with root permission!\n"
728 << "This is not allowed because it can alter home directory \n"
729 << "permissions and opens your system to vulnerabilities. \n"
730 << "(You received this message because you were either \n"
731 <<" logged in as root or used e.g. the 'sudo' command.) \n"
732 << "********************************************************\n\n";
737 EarlyInit(); // must come at beginning of main
739 // static_cast is ok, argc is never negative.
740 RunGameOrAtlas({argv
, static_cast<std::size_t>(argc
)});
742 // Shut down profiler initialised by EarlyInit
743 g_Profiler2
.Shutdown();