[Gameplay] Reduce loom cost
[0ad.git] / source / main.cpp
blob98f770737e5f11342945e3f7a1b9bb930ed00020
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
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()
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 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
392 // debugging easier.
393 if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
395 PROFILE3("non-focus delay");
396 need_update = false;
397 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
398 SDL_Delay(10);
401 // this scans for changed files/directories and reloads them, thus
402 // allowing hotloading (changes are immediately assimilated in-game).
403 ReloadChangedFiles();
405 ProgressiveLoad();
407 RendererIncrementalLoad();
409 PumpEvents();
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
413 // before rendering
414 if (g_Shutdown != ShutdownType::None)
415 return;
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;
424 if (g_NetClient)
425 g_NetClient->Poll();
427 g_GUI->TickObjects();
429 if (g_RLInterface)
430 g_RLInterface->TryApplyMessage();
432 if (g_Game && g_Game->IsGameStarted() && need_update)
434 if (!g_RLInterface)
435 g_Game->Update(realTimeSinceLastFrame);
437 g_Game->GetView()->Update(float(realTimeSinceLastFrame));
440 // Keep us connected to any XMPP servers
441 if (g_XmppClient)
442 g_XmppClient->recv();
444 g_UserReporter.Update();
446 g_Console->Update(realTimeSinceLastFrame);
448 if (g_SoundManager)
449 g_SoundManager->IdleTask();
451 g_Renderer.RenderFrame(true);
453 g_Profiler.Frame();
455 LimitFPS();
458 static void NonVisualFrame()
460 g_Profiler2.RecordFrameStart();
461 PROFILE2("frame");
462 g_Profiler2.IncrementFrameNumber();
463 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
465 if (g_NetClient)
466 g_NetClient->Poll();
468 static u32 turn = 0;
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);
473 g_Profiler.Frame();
475 if (g_Game->IsGameFinished())
476 QuitEngine();
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()
489 in_reset_handlers();
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);
515 return;
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.");
521 return;
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());
541 return;
543 if (DirectoryExists(replayFile))
545 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
546 return;
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());
557 continue;
559 if (!FileExists(modPath))
561 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
562 continue;
564 if (DirectoryExists(modPath))
566 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
567 continue;
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();
583 return;
586 if (isNonVisualReplay)
588 Paths paths(args);
589 g_VFS = CreateVfs();
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);
596 replay.Replay(
597 args.Has("serializationtest"),
598 args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
599 args.Has("ooslog"),
600 !args.Has("hashtest-full") || args.Get("hashtest-full") == "true",
601 args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true");
604 g_VFS.reset();
606 CXeromyces::Terminate();
607 return;
610 // run in archive-building mode if requested
611 if (args.Has("archivebuild"))
613 Paths paths(args);
615 OsPath mod(args.Get("archivebuild"));
616 OsPath zip;
617 if (args.Has("archivebuild-output"))
618 zip = args.Get("archivebuild-output");
619 else
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();
633 return;
636 const double res = timer_Resolution();
637 g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
639 // run the game
640 int flags = INIT_MODS;
643 g_Shutdown = ShutdownType::None;
645 if (!Init(args, flags))
647 flags &= ~INIT_MODS;
648 Shutdown(SHUTDOWN_FROM_CONFIG);
649 continue;
652 std::vector<CStr> installedMods;
653 if (!modsToInstall.empty())
655 Paths paths(args);
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);
672 if (isNonVisual)
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();
683 else
684 NonVisualFrame();
687 else
689 InitGraphics(args, 0, installedMods);
690 MainControllerInit();
691 if (isUsingRLInterface)
692 StartRLInterface(args);
693 while (g_Shutdown == ShutdownType::None)
694 Frame();
697 // Do not install mods again in case of restart (typically from the mod selector)
698 modsToInstall.clear();
700 Shutdown(0);
701 MainControllerShutdown();
702 flags &= ~INIT_MODS;
704 } while (g_Shutdown == ShutdownType::Restart);
706 #if OS_MACOSX
707 if (g_Shutdown == ShutdownType::RestartAsAtlas)
708 startNewAtlasProcess(g_Mods.GetEnabledMods());
709 #else
710 if (g_Shutdown == ShutdownType::RestartAsAtlas)
711 ATLAS_RunIfOnCmdLine(args, true);
712 #endif
714 Threading::TaskManager::Instance().ClearQueue();
715 CXeromyces::Terminate();
718 #if OS_ANDROID
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
721 #undef main
722 #define main pyrogenesis_main
723 extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
724 #endif
726 extern "C" int main(int argc, char* argv[])
728 #if OS_UNIX
729 // Don't allow people to run the game with root permissions,
730 // because bad things can happen, check before we do anything
731 if (geteuid() == 0)
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";
740 return EXIT_FAILURE;
742 #endif // OS_UNIX
744 #if OS_WIN
745 wutil_Init();
746 wdir_watch_Init();
747 #endif
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();
757 #if OS_WIN
758 // All calls to Windows specific functions have to happen before the following
759 // shutdowns.
760 wdir_watch_Shutdown();
761 waio_Shutdown();
762 wutil_Shutdown();
763 #endif
765 return EXIT_SUCCESS;