The Battle for Wesnoth  1.19.15+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  // Update lobby content
588  connection->send_data(config{"refresh_lobby"});
589  }
590  }
591 
592  return true;
593 }
594 
595 void mp_manager::enter_create_mode(utils::optional<config> preset, int queue_id)
596 {
597  DBG_MP << "entering create mode";
598 
599  // if this is using pre-determined settings and the settings came from the server, use those
600  // else look for them locally
601  if(preset) {
602  gui2::dialogs::mp_create_game::quick_mp_setup(state, preset.value());
603  enter_staging_mode(queue_id >= 0 ? queue_type::type::server_preset : queue_type::type::normal, queue_id);
604  } else if(gui2::dialogs::mp_create_game::execute(state, connection == nullptr)) {
605  enter_staging_mode(queue_type::type::normal);
606  } else if(connection) {
607  connection->send_data(config{"refresh_lobby"});
608  }
609 }
610 
611 void mp_manager::enter_staging_mode(queue_type::type queue_type, int queue_id)
612 {
613  DBG_MP << "entering connect mode";
614 
615  std::unique_ptr<mp_game_metadata> metadata;
616 
617  // If we have a connection, set the appropriate info. No connection means we're in local game mode.
618  if(connection) {
619  metadata = std::make_unique<mp_game_metadata>(*connection);
620  metadata->connected_players.insert(prefs::get().login());
621  metadata->is_host = true;
622  metadata->queue_type = queue_type::get_string(queue_type);
623  metadata->queue_id = queue_id;
624  }
625 
626  bool dlg_ok = false;
627  {
628  ng::connect_engine connect_engine(state, true, metadata.get());
629  dlg_ok = gui2::dialogs::mp_staging::execute(connect_engine, connection.get());
630  } // end connect_engine
631 
632  if(dlg_ok) {
634  controller.set_mp_info(metadata.get());
635  controller.play_game();
636  }
637 
638  if(connection) {
639  connection->send_data(config{"leave_game"});
640  }
641 }
642 
643 void mp_manager::enter_wait_mode(int game_id, bool observe)
644 {
645  DBG_MP << "entering wait mode";
646 
647  // The connection should never be null here, since one should never reach this screen in local game mode.
648  assert(connection);
649 
650  mp_game_metadata metadata(*connection);
651  metadata.is_host = false;
652 
653  if(const mp::game_info* gi = lobby_info.get_game_by_id(game_id)) {
654  metadata.current_turn = gi->current_turn;
655  }
656 
657  if(prefs::get().skip_mp_replay() || prefs::get().blindfold_replay()) {
658  metadata.skip_replay = true;
659  metadata.skip_replay_blindfolded = prefs::get().blindfold_replay();
660  }
661 
662  bool dlg_ok = false;
663  {
664  gui2::dialogs::mp_join_game dlg(state, *connection, true, observe);
665 
666  if(!dlg.fetch_game_config()) {
667  connection->send_data(config{"leave_game"});
668  return;
669  }
670 
671  dlg_ok = dlg.show();
672  }
673 
674  if(dlg_ok) {
676  controller.set_mp_info(&metadata);
677  controller.play_game();
678  }
679 
680  connection->send_data(config{"leave_game"});
681 }
682 
683 bool mp_manager::post_scenario_staging(ng::connect_engine& engine)
684 {
685  return gui2::dialogs::mp_staging::execute(engine, connection.get());
686 }
687 
688 bool mp_manager::post_scenario_wait(bool observe)
689 {
690  gui2::dialogs::mp_join_game dlg(state, *connection, false, observe);
691 
692  if(!dlg.fetch_game_config()) {
693  connection->send_data(config{"leave_game"});
694  return false;
695  }
696 
697  if(dlg.started()) {
698  return true;
699  }
700 
701  return dlg.show();
702 }
703 
704 } // end anon namespace
705 
706 /** Pubic entry points for the MP workflow */
707 
708 void start_client(const std::string& host)
709 {
710  DBG_MP << "starting client";
711  mp_manager(host).run_lobby_loop();
712 }
713 
715 {
716  DBG_MP << "starting local game";
717 
719 
720  mp_manager(utils::nullopt).enter_create_mode();
721 }
722 
724 {
725  DBG_MP << "starting local MP game from commandline";
726 
728 
729  // The setup is done equivalently to lobby MP games using as much of existing
730  // code as possible. This means that some things are set up that are not
731  // needed in commandline mode, but they are required by the functions called.
733 
734  DBG_MP << "entering create mode";
735 
736  // Set the default parameters
737  saved_game state;
738  state.classification().type = campaign_type::type::multiplayer;
739 
740  mp_game_settings& parameters = state.mp_settings();
741 
742  // Hardcoded default values
743  state.classification().era_id = "era_default";
744  parameters.name = "multiplayer_The_Freelands";
745 
746  // Default values for which at getter function exists
747  parameters.num_turns = settings::get_turns("");
748  parameters.village_gold = settings::get_village_gold("");
750  parameters.xp_modifier = settings::get_xp_modifier("");
751 
752  // Do not use map settings if --ignore-map-settings commandline option is set
753  if(cmdline_opts.multiplayer_ignore_map_settings) {
754  DBG_MP << "ignoring map settings";
755  parameters.use_map_settings = false;
756  } else {
757  parameters.use_map_settings = true;
758  }
759 
760  // None of the other parameters need to be set, as their creation values above are good enough for CL mode.
761  // In particular, we do not want to use the preferences values.
762 
763  state.classification().type = campaign_type::type::multiplayer;
764 
765  // [era] define.
766  if(cmdline_opts.multiplayer_era) {
767  state.classification().era_id = *cmdline_opts.multiplayer_era;
768  }
769 
770  if(auto cfg_era = game_config.find_child("era", "id", state.classification().era_id)) {
771  state.classification().era_define = cfg_era["define"].str();
772  } else {
773  PLAIN_LOG << "Could not find era '" << state.classification().era_id << "'";
774  return;
775  }
776 
777  // [multiplayer] define.
778  if(cmdline_opts.multiplayer_scenario) {
779  parameters.name = *cmdline_opts.multiplayer_scenario;
780  }
781 
782  if(auto cfg_multiplayer = game_config.find_child("multiplayer", "id", parameters.name)) {
783  state.classification().scenario_define = cfg_multiplayer["define"].str();
784  } else {
785  PLAIN_LOG << "Could not find [multiplayer] '" << parameters.name << "'";
786  return;
787  }
788 
790  config {"next_scenario", parameters.name}
791  );
792 
794 
795  state.expand_random_scenario();
796  state.expand_mp_events();
797  state.expand_mp_options();
798 
799  // Should number of turns be determined from scenario data?
800  if(parameters.use_map_settings && state.get_starting_point().has_attribute("turns")) {
801  DBG_MP << "setting turns from scenario data: " << state.get_starting_point()["turns"];
802  parameters.num_turns = state.get_starting_point()["turns"].to_int();
803  }
804 
805  DBG_MP << "entering connect mode";
806 
807  {
808  ng::connect_engine connect_engine(state, true, nullptr);
809 
810  // Update the parameters to reflect game start conditions
811  connect_engine.start_game_commandline(cmdline_opts, game_config);
812  }
813 
814  if(resources::recorder && cmdline_opts.multiplayer_label) {
815  std::string label = *cmdline_opts.multiplayer_label;
816  resources::recorder->add_log_data("ai_log","ai_label",label);
817  }
818 
819  unsigned int repeat = (cmdline_opts.multiplayer_repeat) ? *cmdline_opts.multiplayer_repeat : 1;
820  for(unsigned int i = 0; i < repeat; i++){
821  saved_game state_copy(state);
822  campaign_controller controller(state_copy);
823  controller.play_game();
824  }
825 }
826 
828 {
829  return manager && manager->post_scenario_staging(engine);
830 }
831 
832 bool goto_mp_wait(bool observe)
833 {
834  return manager && manager->post_scenario_wait(observe);
835 }
836 
838 {
839  return manager && manager->get_session_info().is_moderator;
840 }
841 
842 std::string get_profile_link(int user_id)
843 {
844  if(manager) {
845  const std::string& prefix = manager->get_session_info().profile_url_prefix;
846 
847  if(!prefix.empty()) {
848  return prefix + std::to_string(user_id);
849  }
850  }
851 
852  return "";
853 }
854 
855 std::vector<queue_info>& get_server_queues()
856 {
857  static std::vector<queue_info> queues;
858  if(manager) {
859  return manager->get_session_info().queues;
860  }
861  return queues;
862 }
863 
865 {
866  if(manager && manager->connection) {
867  manager->connection->send_data(data);
868  }
869 }
870 
872 {
873  if(manager /*&& manager->connection*/) {
874  remove_handler = manager->add_network_handler(func);
875  }
876 }
877 
879 {
880  if(remove_handler) {
881  remove_handler();
882  }
883 }
884 
886 {
887  return manager ? &manager->lobby_info : nullptr;
888 }
889 
890 } // 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: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:300
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: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:778
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:611
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:689
void stop_music()
Definition: sound.cpp:554
void commit_music_changes()
Definition: sound.cpp:817
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