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
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/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"
61 #include "ps/VideoMode.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"
83 #include <unistd.h> // geteuid
88 #define getpid _getpid // Use the non-deprecated function name
91 extern bool g_GameRestarted
;
92 extern CStrW g_UniqueLogPostfix
;
96 // to avoid redundant and/or recursive resizing, we save the new
97 // size after VIDEORESIZE messages and only update the video mode
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
)
111 case SDL_WINDOWEVENT
:
112 switch(ev
->ev
.window
.event
)
114 case SDL_WINDOWEVENT_ENTER
:
117 case SDL_WINDOWEVENT_LEAVE
:
120 case SDL_WINDOWEVENT_RESIZED
:
121 g_ResizedW
= ev
->ev
.window
.data1
;
122 g_ResizedH
= ev
->ev
.window
.data2
;
124 case SDL_WINDOWEVENT_MOVED
:
125 g_VideoMode
.UpdatePosition(ev
->ev
.window
.data1
, ev
->ev
.window
.data2
);
134 std::string hotkey
= static_cast<const char*>(ev
->ev
.user
.data1
);
135 if (hotkey
== "exit")
140 else if (hotkey
== "screenshot")
142 WriteScreenshot(L
".png");
145 else if (hotkey
== "bigscreenshot")
147 WriteBigScreenshot(L
".bmp", 10);
150 else if (hotkey
== "togglefullscreen")
152 g_VideoMode
.ToggleFullscreen();
155 else if (hotkey
== "profile2.toggle")
157 g_Profiler2
.Toggle();
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");
176 while (in_poll_event(&ev
))
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()
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)
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;
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
);
229 // no load active => no-op (skip code below)
232 // current task didn't complete. we only care about this insofar as the
233 // load process is therefore not yet finished.
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
245 WARN_RETURN_STATUS_IF_ERR(ret
);
246 // can't do this above due to legit ERR::TIMED_OUT
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
);
264 static void RendererIncrementalLoad()
266 PROFILE3("renderer incremental load");
268 const double maxTime
= 0.1f
;
270 double startTime
= timer_Time();
273 more
= g_Renderer
.GetTextureManager().MakeProgress();
275 while (more
&& timer_Time() - startTime
< maxTime
);
279 static bool quit
= false; // break out of main loop
283 g_Profiler2
.RecordFrameStart();
285 g_Profiler2
.IncrementFrameNumber();
286 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
291 const double time
= timer_Time();
292 g_frequencyFilter
->Update(time
);
293 // .. old method - "exact" but contains jumps
295 static double last_time
;
296 const double time
= timer_Time();
297 const float TimeSinceLastFrame
= (float)(time
-last_time
);
299 ONCE(return); // first call: set last_time and return
301 // .. new method - filtered and more smooth, but errors may accumulate
303 const float realTimeSinceLastFrame
= 1.0 / g_frequencyFilter
->SmoothedFrequency();
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
314 if (g_PauseOnFocusLoss
&& !g_NetClient
&& !g_app_has_focus
)
316 PROFILE3("non-focus delay");
318 // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
322 // this scans for changed files/directories and reloads them, thus
323 // allowing hotloading (changes are immediately assimilated in-game).
324 ReloadChangedFiles();
328 RendererIncrementalLoad();
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
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;
350 g_GUI
->TickObjects();
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
363 g_NetClient
->Flush();
365 // Keep us connected to any XMPP servers
367 g_XmppClient
->recv();
369 g_UserReporter
.Update();
371 g_Console
->Update(realTimeSinceLastFrame
);
378 PROFILE3("swap buffers");
379 SDL_GL_SwapWindow(g_VideoMode
.GetWindow());
385 g_GameRestarted
= false;
390 static void NonVisualFrame()
392 g_Profiler2
.RecordFrameStart();
394 g_Profiler2
.IncrementFrameNumber();
395 PROFILE2_ATTR("%d", g_Profiler2
.GetFrameNumber());
398 debug_printf("Turn %u (%u)...\n", turn
++, DEFAULT_TURN_LENGTH_SP
);
400 g_Game
->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP
);
404 if (g_Game
->IsGameFinished())
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()
425 // stop the main loop and trigger orderly shutdown. called from several
426 // places: the event handler (SDL_QUIT and hotkey) and JS exitProgram.
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()
439 restart_in_atlas
= true;
442 static bool restart
= false;
443 // trigger an orderly shutdown and restart the game.
444 void restart_engine()
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
);
460 if (args
.Has("version") || args
.Has("-version"))
462 debug_printf("Pyrogenesis %s\n", engine_version
);
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.");
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());
490 if (DirectoryExists(replayFile
))
492 debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile
.string8().c_str());
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();
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();
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
);
526 args
.Has("serializationtest"),
527 args
.Has("rejointest") ? args
.Get("rejointest").ToInt() : -1,
533 CXeromyces::Terminate();
537 // run in archive-building mode if requested
538 if (args
.Has("archivebuild"))
542 OsPath
mod(args
.Get("archivebuild"));
544 if (args
.Has("archivebuild-output"))
545 zip
= args
.Get("archivebuild-output");
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();
563 const double res
= timer_Resolution();
564 g_frequencyFilter
= CreateFrequencyFilter(res
, 30.0);
567 int flags
= INIT_MODS
;
572 if (!Init(args
, flags
))
575 Shutdown(SHUTDOWN_FROM_CONFIG
);
587 InitGraphics(args
, 0);
588 MainControllerInit();
594 MainControllerShutdown();
598 if (restart_in_atlas
)
599 ATLAS_RunIfOnCmdLine(args
, true);
601 CXeromyces::Terminate();
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
608 #define main pyrogenesis_main
609 extern "C" __attribute__((visibility ("default"))) int main(int argc
, char* argv
[]);
612 extern "C" int main(int argc
, char* argv
[])
615 // Don't allow people to run the game with root permissions,
616 // because bad things can happen, check before we do anything
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";
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();