Use @file JSDoc tag for the rmgen library, so that these comments are distinguished...
[0ad.git] / source / main.cpp
blob8ff0e89bb643225fef84a65e225bc7ee119a48e2
1 /* Copyright (C) 2017 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/Profile.h"
55 #include "ps/Profiler2.h"
56 #include "ps/Pyrogenesis.h"
57 #include "ps/Replay.h"
58 #include "ps/TouchInput.h"
59 #include "ps/UserReport.h"
60 #include "ps/Util.h"
61 #include "ps/VideoMode.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/Renderer.h"
78 #include "scriptinterface/ScriptEngine.h"
79 #include "simulation2/Simulation2.h"
80 #include "simulation2/system/TurnManager.h"
82 #if OS_UNIX
83 #include <unistd.h> // geteuid
84 #endif // OS_UNIX
86 #if MSC_VERSION
87 #include <process.h>
88 #define getpid _getpid // Use the non-deprecated function name
89 #endif
91 extern bool g_GameRestarted;
92 extern CStrW g_UniqueLogPostfix;
94 void kill_mainloop();
96 // to avoid redundant and/or recursive resizing, we save the new
97 // size after VIDEORESIZE messages and only update the video mode
98 // once per frame.
99 // these values are the latest resize message, and reset to 0 once we've
100 // updated the video mode
101 static int g_ResizedW;
102 static int g_ResizedH;
104 static std::chrono::high_resolution_clock::time_point lastFrameTime;
106 // main app message handler
107 static InReaction MainInputHandler(const SDL_Event_* ev)
109 switch(ev->ev.type)
111 case SDL_WINDOWEVENT:
112 switch(ev->ev.window.event)
114 case SDL_WINDOWEVENT_ENTER:
115 RenderCursor(true);
116 break;
117 case SDL_WINDOWEVENT_LEAVE:
118 RenderCursor(false);
119 break;
120 case SDL_WINDOWEVENT_RESIZED:
121 g_ResizedW = ev->ev.window.data1;
122 g_ResizedH = ev->ev.window.data2;
123 break;
124 case SDL_WINDOWEVENT_MOVED:
125 g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
127 break;
129 case SDL_QUIT:
130 kill_mainloop();
131 break;
133 case SDL_HOTKEYDOWN:
134 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
135 if (hotkey == "exit")
137 kill_mainloop();
138 return IN_HANDLED;
140 else if (hotkey == "screenshot")
142 WriteScreenshot(L".png");
143 return IN_HANDLED;
145 else if (hotkey == "bigscreenshot")
147 WriteBigScreenshot(L".bmp", 10);
148 return IN_HANDLED;
150 else if (hotkey == "togglefullscreen")
152 g_VideoMode.ToggleFullscreen();
153 return IN_HANDLED;
155 else if (hotkey == "profile2.toggle")
157 g_Profiler2.Toggle();
158 return IN_HANDLED;
160 break;
163 return IN_PASS;
167 // dispatch all pending events to the various receivers.
168 static void PumpEvents()
170 JSContext* cx = g_GUI->GetScriptInterface()->GetContext();
171 JSAutoRequest rq(cx);
173 PROFILE3("dispatch events");
175 SDL_Event_ ev;
176 while (in_poll_event(&ev))
178 PROFILE2("event");
179 if (g_GUI)
181 JS::RootedValue tmpVal(cx);
182 ScriptInterface::ToJSVal(cx, &tmpVal, ev);
183 std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal);
184 PROFILE2_ATTR("%s", data.c_str());
186 in_dispatch_event(&ev);
189 g_TouchInput.Frame();
193 * Optionally throttle the render frequency in order to
194 * prevent 100% workload of the currently used CPU core.
196 inline static void LimitFPS()
198 if (g_VSync)
199 return;
201 double fpsLimit = 0.0;
202 CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit);
204 // Keep in sync with options.json
205 if (fpsLimit < 20.0 || fpsLimit >= 100.0)
206 return;
208 double wait = 1000.0 / fpsLimit -
209 std::chrono::duration_cast<std::chrono::microseconds>(
210 std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0;
212 if (wait > 0.0)
213 SDL_Delay(wait);
215 lastFrameTime = std::chrono::high_resolution_clock::now();
218 static int ProgressiveLoad()
220 PROFILE3("progressive load");
222 wchar_t description[100];
223 int progress_percent;
226 Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent);
227 switch(ret)
229 // no load active => no-op (skip code below)
230 case INFO::OK:
231 return 0;
232 // current task didn't complete. we only care about this insofar as the
233 // load process is therefore not yet finished.
234 case ERR::TIMED_OUT:
235 break;
236 // just finished loading
237 case INFO::ALL_COMPLETE:
238 g_Game->ReallyStartGame();
239 wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting..");
240 // LDR_ProgressiveLoad returns L""; set to valid text to
241 // avoid problems in converting to JSString
242 break;
243 // error!
244 default:
245 WARN_RETURN_STATUS_IF_ERR(ret);
246 // can't do this above due to legit ERR::TIMED_OUT
247 break;
250 catch (PSERROR_Game_World_MapLoadFailed& e)
252 // Map loading failed
254 // Call script function to do the actual work
255 // (delete game data, switch GUI page, show error, etc.)
256 CancelLoad(CStr(e.what()).FromUTF8());
259 GUI_DisplayLoadProgress(progress_percent, description);
260 return 0;
264 static void RendererIncrementalLoad()
266 PROFILE3("renderer incremental load");
268 const double maxTime = 0.1f;
270 double startTime = timer_Time();
271 bool more;
272 do {
273 more = g_Renderer.GetTextureManager().MakeProgress();
275 while (more && timer_Time() - startTime < maxTime);
279 static bool quit = false; // break out of main loop
281 static void Frame()
283 g_Profiler2.RecordFrameStart();
284 PROFILE2("frame");
285 g_Profiler2.IncrementFrameNumber();
286 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
288 ogl_WarnIfError();
290 // get elapsed time
291 const double time = timer_Time();
292 g_frequencyFilter->Update(time);
293 // .. old method - "exact" but contains jumps
294 #if 0
295 static double last_time;
296 const double time = timer_Time();
297 const float TimeSinceLastFrame = (float)(time-last_time);
298 last_time = time;
299 ONCE(return); // first call: set last_time and return
301 // .. new method - filtered and more smooth, but errors may accumulate
302 #else
303 const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
304 #endif
305 ENSURE(realTimeSinceLastFrame > 0.0f);
307 // decide if update/render is necessary
308 bool need_render = !g_app_minimized;
309 bool need_update = true;
311 // If we are not running a multiplayer game, disable updates when the game is
312 // minimized or out of focus and relinquish the CPU a bit, in order to make
313 // debugging easier.
314 if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
316 PROFILE3("non-focus delay");
317 need_update = false;
318 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
319 SDL_Delay(10);
322 // this scans for changed files/directories and reloads them, thus
323 // allowing hotloading (changes are immediately assimilated in-game).
324 ReloadChangedFiles();
326 ProgressiveLoad();
328 RendererIncrementalLoad();
330 PumpEvents();
332 // if the user quit by closing the window, the GL context will be broken and
333 // may crash when we call Render() on some drivers, so leave this loop
334 // before rendering
335 if (quit)
336 return;
338 // respond to pumped resize events
339 if (g_ResizedW || g_ResizedH)
341 g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH);
342 g_ResizedW = g_ResizedH = 0;
345 if (g_NetClient)
346 g_NetClient->Poll();
348 ogl_WarnIfError();
350 g_GUI->TickObjects();
352 ogl_WarnIfError();
354 if (g_Game && g_Game->IsGameStarted() && need_update)
356 g_Game->Update(realTimeSinceLastFrame);
358 g_Game->GetView()->Update(float(realTimeSinceLastFrame));
361 // Immediately flush any messages produced by simulation code
362 if (g_NetClient)
363 g_NetClient->Flush();
365 // Keep us connected to any XMPP servers
366 if (g_XmppClient)
367 g_XmppClient->recv();
369 g_UserReporter.Update();
371 g_Console->Update(realTimeSinceLastFrame);
373 ogl_WarnIfError();
374 if (need_render)
376 Render();
378 PROFILE3("swap buffers");
379 SDL_GL_SwapWindow(g_VideoMode.GetWindow());
381 ogl_WarnIfError();
383 g_Profiler.Frame();
385 g_GameRestarted = false;
387 LimitFPS();
390 static void NonVisualFrame()
392 g_Profiler2.RecordFrameStart();
393 PROFILE2("frame");
394 g_Profiler2.IncrementFrameNumber();
395 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
397 static u32 turn = 0;
398 debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP);
400 g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP);
402 g_Profiler.Frame();
404 if (g_Game->IsGameFinished())
405 kill_mainloop();
409 static void MainControllerInit()
411 // add additional input handlers only needed by this controller:
413 // must be registered after gui_handler. Should mayhap even be last.
414 in_add_handler(MainInputHandler);
419 static void MainControllerShutdown()
421 in_reset_handlers();
425 // stop the main loop and trigger orderly shutdown. called from several
426 // places: the event handler (SDL_QUIT and hotkey) and JS exitProgram.
427 void kill_mainloop()
429 quit = true;
433 static bool restart_in_atlas = false;
434 // called by game code to indicate main() should restart in Atlas mode
435 // instead of terminating
436 void restart_mainloop_in_atlas()
438 quit = true;
439 restart_in_atlas = true;
442 static bool restart = false;
443 // trigger an orderly shutdown and restart the game.
444 void restart_engine()
446 quit = true;
447 restart = true;
450 extern CmdLineArgs g_args;
452 // moved into a helper function to ensure args is destroyed before
453 // exit(), which may result in a memory leak.
454 static void RunGameOrAtlas(int argc, const char* argv[])
456 CmdLineArgs args(argc, argv);
458 g_args = args;
460 if (args.Has("version"))
462 debug_printf("Pyrogenesis %s\n", engine_version);
463 return;
466 if (args.Has("autostart-nonvisual") && args.Get("autostart").empty())
468 LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
469 return;
472 if (args.Has("unique-logs"))
473 g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid());
475 const bool isVisualReplay = args.Has("replay-visual");
476 const bool isNonVisualReplay = args.Has("replay");
477 const bool isNonVisual = args.Has("autostart-nonvisual");
479 const OsPath replayFile(
480 isVisualReplay ? args.Get("replay-visual") :
481 isNonVisualReplay ? args.Get("replay") : "");
483 if (isVisualReplay || isNonVisualReplay)
485 if (!FileExists(replayFile))
487 debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str());
488 return;
490 if (DirectoryExists(replayFile))
492 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
493 return;
497 // We need to initialize SpiderMonkey and libxml2 in the main thread before
498 // any thread uses them. So initialize them here before we might run Atlas.
499 ScriptEngine scriptEngine;
500 CXeromyces::Startup();
502 if (ATLAS_RunIfOnCmdLine(args, false))
504 CXeromyces::Terminate();
505 return;
508 if (isNonVisualReplay)
510 if (!args.Has("mod"))
512 LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?");
513 CXeromyces::Terminate();
514 return;
517 Paths paths(args);
518 g_VFS = CreateVfs(20 * MiB);
519 g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE);
520 MountMods(paths, GetMods(args, INIT_MODS));
523 CReplayPlayer replay;
524 replay.Load(replayFile);
525 replay.Replay(
526 args.Has("serializationtest"),
527 args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
528 args.Has("ooslog"));
531 g_VFS.reset();
533 CXeromyces::Terminate();
534 return;
537 // run in archive-building mode if requested
538 if (args.Has("archivebuild"))
540 Paths paths(args);
542 OsPath mod(args.Get("archivebuild"));
543 OsPath zip;
544 if (args.Has("archivebuild-output"))
545 zip = args.Get("archivebuild-output");
546 else
547 zip = mod.Filename().ChangeExtension(L".zip");
549 CArchiveBuilder builder(mod, paths.Cache());
551 // Add mods provided on the command line
552 // NOTE: We do not handle mods in the user mod path here
553 std::vector<CStr> mods = args.GetMultiple("mod");
554 for (size_t i = 0; i < mods.size(); ++i)
555 builder.AddBaseMod(paths.RData()/"mods"/mods[i]);
557 builder.Build(zip, args.Has("archivebuild-compress"));
559 CXeromyces::Terminate();
560 return;
563 const double res = timer_Resolution();
564 g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
566 // run the game
567 int flags = INIT_MODS;
570 restart = false;
571 quit = false;
572 if (!Init(args, flags))
574 flags &= ~INIT_MODS;
575 Shutdown(SHUTDOWN_FROM_CONFIG);
576 continue;
579 if (isNonVisual)
581 InitNonVisual(args);
582 while (!quit)
583 NonVisualFrame();
585 else
587 InitGraphics(args, 0);
588 MainControllerInit();
589 while (!quit)
590 Frame();
593 Shutdown(0);
594 MainControllerShutdown();
595 flags &= ~INIT_MODS;
596 } while (restart);
598 if (restart_in_atlas)
599 ATLAS_RunIfOnCmdLine(args, true);
601 CXeromyces::Terminate();
604 #if OS_ANDROID
605 // In Android we compile the engine as a shared library, not an executable,
606 // so rename main() to a different symbol that the wrapper library can load
607 #undef main
608 #define main pyrogenesis_main
609 extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
610 #endif
612 extern "C" int main(int argc, char* argv[])
614 #if OS_UNIX
615 // Don't allow people to run the game with root permissions,
616 // because bad things can happen, check before we do anything
617 if (geteuid() == 0)
619 std::cerr << "********************************************************\n"
620 << "WARNING: Attempted to run the game with root permission!\n"
621 << "This is not allowed because it can alter home directory \n"
622 << "permissions and opens your system to vulnerabilities. \n"
623 << "(You received this message because you were either \n"
624 <<" logged in as root or used e.g. the 'sudo' command.) \n"
625 << "********************************************************\n\n";
626 return EXIT_FAILURE;
628 #endif // OS_UNIX
630 EarlyInit(); // must come at beginning of main
632 RunGameOrAtlas(argc, const_cast<const char**>(argv));
634 // Shut down profiler initialised by EarlyInit
635 g_Profiler2.Shutdown();
637 return EXIT_SUCCESS;