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