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