The Battle for Wesnoth  1.19.11+dev
multiplayer.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2007 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
17 
18 #include "build_info.hpp"
19 #include "commandline_options.hpp"
20 #include "connect_engine.hpp"
21 #include "events.hpp"
23 #include "formula/string_utils.hpp"
24 #include "game_config_manager.hpp"
26 #include "gettext.hpp"
28 #include "gui/dialogs/message.hpp"
34 #include "log.hpp"
35 #include "map_settings.hpp"
38 #include "replay.hpp"
39 #include "resources.hpp"
40 #include "saved_game.hpp"
41 #include "sound.hpp"
43 #include "wesnothd_connection.hpp"
44 
45 #include <functional>
46 #include "utils/optional_fwd.hpp"
47 #include <thread>
48 
49 static lg::log_domain log_mp("mp/main");
50 #define DBG_MP LOG_STREAM(debug, log_mp)
51 #define LOG_MP LOG_STREAM(info, log_mp)
52 #define WRN_MP LOG_STREAM(warn, log_mp)
53 #define ERR_MP LOG_STREAM(err, log_mp)
54 
55 namespace mp
56 {
57 namespace
58 {
59 /** Pointer to the current mp_manager instance. */
60 class mp_manager* manager = nullptr;
61 
62 /** The main controller of the MP workflow. */
63 class mp_manager
64 {
65 public:
66  // Declare this as a friend to allow direct access to enter_create_mode
67  friend void mp::start_local_game();
68  friend void mp::send_to_server(const config&);
70 
71  mp_manager(const utils::optional<std::string> host);
72 
73  ~mp_manager()
74  {
75  assert(manager);
76  manager = nullptr;
77 
78  if(network_worker.joinable()) {
79  stop = true;
80  network_worker.join();
81  }
82  }
83 
84  /**
85  * Enters the mp loop. It consists of four screens:
86  *
87  * Host POV: LOBBY <---> CREATE GAME ---> STAGING -----> GAME BEGINS
88  * Player POV: LOBBY <--------------------> JOIN GAME ---> GAME BEGINS
89  */
90  void run_lobby_loop();
91 
92  bool post_scenario_staging(ng::connect_engine& engine);
93 
94  bool post_scenario_wait(bool observe);
95 
96 private:
97  /** Represents the contents of the [join_lobby] response. */
98  struct session_metadata
99  {
100  session_metadata() = default;
101 
102  session_metadata(const config& cfg)
103  : is_moderator(cfg["is_moderator"].to_bool(false))
104  , profile_url_prefix(cfg["profile_url_prefix"].str())
105  , queues()
106  {
107  if(cfg.has_child("queues")) {
108  for(const config& queue : cfg.mandatory_child("queues").child_range("queue")) {
109  queue_info info;
110  info.id = queue["id"].to_int();
111  info.scenario_id = queue["scenario_id"].str();
112  info.display_name = queue["display_name"].str();
113  info.players_required = queue["players_required"].to_int();
114  info.current_players = utils::split_set(queue["current_players"].str());
115  queues.emplace_back(info);
116  }
117  }
118  }
119 
120  /** Whether you are logged in as a server moderator. */
121  bool is_moderator = false;
122 
123  /** The external URL prefix for player profiles (empty if the server doesn't have an attached database). */
124  std::string profile_url_prefix;
125 
126  /** The list of server-side queues */
127  std::vector<queue_info> queues;
128  };
129 
130  /** Opens a new server connection and prompts the client for login credentials, if necessary. */
131  std::unique_ptr<wesnothd_connection> open_connection(const std::string& host);
132 
133  /** Opens the MP lobby. */
134  bool enter_lobby_mode();
135 
136  /**
137  * Opens the MP Create screen for hosts to configure a new game.
138  * @param preset_scenario contains a scenario id if present
139  */
140  void enter_create_mode(utils::optional<std::string> preset_scenario = utils::nullopt, utils::optional<config> server_preset = utils::nullopt, int queue_id = 0);
141 
142  /** Opens the MP Staging screen for hosts to wait for players. */
143  void enter_staging_mode(queue_type::type queue_type, int queue_id = 0);
144 
145  /** Opens the MP Join Game screen for non-host players and observers. */
146  void enter_wait_mode(int game_id, bool observe);
147 
148  /** Worker thread to handle receiving and processing network data. */
149  std::thread network_worker;
150 
151  /** Flag to signal the worker thread terminate. */
152  std::atomic_bool stop;
153 
154  /** The connection to the server. */
155  std::unique_ptr<wesnothd_connection> connection;
156 
157  /** The current session's info sent by the server on login. */
158  session_metadata session_info;
159 
160  /** This single instance is reused for all games played during the current connection to the server. */
161  saved_game state;
162 
163  mp::lobby_info lobby_info;
164 
165  std::list<mp::network_registrar::handler> process_handlers;
166 
167 public:
168  const session_metadata& get_session_info() const
169  {
170  return session_info;
171  }
172  session_metadata& get_session_info()
173  {
174  return session_info;
175  }
176 
177  auto add_network_handler(const decltype(process_handlers)::value_type& func)
178  {
179  return [this, iter = process_handlers.insert(process_handlers.end(), func)]() { process_handlers.erase(iter); };
180  }
181 };
182 
183 mp_manager::mp_manager(const utils::optional<std::string> host)
184  : network_worker()
185  , stop(false)
186  , connection(nullptr)
187  , session_info()
188  , state()
189  , lobby_info()
190  , process_handlers()
191 {
192  state.classification().type = campaign_type::type::multiplayer;
193 
194  if(host) {
196  connection = open_connection(*host);
197 
198  // If for whatever reason our connection is null at this point (dismissing the password prompt, for
199  // instance), treat it as a normal condition and exit. Any actual error conditions throw exceptions
200  // which can be handled higher up the stack.
201  if(connection == nullptr) {
202  return;
203  }
204 
206 
207  config data;
208 
209  while(!stop) {
210  connection->wait_and_receive_data(data);
211 
212  if(const auto error = data.optional_child("error")) {
213  throw wesnothd_error((*error)["message"]);
214  }
215 
216  else if(data.has_child("gamelist")) {
217  this->lobby_info.process_gamelist(data);
218  break;
219  }
220 
221  else if(const auto gamelist_diff = data.optional_child("gamelist_diff")) {
222  this->lobby_info.process_gamelist_diff(*gamelist_diff);
223  }
224 
225  else {
226  // No special actions to take. Pass the data on to the network handlers.
227  for(const auto& handler : process_handlers) {
228  handler(data);
229  }
230  }
231  }
232  });
233  }
234 
235  // Avoid setting this until the connection has been fully established. open_connection may throw,
236  // in which case we don't want to point to an object instance that has not properly connected.
237  assert(!manager);
238  manager = this;
239 }
240 
241 std::unique_ptr<wesnothd_connection> mp_manager::open_connection(const std::string& host)
242 {
243  DBG_MP << "opening connection";
244 
245  if(host.empty()) {
246  return nullptr;
247  }
248 
249  // shown_hosts is used to prevent the client being locked in a redirect loop.
250  std::set<std::pair<std::string, std::string>> shown_hosts;
251  auto addr = shown_hosts.end();
252 
253  try {
254  std::tie(addr, std::ignore) = shown_hosts.insert(parse_network_address(host, "15000"));
255  } catch(const std::runtime_error&) {
256  throw wesnothd_error(_("Invalid address specified for multiplayer server"));
257  }
258 
259  // Start stage
261 
262  // Initializes the connection to the server.
263  auto conn = std::make_unique<wesnothd_connection>(addr->first, addr->second);
264 
265  // First, spin until we get a handshake from the server.
266  conn->wait_for_handshake();
267 
269 
270  config data;
271 
272  // Then, log in and wait for the lobby/game join prompt.
273  while(true) {
274  data.clear();
275  conn->wait_and_receive_data(data);
276 
277  if(const auto reject = data.optional_child("reject"); reject || data.has_attribute("version")) {
278  std::string version;
279 
280  if(reject) {
281  version = (*reject)["accepted_versions"].str();
282  } else {
283  // Backwards-compatibility "version" attribute
284  version = data["version"].str();
285  }
286 
287  utils::string_map i18n_symbols;
288  i18n_symbols["required_version"] = version;
289  i18n_symbols["your_version"] = game_config::wesnoth_version.str();
290 
291  const std::string errorstring = VGETTEXT("The server accepts versions ‘$required_version’, but you are using version ‘$your_version’", i18n_symbols);
292  throw wesnothd_error(errorstring);
293  }
294 
295  // Check for "redirect" messages
296  if(const auto redirect = data.optional_child("redirect")) {
297  auto redirect_host = (*redirect)["host"].str();
298  auto redirect_port = (*redirect)["port"].str("15000");
299 
300  bool recorded_host;
301  std::tie(std::ignore, recorded_host) = shown_hosts.emplace(redirect_host, redirect_port);
302 
303  if(!recorded_host) {
304  throw wesnothd_error(_("Server-side redirect loop"));
305  }
306 
308 
309  // Open a new connection with the new host and port.
310  conn.reset();
311  conn = std::make_unique<wesnothd_connection>(redirect_host, redirect_port);
312 
313  // Wait for new handshake.
314  conn->wait_for_handshake();
315 
317  continue;
318  }
319 
320  if(data.has_child("version")) {
321  config res;
322  config& cfg = res.add_child("version");
323  cfg["version"] = game_config::wesnoth_version.str();
324  cfg["client_source"] = game_config::dist_channel_id();
325  conn->send_data(res);
326  }
327 
328  if(const auto error = data.optional_child("error")) {
329  throw wesnothd_rejected_client_error((*error)["message"].str());
330  }
331 
332  // Continue if we did not get a direction to login
333  if(!data.has_child("mustlogin")) {
334  continue;
335  }
336 
337  // Enter login loop
338  while(true) {
339  std::string login = prefs::get().login();
340 
341  config response;
342  config& sp = response.add_child("login");
343  sp["username"] = login;
344 
345  conn->send_data(response);
346  conn->wait_and_receive_data(data);
347 
349 
350  if(const auto warning = data.optional_child("warning")) {
351  std::string warning_msg;
352 
353  if((*warning)["warning_code"] == MP_NAME_INACTIVE_WARNING) {
354  warning_msg = VGETTEXT("The nickname ‘$nick’ is inactive. "
355  "You cannot claim ownership of this nickname until you "
356  "activate your account via email or ask an "
357  "administrator to do it for you.", {{"nick", login}});
358  } else {
359  warning_msg = (*warning)["message"].str();
360  }
361 
362  warning_msg += "\n\n";
363  warning_msg += _("Do you want to continue?");
364 
366  return nullptr;
367  } else {
368  continue;
369  }
370  }
371 
372  auto error = data.optional_child("error");
373 
374  // ... and get us out of here if the server did not complain
375  if(!error) break;
376 
377  do {
378  std::string password = prefs::get().password(host, login);
379 
380  const bool fall_through = (*error)["force_confirmation"].to_bool()
382  : false;
383 
384  // If:
385  // * the server asked for a password
386  // * the password isn't empty
387  // * the user didn't press Cancel
388  // * the connection is secure or the client was started with the option to use insecure connections
389  // send the password to the server
390  // otherwise go directly to the username/password dialog
391  if(!(*error)["password_request"].empty() && !password.empty() && !fall_through && (conn->using_tls() || game_config::allow_insecure)) {
392  // the possible cases here are that either:
393  // 1) TLS encryption is enabled, thus sending the plaintext password is still secure
394  // 2) TLS encryption is not enabled, in which case the server should not be requesting a password in the first place
395  // 3) This is being used for local testing/development, so using an insecure connection is enabled manually
396 
397  sp["password"] = password;
398 
399  // Once again send our request...
400  conn->send_data(response);
401  conn->wait_and_receive_data(data);
402 
404 
405  error = data.optional_child("error");
406 
407  // ... and get us out of here if the server is happy now
408  if(!error) break;
409  }
410 
411  // Providing a password either was not attempted because we did not
412  // have any or failed:
413  // Now show a dialog that displays the error and allows to
414  // enter a new user name and/or password
415 
416  std::string error_message;
417  utils::string_map i18n_symbols;
418  i18n_symbols["nick"] = login;
419 
420  const auto extra_data = error->optional_child("data");
421  if(extra_data) {
422  using namespace std::chrono_literals;
423  i18n_symbols["duration"] = utils::format_timespan(chrono::parse_duration((*extra_data)["duration"], 0s));
424  }
425 
426  const std::string ec = (*error)["error_code"];
427 
428  if(!(*error)["password_request"].empty() && !conn->using_tls() && !game_config::allow_insecure) {
429  error_message = _("The remote server requested a password while using an insecure connection.");
430  } else if(ec == MP_MUST_LOGIN) {
431  error_message = _("You must login first.");
432  } else if(ec == MP_NAME_TAKEN_ERROR) {
433  error_message = VGETTEXT("The nickname ‘$nick’ is already taken.", i18n_symbols);
434  } else if(ec == MP_INVALID_CHARS_IN_NAME_ERROR) {
435  error_message = VGETTEXT("The nickname ‘$nick’ contains invalid "
436  "characters. Only alpha-numeric characters (one at minimum), underscores and "
437  "hyphens are allowed.", i18n_symbols);
438  } else if(ec == MP_NAME_TOO_LONG_ERROR) {
439  error_message = VGETTEXT("The nickname ‘$nick’ is too long. Nicks must be 20 characters or less.", i18n_symbols);
440  } else if(ec == MP_NAME_RESERVED_ERROR) {
441  error_message = VGETTEXT("The nickname ‘$nick’ is reserved and cannot be used by players.", i18n_symbols);
442  } else if(ec == MP_NAME_UNREGISTERED_ERROR) {
443  error_message = VGETTEXT("The nickname ‘$nick’ is not registered on this server.", i18n_symbols)
444  + _(" This server disallows unregistered nicknames.");
445  } else if(ec == MP_SERVER_IP_BAN_ERROR) {
446  if(extra_data) {
447  error_message = VGETTEXT("Your IP address is banned on this server for $duration|.", i18n_symbols);
448  } else {
449  error_message = _("Your IP address is banned on this server.");
450  }
451  } else if(ec == MP_NAME_AUTH_BAN_USER_ERROR) {
452  if(extra_data) {
453  error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
454  } else {
455  error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
456  }
457  } else if(ec == MP_NAME_AUTH_BAN_IP_ERROR) {
458  if(extra_data) {
459  error_message = VGETTEXT("Your IP address is banned on this server’s forums for $duration|.", i18n_symbols);
460  } else {
461  error_message = _("Your IP address is banned on this server’s forums.");
462  }
463  } else if(ec == MP_NAME_AUTH_BAN_EMAIL_ERROR) {
464  if(extra_data) {
465  error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
466  } else {
467  error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
468  }
469  } else if(ec == MP_PASSWORD_REQUEST) {
470  error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols);
471  } else if(ec == MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME) {
472  error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols)
473  + "\n\n" + _("WARNING: There is already a client using this nickname, "
474  "logging in will cause that client to be kicked!");
475  } else if(ec == MP_INCORRECT_PASSWORD_ERROR) {
476  error_message = _("The password you provided was incorrect.");
477  } else if(ec == MP_TOO_MANY_ATTEMPTS_ERROR) {
478  error_message = _("You have made too many login attempts.");
479  } else if(ec == MP_HASHING_PASSWORD_FAILED) {
480  error_message = _("Password hashing failed.");
481  } else {
482  error_message = (*error)["message"].str();
483  }
484 
485  gui2::dialogs::mp_login dlg(host, error_message, !((*error)["password_request"].empty()));
486 
487  // Need to show the dialog from the main thread or it won't appear.
488  events::call_in_main_thread([&dlg]() { dlg.show(); });
489 
490  switch(dlg.get_retval()) {
491  // Log in with password
492  case gui2::retval::OK:
493  break;
494  // Cancel
495  default:
496  return nullptr;
497  }
498 
499  // If we have got a new username we have to start all over again
500  } while(login == prefs::get().login());
501 
502  // Somewhat hacky...
503  // If we broke out of the do-while loop above error is still going to be nullopt
504  if(!error) break;
505  } // end login loop
506 
507  if(const auto join_lobby = data.optional_child("join_lobby")) {
508  // Note any session data sent with the response. This should be the only place session_info is set.
509  session_info = { join_lobby.value() };
510 
511  // All done!
512  break;
513  }
514  }
515 
516  return conn;
517 }
518 
519 void mp_manager::run_lobby_loop()
520 {
521  // This should only work if we have a connection. If we're in a local mode,
522  // enter_create_mode should be accessed directly.
523  if(!connection) {
524  return;
525  }
526 
527  // A return of false means a config reload was requested, so do that and then loop.
528  while(!enter_lobby_mode()) {
531  gcm->load_game_config_for_create(true); // NOTE: Using reload_changed_game_config only doesn't seem to work here
532 
533  lobby_info.refresh_installed_addons_cache();
534 
535  connection->send_data(config("refresh_lobby"));
536  }
537 }
538 
539 bool mp_manager::enter_lobby_mode()
540 {
541  DBG_MP << "entering lobby mode";
542 
543  // Connection should never be null in the lobby.
544  assert(connection);
545 
546  // We use a loop here to allow returning to the lobby if you, say, cancel game creation.
547  while(true) {
548  if(auto cfg = game_config_manager::get()->game_config().optional_child("lobby_music")) {
549  for(const config& i : cfg->child_range("music")) {
551  }
552 
554  } else {
557  }
558 
559  int dlg_retval = 0;
560  int dlg_joined_game_id = 0;
561  std::string preset_scenario = "";
562  config server_preset;
563  int queue_id = 0;
564  {
565  gui2::dialogs::mp_lobby dlg(lobby_info, *connection, dlg_joined_game_id);
566  dlg.show();
567  dlg_retval = dlg.get_retval();
568  preset_scenario = dlg.queue_game_scenario_id();
569  server_preset = dlg.queue_game_server_preset();
570  queue_id = dlg.queue_id();
571  }
572 
573  try {
574  switch(dlg_retval) {
576  enter_create_mode(utils::make_optional(preset_scenario), utils::make_optional(server_preset), queue_id);
577  break;
579  enter_create_mode();
580  break;
582  [[fallthrough]];
584  enter_wait_mode(dlg_joined_game_id, dlg_retval == gui2::dialogs::mp_lobby::OBSERVE);
585  break;
587  // Let this function's caller reload the config and re-call.
588  return false;
589  default:
590  // Needed to handle the Quit signal and exit the loop
591  return true;
592  }
593  } catch(const config::error& error) {
594  if(!error.message.empty()) {
596  }
597 
598  // Update lobby content
599  connection->send_data(config("refresh_lobby"));
600  }
601  }
602 
603  return true;
604 }
605 
606 void mp_manager::enter_create_mode(utils::optional<std::string> preset_scenario, utils::optional<config> server_preset, int queue_id)
607 {
608  DBG_MP << "entering create mode";
609 
610  // if this is using pre-determined settings and the settings came from the server, use those
611  // else look for them locally
612  if(preset_scenario && server_preset) {
613  gui2::dialogs::mp_create_game::quick_mp_setup(state, server_preset.value());
614  enter_staging_mode(queue_type::type::server_preset, queue_id);
615  } else if(preset_scenario && !server_preset) {
616  for(const config& game : game_config_manager::get()->game_config().mandatory_child("game_presets").child_range("game")) {
617  if(game["scenario"].str() == preset_scenario.value()) {
619  enter_staging_mode(queue_type::type::client_preset);
620  return;
621  }
622  }
623  } else if(gui2::dialogs::mp_create_game::execute(state, connection == nullptr)) {
624  enter_staging_mode(queue_type::type::normal);
625  } else if(connection) {
626  connection->send_data(config("refresh_lobby"));
627  }
628 }
629 
630 void mp_manager::enter_staging_mode(queue_type::type queue_type, int queue_id)
631 {
632  DBG_MP << "entering connect mode";
633 
634  std::unique_ptr<mp_game_metadata> metadata;
635 
636  // If we have a connection, set the appropriate info. No connection means we're in local game mode.
637  if(connection) {
638  metadata = std::make_unique<mp_game_metadata>(*connection);
639  metadata->connected_players.insert(prefs::get().login());
640  metadata->is_host = true;
641  metadata->queue_type = queue_type::get_string(queue_type);
642  metadata->queue_id = queue_id;
643  }
644 
645  bool dlg_ok = false;
646  {
647  ng::connect_engine connect_engine(state, true, metadata.get());
648  dlg_ok = gui2::dialogs::mp_staging::execute(connect_engine, connection.get());
649  } // end connect_engine
650 
651  if(dlg_ok) {
653  controller.set_mp_info(metadata.get());
654  controller.play_game();
655  }
656 
657  if(connection) {
658  connection->send_data(config("leave_game"));
659  }
660 }
661 
662 void mp_manager::enter_wait_mode(int game_id, bool observe)
663 {
664  DBG_MP << "entering wait mode";
665 
666  // The connection should never be null here, since one should never reach this screen in local game mode.
667  assert(connection);
668 
669  mp_game_metadata metadata(*connection);
670  metadata.is_host = false;
671 
672  if(const mp::game_info* gi = lobby_info.get_game_by_id(game_id)) {
673  metadata.current_turn = gi->current_turn;
674  }
675 
676  if(prefs::get().skip_mp_replay() || prefs::get().blindfold_replay()) {
677  metadata.skip_replay = true;
678  metadata.skip_replay_blindfolded = prefs::get().blindfold_replay();
679  }
680 
681  bool dlg_ok = false;
682  {
683  gui2::dialogs::mp_join_game dlg(state, *connection, true, observe);
684 
685  if(!dlg.fetch_game_config()) {
686  connection->send_data(config("leave_game"));
687  return;
688  }
689 
690  dlg_ok = dlg.show();
691  }
692 
693  if(dlg_ok) {
695  controller.set_mp_info(&metadata);
696  controller.play_game();
697  }
698 
699  connection->send_data(config("leave_game"));
700 }
701 
702 bool mp_manager::post_scenario_staging(ng::connect_engine& engine)
703 {
704  return gui2::dialogs::mp_staging::execute(engine, connection.get());
705 }
706 
707 bool mp_manager::post_scenario_wait(bool observe)
708 {
709  gui2::dialogs::mp_join_game dlg(state, *connection, false, observe);
710 
711  if(!dlg.fetch_game_config()) {
712  connection->send_data(config("leave_game"));
713  return false;
714  }
715 
716  if(dlg.started()) {
717  return true;
718  }
719 
720  return dlg.show();
721 }
722 
723 } // end anon namespace
724 
725 /** Pubic entry points for the MP workflow */
726 
727 void start_client(const std::string& host)
728 {
729  DBG_MP << "starting client";
730  mp_manager(host).run_lobby_loop();
731 }
732 
734 {
735  DBG_MP << "starting local game";
736 
738 
739  mp_manager(utils::nullopt).enter_create_mode();
740 }
741 
743 {
744  DBG_MP << "starting local MP game from commandline";
745 
747 
748  // The setup is done equivalently to lobby MP games using as much of existing
749  // code as possible. This means that some things are set up that are not
750  // needed in commandline mode, but they are required by the functions called.
752 
753  DBG_MP << "entering create mode";
754 
755  // Set the default parameters
756  saved_game state;
757  state.classification().type = campaign_type::type::multiplayer;
758 
759  mp_game_settings& parameters = state.mp_settings();
760 
761  // Hardcoded default values
762  state.classification().era_id = "era_default";
763  parameters.name = "multiplayer_The_Freelands";
764 
765  // Default values for which at getter function exists
766  parameters.num_turns = settings::get_turns("");
767  parameters.village_gold = settings::get_village_gold("");
769  parameters.xp_modifier = settings::get_xp_modifier("");
770 
771  // Do not use map settings if --ignore-map-settings commandline option is set
772  if(cmdline_opts.multiplayer_ignore_map_settings) {
773  DBG_MP << "ignoring map settings";
774  parameters.use_map_settings = false;
775  } else {
776  parameters.use_map_settings = true;
777  }
778 
779  // None of the other parameters need to be set, as their creation values above are good enough for CL mode.
780  // In particular, we do not want to use the preferences values.
781 
782  state.classification().type = campaign_type::type::multiplayer;
783 
784  // [era] define.
785  if(cmdline_opts.multiplayer_era) {
786  state.classification().era_id = *cmdline_opts.multiplayer_era;
787  }
788 
789  if(auto cfg_era = game_config.find_child("era", "id", state.classification().era_id)) {
790  state.classification().era_define = cfg_era["define"].str();
791  } else {
792  PLAIN_LOG << "Could not find era '" << state.classification().era_id << "'";
793  return;
794  }
795 
796  // [multiplayer] define.
797  if(cmdline_opts.multiplayer_scenario) {
798  parameters.name = *cmdline_opts.multiplayer_scenario;
799  }
800 
801  if(auto cfg_multiplayer = game_config.find_child("multiplayer", "id", parameters.name)) {
802  state.classification().scenario_define = cfg_multiplayer["define"].str();
803  } else {
804  PLAIN_LOG << "Could not find [multiplayer] '" << parameters.name << "'";
805  return;
806  }
807 
809  config {"next_scenario", parameters.name}
810  );
811 
813 
814  state.expand_random_scenario();
815  state.expand_mp_events();
816  state.expand_mp_options();
817 
818  // Should number of turns be determined from scenario data?
819  if(parameters.use_map_settings && state.get_starting_point().has_attribute("turns")) {
820  DBG_MP << "setting turns from scenario data: " << state.get_starting_point()["turns"];
821  parameters.num_turns = state.get_starting_point()["turns"].to_int();
822  }
823 
824  DBG_MP << "entering connect mode";
825 
826  {
827  ng::connect_engine connect_engine(state, true, nullptr);
828 
829  // Update the parameters to reflect game start conditions
830  connect_engine.start_game_commandline(cmdline_opts, game_config);
831  }
832 
833  if(resources::recorder && cmdline_opts.multiplayer_label) {
834  std::string label = *cmdline_opts.multiplayer_label;
835  resources::recorder->add_log_data("ai_log","ai_label",label);
836  }
837 
838  unsigned int repeat = (cmdline_opts.multiplayer_repeat) ? *cmdline_opts.multiplayer_repeat : 1;
839  for(unsigned int i = 0; i < repeat; i++){
840  saved_game state_copy(state);
841  campaign_controller controller(state_copy);
842  controller.play_game();
843  }
844 }
845 
847 {
848  return manager && manager->post_scenario_staging(engine);
849 }
850 
851 bool goto_mp_wait(bool observe)
852 {
853  return manager && manager->post_scenario_wait(observe);
854 }
855 
857 {
858  return manager && manager->get_session_info().is_moderator;
859 }
860 
861 std::string get_profile_link(int user_id)
862 {
863  if(manager) {
864  const std::string& prefix = manager->get_session_info().profile_url_prefix;
865 
866  if(!prefix.empty()) {
867  return prefix + std::to_string(user_id);
868  }
869  }
870 
871  return "";
872 }
873 
874 std::vector<queue_info>& get_server_queues()
875 {
876  static std::vector<queue_info> queues;
877  if(manager) {
878  return manager->get_session_info().queues;
879  }
880  return queues;
881 }
882 
884 {
885  if(manager && manager->connection) {
886  manager->connection->send_data(data);
887  }
888 }
889 
891 {
892  if(manager /*&& manager->connection*/) {
893  remove_handler = manager->add_network_handler(func);
894  }
895 }
896 
898 {
899  if(remove_handler) {
900  remove_handler();
901  }
902 }
903 
905 {
906  return manager ? &manager->lobby_info : nullptr;
907 }
908 
909 } // end namespace mp
utils::optional< std::string > multiplayer_scenario
Non-empty if –scenario was given on the command line.
utils::optional< unsigned int > multiplayer_repeat
Repeats specified by –multiplayer-repeat option.
utils::optional< std::string > multiplayer_label
Non-empty if –label was given on the command line.
utils::optional< std::string > multiplayer_era
Non-empty if –era was given on the command line.
bool multiplayer_ignore_map_settings
True if –ignore-map-settings was given at the command line.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:362
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:312
bool has_attribute(config_key_type key) const
Definition: config.cpp:157
child_itors child_range(config_key_type key)
Definition: config.cpp:268
config & add_child(config_key_type key)
Definition: config.cpp:436
std::string scenario_define
If there is a define the scenario uses to customize data.
std::string era_define
If there is a define the era uses to customize data.
campaign_type::type type
static game_config_manager * get()
void load_game_config_for_game(const game_classification &classification, const std::string &scenario_id)
void load_game_config_for_create(bool is_mp, bool is_test=false)
const game_config_view & game_config() const
A class grating read only view to a vector of config objects, viewed as one config with all children ...
static void progress(loading_stage stage=loading_stage::none)
Report what is being loaded to the loading screen.
static void display(const std::function< void()> &f)
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
@ ok_cancel_buttons
Shows an ok and cancel button.
Definition: message.hpp:77
bool show(const unsigned auto_close_time=0)
Shows the window.
static void quick_mp_setup(saved_game &state, const config presets)
presets needs to be a copy! Otherwise you'll get segfaults when clicking the Join button since it res...
const std::string queue_game_scenario_id() const
Definition: lobby.hpp:69
int queue_id() const
Definition: lobby.hpp:71
const config queue_game_server_preset() const
Definition: lobby.hpp:70
@ RELOAD_CONFIG
player clicked the Create button
Definition: lobby.hpp:65
@ CREATE_PRESET
player clicked Join button on an [mp_queue] game, but there was no existing game to join
Definition: lobby.hpp:66
int get_retval()
Definition: window.hpp:402
This class represents the collective information the client has about the players and games on the se...
Definition: lobby_info.hpp:31
network_registrar(const handler &func)
std::function< void()> remove_handler
std::function< void(const config &)> handler
Definition: multiplayer.hpp:94
void start_game_commandline(const commandline_options &cmdline_opts, const game_config_view &game_config)
static prefs & get()
void set_message_private(bool value)
std::string login()
std::string password(const std::string &server, const std::string &login)
void add_log_data(const std::string &key, const std::string &var)
Definition: replay.cpp:299
game_classification & classification()
Definition: saved_game.hpp:56
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:449
void set_carryover_sides_start(config carryover_sides_start)
Definition: saved_game.cpp:165
std::string get_scenario_id() const
Definition: saved_game.cpp:698
mp_game_settings & mp_settings()
Multiplayer parameters for this game.
Definition: saved_game.hpp:60
config & get_starting_point()
Definition: saved_game.cpp:631
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:408
void expand_random_scenario()
takes care of generate_map=, generate_scenario=, map= attributes This should be called before expandi...
Definition: saved_game.cpp:524
std::string str() const
Serializes the version number into string form.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1030
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string label
What to show in the filter's drop-down list.
Definition: manager.cpp:201
Standard logging facilities (interface).
#define PLAIN_LOG
Definition: log.hpp:296
General settings and defaults for scenarios.
static lg::log_domain log_mp("mp/main")
#define DBG_MP
Definition: multiplayer.cpp:50
Define the errors the server may send during the login procedure.
#define MP_INCORRECT_PASSWORD_ERROR
#define MP_NAME_AUTH_BAN_USER_ERROR
#define MP_MUST_LOGIN
#define MP_NAME_RESERVED_ERROR
#define MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME
#define MP_NAME_AUTH_BAN_EMAIL_ERROR
#define MP_TOO_MANY_ATTEMPTS_ERROR
#define MP_HASHING_PASSWORD_FAILED
#define MP_SERVER_IP_BAN_ERROR
#define MP_NAME_TOO_LONG_ERROR
#define MP_NAME_AUTH_BAN_IP_ERROR
#define MP_PASSWORD_REQUEST
#define MP_NAME_INACTIVE_WARNING
#define MP_NAME_UNREGISTERED_ERROR
#define MP_INVALID_CHARS_IN_NAME_ERROR
#define MP_NAME_TAKEN_ERROR
auto parse_duration(const config_attribute_value &val, const Duration &def=Duration{0})
Definition: chrono.hpp:71
void call_in_main_thread(const std::function< void(void)> &f)
Definition: events.cpp:777
Game configuration data as global variables.
Definition: build_info.cpp:61
const version_info wesnoth_version(VERSION)
bool allow_insecure
Definition: game_config.cpp:78
std::string dist_channel_id()
Return the distribution channel identifier, or "Default" if missing.
Definition: build_info.cpp:396
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition: message.cpp:201
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:148
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
logger & info()
Definition: log.cpp:318
Main entry points of multiplayer mode.
Definition: lobby_data.cpp:49
lobby_info * get_lobby_info()
Returns the lobby_info object for the given session.
void send_to_server(const config &data)
Attempts to send given data to server if a connection is open.
void start_local_game()
Starts a multiplayer game in single-user mode.
std::vector< queue_info > & get_server_queues()
Gets the list of server-side queues received on login.
bool goto_mp_staging(ng::connect_engine &engine)
Opens the MP Staging screen and sets the game state according to the changes made.
void start_local_game_commandline(const commandline_options &cmdline_opts)
Starts a multiplayer game in single-user mode using command line settings.
std::string get_profile_link(int user_id)
Gets the forum profile link for the given user.
void start_client(const std::string &host)
Pubic entry points for the MP workflow.
bool logged_in_as_moderator()
Gets whether the currently logged-in user is a moderator.
bool goto_mp_wait(bool observe)
Opens the MP Join Game screen and sets the game state according to the changes made.
replay * recorder
Definition: resources.cpp:28
int get_village_support(const std::string &value)
Gets the village unit level support.
int get_turns(const std::string &value)
Gets the number of turns.
int get_xp_modifier(const std::string &value)
Gets the xp modifier.
int get_village_gold(const std::string &value, const game_classification *classification)
Gets the village gold.
void empty_playlist()
Definition: sound.cpp:609
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:712
void stop_music()
Definition: sound.cpp:554
void commit_music_changes()
Definition: sound.cpp:839
static std::string format_timespan(const std::chrono::duration< Rep, Period > &span, bool detailed=false)
Formats a timespan into human-readable text for player authentication functions.
std::set< std::string > split_set(std::string_view s, char sep, const int flags)
std::map< std::string, t_string > string_map
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.
std::string_view data
Definition: picture.cpp:188
Replay control code.
std::string message
Definition: exceptions.hpp:30
This class represents the info a client has about a game on the server.
Definition: lobby_data.hpp:62
unsigned current_turn
bool skip_replay_blindfolded
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
An error occurred during when trying to communicate with the wesnothd server.
Error used when the client is rejected by the MP server.
static map_location::direction s