1 #ifdef HAVE_CONFIG_H
2 # include <config.h>
3 #endif
4 #include "HumanClientApp.h"
5 
6 #include "HumanClientFSM.h"
7 #include "../../UI/ChatWnd.h"
8 #include "../../UI/CUIControls.h"
9 #include "../../UI/CUIStyle.h"
10 #include "../../UI/MapWnd.h"
11 #include "../../UI/DesignWnd.h"
12 #include "../../UI/Hotkeys.h"
13 #include "../../UI/IntroScreen.h"
14 #include "../../UI/GalaxySetupWnd.h"
15 #include "../../UI/MultiplayerLobbyWnd.h"
16 #include "../../UI/SaveFileDialog.h"
17 #include "../../UI/ServerConnectWnd.h"
18 #include "../../UI/Sound.h"
19 #include "../../network/Message.h"
20 #include "../ClientNetworking.h"
21 #include "../../util/i18n.h"
22 #include "../../util/LoggerWithOptionsDB.h"
23 #include "../../util/GameRules.h"
24 #include "../../util/OptionsDB.h"
25 #include "../../util/Process.h"
26 #include "../../util/SaveGamePreviewUtils.h"
27 #include "../../util/Serialize.h"
28 #include "../../util/SitRepEntry.h"
29 #include "../../util/Directories.h"
30 #include "../../util/Version.h"
31 #include "../../util/ScopedTimer.h"
32 #include "../../universe/Planet.h"
33 #include "../../universe/Species.h"
34 #include "../../universe/Enums.h"
35 #include "../../Empire/Empire.h"
36 #include "../../combat/CombatLogManager.h"
37 #include "../../parse/Parse.h"
38 
39 #include <GG/BrowseInfoWnd.h>
40 #include <GG/dialogs/ThreeButtonDlg.h>
41 #include <GG/Cursor.h>
42 #include <GG/utf8/checked.h>
43 
44 #include <boost/algorithm/string/replace.hpp>
45 #include <boost/algorithm/string/trim.hpp>
46 #include <boost/lexical_cast.hpp>
47 #include <boost/filesystem/operations.hpp>
48 #include <boost/filesystem/fstream.hpp>
49 #include <boost/format.hpp>
50 #include <boost/optional/optional.hpp>
51 #include <boost/serialization/vector.hpp>
52 #include <boost/algorithm/string.hpp>
53 #include <boost/uuid/uuid_io.hpp>
54 #include <boost/uuid/nil_generator.hpp>
55 #include <boost/uuid/string_generator.hpp>
56 
57 #include <chrono>
58 #include <thread>
59 
60 #include <sstream>
61 
62 
63 namespace fs = boost::filesystem;
64 
65 #ifdef ENABLE_CRASH_BACKTRACE
66 # include <signal.h>
67 # include <execinfo.h>
68 # include <sys/types.h>
69 # include <sys/stat.h>
70 # include <fcntl.h>
71 # include <unistd.h>
72 
SigHandler(int sig)73 void SigHandler(int sig) {
74     void* backtrace_buffer[100];
75     int num;
76     int fd;
77 
78     signal(sig, SIG_DFL);
79     fd = open("crash.txt",O_WRONLY|O_CREAT|O_APPEND|O_SYNC,0666);
80     if (fd != -1) {
81         write(fd, "--- New crash backtrace begins here ---\n", 24);
82         num = backtrace(backtrace_buffer, 100);
83         backtrace_symbols_fd(backtrace_buffer, num, fd);
84         backtrace_symbols_fd(backtrace_buffer, num, 2);
85         close(fd);
86     }
87 
88     // Now we try to display a MessageBox; this might fail and also
89     // corrupt the heap, but since we're dying anyway that's no big
90     // deal
91 
92     ClientUI::MessageBox("The client has just crashed!\nFile a bug report and\nattach the file called 'crash.txt'\nif necessary", true);
93 
94     raise(sig);
95 }
96 #endif //ENABLE_CRASH_BACKTRACE
97 
98 namespace {
99     const bool          INSTRUMENT_MESSAGE_HANDLING = false;
100 
101     // command-line options
AddOptions(OptionsDB & db)102     void AddOptions(OptionsDB& db) {
103         db.Add("save.auto.turn.start.enabled",              UserStringNop("OPTIONS_DB_AUTOSAVE_SINGLE_PLAYER_TURN_START"),  true,               Validator<bool>());
104         db.Add("save.auto.turn.end.enabled",                UserStringNop("OPTIONS_DB_AUTOSAVE_SINGLE_PLAYER_TURN_END"),    false,              Validator<bool>());
105         db.Add("save.auto.turn.multiplayer.start.enabled",  UserStringNop("OPTIONS_DB_AUTOSAVE_MULTIPLAYER_TURN_START"),    true,               Validator<bool>());
106         db.Add("save.auto.turn.interval",                   UserStringNop("OPTIONS_DB_AUTOSAVE_TURNS"),                     1,                  RangedValidator<int>(1, 50));
107         db.Add("save.auto.file.limit",                      UserStringNop("OPTIONS_DB_AUTOSAVE_LIMIT"),                     10,                 RangedValidator<int>(1, 10000));
108         db.Add("save.auto.initial.enabled",                 UserStringNop("OPTIONS_DB_AUTOSAVE_GALAXY_CREATION"),           true,               Validator<bool>());
109         db.Add("ui.input.mouse.button.swap.enabled",        UserStringNop("OPTIONS_DB_UI_MOUSE_LR_SWAP"),                   false);
110         db.Add("ui.input.keyboard.repeat.delay",            UserStringNop("OPTIONS_DB_KEYPRESS_REPEAT_DELAY"),              360,                RangedValidator<int>(0, 1000));
111         db.Add("ui.input.keyboard.repeat.interval",         UserStringNop("OPTIONS_DB_KEYPRESS_REPEAT_INTERVAL"),           20,                 RangedValidator<int>(0, 1000));
112         db.Add("ui.input.mouse.button.repeat.delay",        UserStringNop("OPTIONS_DB_MOUSE_REPEAT_DELAY"),                 360,                RangedValidator<int>(0, 1000));
113         db.Add("ui.input.mouse.button.repeat.interval",     UserStringNop("OPTIONS_DB_MOUSE_REPEAT_INTERVAL"),              15,                 RangedValidator<int>(0, 1000));
114         db.Add("ui.map.messages.timestamp.shown",           UserStringNop("OPTIONS_DB_DISPLAY_TIMESTAMP"),                  true,               Validator<bool>());
115 
116         Hotkey::AddHotkey("exit",                           UserStringNop("HOTKEY_EXIT"),                                   GG::GGK_NONE,       GG::MOD_KEY_NONE);
117         Hotkey::AddHotkey("quit",                           UserStringNop("HOTKEY_QUIT"),                                   GG::GGK_NONE,       GG::MOD_KEY_NONE);
118         Hotkey::AddHotkey("video.fullscreen",               UserStringNop("HOTKEY_FULLSCREEN"),                             GG::GGK_RETURN,     GG::MOD_KEY_ALT);
119     }
120     bool temp_bool = RegisterOptions(&AddOptions);
121 
122     /** These options can only be validated after the graphics system (SDL) is initialized,
123         so that display size can be detected
124      */
125     const int DEFAULT_WIDTH = 1024;
126     const int DEFAULT_HEIGHT = 768;
127     const int DEFAULT_LEFT = static_cast<int>(SDL_WINDOWPOS_CENTERED);
128     const int DEFAULT_TOP = 50;
129     const int MIN_WIDTH = 800;
130     const int MIN_HEIGHT = 600;
131 
132     /** Sets the default and current values for the string option @p option_name to @p option_value if initially empty */
SetEmptyStringDefaultOption(const std::string & option_name,const std::string & option_value)133     void SetEmptyStringDefaultOption(const std::string& option_name, const std::string& option_value) {
134         OptionsDB& db = GetOptionsDB();
135         if (db.Get<std::string>(option_name).empty()) {
136             db.SetDefault<std::string>(option_name, option_value);
137             db.Set(option_name, option_value);
138         }
139     }
140 
141     /* Sets the value of options that need language-dependent default values.*/
SetStringtableDependentOptionDefaults()142     void SetStringtableDependentOptionDefaults() {
143         SetEmptyStringDefaultOption("setup.empire.name", UserString("DEFAULT_EMPIRE_NAME"));
144         std::string player_name = UserString("DEFAULT_PLAYER_NAME");
145         SetEmptyStringDefaultOption("setup.player.name", player_name);
146         SetEmptyStringDefaultOption("setup.multiplayer.player.name", player_name);
147     }
148 
GetGLVersionString()149     std::string GetGLVersionString()
150     { return boost::lexical_cast<std::string>(glGetString(GL_VERSION)); }
151 
152     static float stored_gl_version = -1.0f;  // to be replaced when gl version first checked
153 
GetGLVersion()154     float GetGLVersion() {
155         if (stored_gl_version != -1.0f)
156             return stored_gl_version;
157 
158         // get OpenGL version string and parse to get version number
159         std::string gl_version_string = GetGLVersionString();
160 
161         float version_number = 0.0f;
162         std::istringstream iss(gl_version_string);
163         iss >> version_number;
164         version_number += 0.05f;    // ensures proper rounding of 1.1 digit number
165 
166         stored_gl_version = version_number;
167 
168         return stored_gl_version;
169     }
170 
SetGLVersionDependentOptionDefaults()171     void SetGLVersionDependentOptionDefaults() {
172         // get OpenGL version string and parse to get version number
173         float version_number = GetGLVersion();
174         DebugLogger() << "OpenGL Version Number: " << DoubleToString(version_number, 2, false);
175         if (version_number < 2.0) {
176             ErrorLogger() << "OpenGL Version is less than 2.0. FreeOrion may crash when trying to start a game.";
177         }
178 
179         // only execute default option setting once
180         if (GetOptionsDB().Get<bool>("version.gl.check.done"))
181             return;
182         GetOptionsDB().Set<bool>("version.gl.check.done", true);
183 
184         // if GL version is too low, set various map rendering options to
185         // disabled, to hopefully improve frame rate.
186         if (version_number < 2.0) {
187             GetOptionsDB().Set<bool>("ui.map.background.gas.shown", false);
188             GetOptionsDB().Set<bool>("ui.map.background.starfields.shown", false);
189             GetOptionsDB().Set<bool>("ui.map.scanlines.shown", false);
190         }
191     }
192 }
193 
AddWindowSizeOptionsAfterMainStart(OptionsDB & db)194 void HumanClientApp::AddWindowSizeOptionsAfterMainStart(OptionsDB& db) {
195     const int max_width_plus_one = HumanClientApp::MaximumPossibleWidth() + 1;
196     const int max_height_plus_one = HumanClientApp::MaximumPossibleHeight() + 1;
197 
198     db.Add("video.fullscreen.width", UserStringNop("OPTIONS_DB_APP_WIDTH"),             DEFAULT_WIDTH,  RangedValidator<int>(MIN_WIDTH, max_width_plus_one));
199     db.Add("video.fullscreen.height", UserStringNop("OPTIONS_DB_APP_HEIGHT"),           DEFAULT_HEIGHT, RangedValidator<int>(MIN_HEIGHT, max_height_plus_one));
200     db.Add("video.windowed.width",  UserStringNop("OPTIONS_DB_APP_WIDTH_WINDOWED"),     DEFAULT_WIDTH,  RangedValidator<int>(MIN_WIDTH, max_width_plus_one));
201     db.Add("video.windowed.height", UserStringNop("OPTIONS_DB_APP_HEIGHT_WINDOWED"),    DEFAULT_HEIGHT, RangedValidator<int>(MIN_HEIGHT, max_height_plus_one));
202     db.Add("video.windowed.left", UserStringNop("OPTIONS_DB_APP_LEFT_WINDOWED"),        DEFAULT_LEFT,   OrValidator<int>( RangedValidator<int>(-max_width_plus_one, max_width_plus_one), DiscreteValidator<int>(DEFAULT_LEFT) ));
203     db.Add("video.windowed.top", UserStringNop("OPTIONS_DB_APP_TOP_WINDOWED"),          DEFAULT_TOP,    RangedValidator<int>(-max_height_plus_one, max_height_plus_one));
204 }
205 
EncodeServerAddressOption(const std::string & server)206 std::string HumanClientApp::EncodeServerAddressOption(const std::string& server) {
207     std::string server_encoded = boost::replace_all_copy(server, ".", "_");
208     boost::replace_all(server_encoded, ":", "_");
209     return "network.known-servers._" + server_encoded;
210 }
211 
HumanClientApp(int width,int height,bool calculate_fps,const std::string & name,int x,int y,bool fullscreen,bool fake_mode_change)212 HumanClientApp::HumanClientApp(int width, int height, bool calculate_fps, const std::string& name,
213                                int x, int y, bool fullscreen, bool fake_mode_change) :
214     ClientApp(),
215     SDLGUI(width, height, calculate_fps, name, x, y, fullscreen, fake_mode_change)
216 {
217 #ifdef ENABLE_CRASH_BACKTRACE
218     signal(SIGSEGV, SigHandler);
219 #endif
220 #ifdef FREEORION_MACOSX
221     SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");
222 #endif
223     m_fsm.reset(new HumanClientFSM(*this));
224 
225     // Force the log file if requested.
226     if (GetOptionsDB().Get<std::string>("log-file").empty()) {
227         const std::string HUMAN_CLIENT_LOG_FILENAME((GetUserDataDir() / "freeorion.log").string());
228         GetOptionsDB().Set("log-file", HUMAN_CLIENT_LOG_FILENAME);
229     }
230     // Force the log threshold if requested.
231     auto force_log_level = GetOptionsDB().Get<std::string>("log-level");
232     if (!force_log_level.empty())
233         OverrideAllLoggersThresholds(to_LogLevel(force_log_level));
234 
235     InitLoggingSystem(GetOptionsDB().Get<std::string>("log-file"), "Client");
236     InitLoggingOptionsDBSystem();
237 
238     // Force loggers to always appear in the config.xml and OptionsWnd even before their
239     // initialization on first use.
240     // This is not needed for the loggers to work correctly.
241     // this is not needed for the loggers to automatically be added to the config.xml on
242     // first use.
243     // This only needs to be done in one of the executables connected to the same config.xml
244     RegisterLoggerWithOptionsDB("ai", true);
245     RegisterLoggerWithOptionsDB("client", true);
246     RegisterLoggerWithOptionsDB("server", true);
247     RegisterLoggerWithOptionsDB("combat_log");
248     RegisterLoggerWithOptionsDB("combat");
249     RegisterLoggerWithOptionsDB("supply");
250     RegisterLoggerWithOptionsDB("effects");
251     RegisterLoggerWithOptionsDB("conditions");
252     RegisterLoggerWithOptionsDB("FSM");
253     RegisterLoggerWithOptionsDB("network");
254     RegisterLoggerWithOptionsDB("python");
255     RegisterLoggerWithOptionsDB("timer");
256     RegisterLoggerWithOptionsDB("IDallocator");
257 
258     InfoLogger() << FreeOrionVersionString();
259 
260     try {
261         InfoLogger() << "GL Version String: " << GetGLVersionString();
262     } catch (...) {
263         ErrorLogger() << "Unable to get GL Version String?";
264     }
265 
266     LogDependencyVersions();
267 
268     float version_number = GetGLVersion();
269     if (version_number < 2.0f) {
270         ErrorLogger() << "OpenGL version is less than 2; FreeOrion will likely crash while starting up...";
271         if (!GetOptionsDB().Get<bool>("version.gl.check.done")) {
272             auto mb_result = SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, UserString("OPENGL_VERSION_LOW_TITLE").c_str(),
273                                                       UserString("OPENGL_VERSION_LOW_TEXT").c_str(), nullptr);
274             if (mb_result)
275                 std::cerr << "OpenGL version is less than 2; FreeOrion will likely crash during initialization";
276         }
277     } else if (version_number < 2.1f) {
278         ErrorLogger() << "OpenGL version is less than 2.1; FreeOrion may crash during initialization";
279     }
280 
281     SetStyleFactory(std::make_shared<CUIStyle>());
282 
283     SetMinDragTime(0);
284 
285     bool inform_user_sound_failed(false);
286     try {
287         if (GetOptionsDB().Get<bool>("audio.effects.enabled") || GetOptionsDB().Get<bool>("audio.music.enabled"))
288             Sound::GetSound().Enable();
289 
290         if ((GetOptionsDB().Get<bool>("audio.music.enabled")))
291             Sound::GetSound().PlayMusic(GetOptionsDB().Get<std::string>("audio.music.path"), -1);
292 
293         Sound::GetSound().SetMusicVolume(GetOptionsDB().Get<int>("audio.music.volume"));
294         Sound::GetSound().SetUISoundsVolume(GetOptionsDB().Get<int>("audio.effects.volume"));
295     } catch (const Sound::InitializationFailureException&) {
296         inform_user_sound_failed = true;
297     }
298 
299     m_ui.reset(new ClientUI());
300 
301     EnableFPS();
302     UpdateFPSLimit();
303     GetOptionsDB().OptionChangedSignal("video.fps.shown").connect(
304         boost::bind(&HumanClientApp::UpdateFPSLimit, this));
305     GetOptionsDB().OptionChangedSignal("video.fps.max").connect(
306         boost::bind(&HumanClientApp::UpdateFPSLimit, this));
307 
308     std::shared_ptr<GG::BrowseInfoWnd> default_browse_info_wnd(
309         GG::Wnd::Create<GG::TextBoxBrowseInfoWnd>(
310             GG::X(400), ClientUI::GetFont(),
311             GG::Clr(0, 0, 0, 200), ClientUI::WndOuterBorderColor(), ClientUI::TextColor(),
312             GG::FORMAT_LEFT | GG::FORMAT_WORDBREAK, 1));
313     GG::Wnd::SetDefaultBrowseInfoWnd(default_browse_info_wnd);
314 
315     auto cursor_texture = m_ui->GetTexture(ClientUI::ArtDir() / "cursors" / "default_cursor.png");
316     SetCursor(std::make_shared<GG::TextureCursor>(cursor_texture, GG::Pt(GG::X(6), GG::Y(3))));
317     RenderCursor(true);
318 
319     EnableKeyPressRepeat(GetOptionsDB().Get<int>("ui.input.keyboard.repeat.delay"),
320                          GetOptionsDB().Get<int>("ui.input.keyboard.repeat.interval"));
321     EnableMouseButtonDownRepeat(GetOptionsDB().Get<int>("ui.input.mouse.button.repeat.delay"),
322                                 GetOptionsDB().Get<int>("ui.input.mouse.button.repeat.interval"));
323     EnableModalAcceleratorSignals(true);
324 
325 #if BOOST_VERSION >= 106000
326     using boost::placeholders::_1;
327     using boost::placeholders::_2;
328 #endif
329 
330     WindowResizedSignal.connect(boost::bind(&HumanClientApp::HandleWindowResize,this, _1, _2));
331     FocusChangedSignal.connect( boost::bind(&HumanClientApp::HandleFocusChange, this, _1));
332     WindowMovedSignal.connect(  boost::bind(&HumanClientApp::HandleWindowMove,  this, _1, _2));
333     WindowClosingSignal.connect(boost::bind(&HumanClientApp::HandleAppQuitting, this));
334     AppQuittingSignal.connect(  boost::bind(&HumanClientApp::HandleAppQuitting, this));
335 
336     SetStringtableDependentOptionDefaults();
337     SetGLVersionDependentOptionDefaults();
338 
339     this->SetMouseLRSwapped(GetOptionsDB().Get<bool>("ui.input.mouse.button.swap.enabled"));
340 
341     ConnectKeyboardAcceleratorSignals();
342 
343     m_auto_turns = GetOptionsDB().Get<int>("auto-advance-n-turns");
344 
345     if (fake_mode_change && !FramebuffersAvailable()) {
346         ErrorLogger() << "Requested fake mode changes, but the framebuffer opengl extension is not available. Ignoring.";
347     }
348 
349     // Placed after mouse initialization.
350     if (inform_user_sound_failed)
351         ClientUI::MessageBox(UserString("ERROR_SOUND_INITIALIZATION_FAILED"), false);
352 
353     // Register LinkText tags with GG::Font
354     RegisterLinkTags();
355 
356     m_fsm->initiate();
357 
358     // Start parsing content
359     StartBackgroundParsing();
360     GetOptionsDB().OptionChangedSignal("resource.path").connect(
361         boost::bind(&HumanClientApp::HandleResoureDirChange, this));
362 }
363 
ConnectKeyboardAcceleratorSignals()364 void HumanClientApp::ConnectKeyboardAcceleratorSignals() {
365     // Add global hotkeys
366     HotkeyManager *hkm = HotkeyManager::GetManager();
367 
368     hkm->Connect(boost::bind(&HumanClientApp::HandleHotkeyExitApp, this), "exit",
369                  NoModalWndsOpenCondition);
370     hkm->Connect(boost::bind(&HumanClientApp::HandleHotkeyResetGame, this), "quit",
371                  NoModalWndsOpenCondition);
372     hkm->Connect(boost::bind(&HumanClientApp::ToggleFullscreen, this), "video.fullscreen",
373                  NoModalWndsOpenCondition);
374 
375     hkm->RebuildShortcuts();
376 }
377 
~HumanClientApp()378 HumanClientApp::~HumanClientApp() {
379     m_networking->DisconnectFromServer();
380     m_server_process.RequestTermination();
381     DebugLogger() << "HumanClientApp exited cleanly.";
382 }
383 
SinglePlayerGame() const384 bool HumanClientApp::SinglePlayerGame() const
385 { return m_single_player_game; }
386 
CanSaveNow() const387 bool HumanClientApp::CanSaveNow() const {
388     // only host can save in multiplayer
389     if (!SinglePlayerGame() && !Networking().PlayerIsHost(PlayerID()))
390         return false;
391 
392     // can't save while AIs are playing their turns...
393     for (const auto& entry : Empires()) {
394         if (GetEmpireClientType(entry.first) != Networking::CLIENT_TYPE_AI_PLAYER)
395             continue;   // only care about AIs
396 
397         if (!entry.second->Ready()) {
398             return false;
399         }
400     }
401 
402     return true;
403 }
404 
SetSinglePlayerGame(bool sp)405 void HumanClientApp::SetSinglePlayerGame(bool sp/* = true*/)
406 { m_single_player_game = sp; }
407 
408 namespace {
ServerClientExe()409     std::string ServerClientExe() {
410 #ifdef FREEORION_WIN32
411         return PathToString(GetBinDir() / "freeoriond.exe");
412 #else
413         return (GetBinDir() / "freeoriond").string();
414 #endif
415     }
416 }
417 
418 #ifdef FREEORION_MACOSX
419 #include <stdlib.h>
420 #endif
421 
422 namespace {
423     class LocalServerAlreadyRunningException : public std::runtime_error {
424     public:
LocalServerAlreadyRunningException()425         LocalServerAlreadyRunningException() :
426             std::runtime_error("LOCAL_SERVER_ALREADY_RUNNING_ERROR")
427         {}
428     };
429 
ClearPreviousPendingSaves(std::queue<std::string> & pending_saves)430     void ClearPreviousPendingSaves(std::queue<std::string>& pending_saves) {
431         if (pending_saves.empty())
432             return;
433         WarnLogger() << "Clearing " << std::to_string(pending_saves.size()) << " pending save game request(s)";
434         std::queue<std::string>().swap(pending_saves);
435     }
436 }
437 
StartServer()438 void HumanClientApp::StartServer() {
439     if (m_networking->PingLocalHostServer(std::chrono::milliseconds(100))) {
440         ErrorLogger() << "Can't start local server because a server is already connecting at 127.0.0.0.";
441         throw LocalServerAlreadyRunningException();
442     }
443 
444     std::string SERVER_CLIENT_EXE = ServerClientExe();
445     DebugLogger() << "HumanClientApp::StartServer: " << SERVER_CLIENT_EXE;
446 
447 #ifdef FREEORION_MACOSX
448     // On OSX set environment variable DYLD_LIBRARY_PATH to python framework folder
449     // bundled with app, so the dynamic linker uses the bundled python library.
450     // Otherwise the dynamic linker will look for a correct python lib in system
451     // paths, and if it can't find it, throw an error and terminate!
452     // Setting environment variable here, spawned child processes will inherit it.
453     setenv("DYLD_LIBRARY_PATH", GetPythonHome().string().c_str(), 1);
454 #endif
455 
456     std::vector<std::string> args;
457     std::string ai_config = GetOptionsDB().Get<std::string>("ai-config");
458     std::string ai_path = GetOptionsDB().Get<std::string>("ai-path");
459     args.push_back("\"" + SERVER_CLIENT_EXE + "\"");
460     args.push_back("--resource.path");
461     args.push_back("\"" + GetOptionsDB().Get<std::string>("resource.path") + "\"");
462 
463     auto force_log_level = GetOptionsDB().Get<std::string>("log-level");
464     if (!force_log_level.empty()) {
465         args.push_back("--log-level");
466         args.push_back(GetOptionsDB().Get<std::string>("log-level"));
467     }
468 
469     if (ai_path != GetOptionsDB().GetDefaultValueString("ai-path")) {
470         args.push_back("--ai-path");
471         args.push_back(ai_path);
472         DebugLogger() << "ai-path set to '" << ai_path << "'";
473     }
474     if (!ai_config.empty()) {
475         args.push_back("--ai-config");
476         args.push_back(ai_config);
477         DebugLogger() << "ai-config set to '" << ai_config << "'";
478     } else {
479         DebugLogger() << "ai-config not set.";
480     }
481     if (m_single_player_game) {
482         args.push_back("--singleplayer");
483         args.push_back("--skip-checksum");
484     }
485     DebugLogger() << "Launching server process with args: ";
486     for (auto arg : args)
487         DebugLogger() << arg;
488     m_server_process = Process(SERVER_CLIENT_EXE, args);
489     DebugLogger() << "... finished launching server process.";
490 }
491 
FreeServer()492 void HumanClientApp::FreeServer() {
493     m_server_process.Free();
494     m_networking->SetPlayerID(Networking::INVALID_PLAYER_ID);
495     m_networking->SetHostPlayerID(Networking::INVALID_PLAYER_ID);
496     SetEmpireID(ALL_EMPIRES);
497 }
498 
NewSinglePlayerGame(bool quickstart)499 void HumanClientApp::NewSinglePlayerGame(bool quickstart) {
500     TraceLogger() << "HumanClientApp::NewSinglePlayerGame start";
501     ClearPreviousPendingSaves(m_game_saves_in_progress);
502 
503     if (!GetOptionsDB().Get<bool>("network.server.external.force")) {
504         m_single_player_game = true;
505         try {
506             StartServer();
507         } catch (const LocalServerAlreadyRunningException& err) {
508             ClientUI::MessageBox(UserString("LOCAL_SERVER_ALREADY_RUNNING_ERROR"), true);
509             return;
510         } catch (const std::runtime_error& err) {
511             ErrorLogger() << "HumanClientApp::NewSinglePlayerGame : Couldn't start server.  Got error message: " << err.what();
512             ClientUI::MessageBox(UserString("SERVER_WONT_START"), true);
513             return;
514         }
515     }
516 
517     bool ended_with_ok = false;
518     auto game_rules = GetGameRules().GetRulesAsStrings();
519     if (!quickstart) {
520         DebugLogger() << "Initializing galaxy setup window";
521         auto galaxy_wnd = GG::Wnd::Create<GalaxySetupWnd>();
522         TraceLogger() << "Running galaxy setup window";
523         galaxy_wnd->Run();
524         ended_with_ok = galaxy_wnd->EndedWithOk();
525         TraceLogger() << "Setup ran, " << (ended_with_ok ? "ended with OK" : "ended without OK");
526         if (ended_with_ok)
527             game_rules = galaxy_wnd->GetRulesAsStrings();
528         TraceLogger() << "Got rules as strings";
529     }
530 
531 
532     m_connected = m_networking->ConnectToLocalHostServer();
533     if (!m_connected) {
534         DebugLogger() << "Not connected; returning to intro screen and showing timed out error";
535         ResetToIntro(true);
536         ClientUI::MessageBox(UserString("ERR_CONNECT_TIMED_OUT"), true);
537         return;
538     }
539 
540     if (!(quickstart || ended_with_ok)) {
541         ErrorLogger() << "HumanClientApp::NewSinglePlayerGame failed to start new game, killing server.";
542         ResetToIntro(true);
543     }
544 
545     SinglePlayerSetupData setup_data;
546     setup_data.m_new_game = true;
547     setup_data.m_filename.clear();  // not used for new game
548 
549     // get values stored in options from previous time game was run or
550     // from just having run GalaxySetupWnd
551 
552     // GalaxySetupData
553     setup_data.SetSeed(GetOptionsDB().Get<std::string>("setup.seed"));
554     setup_data.m_size =             GetOptionsDB().Get<int>("setup.star.count");
555     setup_data.m_shape =            GetOptionsDB().Get<Shape>("setup.galaxy.shape");
556     setup_data.m_age =              GetOptionsDB().Get<GalaxySetupOption>("setup.galaxy.age");
557     setup_data.m_starlane_freq =    GetOptionsDB().Get<GalaxySetupOption>("setup.starlane.frequency");
558     setup_data.m_planet_density =   GetOptionsDB().Get<GalaxySetupOption>("setup.planet.density");
559     setup_data.m_specials_freq =    GetOptionsDB().Get<GalaxySetupOption>("setup.specials.frequency");
560     setup_data.m_monster_freq =     GetOptionsDB().Get<GalaxySetupOption>("setup.monster.frequency");
561     setup_data.m_native_freq =      GetOptionsDB().Get<GalaxySetupOption>("setup.native.frequency");
562     setup_data.m_ai_aggr =          GetOptionsDB().Get<Aggression>("setup.ai.aggression");
563     setup_data.m_game_rules =       game_rules;
564 
565 
566     // SinglePlayerSetupData contains a map of PlayerSetupData, for
567     // the human and AI players.  Need to compile this information
568     // from the specified human options and number of requested AIs
569 
570     // Human player setup data
571     PlayerSetupData human_player_setup_data;
572     human_player_setup_data.m_player_name = GetOptionsDB().Get<std::string>("setup.player.name");
573     human_player_setup_data.m_empire_name = GetOptionsDB().Get<std::string>("setup.empire.name");
574 
575     // DB stores index into array of available colours, so need to get that array to look up value of index.
576     // if stored value is invalid, use a default colour
577     const std::vector<GG::Clr>& empire_colours = EmpireColors();
578     int colour_index = GetOptionsDB().Get<int>("setup.empire.color.index");
579     if (colour_index >= 0 && colour_index < static_cast<int>(empire_colours.size()))
580         human_player_setup_data.m_empire_color = empire_colours[colour_index];
581     else
582         human_player_setup_data.m_empire_color = GG::CLR_GREEN;
583 
584     human_player_setup_data.m_starting_species_name = GetOptionsDB().Get<std::string>("setup.initial.species");
585     if (human_player_setup_data.m_starting_species_name == "1")
586         human_player_setup_data.m_starting_species_name = "SP_HUMAN";   // kludge / bug workaround for bug with options storage and retreival.  Empty-string options are stored, but read in as "true" boolean, and converted to string equal to "1"
587 
588     if (human_player_setup_data.m_starting_species_name != "RANDOM" &&
589         !GetSpecies(human_player_setup_data.m_starting_species_name))
590     {
591         const SpeciesManager& sm = GetSpeciesManager();
592         if (sm.NumPlayableSpecies() < 1)
593             human_player_setup_data.m_starting_species_name.clear();
594         else
595             human_player_setup_data.m_starting_species_name = sm.playable_begin()->first;
596     }
597 
598     human_player_setup_data.m_save_game_empire_id = ALL_EMPIRES; // not used for new games
599     human_player_setup_data.m_client_type = Networking::CLIENT_TYPE_HUMAN_PLAYER;
600 
601     // add to setup data players
602     setup_data.m_players.push_back(human_player_setup_data);
603 
604 
605     // AI player setup data.  One entry for each requested AI
606     int num_AIs = GetOptionsDB().Get<int>("setup.ai.player.count");
607     for (int ai_i = 1; ai_i <= num_AIs; ++ai_i) {
608         PlayerSetupData ai_setup_data;
609 
610         ai_setup_data.m_player_name = "AI_" + std::to_string(ai_i);
611         ai_setup_data.m_empire_name.clear();                // leave blank, to be set by server in Universe::GenerateEmpires
612         ai_setup_data.m_empire_color = GG::CLR_ZERO;        // to be set by server
613         ai_setup_data.m_starting_species_name.clear();      // leave blank, to be set by server
614         ai_setup_data.m_save_game_empire_id = ALL_EMPIRES;  // not used for new games
615         ai_setup_data.m_client_type = Networking::CLIENT_TYPE_AI_PLAYER;
616 
617         setup_data.m_players.push_back(ai_setup_data);
618     }
619 
620 
621     TraceLogger() << "Sending host SP setup message";
622     m_networking->SendMessage(HostSPGameMessage(setup_data));
623     m_fsm->process_event(HostSPGameRequested());
624     TraceLogger() << "HumanClientApp::NewSinglePlayerGame done";
625 }
626 
MultiPlayerGame()627 void HumanClientApp::MultiPlayerGame() {
628     ClearPreviousPendingSaves(m_game_saves_in_progress);
629 
630     if (m_networking->IsConnected()) {
631         ErrorLogger() << "HumanClientApp::MultiPlayerGame aborting because already connected to a server";
632         return;
633     }
634 
635     auto server_connect_wnd = GG::Wnd::Create<ServerConnectWnd>();
636     server_connect_wnd->Run();
637 
638     std::string server_dest = server_connect_wnd->GetResult().server_dest;
639 
640     if (server_dest.empty())
641         return;
642 
643     if (server_dest == "HOST GAME SELECTED") {
644         if (!GetOptionsDB().Get<bool>("network.server.external.force")) {
645             m_single_player_game = false;
646             try {
647                 StartServer();
648                 FreeServer();
649             } catch (const LocalServerAlreadyRunningException& err) {
650                 ClientUI::MessageBox(UserString("LOCAL_SERVER_ALREADY_RUNNING_ERROR"), true);
651                 return;
652             } catch (const std::runtime_error& err) {
653                 ErrorLogger() << "Couldn't start server.  Got error message: " << err.what();
654                 ClientUI::MessageBox(UserString("SERVER_WONT_START"), true);
655                 return;
656             }
657             server_dest = "localhost";
658         }
659         server_dest = GetOptionsDB().Get<std::string>("network.server.uri");
660     }
661 
662     m_connected = m_networking->ConnectToServer(server_dest);
663     if (!m_connected) {
664         ClientUI::MessageBox(UserString("ERR_CONNECT_TIMED_OUT"), true);
665         if (server_connect_wnd->GetResult().server_dest == "HOST GAME SELECTED")
666             ResetToIntro(true);
667         return;
668     }
669 
670     if (server_connect_wnd->GetResult().server_dest == "HOST GAME SELECTED") {
671         m_networking->SendMessage(HostMPGameMessage(server_connect_wnd->GetResult().player_name));
672         m_fsm->process_event(HostMPGameRequested());
673     } else {
674         boost::uuids::uuid cookie = boost::uuids::nil_uuid();
675         try {
676             std::string cookie_option = EncodeServerAddressOption(server_dest);
677             if (!GetOptionsDB().OptionExists(cookie_option + ".cookie"))
678                 GetOptionsDB().Add<std::string>(cookie_option + ".cookie", "OPTIONS_DB_SERVER_COOKIE", boost::uuids::to_string(cookie));
679             if (!GetOptionsDB().OptionExists(cookie_option + ".address"))
680                 GetOptionsDB().Add<std::string>(cookie_option + ".address", "OPTIONS_DB_SERVER_COOKIE", "");
681             GetOptionsDB().Set(cookie_option + ".address", server_dest);
682             std::string cookie_str = GetOptionsDB().Get<std::string>(cookie_option + ".cookie");
683             boost::uuids::string_generator gen;
684             cookie = gen(cookie_str);
685         } catch(const std::exception& err) {
686             WarnLogger() << "Cann't get cookie for server " << server_dest << ". Get error message"
687                          << err.what();
688             // ignore
689         }
690 
691         m_networking->SendMessage(JoinGameMessage(server_connect_wnd->GetResult().player_name,
692                                                   server_connect_wnd->GetResult().type,
693                                                   cookie));
694         m_fsm->process_event(JoinMPGameRequested());
695     }
696 }
697 
StartMultiPlayerGameFromLobby()698 void HumanClientApp::StartMultiPlayerGameFromLobby()
699 { m_fsm->process_event(StartMPGameClicked()); }
700 
CancelMultiplayerGameFromLobby()701 void HumanClientApp::CancelMultiplayerGameFromLobby()
702 { m_fsm->process_event(CancelMPGameClicked()); }
703 
SaveGame(const std::string & filename)704 void HumanClientApp::SaveGame(const std::string& filename) {
705     m_game_saves_in_progress.push(filename);
706 
707     // Start a save if there is not one in progress
708     if (m_game_saves_in_progress.size() > 1) {
709         DebugLogger() << "Add pending save to queue.";
710         return;
711     }
712 
713     m_networking->SendMessage(HostSaveGameInitiateMessage(filename));
714     DebugLogger() << "Sent save initiate message to server.";
715 }
716 
SaveGameCompleted()717 void HumanClientApp::SaveGameCompleted() {
718     if (!m_game_saves_in_progress.empty())
719         m_game_saves_in_progress.pop();
720 
721     // Either indicate that all saves are completed or start the next save.
722     // Autosaves and player saves can be concurrent.
723     if (m_game_saves_in_progress.empty()) {
724         DebugLogger() << "Save games completed.";
725         SaveGamesCompletedSignal();
726     } else {
727         m_networking->SendMessage(HostSaveGameInitiateMessage(m_game_saves_in_progress.front()));
728         DebugLogger() << "Sent next save initiate message to server.";
729     }
730 }
731 
LoadSinglePlayerGame(std::string filename)732 void HumanClientApp::LoadSinglePlayerGame(std::string filename/* = ""*/) {
733     DebugLogger() << "HumanClientApp::LoadSinglePlayerGame";
734 
735     if (!filename.empty()) {
736         if (!exists(FilenameToPath(filename))) {
737             std::string msg = "HumanClientApp::LoadSinglePlayerGame() given a nonexistent file \""
738                             + filename + "\" to load; aborting.";
739             DebugLogger() << msg;
740             std::cerr << msg << '\n';
741             abort();
742         }
743     } else {
744         try {
745             filename = ClientUI::GetClientUI()->GetFilenameWithSaveFileDialog(
746                 SaveFileDialog::Purpose::Load,
747                 SaveFileDialog::SaveType::SinglePlayer);
748 
749             // Update intro screen Load & Continue buttons if all savegames are deleted.
750             m_ui->GetIntroScreen()->RequirePreRender();
751 
752         } catch (const std::exception& e) {
753             ClientUI::MessageBox(e.what(), true);
754         }
755     }
756 
757     if (filename.empty()) {
758         DebugLogger() << "HumanClientApp::LoadSinglePlayerGame has empty filename. Aborting load.";
759         return;
760     }
761 
762     // end any currently-playing game before loading new one
763     if (m_game_started) {
764         ResetToIntro(true);
765         // delay to make sure old game is fully cleaned up before attempting to start a new one
766         std::this_thread::sleep_for(std::chrono::seconds(3));
767     } else {
768         DebugLogger() << "HumanClientApp::LoadSinglePlayerGame() not already in a game, so don't need to end it";
769     }
770 
771     if (!GetOptionsDB().Get<bool>("network.server.external.force")) {
772         m_single_player_game = true;
773         try {
774             StartServer();
775         } catch (const LocalServerAlreadyRunningException& err) {
776             ClientUI::MessageBox(UserString("LOCAL_SERVER_ALREADY_RUNNING_ERROR"), true);
777             return;
778         } catch (const std::runtime_error& err) {
779             ErrorLogger() << "HumanClientApp::NewSinglePlayerGame : Couldn't start server.  Got error message: " << err.what();
780             ClientUI::MessageBox(UserString("SERVER_WONT_START"), true);
781             return;
782         }
783     } else {
784         DebugLogger() << "HumanClientApp::LoadSinglePlayerGame() assuming external server will be available";
785     }
786 
787     DebugLogger() << "HumanClientApp::LoadSinglePlayerGame() Connecting to server";
788     m_connected = m_networking->ConnectToLocalHostServer();
789     if (!m_connected) {
790         ResetToIntro(true);
791         ClientUI::MessageBox(UserString("ERR_CONNECT_TIMED_OUT"), true);
792         return;
793     }
794 
795     m_networking->SetPlayerID(Networking::INVALID_PLAYER_ID);
796     m_networking->SetHostPlayerID(Networking::INVALID_PLAYER_ID);
797     SetEmpireID(ALL_EMPIRES);
798 
799     SinglePlayerSetupData setup_data;
800     // leving GalaxySetupData information default / blank : not used when loading a game
801     setup_data.m_new_game = false;
802     setup_data.m_filename = filename;
803     // leving setup_data.m_players empty : not specified when loading a game, as server will generate from save file
804 
805 
806     m_networking->SendMessage(HostSPGameMessage(setup_data));
807     m_fsm->process_event(HostSPGameRequested());
808 }
809 
RequestSavePreviews(const std::string & relative_directory)810 void HumanClientApp::RequestSavePreviews(const std::string& relative_directory) {
811     TraceLogger() << "HumanClientApp::RequestSavePreviews directory: " << relative_directory
812                   << " valid UTF-8: " << utf8::is_valid(relative_directory.begin(), relative_directory.end());
813 
814     std::string  generic_directory = relative_directory;
815     if (!m_networking->IsConnected()) {
816         DebugLogger() << "HumanClientApp::RequestSavePreviews: No game running. Start a server for savegame queries.";
817 
818         m_single_player_game = true;
819         try {
820             StartServer();
821         } catch (const LocalServerAlreadyRunningException& err) {
822             ClientUI::MessageBox(UserString("LOCAL_SERVER_ALREADY_RUNNING_ERROR"), true);
823             return;
824         } catch (const std::runtime_error& err) {
825             ErrorLogger() << "HumanClientApp::NewSinglePlayerGame : Couldn't start server.  Got error message: " << err.what();
826             ClientUI::MessageBox(UserString("SERVER_WONT_START"), true);
827             return;
828         }
829 
830         DebugLogger() << "HumanClientApp::RequestSavePreviews Connecting to server";
831         m_connected = m_networking->ConnectToLocalHostServer();
832         if (!m_connected) {
833             ResetToIntro(true);
834             ClientUI::MessageBox(UserString("ERR_CONNECT_TIMED_OUT"), true);
835             return;
836         }
837 
838         // This will only generate an error message and use the server's config.xml
839         // because there is no host client for this temporary server.
840         SendLoggingConfigToServer();
841     }
842     DebugLogger() << "HumanClientApp::RequestSavePreviews Requesting previews for " << generic_directory;
843     m_networking->SendMessage(RequestSavePreviewsMessage(generic_directory));
844 }
845 
GetWindowLeftTop()846 std::pair<int, int> HumanClientApp::GetWindowLeftTop() {
847     int left(0), top(0);
848 
849     left = GetOptionsDB().Get<int>("video.windowed.left");
850     top = GetOptionsDB().Get<int>("video.windowed.top");
851 
852     // clamp to edges to avoid weird bug with maximizing windows setting their
853     // left and top to -9 which lead to weird issues when attmepting to recreate
854     // the window at those positions next execution
855     if (std::abs(left) < 10)
856         left = 0;
857     if (std::abs(top) < 10)
858         top = 0;
859 
860     return {left, top};
861 }
862 
GetWindowWidthHeight()863 std::pair<int, int> HumanClientApp::GetWindowWidthHeight() {
864     int width(800), height(600);
865 
866     bool fullscreen = GetOptionsDB().Get<bool>("video.fullscreen.enabled");
867     if (!fullscreen) {
868         width = GetOptionsDB().Get<int>("video.windowed.width");
869         height = GetOptionsDB().Get<int>("video.windowed.height");
870         return {width, height};
871     }
872 
873     bool reset_fullscreen = GetOptionsDB().Get<bool>("video.fullscreen.reset");
874     if (!reset_fullscreen) {
875         width = GetOptionsDB().Get<int>("video.fullscreen.width");
876         height = GetOptionsDB().Get<int>("video.fullscreen.height");
877         return {width, height};
878     }
879 
880     GetOptionsDB().Set<bool>("video.fullscreen.reset", false);
881     GG::Pt default_resolution = GetDefaultResolutionStatic(GetOptionsDB().Get<int>("video.monitor.id"));
882     GetOptionsDB().Set("video.fullscreen.width", Value(default_resolution.x));
883     GetOptionsDB().Set("video.fullscreen.height", Value(default_resolution.y));
884     GetOptionsDB().Commit();
885     return {Value(default_resolution.x), Value(default_resolution.y)};
886 }
887 
Reinitialize()888 void HumanClientApp::Reinitialize() {
889     bool fullscreen = GetOptionsDB().Get<bool>("video.fullscreen.enabled");
890     bool fake_mode_change = GetOptionsDB().Get<bool>("video.fullscreen.fake.enabled");
891     std::pair<int, int> size = GetWindowWidthHeight();
892 
893     bool fullscreen_transition = Fullscreen() != fullscreen;
894     GG::X old_width = AppWidth();
895     GG::Y old_height = AppHeight();
896 
897     SetVideoMode(GG::X(size.first), GG::Y(size.second), fullscreen, fake_mode_change);
898     if (fullscreen_transition) {
899         FullscreenSwitchSignal(fullscreen); // after video mode is changed but before DoLayout() calls
900     } else if (fullscreen &&
901                (old_width != size.first || old_height != size.second) &&
902                GetOptionsDB().Get<bool>("ui.reposition.auto.enabled"))
903     {
904         // Reposition windows if in fullscreen mode... handled here instead of
905         // HandleWindowResize() because the prev. fullscreen resolution is only
906         // available here.
907         RepositionWindowsSignal();
908     }
909 
910     // HandleWindowResize is already called via this signal sent from
911     // SDLGUI::HandleSystemEvents() when in windowed mode.  This sends the
912     // signal (and hence calls HandleWindowResize()) when in fullscreen mode,
913     // making the signal more consistent...
914     if (fullscreen) {
915         WindowResizedSignal(GG::X(size.first), GG::Y(size.second));
916     }
917 }
918 
GLVersion() const919 float HumanClientApp::GLVersion() const
920 { return GetGLVersion(); }
921 
StartTurn(const SaveGameUIData & ui_data)922 void HumanClientApp::StartTurn(const SaveGameUIData& ui_data) {
923     DebugLogger() << "HumanClientApp::StartTurn";
924 
925     if (const Empire* empire = GetEmpire(EmpireID())) {
926         double RP = empire->ResourceOutput(RE_RESEARCH);
927         double PP = empire->ResourceOutput(RE_INDUSTRY);
928         int turn_number = CurrentTurn();
929         float ratio = (RP/(PP+0.0001));
930         const GG::Clr color = empire->Color();
931         DebugLogger() << "Current Output (turn " << turn_number << ") RP/PP: " << ratio << " (" << RP << "/" << PP << ")";
932         DebugLogger() << "EmpireColors: " << static_cast<int>(color.r)
933                       << " " << static_cast<int>(color.g)
934                       << " " << static_cast<int>(color.b)
935                       << " " << static_cast<int>(color.a);
936     }
937 
938     // Do the turn end autosave.
939     if (m_single_player_game && GetOptionsDB().Get<bool>("save.auto.turn.end.enabled")) {
940         DebugLogger() << "Starting end of turn autosave.";
941         Autosave();
942     }
943 
944     ClientApp::StartTurn(ui_data);
945     m_fsm->process_event(TurnEnded());
946 }
947 
UnreadyTurn()948 void HumanClientApp::UnreadyTurn() {
949     m_networking->SendMessage(UnreadyMessage());
950 }
951 
HandleTurnPhaseUpdate(Message::TurnProgressPhase phase_id)952 void HumanClientApp::HandleTurnPhaseUpdate(Message::TurnProgressPhase phase_id) {
953     ClientApp::HandleTurnPhaseUpdate(phase_id);
954 
955     // Pass updates to message window.
956     GetClientUI().GetMessageWnd()->HandleTurnPhaseUpdate(phase_id);
957 }
958 
HandleSystemEvents()959 void HumanClientApp::HandleSystemEvents() {
960     try {
961         SDLGUI::HandleSystemEvents();
962     } catch (const utf8::invalid_utf8& e) {
963         ErrorLogger() << "UTF-8 error handling system event: " << e.what();
964     }
965     if (m_connected && !m_networking->IsConnected()) {
966         m_connected = false;
967         DisconnectedFromServer();
968     } else if (auto msg = Networking().GetMessage()) {
969         HandleMessage(*msg);
970     }
971 }
972 
RenderBegin()973 void HumanClientApp::RenderBegin() {
974     SDLGUI::RenderBegin();
975     Sound::GetSound().DoFrame();
976 }
977 
HandleMessage(Message & msg)978 void HumanClientApp::HandleMessage(Message& msg) {
979     if (INSTRUMENT_MESSAGE_HANDLING)
980         std::cerr << "HumanClientApp::HandleMessage(" << msg.Type() << ")\n";
981 
982     switch (msg.Type()) {
983     case Message::ERROR_MSG:                m_fsm->process_event(Error(msg));                   break;
984     case Message::HOST_MP_GAME:             m_fsm->process_event(HostMPGame(msg));              break;
985     case Message::HOST_SP_GAME:             m_fsm->process_event(HostSPGame(msg));              break;
986     case Message::JOIN_GAME:                m_fsm->process_event(JoinGame(msg));                break;
987     case Message::HOST_ID:                  m_fsm->process_event(HostID(msg));                  break;
988     case Message::LOBBY_UPDATE:             m_fsm->process_event(LobbyUpdate(msg));             break;
989     case Message::SAVE_GAME_COMPLETE:       m_fsm->process_event(SaveGameComplete(msg));        break;
990     case Message::CHECKSUM:                 m_fsm->process_event(CheckSum(msg));                break;
991     case Message::GAME_START:               m_fsm->process_event(GameStart(msg));               break;
992     case Message::TURN_UPDATE:              m_fsm->process_event(TurnUpdate(msg));              break;
993     case Message::TURN_PARTIAL_UPDATE:      m_fsm->process_event(TurnPartialUpdate(msg));       break;
994     case Message::TURN_PROGRESS:            m_fsm->process_event(TurnProgress(msg));            break;
995     case Message::UNREADY:                  m_fsm->process_event(TurnRevoked(msg));             break;
996     case Message::PLAYER_STATUS:            m_fsm->process_event(::PlayerStatus(msg));          break;
997     case Message::PLAYER_CHAT:              m_fsm->process_event(PlayerChat(msg));              break;
998     case Message::DIPLOMACY:                m_fsm->process_event(Diplomacy(msg));               break;
999     case Message::DIPLOMATIC_STATUS:        m_fsm->process_event(DiplomaticStatusUpdate(msg));  break;
1000     case Message::END_GAME:                 m_fsm->process_event(::EndGame(msg));               break;
1001 
1002     case Message::DISPATCH_COMBAT_LOGS:     m_fsm->process_event(DispatchCombatLogs(msg));      break;
1003     case Message::DISPATCH_SAVE_PREVIEWS:   HandleSaveGamePreviews(msg);                        break;
1004     case Message::AUTH_REQUEST:             m_fsm->process_event(AuthRequest(msg));             break;
1005     case Message::CHAT_HISTORY:             m_fsm->process_event(ChatHistory(msg));             break;
1006     case Message::SET_AUTH_ROLES:           HandleSetAuthRoles(msg);                            break;
1007     case Message::TURN_TIMEOUT:             m_fsm->process_event(TurnTimeout(msg));             break;
1008     case Message::PLAYER_INFO:              m_fsm->process_event(PlayerInfoMsg(msg));           break;
1009     default:
1010         ErrorLogger() << "HumanClientApp::HandleMessage : Received an unknown message type \"" << msg.Type() << "\".";
1011     }
1012 }
1013 
UpdateCombatLogs(const Message & msg)1014 void HumanClientApp::UpdateCombatLogs(const Message& msg){
1015     ScopedTimer timer("HumanClientApp::UpdateCombatLogs");
1016 
1017     // Unpack the combat logs from the message
1018     std::vector<std::pair<int, CombatLog>> logs;
1019     ExtractDispatchCombatLogsMessageData(msg, logs);
1020 
1021     // Update the combat log manager with the completed logs.
1022     for (auto it = logs.begin(); it != logs.end(); ++it)
1023         GetCombatLogManager().CompleteLog(it->first, it->second);
1024 }
1025 
HandleSaveGamePreviews(const Message & msg)1026 void HumanClientApp::HandleSaveGamePreviews(const Message& msg) {
1027     auto sfd = GetClientUI().GetSaveFileDialog();
1028     if (!sfd)
1029         return;
1030 
1031     PreviewInformation previews;
1032     ExtractDispatchSavePreviewsMessageData(msg, previews);
1033     DebugLogger() << "HumanClientApp::RequestSavePreviews Got " << previews.previews.size() << " previews.";
1034 
1035     sfd->SetPreviewList(previews);
1036 }
1037 
HandleSetAuthRoles(const Message & msg)1038 void HumanClientApp::HandleSetAuthRoles(const Message& msg) {
1039     ExtractSetAuthorizationRolesMessage(msg, m_networking->AuthorizationRoles());
1040     DebugLogger() << "New roles: " << m_networking->AuthorizationRoles().Text();
1041 }
1042 
ChangeLoggerThreshold(const std::string & option_name,LogLevel option_value)1043 void HumanClientApp::ChangeLoggerThreshold(const std::string& option_name, LogLevel option_value) {
1044     // Update the logger threshold in OptionsDB
1045     ChangeLoggerThresholdInOptionsDB(option_name, option_value);
1046 
1047     SendLoggingConfigToServer();
1048 }
1049 
SendLoggingConfigToServer()1050 void HumanClientApp::SendLoggingConfigToServer() {
1051     // If not host then done.
1052     if (!m_networking->PlayerIsHost(Networking().PlayerID()))
1053         return;
1054 
1055     // Host updates the server
1056     const auto sources = LoggerOptionsLabelsAndLevels(LoggerTypes::both);
1057 
1058     m_networking->SendMessage(LoggerConfigMessage(PlayerID(), sources));
1059 }
1060 
HandleWindowMove(GG::X w,GG::Y h)1061 void HumanClientApp::HandleWindowMove(GG::X w, GG::Y h) {
1062     if (!Fullscreen()) {
1063         GetOptionsDB().Set<int>("video.windowed.left", Value(w));
1064         GetOptionsDB().Set<int>("video.windowed.top", Value(h));
1065         GetOptionsDB().Commit();
1066     }
1067 }
1068 
HandleWindowResize(GG::X w,GG::Y h)1069 void HumanClientApp::HandleWindowResize(GG::X w, GG::Y h) {
1070     if (ClientUI* ui = ClientUI::GetClientUI()) {
1071         if (auto&& map_wnd = ui->GetMapWnd())
1072             map_wnd->DoLayout();
1073         if (auto&& intro_screen = ui->GetIntroScreen())
1074             intro_screen->Resize(GG::Pt(w, h));
1075     }
1076 
1077     if (!GetOptionsDB().Get<bool>("video.fullscreen.enabled") &&
1078          (GetOptionsDB().Get<int>("video.windowed.width") != w ||
1079           GetOptionsDB().Get<int>("video.windowed.height") != h))
1080     {
1081         if (GetOptionsDB().Get<bool>("ui.reposition.auto.enabled")) {
1082             // Reposition windows if in windowed mode.
1083             RepositionWindowsSignal();
1084         }
1085         // store resize if window is not full-screen (so that fullscreen
1086         // resolution doesn't overwrite windowed resolution)
1087         GetOptionsDB().Set<int>("video.windowed.width", Value(w));
1088         GetOptionsDB().Set<int>("video.windowed.height", Value(h));
1089     }
1090 
1091     glViewport(0, 0, Value(w), Value(h));
1092 
1093     GetOptionsDB().Commit();
1094 }
1095 
HandleFocusChange(bool gained_focus)1096 void HumanClientApp::HandleFocusChange(bool gained_focus) {
1097     DebugLogger() << "HumanClientApp::HandleFocusChange("
1098                   << (gained_focus ? "Gained Focus" : "Lost Focus")
1099                   << ")";
1100 
1101     m_have_window_focus = gained_focus;
1102 
1103     // limit rendering frequency when defocused to limit CPU use, and disable sound
1104     if (!m_have_window_focus) {
1105         if (GetOptionsDB().Get<bool>("video.fps.unfocused.enabled"))
1106             this->SetMaxFPS(GetOptionsDB().Get<double>("video.fps.unfocused"));
1107         else
1108             this->SetMaxFPS(0.0);
1109 
1110         if (GetOptionsDB().Get<bool>("audio.music.enabled"))
1111             Sound::GetSound().PauseMusic();
1112     }
1113     else {
1114         if (GetOptionsDB().Get<bool>("video.fps.max.enabled"))
1115             this->SetMaxFPS(GetOptionsDB().Get<double>("video.fps.max"));
1116         else
1117             this->SetMaxFPS(0.0);
1118 
1119         if (GetOptionsDB().Get<bool>("audio.music.enabled"))
1120             Sound::GetSound().ResumeMusic();
1121     }
1122 
1123     CancelDragDrop();
1124     ClearEventState();
1125 }
1126 
HandleAppQuitting()1127 void HumanClientApp::HandleAppQuitting() {
1128     DebugLogger() << "HumanClientApp::HandleAppQuitting()";
1129     ExitApp(0);
1130 }
1131 
HandleHotkeyResetGame()1132 bool HumanClientApp::HandleHotkeyResetGame() {
1133     DebugLogger() << "HumanClientApp::HandleHotkeyResetGame()";
1134     ResetToIntro(false);
1135     return true;
1136 }
1137 
HandleHotkeyExitApp()1138 bool HumanClientApp::HandleHotkeyExitApp() {
1139     DebugLogger() << "HumanClientApp::HandleHotkeyExitApp()";
1140     HandleAppQuitting();
1141     // Not reached, but required for HotkeyManager::Connect()
1142     return true;
1143 }
1144 
ToggleFullscreen()1145 bool HumanClientApp::ToggleFullscreen() {
1146     bool fs = GetOptionsDB().Get<bool>("video.fullscreen.enabled");
1147     GetOptionsDB().Set<bool>("video.fullscreen.enabled", !fs);
1148     Reinitialize();
1149     return true;
1150 }
1151 
StartGame(bool is_new_game)1152 void HumanClientApp::StartGame(bool is_new_game) {
1153     m_game_started = true;
1154 
1155     if (auto&& map_wnd = ClientUI::GetClientUI()->GetMapWnd())
1156         map_wnd->ResetEmpireShown();
1157 
1158     ClientUI::GetClientUI()->GetShipDesignManager()->StartGame(EmpireID(), is_new_game);
1159 
1160     UpdateCombatLogManager();
1161 }
1162 
HandleTurnUpdate()1163 void HumanClientApp::HandleTurnUpdate()
1164 { UpdateCombatLogManager(); }
1165 
UpdateCombatLogManager()1166 void HumanClientApp::UpdateCombatLogManager() {
1167     boost::optional<std::vector<int>> incomplete_ids = GetCombatLogManager().IncompleteLogIDs();
1168     if (incomplete_ids) {
1169         for (auto it = incomplete_ids->begin(); it != incomplete_ids->end();) {
1170             // request at most 50 logs per message to avoid trying to allocate too much space to send all at once
1171             std::vector<int> a_few_log_ids;
1172             for (unsigned int count = 0; count < 50 && it != incomplete_ids->end(); ++it, ++count)
1173                 a_few_log_ids.push_back(*it);
1174             m_networking->SendMessage(RequestCombatLogsMessage(a_few_log_ids));
1175         }
1176     }
1177 }
1178 
1179 namespace {
NewestSinglePlayerSavegame()1180     boost::optional<std::string> NewestSinglePlayerSavegame() {
1181         using namespace boost::filesystem;
1182         try {
1183             std::map<std::time_t, path> files_by_write_time;
1184 
1185             auto add_all_savegames_in = [&files_by_write_time](const path& path) {
1186                 if (!is_directory(path))
1187                     return;
1188 
1189                 for (directory_iterator dir_it(path);
1190                      dir_it != directory_iterator(); ++dir_it)
1191                 {
1192                     const auto& file_path = dir_it->path();
1193                     if (!is_regular_file(file_path))
1194                         continue;
1195                     if (file_path.extension() != SP_SAVE_FILE_EXTENSION)
1196                         continue;
1197 
1198                     std::time_t t = last_write_time(file_path);
1199                     files_by_write_time.insert({t, file_path});
1200                 }
1201             };
1202 
1203             // Find all save games in either player or autosaves
1204             add_all_savegames_in(GetSaveDir());
1205             add_all_savegames_in(GetSaveDir() / "auto");
1206 
1207             if (files_by_write_time.empty())
1208                 return boost::none;
1209 
1210             // Return the newest file that has a valid header
1211             for (auto file_it = files_by_write_time.rbegin();
1212                  file_it != files_by_write_time.rend(); ++file_it)
1213             {
1214                 auto file = file_it->second;
1215                 // attempt to load header
1216                 if (SaveFileWithValidHeader(file))
1217                     return PathToString(file);  // load succeeded, return path to OK file
1218             }
1219 
1220             return boost::none;
1221 
1222         } catch (const boost::filesystem::filesystem_error& e) {
1223             ErrorLogger() << "File system error " << e.what() << " while finding newest autosave";
1224             return boost::none;
1225         }
1226     }
1227 
RemoveOldestFiles(int files_limit,boost::filesystem::path & p)1228     void RemoveOldestFiles(int files_limit, boost::filesystem::path& p) {
1229         using namespace boost::filesystem;
1230         try {
1231             if (!is_directory(p))
1232                 return;
1233             if (files_limit < 0)
1234                 return;
1235 
1236             std::multimap<std::time_t, path> files_by_write_time;
1237 
1238             for (directory_iterator dir_it(p); dir_it != directory_iterator(); ++dir_it) {
1239                 const path& file_path = dir_it->path();
1240                 if (!is_regular_file(file_path))
1241                     continue;
1242                 if (file_path.extension() != SP_SAVE_FILE_EXTENSION &&
1243                     file_path.extension() != MP_SAVE_FILE_EXTENSION)
1244                 { continue; }
1245 
1246                 std::time_t t = last_write_time(file_path);
1247                 files_by_write_time.insert({t, file_path});
1248             }
1249 
1250             //DebugLogger() << "files by write time:";
1251             //for (auto& entry : files_by_write_time)
1252             //{ DebugLogger() << entry.first << " : " << entry.second.filename(); }
1253 
1254             int num_to_delete = files_by_write_time.size() - files_limit + 1;   // +1 because will add a new file after deleting, bringing number back up to limit
1255             if (num_to_delete <= 0)
1256                 return; // don't need to delete anything.
1257 
1258             int num_deleted = 0;
1259             for (auto& entry : files_by_write_time) {
1260                 if (num_deleted >= num_to_delete)
1261                     break;
1262                 remove(entry.second);
1263                 ++num_deleted;
1264             }
1265         } catch (...) {
1266             ErrorLogger() << "Error removing oldest files";
1267         }
1268     }
1269 
CreateNewAutosaveFilePath(int client_empire_id,bool is_single_player)1270     boost::filesystem::path CreateNewAutosaveFilePath(int client_empire_id, bool is_single_player) {
1271         const char* legal_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-";
1272 
1273         // get empire name, filtered for filename acceptability
1274         const Empire* empire = GetEmpire(client_empire_id);
1275         std::string empire_name;
1276         if (empire)
1277             empire_name = empire->Name();
1278         else
1279             empire_name = UserString("OBSERVER");
1280         std::string::size_type first_good_empire_char = empire_name.find_first_of(legal_chars);
1281         if (first_good_empire_char == std::string::npos) {
1282             empire_name.clear();
1283         } else {
1284             std::string::size_type first_bad_empire_char = empire_name.find_first_not_of(legal_chars, first_good_empire_char);
1285             empire_name = empire_name.substr(first_good_empire_char, first_bad_empire_char - first_good_empire_char);
1286         }
1287 
1288         // get player name, also filtered
1289         std::string player_name;
1290         if (empire)
1291             player_name = empire->PlayerName();
1292         std::string::size_type first_good_player_char = player_name.find_first_of(legal_chars);
1293         if (first_good_player_char == std::string::npos) {
1294             player_name.clear();
1295         } else {
1296             std::string::size_type first_bad_player_char = player_name.find_first_not_of(legal_chars, first_good_player_char);
1297             player_name = player_name.substr(first_good_player_char, first_bad_player_char - first_good_player_char);
1298         }
1299 
1300         // select filename extension
1301         const auto& extension = is_single_player ? SP_SAVE_FILE_EXTENSION : MP_SAVE_FILE_EXTENSION;
1302 
1303         // Add timestamp to autosave generated files
1304         std::string datetime_str = FilenameTimestamp();
1305 
1306         boost::filesystem::path autosave_dir_path((is_single_player ? GetSaveDir() : GetServerSaveDir()) / "auto");
1307 
1308         std::string save_filename = boost::io::str(boost::format("FreeOrion_%s_%s_%04d_%s%s") % player_name % empire_name % CurrentTurn() % datetime_str % extension);
1309         boost::filesystem::path save_path(autosave_dir_path / save_filename);
1310 
1311         try {
1312             // ensure autosave directory exists
1313             if (!exists(autosave_dir_path))
1314                 boost::filesystem::create_directories(autosave_dir_path);
1315         } catch (const std::exception& e) {
1316             ErrorLogger() << "Autosave unable to check / create autosave directory: " << e.what();
1317         }
1318 
1319         return save_path;
1320     }
1321 }
1322 
Autosave()1323 void HumanClientApp::Autosave() {
1324     // only host can save in multiplayer
1325     if (!m_single_player_game && !Networking().PlayerIsHost(PlayerID()))
1326         return;
1327 
1328     // Create an auto save for 1) new games on turn 1, 2) if auto save is
1329     // requested on turn number modulo save.auto.turn.interval or 3) on the last turn of
1330     // play.
1331 
1332     // autosave only on appropriate turn numbers, and when enabled for current
1333     // game type (single vs. multiplayer)
1334     int autosave_turns = GetOptionsDB().Get<int>("save.auto.turn.interval");
1335     bool is_single_player_enabled =
1336         (m_single_player_game
1337          && (GetOptionsDB().Get<bool>("save.auto.turn.start.enabled")
1338              || GetOptionsDB().Get<bool>("save.auto.turn.end.enabled")));
1339     bool is_multi_player_enabled =
1340         (!m_single_player_game
1341          && GetOptionsDB().Get<bool>("save.auto.turn.multiplayer.start.enabled"));
1342     bool is_valid_autosave =
1343         (autosave_turns > 0
1344          && CurrentTurn() % autosave_turns == 0
1345          && (is_single_player_enabled || is_multi_player_enabled));
1346 
1347     // is_initial_save is gated in HumanClientFSM for new game vs loaded game
1348     bool is_initial_save = (GetOptionsDB().Get<bool>("save.auto.initial.enabled") && CurrentTurn() == 1);
1349     bool is_final_save = (GetOptionsDB().Get<bool>("save.auto.exit.enabled") && !m_game_started);
1350 
1351     if (!(is_initial_save || is_valid_autosave || is_final_save))
1352         return;
1353 
1354     auto autosave_file_path = CreateNewAutosaveFilePath(EmpireID(), m_single_player_game);
1355 
1356     // check for and remove excess oldest autosaves.
1357     boost::filesystem::path autosave_dir_path((m_single_player_game ? GetSaveDir() : GetServerSaveDir()) / "auto");
1358     int max_turns = std::max(1, GetOptionsDB().Get<int>("save.auto.file.limit"));
1359     bool is_two_saves_per_turn =
1360         (m_single_player_game
1361          && GetOptionsDB().Get<bool>("save.auto.turn.start.enabled")
1362          && GetOptionsDB().Get<bool>("save.auto.turn.end.enabled"))
1363         ||
1364         (!m_single_player_game
1365          && GetOptionsDB().Get<bool>("save.auto.turn.multiplayer.start.enabled"));
1366     int max_autosaves =
1367         (max_turns * (is_two_saves_per_turn ? 2 : 1)
1368          + (GetOptionsDB().Get<bool>("save.auto.initial.enabled") ? 1 : 0)
1369          + (GetOptionsDB().Get<bool>("save.auto.exit.enabled") ? 1 : 0));
1370     RemoveOldestFiles(max_autosaves, autosave_dir_path);
1371 
1372     // create new save
1373     auto path_string = PathToString(autosave_file_path);
1374 
1375     if (is_initial_save)
1376         DebugLogger() << "Turn 0 autosave to: " << path_string;
1377     if (is_valid_autosave)
1378         DebugLogger() << "Autosave to: " << path_string;
1379     if (is_final_save)
1380         DebugLogger() << "End of play autosave to: " << path_string;
1381 
1382     try {
1383         SaveGame(path_string);
1384     } catch (const std::exception& e) {
1385         ErrorLogger() << "Autosave failed: " << e.what();
1386     }
1387 }
1388 
ContinueSinglePlayerGame()1389 void HumanClientApp::ContinueSinglePlayerGame() {
1390     if (const auto file = NewestSinglePlayerSavegame())
1391         LoadSinglePlayerGame(*file);
1392 }
1393 
IsLoadGameAvailable() const1394 bool HumanClientApp::IsLoadGameAvailable() const
1395 { return bool(NewestSinglePlayerSavegame()); }
1396 
SelectLoadFile()1397 std::string HumanClientApp::SelectLoadFile() {
1398     return ClientUI::GetClientUI()->GetFilenameWithSaveFileDialog(
1399         SaveFileDialog::Purpose::Load,
1400         SaveFileDialog::SaveType::MultiPlayer);
1401 }
1402 
ResetClientData(bool save_connection)1403 void HumanClientApp::ResetClientData(bool save_connection) {
1404     if (!save_connection) {
1405         m_networking->SetPlayerID(Networking::INVALID_PLAYER_ID);
1406         m_networking->SetHostPlayerID(Networking::INVALID_PLAYER_ID);
1407     }
1408     SetEmpireID(ALL_EMPIRES);
1409     m_ui->GetMapWnd()->Sanitize();
1410 
1411     m_universe.Clear();
1412     m_empires.Clear();
1413     m_orders.Reset();
1414     GetCombatLogManager().Clear();
1415     ClearPreviousPendingSaves(m_game_saves_in_progress);
1416 }
1417 
ResetToIntro(bool skip_savegame)1418 void HumanClientApp::ResetToIntro(bool skip_savegame)
1419 { ResetOrExitApp(true, skip_savegame); }
1420 
ExitApp(int exit_code)1421 void HumanClientApp::ExitApp(int exit_code)
1422 { ResetOrExitApp(false, false, exit_code); }
1423 
ExitSDL(int exit_code)1424 void HumanClientApp::ExitSDL(int exit_code)
1425 { SDLGUI::ExitApp(exit_code); }
1426 
ResetOrExitApp(bool reset,bool skip_savegame,int exit_code)1427 void HumanClientApp::ResetOrExitApp(bool reset, bool skip_savegame, int exit_code /* = 0*/) {
1428     if (m_exit_handled) {
1429         static int repeat_count = 0;
1430         if (repeat_count++ > 2) {
1431             m_exit_handled = false;
1432             skip_savegame = true;
1433         } else {
1434             return;
1435         }
1436     }
1437     m_exit_handled = true;
1438     DebugLogger() << (reset ? "HumanClientApp::ResetToIntro" : "HumanClientApp::ExitApp");
1439 
1440     auto was_playing = m_game_started;
1441     m_game_started = false;
1442 
1443     // Only save or allow user to cancel if not exiting due to an error.
1444     if (!skip_savegame) {
1445         // Check if this is a multiplayer game and the player has not set status to ready
1446         if (was_playing && !m_single_player_game &&
1447             m_empires.GetEmpire(m_empire_id) != nullptr &&
1448             !m_empires.GetEmpire(m_empire_id)->Ready() &&
1449             GetClientType() == Networking::CLIENT_TYPE_HUMAN_PLAYER)
1450         {
1451             std::shared_ptr<GG::Font> font = ClientUI::GetFont();
1452             auto prompt = GG::GUI::GetGUI()->GetStyleFactory()->NewThreeButtonDlg(
1453                 GG::X(275), GG::Y(75), UserString("GAME_MENU_CONFIRM_NOT_READY"), font,
1454                 ClientUI::CtrlColor(), ClientUI::CtrlBorderColor(), ClientUI::CtrlColor(), ClientUI::TextColor(),
1455                 2, UserString("YES"), UserString("CANCEL"));
1456             prompt->Run();
1457             if (prompt->Result() != 0) {
1458                 // User aborted exit/resign, reset variables
1459                 m_game_started = was_playing;
1460                 m_exit_handled = false;
1461                 return;
1462             }
1463         }
1464 
1465         if (was_playing && GetOptionsDB().Get<bool>("save.auto.exit.enabled"))
1466             Autosave();
1467 
1468         if (!m_game_saves_in_progress.empty()) {
1469             DebugLogger() << "save game in progress. Checking with player.";
1470             // Ask the player if they want to wait for the save game to complete
1471             auto dlg = GG::GUI::GetGUI()->GetStyleFactory()->NewThreeButtonDlg(
1472                 GG::X(320), GG::Y(200), UserString("SAVE_GAME_IN_PROGRESS"),
1473                 ClientUI::GetFont(ClientUI::Pts()+2),
1474                 ClientUI::WndColor(), ClientUI::WndOuterBorderColor(),
1475                 ClientUI::CtrlColor(), ClientUI::TextColor(), 1,
1476                 (reset ?
1477                     UserString("ABORT_SAVE_AND_RESET") :
1478                     UserString("ABORT_SAVE_AND_EXIT")));
1479             // The dialog automatically closes if the save completes while the
1480             // user is waiting
1481             this->SaveGamesCompletedSignal.connect(
1482                 [dlg](){
1483                     DebugLogger() << "SaveGamePendingDialog::SaveCompletedHandler save game completed handled.";
1484 
1485                     dlg->EndRun();
1486                 }
1487             );
1488 
1489             dlg->Run();
1490         }
1491     }
1492 
1493     // Create an action to reset to intro or quit the app as appropriate.
1494     std::function<void()> after_server_shutdown_action;
1495     if (reset)
1496         after_server_shutdown_action = boost::bind(&HumanClientApp::ResetClientData, this, false);
1497     else
1498         // This throws to exit the GUI
1499         after_server_shutdown_action = boost::bind(&HumanClientApp::ExitSDL, this, exit_code);
1500 
1501     m_fsm->process_event(StartQuittingGame(m_server_process, std::move(after_server_shutdown_action)));
1502 
1503     m_exit_handled = false;
1504 }
1505 
InitAutoTurns(int auto_turns)1506 void HumanClientApp::InitAutoTurns(int auto_turns) {
1507     m_auto_turns = auto_turns;
1508     if (!m_game_started || m_auto_turns < 0)
1509         m_auto_turns = 0;
1510 }
1511 
DecAutoTurns(int n)1512 void HumanClientApp::DecAutoTurns(int n)
1513 { InitAutoTurns(m_auto_turns - n); }
1514 
EliminateSelf()1515 void HumanClientApp::EliminateSelf()
1516 { m_networking->SendMessage(EliminateSelfMessage()); }
1517 
AutoTurnsLeft() const1518 int HumanClientApp::AutoTurnsLeft() const
1519 { return m_auto_turns; }
1520 
HaveWindowFocus() const1521 bool HumanClientApp::HaveWindowFocus() const
1522 { return m_have_window_focus; }
1523 
EffectsProcessingThreads() const1524 int HumanClientApp::EffectsProcessingThreads() const
1525 { return GetOptionsDB().Get<int>("effects.ui.threads"); }
1526 
UpdateFPSLimit()1527 void HumanClientApp::UpdateFPSLimit() {
1528     if (GetOptionsDB().Get<bool>("video.fps.max.enabled")) {
1529         double fps = GetOptionsDB().Get<double>("video.fps.max");
1530         SetMaxFPS(fps);
1531         DebugLogger() << "Limited FPS to " << fps;
1532     } else {
1533         SetMaxFPS(0.0); // disable fps limit
1534         DebugLogger() << "Disabled FPS limit";
1535     }
1536 }
1537 
HandleResoureDirChange()1538 void HumanClientApp::HandleResoureDirChange() {
1539     if (!m_game_started) {
1540         DebugLogger() << "Resource directory changed.  Reparsing universe ...";
1541         StartBackgroundParsing();
1542     } else {
1543         WarnLogger() << "Resource directory changes will take effect on application restart.";
1544     }
1545 }
1546 
DisconnectedFromServer()1547 void HumanClientApp::DisconnectedFromServer() {
1548     DebugLogger() << "HumanClientApp::DisconnectedFromServer";
1549     m_fsm->process_event(Disconnection());
1550 }
1551 
GetApp()1552 HumanClientApp* HumanClientApp::GetApp()
1553 { return dynamic_cast<HumanClientApp*>(GG::GUI::GetGUI()); }
1554 
Initialize()1555 void HumanClientApp::Initialize()
1556 {}
1557 
OpenURL(const std::string & url)1558 void HumanClientApp::OpenURL(const std::string& url) {
1559     // make sure it's a legit url
1560     std::string trimmed_url = url;
1561     boost::algorithm::trim(trimmed_url);
1562     // shouldn't be excessively long
1563     if (trimmed_url.size() > 500) { // arbitrary limit
1564         ErrorLogger() << "HumanClientApp::OpenURL given bad-looking url (too long): " << trimmed_url;
1565         return;
1566     }
1567     // should start with http:// or https://
1568     if (trimmed_url.size() < 8) {
1569         ErrorLogger() << "HumanClientApp::OpenURL given bad-looking url (too short): " << trimmed_url;
1570         return;
1571     }
1572     if (trimmed_url.find_first_of("http://") != 0 &&
1573         trimmed_url.find_first_of("https://") != 0)
1574     {
1575         ErrorLogger() << "HumanClientApp::OpenURL given url that doesn't start with http:// :" << trimmed_url;
1576         return;
1577     }
1578     // should not have newlines...
1579     if (trimmed_url.find_first_of("\n") != std::string::npos) {
1580         ErrorLogger() << "HumanClientApp::OpenURL given url that contains a newline. rejecting.";
1581         return;
1582     }
1583 
1584     // append url to OS-specific open command
1585     std::string command;
1586 #ifdef _WIN32
1587     command += "start ";
1588 #elif __APPLE__
1589     command += "open ";
1590 #else
1591     command += "xdg-open ";
1592 #endif
1593     command += url;
1594 
1595     // execute open command
1596     int rv = system(command.c_str());
1597     if (rv != 0)
1598         ErrorLogger() << "HumanClientApp::OpenURL `" << command << "` returned a non-zero exit code: " << rv;
1599 }
1600 
BrowsePath(const boost::filesystem::path & browse_path)1601 void HumanClientApp::BrowsePath(const boost::filesystem::path& browse_path) {
1602     if (browse_path.empty() || browse_path == "/") {
1603         ErrorLogger() << "Invalid path: " << PathToString(browse_path);
1604         return;
1605     }
1606 
1607     boost::filesystem::path full_path(browse_path);
1608 
1609     try {
1610         boost::filesystem::file_status status = boost::filesystem::status(full_path);
1611         if (!boost::filesystem::exists(status)) {
1612             std::string exists_debug_msg("Non-existant path: " + PathToString(full_path));
1613             if (full_path.has_parent_path()) {
1614                 DebugLogger() << exists_debug_msg << ", trying parent directory";
1615                 BrowsePath(full_path.parent_path());
1616             } else {
1617                 DebugLogger() << exists_debug_msg << ", aborting";
1618             }
1619             return;
1620         }
1621 
1622         // Validate as a canonical path
1623         if (boost::filesystem::is_directory(status)) {
1624             full_path = boost::filesystem::canonical(full_path);
1625         } else {
1626             // If given a file, use the files containing directory
1627             DebugLogger() << "Non-directory target: " << PathToString(full_path) << ", using parent directory";
1628             full_path = boost::filesystem::canonical(full_path.parent_path());
1629         }
1630 
1631         // Verify not a regular file
1632         if (boost::filesystem::is_regular_file(full_path)) {
1633             ErrorLogger() << "Target directory " << PathToString(full_path) << " is a regular file, given path argument: "
1634                           << PathToString(browse_path);
1635             return;
1636         }
1637 
1638     } catch (const boost::filesystem::filesystem_error& ec) {
1639         ErrorLogger() << "Filesystem error when attempting to browse directory " << PathToString(full_path)
1640                       << ": " << ec.what();
1641         return;
1642     }
1643 
1644     if (full_path.empty()) {
1645         ErrorLogger() << "Unable to determine directory for path " << PathToString(full_path);
1646         return;
1647     }
1648 
1649     full_path.make_preferred();
1650     // Trailing slash post-fixed to prevent executing a file with same name(minus extension) as folder
1651     full_path += boost::filesystem::path::preferred_separator;
1652     auto target(full_path.native());
1653     decltype(target) command;
1654 
1655     // Double quotes around target to support paths containing spaces
1656     // Non-Windows platforms: Post-fix ampersand to prevent blocking until process exits
1657     // On Windows: the trailing path separator may be interpreted as escaping a double quote.
1658     //    The trailing separator should not be removed, as that poses the risk of executing a file.
1659     //    The trailing separator is escaped by 2 additional back-slashes (total 3).
1660     //    see http://www.windowsinspired.com/how-a-windows-programs-splits-its-command-line-into-individual-arguments/
1661     //
1662     //    Contrary to official documentation for start, the first argument (title) is not always optional.
1663     //    The argument for window title is left as an empty string.
1664     //    see https://ss64.com/nt/start.html
1665 #ifdef _WIN32
1666     command = L"start \"\" \"" + target + L"\\\\\"";
1667 #elif __APPLE__
1668     command = "open \"" + target + "\" &";
1669 #else
1670     command = "xdg-open \"" + target + "\" &";
1671 #endif
1672 
1673 #ifdef _WIN32
1674     std::string u8_command;
1675     utf8::utf16to8(command.begin(), command.end(), std::back_inserter(u8_command));
1676     InfoLogger() << "Sending OS request to browse directory: " << u8_command;
1677     // Flush all streams prior to _wsystem call per https://msdn.microsoft.com/en-us/library/277bwbdz.aspx
1678     std::fflush(NULL);
1679     if (auto sys_retval = _wsystem(command.c_str()))
1680         WarnLogger() << "System call " << u8_command << " returned non-zero value " << sys_retval;
1681 #else
1682     InfoLogger() << "Sending OS request to browse directory: " << command;
1683     if (auto sys_retval = std::system(command.c_str()))
1684         WarnLogger() << "System call " << command << " returned non-zero value " << sys_retval;
1685 #endif
1686 }
1687