The Battle for Wesnoth  1.19.27+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.display_name = queue["display_name"].str();
112  info.players_required = queue["players_required"].to_int();
113  info.current_players = utils::split_set(queue["current_players"].str());
114  queues.emplace_back(info);
115  }
116  }
117  }
118 
119  /** Whether you are logged in as a server moderator. */
120  bool is_moderator = false;
121 
122  /** The external URL prefix for player profiles (empty if the server doesn't have an attached database). */
123  std::string profile_url_prefix;
124 
125  /** The list of server-side queues */
126  std::vector<queue_info> queues;
127  };
128 
129  /** Opens a new server connection and prompts the client for login credentials, if necessary. */
130  std::unique_ptr<wesnothd_connection> open_connection(const std::string& host);
131 
132  /** Opens the MP lobby. */
133  bool enter_lobby_mode();
134 
135  /**
136  * Opens the MP Create screen for hosts to configure a new game.
137  */
138  void enter_create_mode(utils::optional<config> preset = utils::nullopt, int queue_id = 0);
139 
140  /** Opens the MP Staging screen for hosts to wait for players. */
141  void enter_staging_mode(queue_type::type queue_type, int queue_id = 0);
142 
143  /** Opens the MP Join Game screen for non-host players and observers. */
144  void enter_wait_mode(int game_id, bool observe);
145 
146  /** Worker thread to handle receiving and processing network data. */
147  std::thread network_worker;
148 
149  /** Flag to signal the worker thread terminate. */
150  std::atomic_bool stop;
151 
152  /** The connection to the server. */
153  std::unique_ptr<wesnothd_connection> connection;
154 
155  /** The current session's info sent by the server on login. */
156  session_metadata session_info;
157 
158  /** This single instance is reused for all games played during the current connection to the server. */
159  saved_game state;
160 
161  mp::lobby_info lobby_info;
162 
163  std::list<mp::network_registrar::handler> process_handlers;
164 
165 public:
166  const session_metadata& get_session_info() const
167  {
168  return session_info;
169  }
170  session_metadata& get_session_info()
171  {
172  return session_info;
173  }
174 
175  auto add_network_handler(const decltype(process_handlers)::value_type& func)
176  {
177  return [this, iter = process_handlers.insert(process_handlers.end(), func)]() { process_handlers.erase(iter); };
178  }
179 };
180 
181 mp_manager::mp_manager(const utils::optional<std::string> host)
182  : network_worker()
183  , stop(false)
184  , connection(nullptr)
185  , session_info()
186  , state()
187  , lobby_info()
188  , process_handlers()
189 {
190  state.classification().type = campaign_type::type::multiplayer;
191 
192  if(host) {
194  connection = open_connection(*host);
195 
196  // If for whatever reason our connection is null at this point (dismissing the password prompt, for
197  // instance), treat it as a normal condition and exit. Any actual error conditions throw exceptions
198  // which can be handled higher up the stack.
199  if(connection == nullptr) {
200  return;
201  }
202 
204 
205  config data;
206 
207  while(!stop) {
208  connection->wait_and_receive_data(data);
209 
210  if(const auto error = data.optional_child("error")) {
211  throw wesnothd_error((*error)["message"]);
212  }
213 
214  else if(data.has_child("gamelist")) {
215  this->lobby_info.process_gamelist(data);
216  break;
217  }
218 
219  else if(const auto gamelist_diff = data.optional_child("gamelist_diff")) {
220  this->lobby_info.process_gamelist_diff(*gamelist_diff);
221  }
222 
223  else {
224  // No special actions to take. Pass the data on to the network handlers.
225  for(const auto& handler : process_handlers) {
226  handler(data);
227  }
228  }
229  }
230  });
231  }
232 
233  // Avoid setting this until the connection has been fully established. open_connection may throw,
234  // in which case we don't want to point to an object instance that has not properly connected.
235  assert(!manager);
236  manager = this;
237 }
238 
239 std::unique_ptr<wesnothd_connection> mp_manager::open_connection(const std::string& host)
240 {
241  DBG_MP << "opening connection";
242 
243  if(host.empty()) {
244  return nullptr;
245  }
246 
247  // shown_hosts is used to prevent the client being locked in a redirect loop.
248  std::set<std::pair<std::string, std::string>> shown_hosts;
249  auto addr = shown_hosts.end();
250 
251  try {
252  std::tie(addr, std::ignore) = shown_hosts.insert(parse_network_address(host, "15000"));
253  } catch(const std::runtime_error&) {
254  throw wesnothd_error(_("Invalid address specified for multiplayer server"));
255  }
256 
257  // Start stage
259 
260  // Initializes the connection to the server.
261  auto conn = std::make_unique<wesnothd_connection>(addr->first, addr->second);
262 
263  // First, spin until we get a handshake from the server.
264  conn->wait_for_handshake();
265 
267 
268  config data;
269 
270  // Then, log in and wait for the lobby/game join prompt.
271  while(true) {
272  data.clear();
273  conn->wait_and_receive_data(data);
274 
275  if(const auto reject = data.optional_child("reject"); reject || data.has_attribute("version")) {
276  std::string version;
277 
278  if(reject) {
279  version = (*reject)["accepted_versions"].str();
280  } else {
281  // Backwards-compatibility "version" attribute
282  version = data["version"].str();
283  }
284 
285  utils::string_map i18n_symbols;
286  i18n_symbols["required_version"] = version;
287  i18n_symbols["your_version"] = game_config::wesnoth_version.str();
288 
289  const std::string errorstring = VGETTEXT("The server accepts versions ‘$required_version’, but you are using version ‘$your_version’", i18n_symbols);
290  throw wesnothd_error(errorstring);
291  }
292 
293  // Check for "redirect" messages
294  if(const auto redirect = data.optional_child("redirect")) {
295  auto redirect_host = (*redirect)["host"].str();
296  auto redirect_port = (*redirect)["port"].str("15000");
297 
298  bool recorded_host;
299  std::tie(std::ignore, recorded_host) = shown_hosts.emplace(redirect_host, redirect_port);
300 
301  if(!recorded_host) {
302  throw wesnothd_error(_("Server-side redirect loop"));
303  }
304 
306 
307  // Open a new connection with the new host and port.
308  conn.reset();
309  conn = std::make_unique<wesnothd_connection>(redirect_host, redirect_port);
310 
311  // Wait for new handshake.
312  conn->wait_for_handshake();
313 
315  continue;
316  }
317 
318  if(data.has_child("version")) {
319  std::string version = game_config::wesnoth_version;
320  std::string channel = game_config::dist_channel_id();
321  conn->send_data(config{"version", config{"version", version, "client_source", channel}});
322  }
323 
324  if(const auto error = data.optional_child("error")) {
325  throw wesnothd_rejected_client_error((*error)["message"].str());
326  }
327 
328  // Continue if we did not get a direction to login
329  if(!data.has_child("mustlogin")) {
330  continue;
331  }
332 
333  // Enter login loop
334  while(true) {
335  std::string login = prefs::get().login();
336 
337  conn->send_data(config{"login", config{"username", login}});
338  conn->wait_and_receive_data(data);
339 
341 
342  if(const auto warning = data.optional_child("warning")) {
343  std::string warning_msg;
344 
345  if((*warning)["warning_code"] == MP_NAME_INACTIVE_WARNING) {
346  warning_msg = VGETTEXT("The nickname ‘$nick’ is inactive. "
347  "You cannot claim ownership of this nickname until you "
348  "activate your account via email or ask an "
349  "administrator to do it for you.", {{"nick", login}});
350  } else {
351  warning_msg = (*warning)["message"].str();
352  }
353 
354  warning_msg += "\n\n";
355  warning_msg += _("Do you want to continue?");
356 
358  return nullptr;
359  } else {
360  continue;
361  }
362  }
363 
364  auto error = data.optional_child("error");
365 
366  // ... and get us out of here if the server did not complain
367  if(!error) break;
368 
369  do {
370  std::string password = prefs::get().password(host, login);
371 
372  const bool fall_through = (*error)["force_confirmation"].to_bool()
374  : false;
375 
376  // If:
377  // * the server asked for a password
378  // * the password isn't empty
379  // * the user didn't press Cancel
380  // * the connection is secure or the client was started with the option to use insecure connections
381  // send the password to the server
382  // otherwise go directly to the username/password dialog
383  if(!(*error)["password_request"].empty() && !password.empty() && !fall_through && (conn->using_tls() || game_config::allow_insecure)) {
384  // the possible cases here are that either:
385  // 1) TLS encryption is enabled, thus sending the plaintext password is still secure
386  // 2) TLS encryption is not enabled, in which case the server should not be requesting a password in the first place
387  // 3) This is being used for local testing/development, so using an insecure connection is enabled manually
388 
389  // Once again send our request...
390  conn->send_data(config{"login", config{"username", login, "password", password}});
391  conn->wait_and_receive_data(data);
392 
394 
395  error = data.optional_child("error");
396 
397  // ... and get us out of here if the server is happy now
398  if(!error) break;
399  }
400 
401  // Providing a password either was not attempted because we did not
402  // have any or failed:
403  // Now show a dialog that displays the error and allows to
404  // enter a new user name and/or password
405 
406  std::string error_message;
407  utils::string_map i18n_symbols;
408  i18n_symbols["nick"] = login;
409 
410  const auto extra_data = error->optional_child("data");
411  if(extra_data) {
412  using namespace std::chrono_literals;
413  i18n_symbols["duration"] = utils::format_timespan(chrono::parse_duration((*extra_data)["duration"], 0s));
414  }
415 
416  const std::string ec = (*error)["error_code"];
417 
418  if(!(*error)["password_request"].empty() && !conn->using_tls() && !game_config::allow_insecure) {
419  error_message = _("The remote server requested a password while using an insecure connection.");
420  } else if(ec == MP_MUST_LOGIN) {
421  error_message = _("You must login first.");
422  } else if(ec == MP_NAME_TAKEN_ERROR) {
423  error_message = VGETTEXT("The nickname ‘$nick’ is already taken.", i18n_symbols);
424  } else if(ec == MP_INVALID_CHARS_IN_NAME_ERROR) {
425  error_message = VGETTEXT("The nickname ‘$nick’ contains invalid "
426  "characters. Only alpha-numeric characters (one at minimum), underscores and "
427  "hyphens are allowed.", i18n_symbols);
428  } else if(ec == MP_NAME_TOO_LONG_ERROR) {
429  error_message = VGETTEXT("The nickname ‘$nick’ is too long. Nicks must be 20 characters or less.", i18n_symbols);
430  } else if(ec == MP_NAME_RESERVED_ERROR) {
431  error_message = VGETTEXT("The nickname ‘$nick’ is reserved and cannot be used by players.", i18n_symbols);
432  } else if(ec == MP_NAME_UNREGISTERED_ERROR) {
433  error_message = VGETTEXT("The nickname ‘$nick’ is not registered on this server.", i18n_symbols)
434  + _(" This server disallows unregistered nicknames.");
435  } else if(ec == MP_SERVER_IP_BAN_ERROR) {
436  if(extra_data) {
437  error_message = VGETTEXT("Your IP address is banned on this server for $duration|.", i18n_symbols);
438  } else {
439  error_message = _("Your IP address is banned on this server.");
440  }
441  } else if(ec == MP_NAME_AUTH_BAN_USER_ERROR) {
442  if(extra_data) {
443  error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
444  } else {
445  error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
446  }
447  } else if(ec == MP_NAME_AUTH_BAN_IP_ERROR) {
448  if(extra_data) {
449  error_message = VGETTEXT("Your IP address is banned on this server’s forums for $duration|.", i18n_symbols);
450  } else {
451  error_message = _("Your IP address is banned on this server’s forums.");
452  }
453  } else if(ec == MP_NAME_AUTH_BAN_EMAIL_ERROR) {
454  if(extra_data) {
455  error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
456  } else {
457  error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
458  }
459  } else if(ec == MP_PASSWORD_REQUEST) {
460  error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols);
461  } else if(ec == MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME) {
462  error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols)
463  + "\n\n" + _("WARNING: There is already a client using this nickname, "
464  "logging in will cause that client to be kicked!");
465  } else if(ec == MP_INCORRECT_PASSWORD_ERROR) {
466  error_message = _("The password you provided was incorrect.");
467  } else if(ec == MP_TOO_MANY_ATTEMPTS_ERROR) {
468  error_message = _("You have made too many login attempts.");
469  } else if(ec == MP_HASHING_PASSWORD_FAILED) {
470  error_message = _("Password hashing failed.");
471  } else {
472  error_message = (*error)["message"].str();
473  }
474 
475  gui2::dialogs::mp_login dlg(host, error_message, !((*error)["password_request"].empty()));
476 
477  // Need to show the dialog from the main thread or it won't appear.
478  events::call_in_main_thread([&dlg]() { dlg.show(); });
479 
480  switch(dlg.get_retval()) {
481  // Log in with password
482  case gui2::retval::OK:
483  break;
484  // Cancel
485  default:
486  return nullptr;
487  }
488 
489  // If we have got a new username we have to start all over again
490  } while(login == prefs::get().login());
491 
492  // Somewhat hacky...
493  // If we broke out of the do-while loop above error is still going to be nullopt
494  if(!error) break;
495  } // end login loop
496 
497  if(const auto join_lobby = data.optional_child("join_lobby")) {
498  // Note any session data sent with the response. This should be the only place session_info is set.
499  session_info = { join_lobby.value() };
500 
501  // All done!
502  break;
503  }
504  }
505 
506  return conn;
507 }
508 
509 void mp_manager::run_lobby_loop()
510 {
511  // This should only work if we have a connection. If we're in a local mode,
512  // enter_create_mode should be accessed directly.
513  if(!connection) {
514  return;
515  }
516 
517  // A return of false means a config reload was requested, so do that and then loop.
518  while(!enter_lobby_mode()) {
521  gcm->load_game_config_for_create(true); // NOTE: Using reload_changed_game_config only doesn't seem to work here
522 
523  lobby_info.refresh_installed_addons_cache();
524 
525  connection->send_data(config{"refresh_lobby"});
526  }
527 }
528 
529 bool mp_manager::enter_lobby_mode()
530 {
531  DBG_MP << "entering lobby mode";
532 
533  // Connection should never be null in the lobby.
534  assert(connection);
535 
536  // We use a loop here to allow returning to the lobby if you, say, cancel game creation.
537  while(true) {
538  if(auto cfg = game_config_manager::get()->game_config().optional_child("lobby_music")) {
539  for(const config& i : cfg->child_range("music")) {
541  }
542 
544  } else {
547  }
548 
549  int dlg_retval = 0;
550  int dlg_joined_game_id = 0;
551  std::string preset_scenario = "";
552  config server_preset;
553  int queue_id = 0;
554  {
555  gui2::dialogs::mp_lobby dlg(lobby_info, *connection, dlg_joined_game_id);
556  dlg.show();
557  dlg_retval = dlg.get_retval();
558  server_preset = dlg.queue_game_server_preset();
559  queue_id = dlg.queue_id();
560  }
561 
562  try {
563  switch(dlg_retval) {
565  enter_create_mode(utils::make_optional(server_preset), queue_id);
566  break;
568  enter_create_mode();
569  break;
571  [[fallthrough]];
573  enter_wait_mode(dlg_joined_game_id, dlg_retval == gui2::dialogs::mp_lobby::OBSERVE);
574  break;
576  // Let this function's caller reload the config and re-call.
577  return false;
578  default:
579  // Needed to handle the Quit signal and exit the loop
580  return true;
581  }
582  } catch(const config::error& error) {
583  if(!error.message.empty()) {
585  }
586 
587  if(dlg_retval == gui2::dialogs::mp_lobby::CREATE_PRESET) {
588  connection->send_data(config{"leave_server_queue", config{"queue_id", queue_id}});
589  }
590 
591  // Update lobby content
592  connection->send_data(config{"refresh_lobby"});
593  }
594  }
595 
596  return true;
597 }
598 
599 void mp_manager::enter_create_mode(utils::optional<config> preset, int queue_id)
600 {
601  DBG_MP << "entering create mode";
602 
603  // if this is using pre-determined settings and the settings came from the server, use those
604  // else look for them locally
605  if(preset) {
606  gui2::dialogs::mp_create_game::quick_mp_setup(state, preset.value());
607  enter_staging_mode(queue_id >= 0 ? queue_type::type::server_preset : queue_type::type::normal, queue_id);
608  } else if(gui2::dialogs::mp_create_game::execute(state, connection == nullptr)) {
609  enter_staging_mode(queue_type::type::normal);
610  } else if(connection) {
611  connection->send_data(config{"refresh_lobby"});
612  }
613 }
614 
615 void mp_manager::enter_staging_mode(queue_type::type queue_type, int queue_id)
616 {
617  DBG_MP << "entering connect mode";
618 
619  std::unique_ptr<mp_game_metadata> metadata;
620 
621  // If we have a connection, set the appropriate info. No connection means we're in local game mode.
622  if(connection) {
623  metadata = std::make_unique<mp_game_metadata>(*connection);
624  metadata->connected_players.insert(prefs::get().login());
625  metadata->is_host = true;
626  metadata->queue_type = queue_type::get_string(queue_type);
627  metadata->queue_id = queue_id;
628  }
629 
630  bool dlg_ok = false;
631  {
632  ng::connect_engine connect_engine(state, true, metadata.get());
633  dlg_ok = gui2::dialogs::mp_staging::execute(connect_engine, connection.get());
634  } // end connect_engine
635 
636  if(dlg_ok) {
638  controller.set_mp_info(metadata.get());
639  controller.play_game();
640  }
641 
642  if(connection) {
643  connection->send_data(config{"leave_game"});
644  }
645 }
646 
647 void mp_manager::enter_wait_mode(int game_id, bool observe)
648 {
649  DBG_MP << "entering wait mode";
650 
651  // The connection should never be null here, since one should never reach this screen in local game mode.
652  assert(connection);
653 
654  mp_game_metadata metadata(*connection);
655  metadata.is_host = false;
656 
657  if(const mp::game_info* gi = lobby_info.get_game_by_id(game_id)) {
658  metadata.current_turn = gi->current_turn;
659  }
660 
661  if(prefs::get().skip_mp_replay() || prefs::get().blindfold_replay()) {
662  metadata.skip_replay = true;
663  metadata.skip_replay_blindfolded = prefs::get().blindfold_replay();
664  }
665 
666  bool dlg_ok = false;
667  {
668  gui2::dialogs::mp_join_game dlg(state, *connection, true, observe);
669 
670  if(!dlg.fetch_game_config()) {
671  connection->send_data(config{"leave_game"});
672  return;
673  }
674 
675  dlg_ok = dlg.show();
676  }
677 
678  if(dlg_ok) {
680  controller.set_mp_info(&metadata);
681  controller.play_game();
682  }
683 
684  connection->send_data(config{"leave_game"});
685 }
686 
687 bool mp_manager::post_scenario_staging(ng::connect_engine& engine)
688 {
689  return gui2::dialogs::mp_staging::execute(engine, connection.get());
690 }
691 
692 bool mp_manager::post_scenario_wait(bool observe)
693 {
694  gui2::dialogs::mp_join_game dlg(state, *connection, false, observe);
695 
696  if(!dlg.fetch_game_config()) {
697  connection->send_data(config{"leave_game"});
698  return false;
699  }
700 
701  if(dlg.started()) {
702  return true;
703  }
704 
705  return dlg.show();
706 }
707 
708 } // end anon namespace
709 
710 /** Pubic entry points for the MP workflow */
711 
712 void start_client(const std::string& host)
713 {
714  DBG_MP << "starting client";
715  mp_manager(host).run_lobby_loop();
716 }
717 
719 {
720  DBG_MP << "starting local game";
721 
723 
724  mp_manager(utils::nullopt).enter_create_mode();
725 }
726 
728 {
729  DBG_MP << "starting local MP game from commandline";
730 
732 
733  // The setup is done equivalently to lobby MP games using as much of existing
734  // code as possible. This means that some things are set up that are not
735  // needed in commandline mode, but they are required by the functions called.
737 
738  DBG_MP << "entering create mode";
739 
740  // Set the default parameters
741  saved_game state;
742  state.classification().type = campaign_type::type::multiplayer;
743 
744  mp_game_settings& parameters = state.mp_settings();
745 
746  // Hardcoded default values
747  state.classification().era_id = "era_default";
748  parameters.name = "multiplayer_The_Freelands";
749 
750  // Default values for which at getter function exists
751  parameters.num_turns = settings::get_turns("");
752  parameters.village_gold = settings::get_village_gold("");
754  parameters.xp_modifier = settings::get_xp_modifier("");
755 
756  // Do not use map settings if --ignore-map-settings commandline option is set
757  if(cmdline_opts.multiplayer_ignore_map_settings) {
758  DBG_MP << "ignoring map settings";
759  parameters.use_map_settings = false;
760  } else {
761  parameters.use_map_settings = true;
762  }
763 
764  // None of the other parameters need to be set, as their creation values above are good enough for CL mode.
765  // In particular, we do not want to use the preferences values.
766 
767  state.classification().type = campaign_type::type::multiplayer;
768 
769  // [era] define.
770  if(cmdline_opts.multiplayer_era) {
771  state.classification().era_id = *cmdline_opts.multiplayer_era;
772  }
773 
774  if(auto cfg_era = game_config.find_child("era", "id", state.classification().era_id)) {
775  state.classification().era_define = cfg_era["define"].str();
776  } else {
777  PLAIN_LOG << "Could not find era '" << state.classification().era_id << "'";
778  return;
779  }
780 
781  // [multiplayer] define.
782  if(cmdline_opts.multiplayer_scenario) {
783  parameters.name = *cmdline_opts.multiplayer_scenario;
784  }
785 
786  if(auto cfg_multiplayer = game_config.find_child("multiplayer", "id", parameters.name)) {
787  state.classification().scenario_define = cfg_multiplayer["define"].str();
788  } else {
789  PLAIN_LOG << "Could not find [multiplayer] '" << parameters.name << "'";
790  return;
791  }
792 
794  config {"next_scenario", parameters.name}
795  );
796 
798 
799  state.expand_random_scenario();
800  state.expand_mp_events();
801  state.expand_mp_options();
802 
803  // Should number of turns be determined from scenario data?
804  if(parameters.use_map_settings && state.get_starting_point().has_attribute("turns")) {
805  DBG_MP << "setting turns from scenario data: " << state.get_starting_point()["turns"];
806  parameters.num_turns = state.get_starting_point()["turns"].to_int();
807  }
808 
809  DBG_MP << "entering connect mode";
810 
811  {
812  ng::connect_engine connect_engine(state, true, nullptr);
813 
814  // Update the parameters to reflect game start conditions
815  connect_engine.start_game_commandline(cmdline_opts, game_config);
816  }
817 
818  if(resources::recorder && cmdline_opts.multiplayer_label) {
819  std::string label = *cmdline_opts.multiplayer_label;
820  resources::recorder->add_log_data("ai_log","ai_label",label);
821  }
822 
823  unsigned int repeat = (cmdline_opts.multiplayer_repeat) ? *cmdline_opts.multiplayer_repeat : 1;
824  for(unsigned int i = 0; i < repeat; i++){
825  saved_game state_copy(state);
826  campaign_controller controller(state_copy);
827  controller.play_game();
828  }
829 }
830 
832 {
833  return manager && manager->post_scenario_staging(engine);
834 }
835 
836 bool goto_mp_wait(bool observe)
837 {
838  return manager && manager->post_scenario_wait(observe);
839 }
840 
842 {
843  return manager && manager->get_session_info().is_moderator;
844 }
845 
846 std::string get_profile_link(int user_id)
847 {
848  if(manager) {
849  const std::string& prefix = manager->get_session_info().profile_url_prefix;
850 
851  if(!prefix.empty()) {
852  return prefix + std::to_string(user_id);
853  }
854  }
855 
856  return "";
857 }
858 
859 std::vector<queue_info>& get_server_queues()
860 {
861  static std::vector<queue_info> queues;
862  if(manager) {
863  return manager->get_session_info().queues;
864  }
865  return queues;
866 }
867 
869 {
870  if(manager && manager->connection) {
871  manager->connection->send_data(data);
872  }
873 }
874 
876 {
877  if(manager /*&& manager->connection*/) {
878  remove_handler = manager->add_network_handler(func);
879  }
880 }
881 
883 {
884  if(remove_handler) {
885  remove_handler();
886  }
887 }
888 
890 {
891  return manager ? &manager->lobby_info : nullptr;
892 }
893 
894 } // 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:157
child_itors child_range(std::string_view key)
Definition: config.cpp:268
bool has_attribute(std::string_view key) const
Definition: config.cpp:157
bool has_child(std::string_view key) const
Determine whether a config has a child or not.
Definition: config.cpp:312
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:362
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...
int queue_id() const
Definition: lobby.hpp:70
const config queue_game_server_preset() const
Definition: lobby.hpp:69
@ RELOAD_CONFIG
player clicked the Create button
Definition: lobby.hpp:65
int get_retval()
Definition: window.hpp:394
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
Definition: multiplayer.hpp:99
std::function< void(const config &)> handler
Definition: multiplayer.hpp:93
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:298
game_classification & classification()
Definition: saved_game.hpp:55
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:59
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.
const config * cfg
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1031
static std::string _(const char *str)
Definition: gettext.hpp:100
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:82
void call_in_main_thread(const std::function< void(void)> &f)
Definition: events.cpp:773
Game configuration data as global variables.
Definition: build_info.cpp:68
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:392
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:351
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:605
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:684
void stop_music()
Definition: sound.cpp:533
void commit_music_changes()
Definition: sound.cpp:793
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::string to_string(const Range &range, const Func &op)
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
channel
Definition: utils.hpp:103