Merge 'remotes/trunk'
[0ad.git] / source / main.cpp
blobb68c64b8d71beafce58728bed68f0c81661866bc
1 /* Copyright (C) 2024 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 // Forward declarations to avoid including Windows dependent headers.
104 Status waio_Shutdown();
105 Status wdir_watch_Init();
106 Status wdir_watch_Shutdown();
107 Status wutil_Init();
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.
112 #ifndef DWORD
113 typedef unsigned long DWORD;
114 #endif // !DWORD
115 // Request the high performance GPU on Windows by default if no system override is specified.
116 // See:
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/
120 extern "C"
122 __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
123 __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 0x00000001;
125 #endif
127 #include <chrono>
129 extern CStrW g_UniqueLogPostfix;
131 // Determines the lifetime of the mainloop
132 enum ShutdownType
134 // The application shall continue the main loop.
135 None,
137 // The process shall terminate as soon as possible.
138 Quit,
140 // The engine should be restarted in the same process, for instance to activate different mods.
141 Restart,
143 // Atlas should be started in the same process.
144 RestartAsAtlas
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
151 // once per frame.
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;
164 void QuitEngine()
166 g_Shutdown = ShutdownType::Quit;
169 void RestartEngine()
171 g_Shutdown = ShutdownType::Restart;
174 void StartAtlas()
176 g_Shutdown = ShutdownType::RestartAsAtlas;
179 // main app message handler
180 static InReaction MainInputHandler(const SDL_Event_* ev)
182 switch(ev->ev.type)
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;
190 break;
191 case SDL_WINDOWEVENT_MOVED:
192 g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
194 break;
196 case SDL_QUIT:
197 QuitEngine();
198 break;
200 case SDL_DROPFILE:
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);
209 else
211 LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front());
212 ScriptInterface modInterface("Engine", "Mod", g_ScriptContext);
213 g_Mods.UpdateAvailableMods(modInterface);
214 RestartEngine();
216 break;
219 case SDL_HOTKEYPRESS:
220 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
221 if (hotkey == "exit")
223 QuitEngine();
224 return IN_HANDLED;
226 else if (hotkey == "screenshot")
228 g_Renderer.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::DEFAULT);
229 return IN_HANDLED;
231 else if (hotkey == "bigscreenshot")
233 g_Renderer.MakeScreenShotOnNextFrame(CRenderer::ScreenShotType::BIG);
234 return IN_HANDLED;
236 else if (hotkey == "togglefullscreen")
238 g_VideoMode.ToggleFullscreen();
239 return IN_HANDLED;
241 else if (hotkey == "profile2.toggle")
243 g_Profiler2.Toggle();
244 return IN_HANDLED;
246 break;
249 return IN_PASS;
253 // dispatch all pending events to the various receivers.
254 static void PumpEvents()
256 ScriptRequest rq(g_GUI->GetScriptInterface());
258 PROFILE3("dispatch events");
260 SDL_Event_ ev;
261 while (in_poll_event(&ev))
263 PROFILE2("event");
264 if (g_GUI)
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())
284 return;
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)
291 return;
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;
297 if (wait > 0.0)
298 SDL_Delay(wait);
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);
312 switch(ret)
314 // no load active => no-op (skip code below)
315 case INFO::OK:
316 return 0;
317 // current task didn't complete. we only care about this insofar as the
318 // load process is therefore not yet finished.
319 case ERR::TIMED_OUT:
320 break;
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
327 break;
328 // error!
329 default:
330 WARN_RETURN_STATUS_IF_ERR(ret);
331 // can't do this above due to legit ERR::TIMED_OUT
332 break;
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);
345 return 0;
349 static void RendererIncrementalLoad()
351 PROFILE3("renderer incremental load");
353 const double maxTime = 0.1f;
355 double startTime = timer_Time();
356 bool more;
357 do {
358 more = g_Renderer.GetTextureManager().MakeProgress();
360 while (more && timer_Time() - startTime < maxTime);
363 static void Frame(RL::Interface* rlInterface)
365 g_Profiler2.RecordFrameStart();
366 PROFILE2("frame");
367 g_Profiler2.IncrementFrameNumber();
368 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
370 // get elapsed time
371 const double time = timer_Time();
372 g_frequencyFilter->Update(time);
373 // .. old method - "exact" but contains jumps
374 #if 0
375 static double last_time;
376 const double time = timer_Time();
377 const float TimeSinceLastFrame = (float)(time-last_time);
378 last_time = time;
379 ONCE(return); // first call: set last_time and return
381 // .. new method - filtered and more smooth, but errors may accumulate
382 #else
383 const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
384 #endif
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
392 // debugging easier.
393 if (!needUpdate)
395 PROFILE3("non-focus delay");
396 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
397 SDL_Delay(10);
400 // this scans for changed files/directories and reloads them, thus
401 // allowing hotloading (changes are immediately assimilated in-game).
402 ReloadChangedFiles();
404 ProgressiveLoad();
406 RendererIncrementalLoad();
408 PumpEvents();
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
412 // before rendering
413 if (g_Shutdown != ShutdownType::None)
414 return;
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;
423 if (g_NetClient)
424 g_NetClient->Poll();
426 g_GUI->TickObjects();
428 if (rlInterface)
429 rlInterface->TryApplyMessage();
431 if (g_Game && g_Game->IsGameStarted() && needUpdate)
433 if (!rlInterface)
434 g_Game->Update(realTimeSinceLastFrame);
436 g_Game->GetView()->Update(float(realTimeSinceLastFrame));
439 // Keep us connected to any XMPP servers
440 if (g_XmppClient)
441 g_XmppClient->recv();
443 g_UserReporter.Update();
445 g_Console->Update(realTimeSinceLastFrame);
447 if (g_SoundManager)
448 g_SoundManager->IdleTask();
450 g_Renderer.RenderFrame(true);
452 g_Profiler.Frame();
454 LimitFPS();
457 static void NonVisualFrame()
459 g_Profiler2.RecordFrameStart();
460 PROFILE2("frame");
461 g_Profiler2.IncrementFrameNumber();
462 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
464 if (g_NetClient)
465 g_NetClient->Poll();
467 static u32 turn = 0;
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);
472 g_Profiler.Frame();
474 if (g_Game->IsGameFinished())
475 QuitEngine();
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()
488 in_reset_handlers();
491 static std::optional<RL::Interface> CreateRLInterface(const CmdLineArgs& args)
493 if (!args.Has("rl-interface"))
494 return std::nullopt;
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);
517 return;
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.");
523 return;
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());
542 return;
544 if (DirectoryExists(replayFile))
546 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
547 return;
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());
558 continue;
560 if (!FileExists(modPath))
562 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
563 continue;
565 if (DirectoryExists(modPath))
567 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
568 continue;
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();
584 return;
587 if (isNonVisualReplay)
589 Paths paths(args);
590 g_VFS = CreateVfs();
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);
597 replay.Replay(
598 args.Has("serializationtest"),
599 args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
600 args.Has("ooslog"),
601 !args.Has("hashtest-full") || args.Get("hashtest-full") == "true",
602 args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true");
605 g_VFS.reset();
607 CXeromyces::Terminate();
608 return;
611 // run in archive-building mode if requested
612 if (args.Has("archivebuild"))
614 Paths paths(args);
616 OsPath mod(args.Get("archivebuild"));
617 OsPath zip;
618 if (args.Has("archivebuild-output"))
619 zip = args.Get("archivebuild-output");
620 else
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();
634 return;
637 const double res = timer_Resolution();
638 g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
640 // run the game
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.
648 InitVfs(args);
650 // This must come after VFS init, which sets the current directory (required for finding our
651 // output log files).
652 FileLogger logger;
654 if (!Init(args, flags))
656 flags &= ~INIT_MODS;
657 ShutdownConfigAndSubsequent();
658 continue;
661 std::vector<CStr> installedMods;
662 if (!modsToInstall.empty())
664 Paths paths(args);
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);
681 if (isVisual)
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);
694 else
695 return std::nullopt;
696 }()};
698 while (g_Shutdown == ShutdownType::None)
700 if (isVisual)
701 Frame(rlInterface ? &*rlInterface : nullptr);
702 else if(rlInterface)
703 rlInterface->TryApplyMessage();
704 else
705 NonVisualFrame();
708 // Do not install mods again in case of restart (typically from the mod selector)
709 modsToInstall.clear();
711 ShutdownNetworkAndUI();
712 ShutdownConfigAndSubsequent();
713 MainControllerShutdown();
714 flags &= ~INIT_MODS;
716 } while (g_Shutdown == ShutdownType::Restart);
718 #if OS_MACOSX
719 if (g_Shutdown == ShutdownType::RestartAsAtlas)
720 startNewAtlasProcess(g_Mods.GetEnabledMods());
721 #else
722 if (g_Shutdown == ShutdownType::RestartAsAtlas)
723 ATLAS_RunIfOnCmdLine(args, true);
724 #endif
726 Threading::TaskManager::Instance().ClearQueue();
727 CXeromyces::Terminate();
730 #if OS_ANDROID
731 // In Android we compile the engine as a shared library, not an executable,
732 // so rename main() to a different symbol that the wrapper library can load
733 #undef main
734 #define main pyrogenesis_main
735 extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
736 #endif
738 extern "C" int main(int argc, char* argv[])
740 #if OS_UNIX
741 // Don't allow people to run the game with root permissions,
742 // because bad things can happen, check before we do anything
743 if (geteuid() == 0)
745 std::cerr << "********************************************************\n"
746 << "WARNING: Attempted to run the game with root permission!\n"
747 << "This is not allowed because it can alter home directory \n"
748 << "permissions and opens your system to vulnerabilities. \n"
749 << "(You received this message because you were either \n"
750 <<" logged in as root or used e.g. the 'sudo' command.) \n"
751 << "********************************************************\n\n";
752 return EXIT_FAILURE;
754 #endif // OS_UNIX
756 #if OS_WIN
757 wutil_Init();
758 wdir_watch_Init();
759 #endif
761 EarlyInit(); // must come at beginning of main
763 // static_cast is ok, argc is never negative.
764 RunGameOrAtlas({argv, static_cast<std::size_t>(argc)});
766 // Shut down profiler initialised by EarlyInit
767 g_Profiler2.Shutdown();
769 #if OS_WIN
770 // All calls to Windows specific functions have to happen before the following
771 // shutdowns.
772 wdir_watch_Shutdown();
773 waio_Shutdown();
774 wutil_Shutdown();
775 #endif
777 return EXIT_SUCCESS;