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