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
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
32 #include "lib/precompiled.h"
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"
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"
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"
62 #include "ps/VideoMode.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"
84 #include <unistd.h> // geteuid
89 #define getpid _getpid // Use the non-deprecated function name
92 extern CStrW g_UniqueLogPostfix
;
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
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
)
114 case SDL_WINDOWEVENT
:
115 switch(ev
->ev
.window
.event
)
117 case SDL_WINDOWEVENT_ENTER
:
120 case SDL_WINDOWEVENT_LEAVE
:
123 case SDL_WINDOWEVENT_RESIZED
:
124 g_ResizedW
= ev
->ev
.window
.data1
;
125 g_ResizedH
= ev
->ev
.window
.data2
;
127 case SDL_WINDOWEVENT_MOVED
:
128 g_VideoMode
.UpdatePosition(ev
->ev
.window
.data1
, ev
->ev
.window
.data2
);
137 std::string hotkey
= static_cast<const char*>(ev
->ev
.user
.data1
);
138 if (hotkey
== "exit")
143 else if (hotkey
== "screenshot")
145 WriteScreenshot(L
".png");
148 else if (hotkey
== "bigscreenshot")
150 WriteBigScreenshot(L
".bmp", 10);
153 else if (hotkey
== "togglefullscreen")
155 g_VideoMode
.ToggleFullscreen();
158 else if (hotkey
== "profile2.toggle")
160 g_Profiler2
.Toggle();
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");
179 while (in_poll_event(&ev
))
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()
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)
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;
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
);
232 // no load active => no-op (skip code below)
235 // current task didn't complete. we only care about this insofar as the
236 // load process is therefore not yet finished.
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
248 WARN_RETURN_STATUS_IF_ERR(ret
);
249 // can't do this above due to legit ERR::TIMED_OUT
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
);
267 static void RendererIncrementalLoad()
269 PROFILE3("renderer incremental load");
271 const double maxTime
= 0.1f
;
273 double startTime
= timer_Time();
276 more
= g_Renderer
.GetTextureManager().MakeProgress();
278 while (more
&& timer_Time() - startTime
< maxTime
);
282 static bool quit
= false; // break out of main loop
286 g_Profiler2
.RecordFrameStart();
288 g_Profiler2
.IncrementFrameNumber();
289 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
294 const double time
= timer_Time();
295 g_frequencyFilter
->Update(time
);
296 // .. old method - "exact" but contains jumps
298 static double last_time
;
299 const double time
= timer_Time();
300 const float TimeSinceLastFrame
= (float)(time
-last_time
);
302 ONCE(return); // first call: set last_time and return
304 // .. new method - filtered and more smooth, but errors may accumulate
306 const float realTimeSinceLastFrame
= 1.0 / g_frequencyFilter
->SmoothedFrequency();
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
316 if (g_PauseOnFocusLoss
&& !g_NetClient
&& !g_app_has_focus
)
318 PROFILE3("non-focus delay");
320 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
324 // this scans for changed files/directories and reloads them, thus
325 // allowing hotloading (changes are immediately assimilated in-game).
326 ReloadChangedFiles();
330 RendererIncrementalLoad();
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
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;
352 g_GUI
->TickObjects();
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
365 g_NetClient
->Flush();
367 // Keep us connected to any XMPP servers
369 g_XmppClient
->recv();
371 g_UserReporter
.Update();
373 g_Console
->Update(realTimeSinceLastFrame
);
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()))
382 PROFILE3("swap buffers");
383 SDL_GL_SwapWindow(g_VideoMode
.GetWindow());
389 g_GameRestarted
= false;
394 static void NonVisualFrame()
396 g_Profiler2
.RecordFrameStart();
398 g_Profiler2
.IncrementFrameNumber();
399 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
402 debug_printf("Turn %u (%u)...\n", turn
++, DEFAULT_TURN_LENGTH_SP
);
404 g_Game
->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP
);
408 if (g_Game
->IsGameFinished())
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()
429 // stop the main loop and trigger orderly shutdown. called from several
430 // places: the event handler (SDL_QUIT and hotkey) and JS exitProgram.
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()
443 restart_in_atlas
= true;
446 static bool restart
= false;
447 // trigger an orderly shutdown and restart the game.
448 void restart_engine()
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
);
464 if (args
.Has("version"))
466 debug_printf("Pyrogenesis %s\n", engine_version
);
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.");
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());
494 if (DirectoryExists(replayFile
))
496 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile
.string8().c_str());
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());
510 if (!FileExists(modPath
))
512 debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath
.string8().c_str());
515 if (DirectoryExists(modPath
))
517 debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath
.string8().c_str());
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();
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();
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
);
552 args
.Has("serializationtest"),
553 args
.Has("rejointest") ? args
.Get("rejointest").ToInt() : -1,
559 CXeromyces::Terminate();
563 // run in archive-building mode if requested
564 if (args
.Has("archivebuild"))
568 OsPath
mod(args
.Get("archivebuild"));
570 if (args
.Has("archivebuild-output"))
571 zip
= args
.Get("archivebuild-output");
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();
589 const double res
= timer_Resolution();
590 g_frequencyFilter
= CreateFrequencyFilter(res
, 30.0);
593 int flags
= INIT_MODS
;
598 if (!Init(args
, flags
))
601 Shutdown(SHUTDOWN_FROM_CONFIG
);
605 std::vector
<CStr
> installedMods
;
606 if (!modsToInstall
.empty())
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();
626 InitGraphics(args
, 0, installedMods
);
627 MainControllerInit();
632 // Do not install mods again in case of restart (typically from the mod selector)
633 modsToInstall
.clear();
636 MainControllerShutdown();
640 if (restart_in_atlas
)
641 ATLAS_RunIfOnCmdLine(args
, true);
643 CXeromyces::Terminate();
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
650 #define main pyrogenesis_main
651 extern "C" __attribute__((visibility ("default"))) int main(int argc
, char* argv
[]);
654 extern "C" int main(int argc
, char* argv
[])
657 // Don't allow people to run the game with root permissions,
658 // because bad things can happen, check before we do anything
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";
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();