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