Merge 'remotes/trunk'
[0ad.git] / source / main.cpp
blobadd12cbc3aadff6c109c38c17fad3cf59401caf8
1 /* Copyright (C) 2016 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/ogl.h"
40 #include "lib/timer.h"
41 #include "lib/external_libraries/libsdl.h"
43 #include "ps/ArchiveBuilder.h"
44 #include "ps/CConsole.h"
45 #include "ps/CLogger.h"
46 #include "ps/ConfigDB.h"
47 #include "ps/Filesystem.h"
48 #include "ps/Game.h"
49 #include "ps/Globals.h"
50 #include "ps/Hotkey.h"
51 #include "ps/Loader.h"
52 #include "ps/Profile.h"
53 #include "ps/Profiler2.h"
54 #include "ps/Pyrogenesis.h"
55 #include "ps/Replay.h"
56 #include "ps/TouchInput.h"
57 #include "ps/UserReport.h"
58 #include "ps/Util.h"
59 #include "ps/VideoMode.h"
60 #include "ps/World.h"
61 #include "ps/GameSetup/GameSetup.h"
62 #include "ps/GameSetup/Atlas.h"
63 #include "ps/GameSetup/Config.h"
64 #include "ps/GameSetup/CmdLineArgs.h"
65 #include "ps/GameSetup/Paths.h"
66 #include "ps/XML/Xeromyces.h"
67 #include "network/NetClient.h"
68 #include "network/NetServer.h"
69 #include "network/NetSession.h"
70 #include "lobby/IXmppClient.h"
71 #include "graphics/Camera.h"
72 #include "graphics/GameView.h"
73 #include "graphics/TextureManager.h"
74 #include "gui/GUIManager.h"
75 #include "renderer/Renderer.h"
76 #include "simulation2/Simulation2.h"
78 #if OS_UNIX
79 #include <unistd.h> // geteuid
80 #endif // OS_UNIX
82 extern bool g_GameRestarted;
84 void kill_mainloop();
86 // to avoid redundant and/or recursive resizing, we save the new
87 // size after VIDEORESIZE messages and only update the video mode
88 // once per frame.
89 // these values are the latest resize message, and reset to 0 once we've
90 // updated the video mode
91 static int g_ResizedW;
92 static int g_ResizedH;
94 // main app message handler
95 static InReaction MainInputHandler(const SDL_Event_* ev)
97 switch(ev->ev.type)
99 case SDL_WINDOWEVENT:
100 switch(ev->ev.window.event)
102 case SDL_WINDOWEVENT_ENTER:
103 RenderCursor(true);
104 break;
105 case SDL_WINDOWEVENT_LEAVE:
106 RenderCursor(false);
107 break;
108 case SDL_WINDOWEVENT_RESIZED:
109 g_ResizedW = ev->ev.window.data1;
110 g_ResizedH = ev->ev.window.data2;
111 break;
112 case SDL_WINDOWEVENT_MOVED:
113 g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
115 break;
117 case SDL_QUIT:
118 kill_mainloop();
119 break;
121 case SDL_HOTKEYDOWN:
122 std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
123 if (hotkey == "exit")
125 kill_mainloop();
126 return IN_HANDLED;
128 else if (hotkey == "screenshot")
130 WriteScreenshot(L".png");
131 return IN_HANDLED;
133 else if (hotkey == "bigscreenshot")
135 WriteBigScreenshot(L".bmp", 10);
136 return IN_HANDLED;
138 else if (hotkey == "togglefullscreen")
140 g_VideoMode.ToggleFullscreen();
141 return IN_HANDLED;
143 else if (hotkey == "profile2.toggle")
145 g_Profiler2.Toggle();
146 return IN_HANDLED;
148 break;
151 return IN_PASS;
155 // dispatch all pending events to the various receivers.
156 static void PumpEvents()
158 JSContext* cx = g_GUI->GetScriptInterface()->GetContext();
159 JSAutoRequest rq(cx);
161 PROFILE3("dispatch events");
163 SDL_Event_ ev;
164 while (in_poll_event(&ev))
166 PROFILE2("event");
167 if (g_GUI)
169 JS::RootedValue tmpVal(cx);
170 ScriptInterface::ToJSVal(cx, &tmpVal, ev);
171 std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal);
172 PROFILE2_ATTR("%s", data.c_str());
174 in_dispatch_event(&ev);
177 g_TouchInput.Frame();
181 static int ProgressiveLoad()
183 PROFILE3("progressive load");
185 wchar_t description[100];
186 int progress_percent;
189 Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent);
190 switch(ret)
192 // no load active => no-op (skip code below)
193 case INFO::OK:
194 return 0;
195 // current task didn't complete. we only care about this insofar as the
196 // load process is therefore not yet finished.
197 case ERR::TIMED_OUT:
198 break;
199 // just finished loading
200 case INFO::ALL_COMPLETE:
201 g_Game->ReallyStartGame();
202 wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting..");
203 // LDR_ProgressiveLoad returns L""; set to valid text to
204 // avoid problems in converting to JSString
205 break;
206 // error!
207 default:
208 WARN_RETURN_STATUS_IF_ERR(ret);
209 // can't do this above due to legit ERR::TIMED_OUT
210 break;
213 catch (PSERROR_Game_World_MapLoadFailed& e)
215 // Map loading failed
217 // Call script function to do the actual work
218 // (delete game data, switch GUI page, show error, etc.)
219 CancelLoad(CStr(e.what()).FromUTF8());
222 GUI_DisplayLoadProgress(progress_percent, description);
223 return 0;
227 static void RendererIncrementalLoad()
229 PROFILE3("renderer incremental load");
231 const double maxTime = 0.1f;
233 double startTime = timer_Time();
234 bool more;
235 do {
236 more = g_Renderer.GetTextureManager().MakeProgress();
238 while (more && timer_Time() - startTime < maxTime);
242 static bool quit = false; // break out of main loop
244 static void Frame()
246 g_Profiler2.RecordFrameStart();
247 PROFILE2("frame");
248 g_Profiler2.IncrementFrameNumber();
249 PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
251 ogl_WarnIfError();
253 // get elapsed time
254 const double time = timer_Time();
255 g_frequencyFilter->Update(time);
256 // .. old method - "exact" but contains jumps
257 #if 0
258 static double last_time;
259 const double time = timer_Time();
260 const float TimeSinceLastFrame = (float)(time-last_time);
261 last_time = time;
262 ONCE(return); // first call: set last_time and return
264 // .. new method - filtered and more smooth, but errors may accumulate
265 #else
266 const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
267 #endif
268 ENSURE(realTimeSinceLastFrame > 0.0f);
270 // decide if update/render is necessary
271 bool need_render = !g_app_minimized;
272 bool need_update = true;
274 // If we are not running a multiplayer game, disable updates when the game is
275 // minimized or out of focus and relinquish the CPU a bit, in order to make
276 // debugging easier.
277 if(g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
279 PROFILE3("non-focus delay");
280 need_update = false;
281 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
282 SDL_Delay(10);
285 // Throttling: limit update and render frequency to the minimum to 50 FPS
286 // in the "inactive" state, so that other windows get enough CPU time,
287 // (and it's always nice for power+thermal management).
288 // TODO: when the game performance is high enough, implementing a limit for
289 // in-game framerate might be sensible.
290 const float maxFPSMenu = 50.0;
291 bool limit_fps = false;
292 CFG_GET_VAL("gui.menu.limitfps", limit_fps);
293 if (limit_fps && (!g_Game || !g_Game->IsGameStarted()))
295 float remainingFrameTime = (1000.0 / maxFPSMenu) - realTimeSinceLastFrame;
296 if (remainingFrameTime > 0)
297 SDL_Delay(remainingFrameTime);
301 // this scans for changed files/directories and reloads them, thus
302 // allowing hotloading (changes are immediately assimilated in-game).
303 ReloadChangedFiles();
305 ProgressiveLoad();
307 RendererIncrementalLoad();
309 PumpEvents();
311 // if the user quit by closing the window, the GL context will be broken and
312 // may crash when we call Render() on some drivers, so leave this loop
313 // before rendering
314 if (quit)
315 return;
317 // respond to pumped resize events
318 if (g_ResizedW || g_ResizedH)
320 g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH);
321 g_ResizedW = g_ResizedH = 0;
324 if (g_NetClient)
325 g_NetClient->Poll();
327 ogl_WarnIfError();
329 g_GUI->TickObjects();
331 ogl_WarnIfError();
333 if (g_Game && g_Game->IsGameStarted() && need_update)
335 g_Game->Update(realTimeSinceLastFrame);
337 g_Game->GetView()->Update(float(realTimeSinceLastFrame));
340 // Immediately flush any messages produced by simulation code
341 if (g_NetClient)
342 g_NetClient->Flush();
344 // Keep us connected to any XMPP servers
345 if (g_XmppClient)
346 g_XmppClient->recv();
348 g_UserReporter.Update();
350 g_Console->Update(realTimeSinceLastFrame);
352 ogl_WarnIfError();
353 if(need_render)
355 Render();
357 PROFILE3("swap buffers");
358 SDL_GL_SwapWindow(g_VideoMode.GetWindow());
360 ogl_WarnIfError();
362 g_Profiler.Frame();
364 g_GameRestarted = false;
368 static void MainControllerInit()
370 // add additional input handlers only needed by this controller:
372 // must be registered after gui_handler. Should mayhap even be last.
373 in_add_handler(MainInputHandler);
378 static void MainControllerShutdown()
380 in_reset_handlers();
384 // stop the main loop and trigger orderly shutdown. called from several
385 // places: the event handler (SDL_QUIT and hotkey) and JS exitProgram.
386 void kill_mainloop()
388 quit = true;
392 static bool restart_in_atlas = false;
393 // called by game code to indicate main() should restart in Atlas mode
394 // instead of terminating
395 void restart_mainloop_in_atlas()
397 quit = true;
398 restart_in_atlas = true;
401 static bool restart = false;
402 // trigger an orderly shutdown and restart the game.
403 void restart_engine()
405 quit = true;
406 restart = true;
409 extern CmdLineArgs g_args;
411 // moved into a helper function to ensure args is destroyed before
412 // exit(), which may result in a memory leak.
413 static void RunGameOrAtlas(int argc, const char* argv[])
415 CmdLineArgs args(argc, argv);
417 g_args = args;
419 if (args.Has("version") || args.Has("-version"))
421 debug_printf("Pyrogenesis %s\n", engine_version);
422 return;
425 // We need to initialise libxml2 in the main thread before
426 // any thread uses it. So initialise it here before we
427 // might run Atlas.
428 CXeromyces::Startup();
430 // Atlas handles the whole init/shutdown/etc sequence by itself;
431 if (ATLAS_RunIfOnCmdLine(args, false))
432 return;
434 const bool isReplay = args.Has("replay");
435 const bool isVisualReplay = args.Has("replay-visual");
436 const std::string replayFile = isReplay ? args.Get("replay") : (isVisualReplay ? args.Get("replay-visual") : "");
438 // Ensure the replay file exists
439 if (isReplay || isVisualReplay)
441 if (!FileExists(OsPath(replayFile)))
443 debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.c_str());
444 return;
446 if (DirectoryExists(OsPath(replayFile)))
448 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.c_str());
449 return;
453 // run non-visual simulation replay if requested
454 if (isReplay)
456 Paths paths(args);
457 g_VFS = CreateVfs(20 * MiB);
458 g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE);
459 MountMods(paths, GetMods(args, INIT_MODS));
462 CReplayPlayer replay;
463 replay.Load(replayFile);
464 replay.Replay(args.Has("serializationtest"), args.Has("ooslog"));
467 g_VFS.reset();
469 CXeromyces::Terminate();
470 return;
473 // run in archive-building mode if requested
474 if (args.Has("archivebuild"))
476 Paths paths(args);
478 OsPath mod(args.Get("archivebuild"));
479 OsPath zip;
480 if (args.Has("archivebuild-output"))
481 zip = args.Get("archivebuild-output");
482 else
483 zip = mod.Filename().ChangeExtension(L".zip");
485 CArchiveBuilder builder(mod, paths.Cache());
487 // Add mods provided on the command line
488 // NOTE: We do not handle mods in the user mod path here
489 std::vector<CStr> mods = args.GetMultiple("mod");
490 for (size_t i = 0; i < mods.size(); ++i)
491 builder.AddBaseMod(paths.RData()/"mods"/mods[i]);
493 builder.Build(zip, args.Has("archivebuild-compress"));
495 CXeromyces::Terminate();
496 return;
499 const double res = timer_Resolution();
500 g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
502 // run the game
503 int flags = INIT_MODS;
506 restart = false;
507 quit = false;
508 if (!Init(args, flags))
510 flags &= ~INIT_MODS;
511 Shutdown(SHUTDOWN_FROM_CONFIG);
512 continue;
514 InitGraphics(args, 0);
515 MainControllerInit();
516 while (!quit)
517 Frame();
518 Shutdown(0);
519 MainControllerShutdown();
520 flags &= ~INIT_MODS;
521 } while (restart);
523 if (restart_in_atlas)
525 ATLAS_RunIfOnCmdLine(args, true);
526 return;
529 // Shut down libxml2 (done here to match the Startup call)
530 CXeromyces::Terminate();
533 #if OS_ANDROID
534 // In Android we compile the engine as a shared library, not an executable,
535 // so rename main() to a different symbol that the wrapper library can load
536 #undef main
537 #define main pyrogenesis_main
538 extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
539 #endif
541 extern "C" int main(int argc, char* argv[])
543 #if OS_UNIX
544 // Don't allow people to run the game with root permissions,
545 // because bad things can happen, check before we do anything
546 if (geteuid() == 0)
548 std::cerr << "********************************************************\n"
549 << "WARNING: Attempted to run the game with root permission!\n"
550 << "This is not allowed because it can alter home directory \n"
551 << "permissions and opens your system to vulnerabilities. \n"
552 << "(You received this message because you were either \n"
553 <<" logged in as root or used e.g. the 'sudo' command.) \n"
554 << "********************************************************\n\n";
555 return EXIT_FAILURE;
557 #endif // OS_UNIX
559 EarlyInit(); // must come at beginning of main
561 RunGameOrAtlas(argc, const_cast<const char**>(argv));
563 // Shut down profiler initialised by EarlyInit
564 g_Profiler2.Shutdown();
566 return EXIT_SUCCESS;