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