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