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, "&");
257 for(std::string::size_type pos = 0; (pos = password.find('\"', pos)) != std::string::npos; ++pos)
258 password.replace(pos, 1, """);
259 for(std::string::size_type pos = 0; (pos = password.find('<', pos)) != std::string::npos; ++pos)
260 password.replace(pos, 1, "<");
261 for(std::string::size_type pos = 0; (pos = password.find('>', pos)) != std::string::npos; ++pos)
262 password.replace(pos, 1, ">");
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