In preparation of renaming and grouping main.cpp shutdown variables:
[0ad.git] / source / main.cpp
blobb7f2e6db53da79518ee35448bc2a3f82691947a5
1 /* Copyright (C) 2018 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 <chrono>
36 #include "lib/debug.h"
37 #include "lib/status.h"
38 #include "lib/secure_crt.h"
39 #include "lib/frequency_filter.h"
40 #include "lib/input.h"
41 #include "lib/ogl.h"
42 #include "lib/timer.h"
43 #include "lib/external_libraries/libsdl.h"
45 #include "ps/ArchiveBuilder.h"
46 #include "ps/CConsole.h"
47 #include "ps/CLogger.h"
48 #include "ps/ConfigDB.h"
49 #include "ps/Filesystem.h"
50 #include "ps/Game.h"
51 #include "ps/Globals.h"
52 #include "ps/Hotkey.h"
53 #include "ps/Loader.h"
54 #include "ps/ModInstaller.h"
55 #include "ps/Profile.h"
56 #include "ps/Profiler2.h"
57 #include "ps/Pyrogenesis.h"
58 #include "ps/Replay.h"
59 #include "ps/TouchInput.h"
60 #include "ps/UserReport.h"
61 #include "ps/Util.h"
62 #include "ps/VideoMode.h"
63 #include "ps/World.h"
64 #include "ps/GameSetup/GameSetup.h"
65 #include "ps/GameSetup/Atlas.h"
66 #include "ps/GameSetup/Config.h"
67 #include "ps/GameSetup/CmdLineArgs.h"
68 #include "ps/GameSetup/Paths.h"
69 #include "ps/XML/Xeromyces.h"
70 #include "network/NetClient.h"
71 #include "network/NetServer.h"
72 #include "network/NetSession.h"
73 #include "lobby/IXmppClient.h"
74 #include "graphics/Camera.h"
75 #include "graphics/GameView.h"
76 #include "graphics/TextureManager.h"
77 #include "gui/GUIManager.h"
78 #include "renderer/Renderer.h"
79 #include "scriptinterface/ScriptEngine.h"
80 #include "simulation2/Simulation2.h"
81 #include "simulation2/system/TurnManager.h"
83 #if OS_UNIX
84 #include <unistd.h> // geteuid
85 #endif // OS_UNIX
87 #if MSC_VERSION
88 #include <process.h>
89 #define getpid _getpid // Use the non-deprecated function name
90 #endif
92 extern CStrW g_UniqueLogPostfix;
94 void kill_mainloop();
96 // Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?)
97 bool g_GameRestarted = false;
99 // to avoid redundant and/or recursive resizing, we save the new
100 // size after VIDEORESIZE messages and only update the video mode
101 // once per frame.
102 // these values are the latest resize message, and reset to 0 once we've
103 // updated the video mode
104 static int g_ResizedW;
105 static int g_ResizedH;
107 static std::chrono::high_resolution_clock::time_point lastFrameTime;
109 // main app message handler
110 static InReaction MainInputHandler(const SDL_Event_* ev)
112 switch(ev->ev.type)
114 case SDL_WINDOWEVENT:
115 switch(ev->ev.window.event)
117 case SDL_WINDOWEVENT_ENTER:
118 RenderCursor(true);
119 break;
120 case SDL_WINDOWEVENT_LEAVE:
121 RenderCursor(false);
122 break;
123 case SDL_WINDOWEVENT_RESIZED:
124 g_ResizedW = ev->ev.window.data1;
125 g_ResizedH = ev->ev.window.data2;
126 break;
127 case SDL_WINDOWEVENT_MOVED:
128 g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
130 break;
132 case SDL_QUIT:
133 kill_mainloop();
134 break;
136 case SDL_HOTKEYDOWN:
137 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
138 if (hotkey == "exit")
140 kill_mainloop();
141 return IN_HANDLED;
143 else if (hotkey == "screenshot")
145 WriteScreenshot(L".png");
146 return IN_HANDLED;
148 else if (hotkey == "bigscreenshot")
150 WriteBigScreenshot(L".bmp", 10);
151 return IN_HANDLED;
153 else if (hotkey == "togglefullscreen")
155 g_VideoMode.ToggleFullscreen();
156 return IN_HANDLED;
158 else if (hotkey == "profile2.toggle")
160 g_Profiler2.Toggle();
161 return IN_HANDLED;
163 break;
166 return IN_PASS;
170 // dispatch all pending events to the various receivers.
171 static void PumpEvents()
173 JSContext* cx = g_GUI->GetScriptInterface()->GetContext();
174 JSAutoRequest rq(cx);
176 PROFILE3("dispatch events");
178 SDL_Event_ ev;
179 while (in_poll_event(&ev))
181 PROFILE2("event");
182 if (g_GUI)
184 JS::RootedValue tmpVal(cx);
185 ScriptInterface::ToJSVal(cx, &tmpVal, ev);
186 std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal);
187 PROFILE2_ATTR("%s", data.c_str());
189 in_dispatch_event(&ev);
192 g_TouchInput.Frame();
196 * Optionally throttle the render frequency in order to
197 * prevent 100% workload of the currently used CPU core.
199 inline static void LimitFPS()
201 if (g_VSync)
202 return;
204 double fpsLimit = 0.0;
205 CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit);
207 // Keep in sync with options.json
208 if (fpsLimit < 20.0 || fpsLimit >= 100.0)
209 return;
211 double wait = 1000.0 / fpsLimit -
212 std::chrono::duration_cast<std::chrono::microseconds>(
213 std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0;
215 if (wait > 0.0)
216 SDL_Delay(wait);
218 lastFrameTime = std::chrono::high_resolution_clock::now();
221 static int ProgressiveLoad()
223 PROFILE3("progressive load");
225 wchar_t description[100];
226 int progress_percent;
229 Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent);
230 switch(ret)
232 // no load active => no-op (skip code below)
233 case INFO::OK:
234 return 0;
235 // current task didn't complete. we only care about this insofar as the
236 // load process is therefore not yet finished.
237 case ERR::TIMED_OUT:
238 break;
239 // just finished loading
240 case INFO::ALL_COMPLETE:
241 g_Game->ReallyStartGame();
242 wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting..");
243 // LDR_ProgressiveLoad returns L""; set to valid text to
244 // avoid problems in converting to JSString
245 break;
246 // error!
247 default:
248 WARN_RETURN_STATUS_IF_ERR(ret);
249 // can't do this above due to legit ERR::TIMED_OUT
250 break;
253 catch (PSERROR_Game_World_MapLoadFailed& e)
255 // Map loading failed
257 // Call script function to do the actual work
258 // (delete game data, switch GUI page, show error, etc.)
259 CancelLoad(CStr(e.what()).FromUTF8());
262 GUI_DisplayLoadProgress(progress_percent, description);
263 return 0;
267 static void RendererIncrementalLoad()
269 PROFILE3("renderer incremental load");
271 const double maxTime = 0.1f;
273 double startTime = timer_Time();
274 bool more;
275 do {
276 more = g_Renderer.GetTextureManager().MakeProgress();
278 while (more && timer_Time() - startTime < maxTime);
282 static bool quit = false; // break out of main loop
284 static void Frame()
286 g_Profiler2.RecordFrameStart();
287 PROFILE2("frame");
288 g_Profiler2.IncrementFrameNumber();
289 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
291 ogl_WarnIfError();
293 // get elapsed time
294 const double time = timer_Time();
295 g_frequencyFilter->Update(time);
296 // .. old method - "exact" but contains jumps
297 #if 0
298 static double last_time;
299 const double time = timer_Time();
300 const float TimeSinceLastFrame = (float)(time-last_time);
301 last_time = time;
302 ONCE(return); // first call: set last_time and return
304 // .. new method - filtered and more smooth, but errors may accumulate
305 #else
306 const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
307 #endif
308 ENSURE(realTimeSinceLastFrame > 0.0f);
310 // Decide if update is necessary
311 bool need_update = true;
313 // If we are not running a multiplayer game, disable updates when the game is
314 // minimized or out of focus and relinquish the CPU a bit, in order to make
315 // debugging easier.
316 if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
318 PROFILE3("non-focus delay");
319 need_update = false;
320 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
321 SDL_Delay(10);
324 // this scans for changed files/directories and reloads them, thus
325 // allowing hotloading (changes are immediately assimilated in-game).
326 ReloadChangedFiles();
328 ProgressiveLoad();
330 RendererIncrementalLoad();
332 PumpEvents();
334 // if the user quit by closing the window, the GL context will be broken and
335 // may crash when we call Render() on some drivers, so leave this loop
336 // before rendering
337 if (quit)
338 return;
340 // respond to pumped resize events
341 if (g_ResizedW || g_ResizedH)
343 g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH);
344 g_ResizedW = g_ResizedH = 0;
347 if (g_NetClient)
348 g_NetClient->Poll();
350 ogl_WarnIfError();
352 g_GUI->TickObjects();
354 ogl_WarnIfError();
356 if (g_Game && g_Game->IsGameStarted() && need_update)
358 g_Game->Update(realTimeSinceLastFrame);
360 g_Game->GetView()->Update(float(realTimeSinceLastFrame));
363 // Immediately flush any messages produced by simulation code
364 if (g_NetClient)
365 g_NetClient->Flush();
367 // Keep us connected to any XMPP servers
368 if (g_XmppClient)
369 g_XmppClient->recv();
371 g_UserReporter.Update();
373 g_Console->Update(realTimeSinceLastFrame);
374 ogl_WarnIfError();
376 // We do not have to render an inactive fullscreen frame, because it can
377 // lead to errors for some graphic card families.
378 if (!g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()))
380 Render();
382 PROFILE3("swap buffers");
383 SDL_GL_SwapWindow(g_VideoMode.GetWindow());
385 ogl_WarnIfError();
387 g_Profiler.Frame();
389 g_GameRestarted = false;
391 LimitFPS();
394 static void NonVisualFrame()
396 g_Profiler2.RecordFrameStart();
397 PROFILE2("frame");
398 g_Profiler2.IncrementFrameNumber();
399 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
401 static u32 turn = 0;
402 debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP);
404 g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP);
406 g_Profiler.Frame();
408 if (g_Game->IsGameFinished())
409 kill_mainloop();
413 static void MainControllerInit()
415 // add additional input handlers only needed by this controller:
417 // must be registered after gui_handler. Should mayhap even be last.
418 in_add_handler(MainInputHandler);
423 static void MainControllerShutdown()
425 in_reset_handlers();
429 // stop the main loop and trigger orderly shutdown. called from several
430 // places: the event handler (SDL_QUIT and hotkey) and JS exitProgram.
431 void kill_mainloop()
433 quit = true;
437 static bool restart_in_atlas = false;
438 // called by game code to indicate main() should restart in Atlas mode
439 // instead of terminating
440 void restart_mainloop_in_atlas()
442 quit = true;
443 restart_in_atlas = true;
446 static bool restart = false;
447 // trigger an orderly shutdown and restart the game.
448 void restart_engine()
450 quit = true;
451 restart = true;
454 extern CmdLineArgs g_args;
456 // moved into a helper function to ensure args is destroyed before
457 // exit(), which may result in a memory leak.
458 static void RunGameOrAtlas(int argc, const char* argv[])
460 CmdLineArgs args(argc, argv);
462 g_args = args;
464 if (args.Has("version"))
466 debug_printf("Pyrogenesis %s\n", engine_version);
467 return;
470 if (args.Has("autostart-nonvisual") && args.Get("autostart").empty())
472 LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
473 return;
476 if (args.Has("unique-logs"))
477 g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid());
479 const bool isVisualReplay = args.Has("replay-visual");
480 const bool isNonVisualReplay = args.Has("replay");
481 const bool isNonVisual = args.Has("autostart-nonvisual");
483 const OsPath replayFile(
484 isVisualReplay ? args.Get("replay-visual") :
485 isNonVisualReplay ? args.Get("replay") : "");
487 if (isVisualReplay || isNonVisualReplay)
489 if (!FileExists(replayFile))
491 debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str());
492 return;
494 if (DirectoryExists(replayFile))
496 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
497 return;
501 std::vector<OsPath> modsToInstall;
502 for (const CStr& arg : args.GetArgsWithoutName())
504 const OsPath modPath(arg);
505 if (!CModInstaller::IsDefaultModExtension(modPath.Extension()))
507 debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str());
508 continue;
510 if (!FileExists(modPath))
512 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
513 continue;
515 if (DirectoryExists(modPath))
517 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
518 continue;
520 modsToInstall.emplace_back(std::move(modPath));
523 // We need to initialize SpiderMonkey and libxml2 in the main thread before
524 // any thread uses them. So initialize them here before we might run Atlas.
525 ScriptEngine scriptEngine;
526 CXeromyces::Startup();
528 if (ATLAS_RunIfOnCmdLine(args, false))
530 CXeromyces::Terminate();
531 return;
534 if (isNonVisualReplay)
536 if (!args.Has("mod"))
538 LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?");
539 CXeromyces::Terminate();
540 return;
543 Paths paths(args);
544 g_VFS = CreateVfs();
545 g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE);
546 MountMods(paths, GetMods(args, INIT_MODS));
549 CReplayPlayer replay;
550 replay.Load(replayFile);
551 replay.Replay(
552 args.Has("serializationtest"),
553 args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
554 args.Has("ooslog"));
557 g_VFS.reset();
559 CXeromyces::Terminate();
560 return;
563 // run in archive-building mode if requested
564 if (args.Has("archivebuild"))
566 Paths paths(args);
568 OsPath mod(args.Get("archivebuild"));
569 OsPath zip;
570 if (args.Has("archivebuild-output"))
571 zip = args.Get("archivebuild-output");
572 else
573 zip = mod.Filename().ChangeExtension(L".zip");
575 CArchiveBuilder builder(mod, paths.Cache());
577 // Add mods provided on the command line
578 // NOTE: We do not handle mods in the user mod path here
579 std::vector<CStr> mods = args.GetMultiple("mod");
580 for (size_t i = 0; i < mods.size(); ++i)
581 builder.AddBaseMod(paths.RData()/"mods"/mods[i]);
583 builder.Build(zip, args.Has("archivebuild-compress"));
585 CXeromyces::Terminate();
586 return;
589 const double res = timer_Resolution();
590 g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
592 // run the game
593 int flags = INIT_MODS;
596 restart = false;
597 quit = false;
598 if (!Init(args, flags))
600 flags &= ~INIT_MODS;
601 Shutdown(SHUTDOWN_FROM_CONFIG);
602 continue;
605 std::vector<CStr> installedMods;
606 if (!modsToInstall.empty())
608 Paths paths(args);
609 CModInstaller installer(paths.UserData() / "mods", paths.Cache());
611 // Install the mods without deleting the pyromod files
612 for (const OsPath& modPath : modsToInstall)
613 installer.Install(modPath, g_ScriptRuntime, true);
615 installedMods = installer.GetInstalledMods();
618 if (isNonVisual)
620 InitNonVisual(args);
621 while (!quit)
622 NonVisualFrame();
624 else
626 InitGraphics(args, 0, installedMods);
627 MainControllerInit();
628 while (!quit)
629 Frame();
632 // Do not install mods again in case of restart (typically from the mod selector)
633 modsToInstall.clear();
635 Shutdown(0);
636 MainControllerShutdown();
637 flags &= ~INIT_MODS;
638 } while (restart);
640 if (restart_in_atlas)
641 ATLAS_RunIfOnCmdLine(args, true);
643 CXeromyces::Terminate();
646 #if OS_ANDROID
647 // In Android we compile the engine as a shared library, not an executable,
648 // so rename main() to a different symbol that the wrapper library can load
649 #undef main
650 #define main pyrogenesis_main
651 extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
652 #endif
654 extern "C" int main(int argc, char* argv[])
656 #if OS_UNIX
657 // Don't allow people to run the game with root permissions,
658 // because bad things can happen, check before we do anything
659 if (geteuid() == 0)
661 std::cerr << "********************************************************\n"
662 << "WARNING: Attempted to run the game with root permission!\n"
663 << "This is not allowed because it can alter home directory \n"
664 << "permissions and opens your system to vulnerabilities. \n"
665 << "(You received this message because you were either \n"
666 <<" logged in as root or used e.g. the 'sudo' command.) \n"
667 << "********************************************************\n\n";
668 return EXIT_FAILURE;
670 #endif // OS_UNIX
672 EarlyInit(); // must come at beginning of main
674 RunGameOrAtlas(argc, const_cast<const char**>(argv));
676 // Shut down profiler initialised by EarlyInit
677 g_Profiler2.Shutdown();
679 return EXIT_SUCCESS;