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
);
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 bool need_update
= true;
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
393 if (g_PauseOnFocusLoss
&& !g_NetClient
&& !g_app_has_focus
)
395 PROFILE3("non-focus delay");
397 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
401 // this scans for changed files/directories and reloads them, thus
402 // allowing hotloading (changes are immediately assimilated in-game).
403 ReloadChangedFiles();
407 RendererIncrementalLoad();
411 // if the user quit by closing the window, the GL context will be broken and
412 // may crash when we call Render() on some drivers, so leave this loop
414 if (g_Shutdown
!= ShutdownType::None
)
417 // respond to pumped resize events
418 if (g_ResizedW
|| g_ResizedH
)
420 g_VideoMode
.ResizeWindow(g_ResizedW
, g_ResizedH
);
421 g_ResizedW
= g_ResizedH
= 0;
427 g_GUI
->TickObjects();
430 g_RLInterface
->TryApplyMessage();
432 if (g_Game
&& g_Game
->IsGameStarted() && need_update
)
435 g_Game
->Update(realTimeSinceLastFrame
);
437 g_Game
->GetView()->Update(float(realTimeSinceLastFrame
));
440 // Keep us connected to any XMPP servers
442 g_XmppClient
->recv();
444 g_UserReporter
.Update();
446 g_Console
->Update(realTimeSinceLastFrame
);
449 g_SoundManager
->IdleTask();
451 g_Renderer
.RenderFrame(true);
458 static void NonVisualFrame()
460 g_Profiler2
.RecordFrameStart();
462 g_Profiler2
.IncrementFrameNumber();
463 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
469 if (g_Game
&& g_Game
->IsGameStarted() && g_Game
->GetTurnManager())
470 if (g_Game
->GetTurnManager()->Update(DEFAULT_TURN_LENGTH
, 1))
471 debug_printf("Turn %u (%u)...\n", turn
++, DEFAULT_TURN_LENGTH
);
475 if (g_Game
->IsGameFinished())
479 static void MainControllerInit()
481 // add additional input handlers only needed by this controller:
483 // must be registered after gui_handler. Should mayhap even be last.
484 in_add_handler(MainInputHandler
);
487 static void MainControllerShutdown()
492 static void StartRLInterface(CmdLineArgs args
)
494 std::string server_address
;
495 CFG_GET_VAL("rlinterface.address", server_address
);
497 if (!args
.Get("rl-interface").empty())
498 server_address
= args
.Get("rl-interface");
500 g_RLInterface
= std::make_unique
<RL::Interface
>(server_address
.c_str());
501 debug_printf("RL interface listening on %s\n", server_address
.c_str());
504 // moved into a helper function to ensure args is destroyed before
505 // exit(), which may result in a memory leak.
506 static void RunGameOrAtlas(const PS::span
<const char* const> argv
)
508 const CmdLineArgs
args(argv
);
510 g_CmdLineArgs
= args
;
512 if (args
.Has("version"))
514 debug_printf("Pyrogenesis %s\n", engine_version
);
518 if (args
.Has("autostart-nonvisual") && args
.Get("autostart").empty() && !args
.Has("rl-interface") && !args
.Has("autostart-client"))
520 LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
524 if (args
.Has("unique-logs"))
525 g_UniqueLogPostfix
= L
"_" + std::to_wstring(std::time(nullptr)) + L
"_" + std::to_wstring(getpid());
527 const bool isVisualReplay
= args
.Has("replay-visual");
528 const bool isNonVisualReplay
= args
.Has("replay");
529 const bool isNonVisual
= args
.Has("autostart-nonvisual");
530 const bool isUsingRLInterface
= args
.Has("rl-interface");
532 const OsPath
replayFile(
533 isVisualReplay
? args
.Get("replay-visual") :
534 isNonVisualReplay
? args
.Get("replay") : "");
536 if (isVisualReplay
|| isNonVisualReplay
)
538 if (!FileExists(replayFile
))
540 debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile
.string8().c_str());
543 if (DirectoryExists(replayFile
))
545 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile
.string8().c_str());
550 std::vector
<OsPath
> modsToInstall
;
551 for (const CStr
& arg
: args
.GetArgsWithoutName())
553 const OsPath
modPath(arg
);
554 if (!CModInstaller::IsDefaultModExtension(modPath
.Extension()))
556 debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath
.string8().c_str());
559 if (!FileExists(modPath
))
561 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath
.string8().c_str());
564 if (DirectoryExists(modPath
))
566 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath
.string8().c_str());
569 modsToInstall
.emplace_back(std::move(modPath
));
572 // We need to initialize SpiderMonkey and libxml2 in the main thread before
573 // any thread uses them. So initialize them here before we might run Atlas.
574 ScriptEngine scriptEngine
;
575 CXeromyces::Startup();
577 // Initialise the global task manager at this point (JS & Profiler2 are set up).
578 Threading::TaskManager::Initialise();
580 if (ATLAS_RunIfOnCmdLine(args
, false))
582 CXeromyces::Terminate();
586 if (isNonVisualReplay
)
590 // Mount with highest priority, we don't want mods overwriting this.
591 g_VFS
->Mount(L
"cache/", paths
.Cache(), VFS_MOUNT_ARCHIVABLE
, VFS_MAX_PRIORITY
);
594 CReplayPlayer replay
;
595 replay
.Load(replayFile
);
597 args
.Has("serializationtest"),
598 args
.Has("rejointest") ? args
.Get("rejointest").ToInt() : -1,
600 !args
.Has("hashtest-full") || args
.Get("hashtest-full") == "true",
601 args
.Has("hashtest-quick") && args
.Get("hashtest-quick") == "true");
606 CXeromyces::Terminate();
610 // run in archive-building mode if requested
611 if (args
.Has("archivebuild"))
615 OsPath
mod(args
.Get("archivebuild"));
617 if (args
.Has("archivebuild-output"))
618 zip
= args
.Get("archivebuild-output");
620 zip
= mod
.Filename().ChangeExtension(L
".zip");
622 CArchiveBuilder
builder(mod
, paths
.Cache());
624 // Add mods provided on the command line
625 // NOTE: We do not handle mods in the user mod path here
626 std::vector
<CStr
> mods
= args
.GetMultiple("mod");
627 for (size_t i
= 0; i
< mods
.size(); ++i
)
628 builder
.AddBaseMod(paths
.RData()/"mods"/mods
[i
]);
630 builder
.Build(zip
, args
.Has("archivebuild-compress"));
632 CXeromyces::Terminate();
636 const double res
= timer_Resolution();
637 g_frequencyFilter
= CreateFrequencyFilter(res
, 30.0);
640 int flags
= INIT_MODS
;
643 g_Shutdown
= ShutdownType::None
;
645 if (!Init(args
, flags
))
648 Shutdown(SHUTDOWN_FROM_CONFIG
);
652 std::vector
<CStr
> installedMods
;
653 if (!modsToInstall
.empty())
656 CModInstaller
installer(paths
.UserData() / "mods", paths
.Cache());
658 // Install the mods without deleting the pyromod files
659 for (const OsPath
& modPath
: modsToInstall
)
661 CModInstaller::ModInstallationResult result
= installer
.Install(modPath
, g_ScriptContext
, true);
662 if (result
!= CModInstaller::ModInstallationResult::SUCCESS
)
663 LOGERROR("Failed to install '%s'", modPath
.string8().c_str());
666 installedMods
= installer
.GetInstalledMods();
668 ScriptInterface
modInterface("Engine", "Mod", g_ScriptContext
);
669 g_Mods
.UpdateAvailableMods(modInterface
);
674 if (!InitNonVisual(args
))
675 g_Shutdown
= ShutdownType::Quit
;
676 else if (isUsingRLInterface
)
677 StartRLInterface(args
);
679 while (g_Shutdown
== ShutdownType::None
)
681 if (isUsingRLInterface
)
682 g_RLInterface
->TryApplyMessage();
689 InitGraphics(args
, 0, installedMods
);
690 MainControllerInit();
691 if (isUsingRLInterface
)
692 StartRLInterface(args
);
693 while (g_Shutdown
== ShutdownType::None
)
697 // Do not install mods again in case of restart (typically from the mod selector)
698 modsToInstall
.clear();
701 MainControllerShutdown();
704 } while (g_Shutdown
== ShutdownType::Restart
);
707 if (g_Shutdown
== ShutdownType::RestartAsAtlas
)
708 startNewAtlasProcess(g_Mods
.GetEnabledMods());
710 if (g_Shutdown
== ShutdownType::RestartAsAtlas
)
711 ATLAS_RunIfOnCmdLine(args
, true);
714 Threading::TaskManager::Instance().ClearQueue();
715 CXeromyces::Terminate();
719 // In Android we compile the engine as a shared library, not an executable,
720 // so rename main() to a different symbol that the wrapper library can load
722 #define main pyrogenesis_main
723 extern "C" __attribute__((visibility ("default"))) int main(int argc
, char* argv
[]);
726 extern "C" int main(int argc
, char* argv
[])
729 // Don't allow people to run the game with root permissions,
730 // because bad things can happen, check before we do anything
733 std::cerr
<< "********************************************************\n"
734 << "WARNING: Attempted to run the game with root permission!\n"
735 << "This is not allowed because it can alter home directory \n"
736 << "permissions and opens your system to vulnerabilities. \n"
737 << "(You received this message because you were either \n"
738 <<" logged in as root or used e.g. the 'sudo' command.) \n"
739 << "********************************************************\n\n";
749 EarlyInit(); // must come at beginning of main
751 // static_cast is ok, argc is never negative.
752 RunGameOrAtlas({argv
, static_cast<std::size_t>(argc
)});
754 // Shut down profiler initialised by EarlyInit
755 g_Profiler2
.Shutdown();
758 // All calls to Windows specific functions have to happen before the following
760 wdir_watch_Shutdown();