Merge 'remotes/trunk'
[0ad.git] / source / main.cpp
blobccde1a24c798c500c800120e622b39519dc47122
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
23 simulation.
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
30 // included there.
31 #define MINIMAL_PCH 2
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"
47 #include "ps/Game.h"
48 #include "ps/Globals.h"
49 #include "ps/Hotkey.h"
50 #include "ps/Loader.h"
51 #include "ps/Mod.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"
59 #include "ps/Util.h"
60 #include "ps/VideoMode.h"
61 #include "ps/TaskManager.h"
62 #include "ps/World.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"
88 #if OS_UNIX
89 #include <iostream>
90 #include <unistd.h> // geteuid
91 #endif // OS_UNIX
93 #if OS_MACOSX
94 #include "lib/sysdep/os/osx/osx_atlas.h"
95 #endif
97 #if MSC_VERSION
98 #include <process.h>
99 #define getpid _getpid // Use the non-deprecated function name
100 #endif
102 #if OS_WIN
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.
105 #ifndef DWORD
106 typedef unsigned long DWORD;
107 #endif // !DWORD
108 // Request the high performance GPU on Windows by default if no system override is specified.
109 // See:
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/
113 extern "C"
115 __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
116 __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x00000001;
118 #endif
120 #include <chrono>
122 extern CStrW g_UniqueLogPostfix;
124 // Determines the lifetime of the mainloop
125 enum ShutdownType
127 // The application shall continue the main loop.
128 None,
130 // The process shall terminate as soon as possible.
131 Quit,
133 // The engine should be restarted in the same process, for instance to activate different mods.
134 Restart,
136 // Atlas should be started in the same process.
137 RestartAsAtlas
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
144 // once per frame.
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;
157 void QuitEngine()
159 g_Shutdown = ShutdownType::Quit;
162 void RestartEngine()
164 g_Shutdown = ShutdownType::Restart;
167 void StartAtlas()
169 g_Shutdown = ShutdownType::RestartAsAtlas;
172 // main app message handler
173 static InReaction MainInputHandler(const SDL_Event_* ev)
175 switch(ev->ev.type)
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;
183 break;
184 case SDL_WINDOWEVENT_MOVED:
185 g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
187 break;
189 case SDL_QUIT:
190 QuitEngine();
191 break;
193 case SDL_DROPFILE:
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);
202 else
204 LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front());
205 ScriptInterface modInterface("Engine", "Mod", g_ScriptContext);
206 g_Mods.UpdateAvailableMods(modInterface);
207 RestartEngine();
209 break;
212 case SDL_HOTKEYPRESS:
213 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
214 if (hotkey == "exit")
216 QuitEngine();
217 return IN_HANDLED;
219 else if (hotkey == "screenshot")
221 g_Renderer.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::DEFAULT);
222 return IN_HANDLED;
224 else if (hotkey == "bigscreenshot")
226 g_Renderer.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::BIG);
227 return IN_HANDLED;
229 else if (hotkey == "togglefullscreen")
231 g_VideoMode.ToggleFullscreen();
232 return IN_HANDLED;
234 else if (hotkey == "profile2.toggle")
236 g_Profiler2.Toggle();
237 return IN_HANDLED;
239 break;
242 return IN_PASS;
246 // dispatch all pending events to the various receivers.
247 static void PumpEvents()
249 ScriptRequest rq(g_GUI->GetScriptInterface());
251 PROFILE3("dispatch events");
253 SDL_Event_ ev;
254 while (in_poll_event(&ev))
256 PROFILE2("event");
257 if (g_GUI)
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())
277 return;
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)
284 return;
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;
290 if (wait > 0.0)
291 SDL_Delay(wait);
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);
305 switch(ret)
307 // no load active => no-op (skip code below)
308 case INFO::OK:
309 return 0;
310 // current task didn't complete. we only care about this insofar as the
311 // load process is therefore not yet finished.
312 case ERR::TIMED_OUT:
313 break;
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
320 break;
321 // error!
322 default:
323 WARN_RETURN_STATUS_IF_ERR(ret);
324 // can't do this above due to legit ERR::TIMED_OUT
325 break;
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);
338 return 0;
342 static void RendererIncrementalLoad()
344 PROFILE3("renderer incremental load");
346 const double maxTime = 0.1f;
348 double startTime = timer_Time();
349 bool more;
350 do {
351 more = g_Renderer.GetTextureManager().MakeProgress();
353 while (more && timer_Time() - startTime < maxTime);
356 static void Frame()
358 g_Profiler2.RecordFrameStart();
359 PROFILE2("frame");
360 g_Profiler2.IncrementFrameNumber();
361 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
363 // get elapsed time
364 const double time = timer_Time();
365 g_frequencyFilter->Update(time);
366 // .. old method - "exact" but contains jumps
367 #if 0
368 static double last_time;
369 const double time = timer_Time();
370 const float TimeSinceLastFrame = (float)(time-last_time);
371 last_time = time;
372 ONCE(return); // first call: set last_time and return
374 // .. new method - filtered and more smooth, but errors may accumulate
375 #else
376 const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
377 #endif
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
385 // debugging easier.
386 if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
388 PROFILE3("non-focus delay");
389 need_update = false;
390 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
391 SDL_Delay(10);
394 // this scans for changed files/directories and reloads them, thus
395 // allowing hotloading (changes are immediately assimilated in-game).
396 ReloadChangedFiles();
398 ProgressiveLoad();
400 RendererIncrementalLoad();
402 PumpEvents();
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
406 // before rendering
407 if (g_Shutdown != ShutdownType::None)
408 return;
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;
417 if (g_NetClient)
418 g_NetClient->Poll();
420 g_GUI->TickObjects();
422 if (g_RLInterface)
423 g_RLInterface->TryApplyMessage();
425 if (g_Game && g_Game->IsGameStarted() && need_update)
427 if (!g_RLInterface)
428 g_Game->Update(realTimeSinceLastFrame);
430 g_Game->GetView()->Update(float(realTimeSinceLastFrame));
433 // Keep us connected to any XMPP servers
434 if (g_XmppClient)
435 g_XmppClient->recv();
437 g_UserReporter.Update();
439 g_Console->Update(realTimeSinceLastFrame);
441 if (g_SoundManager)
442 g_SoundManager->IdleTask();
444 g_Renderer.RenderFrame(true);
446 g_Profiler.Frame();
448 LimitFPS();
451 static void NonVisualFrame()
453 g_Profiler2.RecordFrameStart();
454 PROFILE2("frame");
455 g_Profiler2.IncrementFrameNumber();
456 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
458 if (g_NetClient)
459 g_NetClient->Poll();
461 static u32 turn = 0;
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);
466 g_Profiler.Frame();
468 if (g_Game->IsGameFinished())
469 QuitEngine();
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()
482 in_reset_handlers();
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);
508 return;
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.");
514 return;
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());
534 return;
536 if (DirectoryExists(replayFile))
538 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
539 return;
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());
550 continue;
552 if (!FileExists(modPath))
554 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
555 continue;
557 if (DirectoryExists(modPath))
559 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
560 continue;
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();
576 return;
579 if (isNonVisualReplay)
581 Paths paths(args);
582 g_VFS = CreateVfs();
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);
589 replay.Replay(
590 args.Has("serializationtest"),
591 args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
592 args.Has("ooslog"),
593 !args.Has("hashtest-full") || args.Get("hashtest-full") == "true",
594 args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true");
597 g_VFS.reset();
599 CXeromyces::Terminate();
600 return;
603 // run in archive-building mode if requested
604 if (args.Has("archivebuild"))
606 Paths paths(args);
608 OsPath mod(args.Get("archivebuild"));
609 OsPath zip;
610 if (args.Has("archivebuild-output"))
611 zip = args.Get("archivebuild-output");
612 else
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();
626 return;
629 const double res = timer_Resolution();
630 g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
632 // run the game
633 int flags = INIT_MODS;
636 g_Shutdown = ShutdownType::None;
638 if (!Init(args, flags))
640 flags &= ~INIT_MODS;
641 Shutdown(SHUTDOWN_FROM_CONFIG);
642 continue;
645 std::vector<CStr> installedMods;
646 if (!modsToInstall.empty())
648 Paths paths(args);
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);
665 if (isNonVisual)
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();
676 else
677 NonVisualFrame();
680 else
682 InitGraphics(args, 0, installedMods);
683 MainControllerInit();
684 if (isUsingRLInterface)
685 StartRLInterface(args);
686 while (g_Shutdown == ShutdownType::None)
687 Frame();
690 // Do not install mods again in case of restart (typically from the mod selector)
691 modsToInstall.clear();
693 Shutdown(0);
694 MainControllerShutdown();
695 flags &= ~INIT_MODS;
697 } while (g_Shutdown == ShutdownType::Restart);
699 #if OS_MACOSX
700 if (g_Shutdown == ShutdownType::RestartAsAtlas)
701 startNewAtlasProcess(g_Mods.GetEnabledMods());
702 #else
703 if (g_Shutdown == ShutdownType::RestartAsAtlas)
704 ATLAS_RunIfOnCmdLine(args, true);
705 #endif
707 Threading::TaskManager::Instance().ClearQueue();
708 CXeromyces::Terminate();
711 #if OS_ANDROID
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
714 #undef main
715 #define main pyrogenesis_main
716 extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
717 #endif
719 extern "C" int main(int argc, char* argv[])
721 #if OS_UNIX
722 // Don't allow people to run the game with root permissions,
723 // because bad things can happen, check before we do anything
724 if (geteuid() == 0)
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";
733 return EXIT_FAILURE;
735 #endif // OS_UNIX
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();
745 return EXIT_SUCCESS;