1 /*
2    Copyright (C) 2007 - 2018 by David White <dave@whitevine.net>
3    Part of the Battle for Wesnoth Project https://www.wesnoth.org
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY.
11 
12    See the COPYING file for more details.
13 */
14 
15 #include "game_initialization/multiplayer.hpp"
16 
17 #include "addon/manager.hpp" // for installed_addons
18 #include "build_info.hpp"
19 #include "events.hpp"
20 #include "formula/string_utils.hpp"
21 #include "game_config_manager.hpp"
22 #include "game_initialization/mp_game_utils.hpp"
23 #include "game_initialization/playcampaign.hpp"
24 #include "preferences/credentials.hpp"
25 #include "preferences/game.hpp"
26 #include "gettext.hpp"
27 #include "gui/dialogs/loading_screen.hpp"
28 #include "gui/dialogs/message.hpp"
29 #include "gui/dialogs/multiplayer/lobby.hpp"
30 #include "gui/dialogs/multiplayer/mp_create_game.hpp"
31 #include "gui/dialogs/multiplayer/mp_join_game.hpp"
32 #include "gui/dialogs/multiplayer/mp_login.hpp"
33 #include "gui/dialogs/multiplayer/mp_staging.hpp"
34 #include "gui/widgets/settings.hpp"
35 #include "hash.hpp"
36 #include "log.hpp"
37 #include "multiplayer_error_codes.hpp"
38 #include "map_settings.hpp"
39 #include "sound.hpp"
40 #include "statistics.hpp"
41 #include "wesnothd_connection.hpp"
42 #include "resources.hpp"
43 #include "replay.hpp"
44 
45 #include "utils/functional.hpp"
46 
47 #include <fstream>
48 
49 static lg::log_domain log_mp("mp/main");
50 #define DBG_MP LOG_STREAM(debug, log_mp)
51 #define ERR_MP LOG_STREAM(err, log_mp)
52 
53 namespace
54 {
55 /** Opens a new server connection and prompts the client for login credentials, if necessary. */
open_connection(std::string host)56 std::pair<wesnothd_connection_ptr, config> open_connection(std::string host)
57 {
58 	DBG_MP << "opening connection" << std::endl;
59 
60 	wesnothd_connection_ptr sock;
61 	if(host.empty()) {
62 		return std::make_pair(std::move(sock), config());
63 	}
64 
65 	const int colon_index = host.find_first_of(":");
66 	unsigned int port;
67 
68 	if(colon_index == -1) {
69 		port = 15000;
70 	} else {
71 		port = lexical_cast_default<unsigned int>(host.substr(colon_index + 1), 15000);
72 		host = host.substr(0, colon_index);
73 	}
74 
75 	// shown_hosts is used to prevent the client being locked in a redirect loop.
76 	using hostpair = std::pair<std::string, int>;
77 
78 	std::set<hostpair> shown_hosts;
79 	shown_hosts.emplace(host, port);
80 
81 	// Initializes the connection to the server.
82 	sock = wesnothd_connection::create(host, std::to_string(port));
83 	if(!sock) {
84 		return std::make_pair(std::move(sock), config());
85 	}
86 
87 	// Start stage
88 	gui2::dialogs::loading_screen::progress(loading_stage::connect_to_server);
89 
90 	// First, spin until we get a handshake from the server.
91 	while(!sock->handshake_finished()) {
92 		sock->poll();
93 		SDL_Delay(1);
94 	}
95 
96 	gui2::dialogs::loading_screen::progress(loading_stage::waiting);
97 
98 	config data;
99 	config initial_lobby_config;
100 
101 	bool received_join_lobby = false;
102 	bool received_gamelist = false;
103 
104 	// Then, log in and wait for the lobby/game join prompt.
105 	do {
106 		if(!sock) {
107 			return std::make_pair(std::move(sock), config());
108 		}
109 
110 		data.clear();
111 		sock->wait_and_receive_data(data);
112 
113 		if(data.has_child("reject") || data.has_attribute("version")) {
114 			std::string version;
115 
116 			if(const config& reject = data.child("reject")) {
117 				version = reject["accepted_versions"].str();
118 			} else {
119 				// Backwards-compatibility "version" attribute
120 				version = data["version"].str();
121 			}
122 
123 			utils::string_map i18n_symbols;
124 			i18n_symbols["required_version"] = version;
125 			i18n_symbols["your_version"] = game_config::version;
126 
127 			const std::string errorstring = VGETTEXT("The server accepts versions '$required_version', but you are using version '$your_version'", i18n_symbols);
128 			throw wesnothd_error(errorstring);
129 		}
130 
131 		// Check for "redirect" messages
132 		if(const config& redirect = data.child("redirect")) {
133 			host = redirect["host"].str();
134 			port = redirect["port"].to_int(15000);
135 
136 			if(shown_hosts.find(hostpair(host, port)) != shown_hosts.end()) {
137 				throw wesnothd_error(_("Server-side redirect loop"));
138 			}
139 
140 			shown_hosts.emplace(host, port);
141 
142 			gui2::dialogs::loading_screen::progress(loading_stage::redirect);
143 
144 			// Open a new connection with the new host and port.
145 			sock = wesnothd_connection_ptr();
146 			sock = wesnothd_connection::create(host, std::to_string(port));
147 
148 			// Wait for new handshake.
149 			while(!sock->handshake_finished()) {
150 				sock->poll();
151 				SDL_Delay(1);
152 			}
153 
154 			gui2::dialogs::loading_screen::progress(loading_stage::waiting);
155 			continue;
156 		}
157 
158 		if(data.has_child("version")) {
159 			config cfg;
160 			config res;
161 			cfg["version"] = game_config::version;
162 			cfg["client_source"] = game_config::dist_channel_id();
163 			res.add_child("version", std::move(cfg));
164 			sock->send_data(res);
165 		}
166 
167 		// Check for gamelist. This *must* be done before the mustlogin check
168 		// or else this loop will run ad-infinitum.
169 		if(data.has_child("gamelist")) {
170 			received_gamelist = true;
171 
172 			// data should only contain the game and user lists at this point, so just swap it.
173 			std::swap(initial_lobby_config, data);
174 		}
175 
176 		if(data.has_child("error")) {
177 			std::string error_message;
178 			config* error = &data.child("error");
179 			error_message = (*error)["message"].str();
180 			throw wesnothd_rejected_client_error(error_message);
181 		}
182 
183 		// The only message we should get here is the admin authentication message.
184 		// It's sent after [join_lobby] and before the initial gamelist.
185 		if(const config& message = data.child("message")) {
186 			preferences::parse_admin_authentication(message["sender"], message["message"]);
187 		}
188 
189 		// Continue if we did not get a direction to login
190 		if(!data.has_child("mustlogin")) {
191 			continue;
192 		}
193 
194 		// Enter login loop
195 		for(;;) {
196 			std::string login = preferences::login();
197 
198 			config response ;
199 			config& sp = response.add_child("login") ;
200 			sp["username"] = login ;
201 
202 			sock->send_data(response);
203 			sock->wait_and_receive_data(data);
204 
205 			gui2::dialogs::loading_screen::progress(loading_stage::login_response);
206 
207 			config* warning = &data.child("warning");
208 
209 			if(*warning) {
210 				std::string warning_msg;
211 
212 				if((*warning)["warning_code"] == MP_NAME_INACTIVE_WARNING) {
213 					warning_msg = VGETTEXT("The nickname ‘$nick’ is inactive. "
214 						"You cannot claim ownership of this nickname until you "
215 						"activate your account via email or ask an "
216 						"administrator to do it for you.", {{"nick", login}});
217 				} else {
218 					warning_msg = (*warning)["message"].str();
219 				}
220 
221 				warning_msg += "\n\n";
222 				warning_msg += _("Do you want to continue?");
223 
224 				if(gui2::show_message(_("Warning"), warning_msg, gui2::dialogs::message::yes_no_buttons) != gui2::retval::OK) {
225 					return std::make_pair(wesnothd_connection_ptr(), config());
226 				} else {
227 					continue;
228 				}
229 			}
230 
231 			config* error = &data.child("error");
232 
233 			// ... and get us out of here if the server did not complain
234 			if(!*error) break;
235 
236 			do {
237 				std::string password = preferences::password(host, login);
238 
239 				bool fall_through = (*error)["force_confirmation"].to_bool() ?
240 					(gui2::show_message(_("Confirm"), (*error)["message"], gui2::dialogs::message::ok_cancel_buttons) == gui2::retval::CANCEL) :
241 					false;
242 
243 				const bool is_pw_request = !((*error)["password_request"].empty()) && !(password.empty());
244 
245 				// If the server asks for a password, provide one if we can
246 				// or request a password reminder.
247 				// Otherwise or if the user pressed 'cancel' in the confirmation dialog
248 				// above go directly to the username/password dialog
249 				if(is_pw_request && !fall_through) {
250 					if((*error)["phpbb_encryption"].to_bool()) {
251 						// Apparently HTML key-characters are passed to the hashing functions of phpbb in this escaped form.
252 						// I will do closer investigations on this, for now let's just hope these are all of them.
253 
254 						// Note: we must obviously replace '&' first, I wasted some time before I figured that out... :)
255 						for(std::string::size_type pos = 0; (pos = password.find('&', pos)) != std::string::npos; ++pos)
256 							password.replace(pos, 1, "&amp;");
257 						for(std::string::size_type pos = 0; (pos = password.find('\"', pos)) != std::string::npos; ++pos)
258 							password.replace(pos, 1, "&quot;");
259 						for(std::string::size_type pos = 0; (pos = password.find('<', pos)) != std::string::npos; ++pos)
260 							password.replace(pos, 1, "&lt;");
261 						for(std::string::size_type pos = 0; (pos = password.find('>', pos)) != std::string::npos; ++pos)
262 							password.replace(pos, 1, "&gt;");
263 
264 						const std::string salt = (*error)["salt"];
265 
266 						if(salt.length() < 12) {
267 							throw wesnothd_error(_("Bad data received from server"));
268 						}
269 
270 						if(utils::md5::is_valid_prefix(salt)) {
271 							sp["password"] = utils::md5(utils::md5(password, utils::md5::get_salt(salt),
272 								utils::md5::get_iteration_count(salt)).base64_digest(), salt.substr(12, 8)).base64_digest();
273 						} else if(utils::bcrypt::is_valid_prefix(salt)) {
274 							try {
275 								auto bcrypt_salt = utils::bcrypt::from_salted_salt(salt);
276 								auto hash = utils::bcrypt::hash_pw(password, bcrypt_salt);
277 								std::string outer_salt = salt.substr(bcrypt_salt.iteration_count_delim_pos + 23);
278 								if(outer_salt.size() != 32)
279 									throw utils::hash_error("salt wrong size");
280 								sp["password"] = utils::md5(hash.base64_digest(), outer_salt).base64_digest();
281 							} catch(const utils::hash_error& err) {
282 								ERR_MP << "bcrypt hash failed: " << err.what() << std::endl;
283 								throw wesnothd_error(_("Bad data received from server"));
284 							}
285 						} else {
286 							throw wesnothd_error(_("Bad data received from server"));
287 						}
288 					} else {
289 						sp["password"] = password;
290 					}
291 
292 					// Once again send our request...
293 					sock->send_data(response);
294 					sock->wait_and_receive_data(data);
295 
296 					gui2::dialogs::loading_screen::progress(loading_stage::login_response);
297 
298 					error = &data.child("error");
299 
300 					// ... and get us out of here if the server is happy now
301 					if(!*error) break;
302 				}
303 
304 				// Providing a password either was not attempted because we did not
305 				// have any or failed:
306 				// Now show a dialog that displays the error and allows to
307 				// enter a new user name and/or password
308 
309 				std::string error_message;
310 				utils::string_map i18n_symbols;
311 				i18n_symbols["nick"] = login;
312 
313 				const bool has_extra_data = error->has_child("data");
314 				if(has_extra_data) {
315 					i18n_symbols["duration"] = utils::format_timespan((*error).child("data")["duration"]);
316 				}
317 
318 				if((*error)["error_code"] == MP_MUST_LOGIN) {
319 					error_message = _("You must login first.");
320 				} else if((*error)["error_code"] == MP_NAME_TAKEN_ERROR) {
321 					error_message = VGETTEXT("The nickname ‘$nick’ is already taken.", i18n_symbols);
322 				} else if((*error)["error_code"] == MP_INVALID_CHARS_IN_NAME_ERROR) {
323 					error_message = VGETTEXT("The nickname ‘$nick’ contains invalid "
324 							"characters. Only alpha-numeric characters (one at minimum), underscores and "
325 							"hyphens are allowed.", i18n_symbols);
326 				} else if((*error)["error_code"] == MP_NAME_TOO_LONG_ERROR) {
327 					error_message = VGETTEXT("The nickname ‘$nick’ is too long. Nicks must "
328 							"be 20 characters or less.", i18n_symbols);
329 				} else if((*error)["error_code"] == MP_NAME_RESERVED_ERROR) {
330 					error_message = VGETTEXT("The nickname ‘$nick’ is reserved and cannot be used by players.", i18n_symbols);
331 				} else if((*error)["error_code"] == MP_NAME_UNREGISTERED_ERROR) {
332 					error_message = VGETTEXT("The nickname ‘$nick’ is not registered on this server.", i18n_symbols)
333 							+ _(" This server disallows unregistered nicknames.");
334 				} else if((*error)["error_code"] == MP_NAME_AUTH_BAN_USER_ERROR) {
335 					if(has_extra_data) {
336 						error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
337 					} else {
338 						error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
339 					}
340 				} else if((*error)["error_code"] == MP_NAME_AUTH_BAN_IP_ERROR) {
341 					if(has_extra_data) {
342 						error_message = VGETTEXT("Your IP address is banned on this server’s forums for $duration|.", i18n_symbols);
343 					} else {
344 						error_message = _("Your IP address is banned on this server’s forums.");
345 					}
346 				} else if((*error)["error_code"] == MP_NAME_AUTH_BAN_EMAIL_ERROR) {
347 					if(has_extra_data) {
348 						error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
349 					} else {
350 						error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
351 					}
352 				} else if((*error)["error_code"] == MP_PASSWORD_REQUEST) {
353 					error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols);
354 				} else if((*error)["error_code"] == MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME) {
355 					error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols)
356 							+ "\n\n" + _("WARNING: There is already a client using this nickname, "
357 							"logging in will cause that client to be kicked!");
358 				} else if((*error)["error_code"] == MP_NO_SEED_ERROR) {
359 					error_message = _("Error in the login procedure (the server had no "
360 							"seed for your connection).");
361 				} else if((*error)["error_code"] == MP_INCORRECT_PASSWORD_ERROR) {
362 					error_message = _("The password you provided was incorrect.");
363 				} else if((*error)["error_code"] == MP_TOO_MANY_ATTEMPTS_ERROR) {
364 					error_message = _("You have made too many login attempts.");
365 				} else {
366 					error_message = (*error)["message"].str();
367 				}
368 
369 				gui2::dialogs::mp_login dlg(host, error_message, !((*error)["password_request"].empty()));
370 
371 				// Need to show the dialog from the main thread or it won't appear.
372 				events::call_in_main_thread([&dlg]() { dlg.show(); });
373 
374 				switch(dlg.get_retval()) {
375 					//Log in with password
376 					case gui2::retval::OK:
377 						break;
378 					// Cancel
379 					default:
380 						return std::make_pair(wesnothd_connection_ptr(), config());
381 				}
382 
383 			// If we have got a new username we have to start all over again
384 			} while(login == preferences::login());
385 
386 			// Somewhat hacky...
387 			// If we broke out of the do-while loop above error
388 			// is still going to be nullptr
389 			if(!*error) break;
390 		} // end login loop
391 
392 		if(data.has_child("join_lobby")) {
393 			received_join_lobby = true;
394 
395 			gui2::dialogs::loading_screen::progress(loading_stage::download_lobby_data);
396 		}
397 	} while(!received_join_lobby || !received_gamelist);
398 
399 	return std::make_pair(std::move(sock), std::move(initial_lobby_config));
400 }
401 
402 /** Helper struct to manage the MP workflow arguments. */
403 struct mp_workflow_helper
404 {
mp_workflow_helper__anona2fa90ab0111::mp_workflow_helper405 	mp_workflow_helper(const config& gc, saved_game& state, wesnothd_connection* connection, mp::lobby_info* li)
406 		: game_config(gc)
407 		, state(state)
408 		, connection(connection)
409 		, lobby_info(li)
410 	{}
411 
412 	const config& game_config;
413 
414 	saved_game& state;
415 
416 	wesnothd_connection* connection;
417 
418 	mp::lobby_info* lobby_info;
419 };
420 
421 using mp_workflow_helper_ptr = std::shared_ptr<mp_workflow_helper>;
422 
423 /**
424  * The main components of the MP workflow. It consists of four screens:
425  *
426  * Host POV:   LOBBY <---> CREATE GAME ---> STAGING ------------------> GAME BEGINS
427  * Player POV: LOBBY <---------------------------------> JOIN GAME ---> GAME BEGINS
428  *
429  * NOTE: since these functions are static, they appear here in the opposite order they'd be accessed.
430  */
enter_wait_mode(mp_workflow_helper_ptr helper,int game_id,bool observe)431 void enter_wait_mode(mp_workflow_helper_ptr helper, int game_id, bool observe)
432 {
433 	DBG_MP << "entering wait mode" << std::endl;
434 
435 	// The connection should never be null here, since one should never reach this screen in local game mode.
436 	assert(helper->connection);
437 
438 	statistics::fresh_stats();
439 
440 	std::unique_ptr<mp_campaign_info> campaign_info(new mp_campaign_info(*helper->connection));
441 	campaign_info->is_host = false;
442 
443 	if(helper->lobby_info->get_game_by_id(game_id)) {
444 		campaign_info->current_turn = helper->lobby_info->get_game_by_id(game_id)->current_turn;
445 	}
446 
447 	if(preferences::skip_mp_replay() || preferences::blindfold_replay()) {
448 		campaign_info->skip_replay = true;
449 		campaign_info->skip_replay_blindfolded = preferences::blindfold_replay();
450 	}
451 
452 	bool dlg_ok = false;
453 	{
454 		gui2::dialogs::mp_join_game dlg(helper->state, *helper->lobby_info, *helper->connection, true, observe);
455 
456 		if(!dlg.fetch_game_config()) {
457 			helper->connection->send_data(config("leave_game"));
458 			return;
459 		}
460 
461 		dlg.show();
462 		dlg_ok = dlg.get_retval() == gui2::retval::OK;
463 	}
464 
465 	if(dlg_ok) {
466 		campaign_controller controller(helper->state, game_config_manager::get()->terrain_types());
467 		controller.set_mp_info(campaign_info.get());
468 		controller.play_game();
469 	}
470 
471 	helper->connection->send_data(config("leave_game"));
472 }
473 
enter_staging_mode(mp_workflow_helper_ptr helper)474 void enter_staging_mode(mp_workflow_helper_ptr helper)
475 {
476 	DBG_MP << "entering connect mode" << std::endl;
477 
478 	std::unique_ptr<mp_campaign_info> campaign_info;
479 
480 	// If we have a connection, set the appropriate info. No connection means we're in local game mode.
481 	if(helper->connection) {
482 		campaign_info.reset(new mp_campaign_info(*helper->connection));
483 		campaign_info->connected_players.insert(preferences::login());
484 		campaign_info->is_host = true;
485 	}
486 
487 	bool dlg_ok = false;
488 	{
489 		ng::connect_engine_ptr connect_engine(new ng::connect_engine(helper->state, true, campaign_info.get()));
490 
491 		gui2::dialogs::mp_staging dlg(*connect_engine, *helper->lobby_info, helper->connection);
492 		dlg.show();
493 		dlg_ok = dlg.get_retval() == gui2::retval::OK;
494 	} // end connect_engine_ptr, dlg scope
495 
496 	if(dlg_ok) {
497 		campaign_controller controller(helper->state, game_config_manager::get()->terrain_types());
498 		controller.set_mp_info(campaign_info.get());
499 		controller.play_game();
500 	}
501 
502 	if(helper->connection) {
503 		helper->connection->send_data(config("leave_game"));
504 	}
505 }
506 
enter_create_mode(mp_workflow_helper_ptr helper)507 void enter_create_mode(mp_workflow_helper_ptr helper)
508 {
509 	DBG_MP << "entering create mode" << std::endl;
510 
511 	bool dlg_ok = false;
512 	{
513 		bool local_mode = helper->connection == nullptr;
514 		mp::user_info* host_info = helper->lobby_info->get_user(preferences::login());
515 
516 		gui2::dialogs::mp_create_game dlg(helper->game_config, helper->state, local_mode, host_info);
517 		dlg.show();
518 
519 		// The Create Game dialog also has a LOAD_GAME retval besides OK.
520 		// Do a did-not-cancel check here to catch that
521 		dlg_ok = dlg.get_retval() != gui2::retval::CANCEL;
522 	}
523 
524 	if(dlg_ok) {
525 		enter_staging_mode(helper);
526 	} else if(helper->connection) {
527 		helper->connection->send_data(config("refresh_lobby"));
528 	}
529 }
530 
enter_lobby_mode(mp_workflow_helper_ptr helper,const std::vector<std::string> & installed_addons,const config & initial_lobby_config)531 bool enter_lobby_mode(mp_workflow_helper_ptr helper, const std::vector<std::string>& installed_addons, const config& initial_lobby_config)
532 {
533 	DBG_MP << "entering lobby mode" << std::endl;
534 
535 	// Connection should never be null in the lobby.
536 	assert(helper->connection);
537 
538 	// We use a loop here to allow returning to the lobby if you, say, cancel game creation.
539 	while(true) {
540 		if(const config& cfg = helper->game_config.child("lobby_music")) {
541 			for(const config& i : cfg.child_range("music")) {
542 				sound::play_music_config(i);
543 			}
544 
545 			sound::commit_music_changes();
546 		} else {
547 			sound::empty_playlist();
548 			sound::stop_music();
549 		}
550 
551 		mp::lobby_info li(installed_addons);
552 		helper->lobby_info = &li;
553 
554 		if(!initial_lobby_config.empty()) {
555 			li.process_gamelist(initial_lobby_config);
556 		}
557 
558 		int dlg_retval = 0;
559 		int dlg_joined_game_id = 0;
560 		{
561 
562 			gui2::dialogs::mp_lobby dlg(helper->game_config, li, *helper->connection);
563 			dlg.show();
564 			dlg_retval = dlg.get_retval();
565 			dlg_joined_game_id = dlg.get_joined_game_id();
566 		}
567 
568 		switch(dlg_retval) {
569 			case gui2::dialogs::mp_lobby::CREATE:
570 				try {
571 					enter_create_mode(helper);
572 				} catch(const config::error& error) {
573 					if(!error.message.empty()) {
574 						gui2::show_error_message(error.message);
575 					}
576 
577 					// Update lobby content
578 					helper->connection->send_data(config("refresh_lobby"));
579 				}
580 
581 				break;
582 			case gui2::dialogs::mp_lobby::JOIN:
583 			case gui2::dialogs::mp_lobby::OBSERVE:
584 				try {
585 					enter_wait_mode(helper,
586 						dlg_joined_game_id,
587 						dlg_retval == gui2::dialogs::mp_lobby::OBSERVE
588 					);
589 				} catch(const config::error& error) {
590 					if(!error.message.empty()) {
591 						gui2::show_error_message(error.message);
592 					}
593 
594 					// Update lobby content
595 					helper->connection->send_data(config("refresh_lobby"));
596 				}
597 
598 				break;
599 			case gui2::dialogs::mp_lobby::RELOAD_CONFIG:
600 				// Let this function's caller reload the config and re-call.
601 				return false;
602 			default:
603 				// Needed to handle the Quit signal and exit the loop
604 				return true;
605 		}
606 	}
607 
608 	return true;
609 }
610 
611 } // end anon namespace
612 
613 /** Pubic entry points for the MP workflow */
614 namespace mp
615 {
start_client(const config & game_config,saved_game & state,const std::string & host)616 void start_client(const config& game_config,	saved_game& state, const std::string& host)
617 {
618 	const config* game_config_ptr = &game_config;
619 
620 	// This function does not refer to an addon database, it calls filesystem functions.
621 	// For the sanity of the mp lobby, this list should be fixed for the entire lobby session,
622 	// even if the user changes the contents of the addon directory in the meantime.
623 	std::vector<std::string> installed_addons = ::installed_addons();
624 
625 	DBG_MP << "starting client" << std::endl;
626 
627 	preferences::admin_authentication_reset r;
628 
629 	wesnothd_connection_ptr connection;
630 	config lobby_config;
631 
632 	gui2::dialogs::loading_screen::display([&]() {
633 		std::tie(connection, lobby_config) = open_connection(host);
634 	});
635 
636 	if(!connection) {
637 		return;
638 	}
639 
640 	mp_workflow_helper_ptr workflow_helper;
641 	bool re_enter = false;
642 
643 	do {
644 		workflow_helper.reset(new mp_workflow_helper(*game_config_ptr, state, connection.get(), nullptr));
645 
646 		// A return of false means a config reload was requested, so do that and then loop.
647 		re_enter = !enter_lobby_mode(workflow_helper, installed_addons, lobby_config);
648 
649 		if(re_enter) {
650 			game_config_manager* gcm = game_config_manager::get();
651 			gcm->reload_changed_game_config();
652 			gcm->load_game_config_for_game(state.classification()); // NOTE: Using reload_changed_game_config only doesn't seem to work here
653 
654 			game_config_ptr = &gcm->game_config();
655 
656 			installed_addons = ::installed_addons(); // Refresh the installed add-on list for this session.
657 
658 			connection->send_data(config("refresh_lobby"));
659 		}
660 	} while(re_enter);
661 }
662 
goto_mp_connect(ng::connect_engine & engine,wesnothd_connection * connection)663 bool goto_mp_connect(ng::connect_engine& engine, wesnothd_connection* connection)
664 {
665 	lobby_info li({});
666 
667 	gui2::dialogs::mp_staging dlg(engine, li, connection);
668 	return dlg.show();
669 }
670 
goto_mp_wait(saved_game & state,wesnothd_connection * connection,bool observe)671 bool goto_mp_wait(saved_game& state, wesnothd_connection* connection, bool observe)
672 {
673 	lobby_info li({});
674 
675 	gui2::dialogs::mp_join_game dlg(state, li, *connection, false, observe);
676 
677 	if(!dlg.fetch_game_config()) {
678 		connection->send_data(config("leave_game"));
679 		return false;
680 	}
681 
682 	if(dlg.started()) {
683 		return true;
684 	}
685 
686 	return dlg.show();
687 }
688 
start_local_game(const config & game_config,saved_game & state)689 void start_local_game(const config& game_config, saved_game& state)
690 {
691 	DBG_MP << "starting local game" << std::endl;
692 
693 	preferences::set_message_private(false);
694 
695 	// TODO: should lobby_info take a nullptr in this case, or should we pass the installed_addons data here too?
696 	lobby_info li({});
697 	mp_workflow_helper_ptr workflow_helper = std::make_shared<mp_workflow_helper>(game_config, state, nullptr, &li);
698 
699 	enter_create_mode(workflow_helper);
700 }
701 
start_local_game_commandline(const config & game_config,saved_game & state,const commandline_options & cmdline_opts)702 void start_local_game_commandline(const config& game_config, saved_game& state, const commandline_options& cmdline_opts)
703 {
704 	DBG_MP << "starting local MP game from commandline" << std::endl;
705 
706 	// The setup is done equivalently to lobby MP games using as much of existing
707 	// code as possible.  This means that some things are set up that are not
708 	// needed in commandline mode, but they are required by the functions called.
709 	preferences::set_message_private(false);
710 
711 	DBG_MP << "entering create mode" << std::endl;
712 
713 	// Set the default parameters
714 	state.clear(); // This creates these parameters with default values defined in mp_game_settings.cpp
715 	mp_game_settings& parameters = state.mp_settings();
716 
717 	// Hardcoded default values
718 	parameters.mp_era = "era_default";
719 	parameters.name = "multiplayer_The_Freelands";
720 
721 	// Default values for which at getter function exists
722 	parameters.num_turns = settings::get_turns("");
723 	parameters.village_gold = settings::get_village_gold("");
724 	parameters.village_support = settings::get_village_support("");
725 	parameters.xp_modifier = settings::get_xp_modifier("");
726 
727 	// Do not use map settings if --ignore-map-settings commandline option is set
728 	if(cmdline_opts.multiplayer_ignore_map_settings) {
729 		DBG_MP << "ignoring map settings" << std::endl;
730 		parameters.use_map_settings = false;
731 	} else {
732 		parameters.use_map_settings = true;
733 	}
734 
735 	// None of the other parameters need to be set, as their creation values above are good enough for CL mode.
736 	// In particular, we do not want to use the preferences values.
737 
738 	state.classification().campaign_type = game_classification::CAMPAIGN_TYPE::MULTIPLAYER;
739 
740 	// [era] define.
741 	if(cmdline_opts.multiplayer_era) {
742 		parameters.mp_era = *cmdline_opts.multiplayer_era;
743 	}
744 
745 	if(const config& cfg_era = game_config.find_child("era", "id", parameters.mp_era)) {
746 		state.classification().era_define = cfg_era["define"].str();
747 	} else {
748 		std::cerr << "Could not find era '" << parameters.mp_era << "'\n";
749 		return;
750 	}
751 
752 	// [multiplayer] define.
753 	if(cmdline_opts.multiplayer_scenario) {
754 		parameters.name = *cmdline_opts.multiplayer_scenario;
755 	}
756 
757 	if(const config& cfg_multiplayer = game_config.find_child("multiplayer", "id", parameters.name)) {
758 		state.classification().scenario_define = cfg_multiplayer["define"].str();
759 	} else {
760 		std::cerr << "Could not find [multiplayer] '" << parameters.name << "'\n";
761 		return;
762 	}
763 
764 	game_config_manager::get()->load_game_config_for_game(state.classification());
765 	state.set_carryover_sides_start(
766 		config {"next_scenario", parameters.name}
767 	);
768 
769 	state.expand_random_scenario();
770 	state.expand_mp_events();
771 	state.expand_mp_options();
772 
773 	// Should number of turns be determined from scenario data?
774 	if(parameters.use_map_settings && state.get_starting_point()["turns"]) {
775 		DBG_MP << "setting turns from scenario data: " << state.get_starting_point()["turns"] << std::endl;
776 		parameters.num_turns = state.get_starting_point()["turns"];
777 	}
778 
779 	DBG_MP << "entering connect mode" << std::endl;
780 
781 	statistics::fresh_stats();
782 
783 	{
784 		ng::connect_engine_ptr connect_engine(new ng::connect_engine(state, true, nullptr));
785 
786 		// Update the parameters to reflect game start conditions
787 		connect_engine->start_game_commandline(cmdline_opts, game_config);
788 	}
789 
790 	if(resources::recorder && cmdline_opts.multiplayer_label) {
791 		std::string label = *cmdline_opts.multiplayer_label;
792 		resources::recorder->add_log_data("ai_log","ai_label",label);
793 	}
794 
795 	unsigned int repeat = (cmdline_opts.multiplayer_repeat) ? *cmdline_opts.multiplayer_repeat : 1;
796 	for(unsigned int i = 0; i < repeat; i++){
797 		saved_game state_copy(state);
798 		campaign_controller controller(state_copy, game_config_manager::get()->terrain_types());
799 		controller.play_game();
800 	}
801 }
802 
803 } // end namespace mp
804