The Battle for Wesnoth  1.17.0-dev
multiplayer.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2007 - 2021
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
16 
17 #include "addon/manager.hpp" // for installed_addons
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"
26 #include "gettext.hpp"
28 #include "gui/dialogs/message.hpp"
34 #include "log.hpp"
35 #include "map_settings.hpp"
38 #include "preferences/game.hpp"
39 #include "replay.hpp"
40 #include "resources.hpp"
41 #include "saved_game.hpp"
42 #include "sound.hpp"
43 #include "statistics.hpp"
45 #include "wesnothd_connection.hpp"
46 
47 #include <fstream>
48 #include <functional>
49 #include <future>
50 #include <optional>
51 #include <thread>
52 
53 static lg::log_domain log_mp("mp/main");
54 #define DBG_MP LOG_STREAM(debug, log_mp)
55 #define ERR_MP LOG_STREAM(err, log_mp)
56 
57 namespace mp
58 {
59 namespace
60 {
61 /** Pointer to the current mp_manager instance. */
62 class mp_manager* manager = nullptr;
63 
64 /** The main controller of the MP workflow. */
65 class mp_manager
66 {
67 public:
68  // Declare this as a friend to allow direct access to enter_create_mode
69  friend void mp::start_local_game();
70 
71  mp_manager(const std::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(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 public:
148  const session_metadata& get_session_info() const
149  {
150  return session_info;
151  }
152 };
153 
154 mp_manager::mp_manager(const std::optional<std::string> host)
155  : network_worker()
156  , stop(false)
157  , connection(nullptr)
158  , session_info()
159  , state()
160  , lobby_info(::installed_addons())
161 {
162  state.classification().campaign_type = game_classification::CAMPAIGN_TYPE::MULTIPLAYER;
163 
164  if(host) {
166  connection = open_connection(*host);
167 
168  // If for whatever reason our connection is null at this point (dismissing the password prompt, for
169  // instance), treat it as a normal condition and exit. Any actual error conditions throw exceptions
170  // which can be handled higher up the stack.
171  if(connection == nullptr) {
172  return;
173  }
174 
176 
177  std::promise<void> received_initial_gamelist;
178 
179  network_worker = std::thread([this, &received_initial_gamelist]() {
180  config data;
181 
182  while(!stop) {
183  connection->wait_and_receive_data(data);
184 
185  if(const config& error = data.child("error")) {
186  throw wesnothd_error(error["message"]);
187  }
188 
189  else if(data.has_child("gamelist")) {
190  this->lobby_info.process_gamelist(data);
191 
192  try {
193  received_initial_gamelist.set_value();
194  // TODO: only here while we transition away from dialog-bound timer-based handling
195  return;
196  } catch(const std::future_error& e) {
197  if(e.code() == std::future_errc::promise_already_satisfied) {
198  // We only need this for the first gamelist
199  }
200  }
201  }
202 
203  else if(const config& gamelist_diff = data.child("gamelist_diff")) {
204  this->lobby_info.process_gamelist_diff(gamelist_diff);
205  }
206  }
207  });
208 
209  // Wait at the loading screen until the initial gamelist has been processed
210  received_initial_gamelist.get_future().wait();
211  });
212  }
213 
214  // Avoid setting this until the connection has been fully established. open_connection may throw,
215  // in which case we don't want to point to an object instance that has not properly connected.
216  assert(!manager);
217  manager = this;
218 }
219 
220 std::unique_ptr<wesnothd_connection> mp_manager::open_connection(std::string host)
221 {
222  DBG_MP << "opening connection" << std::endl;
223 
224  if(host.empty()) {
225  return nullptr;
226  }
227 
228  // shown_hosts is used to prevent the client being locked in a redirect loop.
229  std::set<std::pair<std::string, std::string>> shown_hosts;
230  auto addr = shown_hosts.end();
231 
232  try {
233  std::tie(addr, std::ignore) = shown_hosts.insert(parse_network_address(host, "15000"));
234  } catch(const std::runtime_error&) {
235  throw wesnothd_error(_("Invalid address specified for multiplayer server"));
236  }
237 
238  // Start stage
240 
241  // Initializes the connection to the server.
242  auto conn = std::make_unique<wesnothd_connection>(addr->first, addr->second);
243 
244  // First, spin until we get a handshake from the server.
245  conn->wait_for_handshake();
246 
248 
249  config data;
250 
251  // Then, log in and wait for the lobby/game join prompt.
252  while(true) {
253  data.clear();
254  conn->wait_and_receive_data(data);
255 
256  if(data.has_child("reject") || data.has_attribute("version")) {
257  std::string version;
258 
259  if(const config& reject = data.child("reject")) {
260  version = reject["accepted_versions"].str();
261  } else {
262  // Backwards-compatibility "version" attribute
263  version = data["version"].str();
264  }
265 
266  utils::string_map i18n_symbols;
267  i18n_symbols["required_version"] = version;
268  i18n_symbols["your_version"] = game_config::wesnoth_version.str();
269 
270  const std::string errorstring = VGETTEXT("The server accepts versions '$required_version', but you are using version '$your_version'", i18n_symbols);
271  throw wesnothd_error(errorstring);
272  }
273 
274  // Check for "redirect" messages
275  if(const auto redirect = data.optional_child("redirect")) {
276  auto redirect_host = (*redirect)["host"].str();
277  auto redirect_port = (*redirect)["port"].str("15000");
278 
279  bool recorded_host;
280  std::tie(std::ignore, recorded_host) = shown_hosts.emplace(redirect_host, redirect_port);
281 
282  if(!recorded_host) {
283  throw wesnothd_error(_("Server-side redirect loop"));
284  }
285 
287 
288  // Open a new connection with the new host and port.
289  conn.reset();
290  conn = std::make_unique<wesnothd_connection>(redirect_host, redirect_port);
291 
292  // Wait for new handshake.
293  conn->wait_for_handshake();
294 
296  continue;
297  }
298 
299  if(data.has_child("version")) {
300  config res;
301  config& cfg = res.add_child("version");
302  cfg["version"] = game_config::wesnoth_version.str();
303  cfg["client_source"] = game_config::dist_channel_id();
304  conn->send_data(res);
305  }
306 
307  if(const auto error = data.optional_child("error")) {
308  throw wesnothd_rejected_client_error((*error)["message"].str());
309  }
310 
311  // Continue if we did not get a direction to login
312  if(!data.has_child("mustlogin")) {
313  continue;
314  }
315 
316  // Enter login loop
317  while(true) {
318  std::string login = preferences::login();
319 
320  config response;
321  config& sp = response.add_child("login");
322  sp["username"] = login;
323 
324  conn->send_data(response);
325  conn->wait_and_receive_data(data);
326 
328 
329  if(const config& warning = data.child("warning")) {
330  std::string warning_msg;
331 
332  if(warning["warning_code"] == MP_NAME_INACTIVE_WARNING) {
333  warning_msg = VGETTEXT("The nickname ‘$nick’ is inactive. "
334  "You cannot claim ownership of this nickname until you "
335  "activate your account via email or ask an "
336  "administrator to do it for you.", {{"nick", login}});
337  } else {
338  warning_msg = warning["message"].str();
339  }
340 
341  warning_msg += "\n\n";
342  warning_msg += _("Do you want to continue?");
343 
345  return nullptr;
346  } else {
347  continue;
348  }
349  }
350 
351  auto error = data.optional_child("error");
352 
353  // ... and get us out of here if the server did not complain
354  if(!error) break;
355 
356  do {
357  std::string password = preferences::password(host, login);
358 
359  const bool fall_through = (*error)["force_confirmation"].to_bool()
361  : false;
362 
363  // If:
364  // * the server asked for a password
365  // * the password isn't empty
366  // * the user didn't press Cancel
367  // * the connection is secure or the client was started with the option to use insecure connections
368  // send the password to the server
369  // otherwise go directly to the username/password dialog
370  if(!(*error)["password_request"].empty() && !password.empty() && !fall_through && (conn->using_tls() || game_config::allow_insecure)) {
371  // the possible cases here are that either:
372  // 1) TLS encryption is enabled, thus sending the plaintext password is still secure
373  // 2) TLS encryption is not enabled, in which case the server should not be requesting a password in the first place
374  // 3) This is being used for local testing/development, so using an insecure connection is enabled manually
375 
376  sp["password"] = password;
377 
378  // Once again send our request...
379  conn->send_data(response);
380  conn->wait_and_receive_data(data);
381 
383 
384  error = data.optional_child("error");
385 
386  // ... and get us out of here if the server is happy now
387  if(!error) break;
388  }
389 
390  // Providing a password either was not attempted because we did not
391  // have any or failed:
392  // Now show a dialog that displays the error and allows to
393  // enter a new user name and/or password
394 
395  std::string error_message;
396  utils::string_map i18n_symbols;
397  i18n_symbols["nick"] = login;
398 
399  const auto extra_data = error->optional_child("data");
400  if(extra_data) {
401  i18n_symbols["duration"] = utils::format_timespan((*extra_data)["duration"]);
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_NAME_AUTH_BAN_USER_ERROR) {
424  if(extra_data) {
425  error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
426  } else {
427  error_message = VGETTEXT("The nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
428  }
429  } else if(ec == MP_NAME_AUTH_BAN_IP_ERROR) {
430  if(extra_data) {
431  error_message = VGETTEXT("Your IP address is banned on this server’s forums for $duration|.", i18n_symbols);
432  } else {
433  error_message = _("Your IP address is banned on this server’s forums.");
434  }
435  } else if(ec == MP_NAME_AUTH_BAN_EMAIL_ERROR) {
436  if(extra_data) {
437  error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums for $duration|.", i18n_symbols);
438  } else {
439  error_message = VGETTEXT("The email address for the nickname ‘$nick’ is banned on this server’s forums.", i18n_symbols);
440  }
441  } else if(ec == MP_PASSWORD_REQUEST) {
442  error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols);
443  } else if(ec == MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME) {
444  error_message = VGETTEXT("The nickname ‘$nick’ is registered on this server.", i18n_symbols)
445  + "\n\n" + _("WARNING: There is already a client using this nickname, "
446  "logging in will cause that client to be kicked!");
447  } else if(ec == MP_INCORRECT_PASSWORD_ERROR) {
448  error_message = _("The password you provided was incorrect.");
449  } else if(ec == MP_TOO_MANY_ATTEMPTS_ERROR) {
450  error_message = _("You have made too many login attempts.");
451  } else if(ec == MP_HASHING_PASSWORD_FAILED) {
452  error_message = _("Password hashing failed.");
453  } else {
454  error_message = (*error)["message"].str();
455  }
456 
457  gui2::dialogs::mp_login dlg(host, error_message, !((*error)["password_request"].empty()));
458 
459  // Need to show the dialog from the main thread or it won't appear.
460  events::call_in_main_thread([&dlg]() { dlg.show(); });
461 
462  switch(dlg.get_retval()) {
463  // Log in with password
464  case gui2::retval::OK:
465  break;
466  // Cancel
467  default:
468  return nullptr;
469  }
470 
471  // If we have got a new username we have to start all over again
472  } while(login == preferences::login());
473 
474  // Somewhat hacky...
475  // If we broke out of the do-while loop above error is still going to be nullopt
476  if(!error) break;
477  } // end login loop
478 
479  if(const auto join_lobby = data.optional_child("join_lobby")) {
480  // Note any session data sent with the response. This should be the only place session_info is set.
481  session_info = { join_lobby.value() };
482 
483  // All done!
484  break;
485  }
486  }
487 
488  return conn;
489 }
490 
491 void mp_manager::run_lobby_loop()
492 {
493  // This should only work if we have a connection. If we're in a local mode,
494  // enter_create_mode should be accessed directly.
495  if(!connection) {
496  return;
497  }
498 
499  // A return of false means a config reload was requested, so do that and then loop.
500  while(!enter_lobby_mode()) {
503  gcm->load_game_config_for_create(true); // NOTE: Using reload_changed_game_config only doesn't seem to work here
504 
505  // This function does not refer to an addon database, it calls filesystem functions.
506  // For the sanity of the mp lobby, this list should be fixed for the entire lobby session,
507  // even if the user changes the contents of the addon directory in the meantime.
508  // TODO: do we want to handle fetching the installed addons in the lobby_info ctor?
509  lobby_info.set_installed_addons(::installed_addons());
510 
511  connection->send_data(config("refresh_lobby"));
512  }
513 }
514 
515 bool mp_manager::enter_lobby_mode()
516 {
517  DBG_MP << "entering lobby mode" << std::endl;
518 
519  // Connection should never be null in the lobby.
520  assert(connection);
521 
522  // We use a loop here to allow returning to the lobby if you, say, cancel game creation.
523  while(true) {
524  if(const config& cfg = game_config_manager::get()->game_config().child("lobby_music")) {
525  for(const config& i : cfg.child_range("music")) {
527  }
528 
530  } else {
533  }
534 
535  int dlg_retval = 0;
536  int dlg_joined_game_id = 0;
537  {
538  gui2::dialogs::mp_lobby dlg(lobby_info, *connection, dlg_joined_game_id);
539  dlg.show();
540  dlg_retval = dlg.get_retval();
541  }
542 
543  try {
544  switch(dlg_retval) {
546  enter_create_mode();
547  break;
549  [[fallthrough]];
551  enter_wait_mode(dlg_joined_game_id, dlg_retval == gui2::dialogs::mp_lobby::OBSERVE);
552  break;
554  // Let this function's caller reload the config and re-call.
555  return false;
556  default:
557  // Needed to handle the Quit signal and exit the loop
558  return true;
559  }
560  } catch(const config::error& error) {
561  if(!error.message.empty()) {
563  }
564 
565  // Update lobby content
566  connection->send_data(config("refresh_lobby"));
567  }
568  }
569 
570  return true;
571 }
572 
573 void mp_manager::enter_create_mode()
574 {
575  DBG_MP << "entering create mode" << std::endl;
576 
577  if(gui2::dialogs::mp_create_game::execute(state, connection == nullptr)) {
578  enter_staging_mode();
579  } else if(connection) {
580  connection->send_data(config("refresh_lobby"));
581  }
582 }
583 
584 void mp_manager::enter_staging_mode()
585 {
586  DBG_MP << "entering connect mode" << std::endl;
587 
588  std::unique_ptr<mp_game_metadata> metadata;
589 
590  // If we have a connection, set the appropriate info. No connection means we're in local game mode.
591  if(connection) {
592  metadata = std::make_unique<mp_game_metadata>(*connection);
593  metadata->connected_players.insert(preferences::login());
594  metadata->is_host = true;
595  }
596 
597  bool dlg_ok = false;
598  {
599  ng::connect_engine connect_engine(state, true, metadata.get());
600  dlg_ok = gui2::dialogs::mp_staging::execute(connect_engine, connection.get());
601  } // end connect_engine
602 
603  if(dlg_ok) {
605  controller.set_mp_info(metadata.get());
606  controller.play_game();
607  }
608 
609  if(connection) {
610  connection->send_data(config("leave_game"));
611  }
612 }
613 
614 void mp_manager::enter_wait_mode(int game_id, bool observe)
615 {
616  DBG_MP << "entering wait mode" << std::endl;
617 
618  // The connection should never be null here, since one should never reach this screen in local game mode.
619  assert(connection);
620 
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 
631  metadata.skip_replay = true;
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" << std::endl;
684  mp_manager(host).run_lobby_loop();
685 }
686 
688 {
689  DBG_MP << "starting local game" << std::endl;
690 
692 
693  mp_manager(std::nullopt).enter_create_mode();
694 }
695 
697 {
698  DBG_MP << "starting local MP game from commandline" << std::endl;
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" << std::endl;
708 
709  // Set the default parameters
710  saved_game state;
711  state.classification().campaign_type = game_classification::CAMPAIGN_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" << std::endl;
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().campaign_type = game_classification::CAMPAIGN_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(const config& cfg_era = game_config.find_child("era", "id", state.classification().era_id)) {
744  state.classification().era_define = cfg_era["define"].str();
745  } else {
746  std::cerr << "Could not find era '" << state.classification().era_id << "'\n";
747  return;
748  }
749 
750  // [multiplayer] define.
751  if(cmdline_opts.multiplayer_scenario) {
752  parameters.name = *cmdline_opts.multiplayer_scenario;
753  }
754 
755  if(const config& cfg_multiplayer = game_config.find_child("multiplayer", "id", parameters.name)) {
756  state.classification().scenario_define = cfg_multiplayer["define"].str();
757  } else {
758  std::cerr << "Could not find [multiplayer] '" << parameters.name << "'\n";
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()["turns"]) {
774  DBG_MP << "setting turns from scenario data: " << state.get_starting_point()["turns"] << std::endl;
775  parameters.num_turns = state.get_starting_point()["turns"];
776  }
777 
778  DBG_MP << "entering connect mode" << std::endl;
779 
781 
782  {
783  ng::connect_engine connect_engine(state, true, nullptr);
784 
785  // Update the parameters to reflect game start conditions
786  connect_engine.start_game_commandline(cmdline_opts, game_config);
787  }
788 
789  if(resources::recorder && cmdline_opts.multiplayer_label) {
790  std::string label = *cmdline_opts.multiplayer_label;
791  resources::recorder->add_log_data("ai_log","ai_label",label);
792  }
793 
794  unsigned int repeat = (cmdline_opts.multiplayer_repeat) ? *cmdline_opts.multiplayer_repeat : 1;
795  for(unsigned int i = 0; i < repeat; i++){
796  saved_game state_copy(state);
797  campaign_controller controller(state_copy);
798  controller.play_game();
799  }
800 }
801 
803 {
804  return manager && manager->post_scenario_staging(engine);
805 }
806 
807 bool goto_mp_wait(bool observe)
808 {
809  return manager && manager->post_scenario_wait(observe);
810 }
811 
813 {
814  return manager && manager->get_session_info().is_moderator;
815 }
816 
817 std::string get_profile_link(int user_id)
818 {
819  if(manager) {
820  const std::string& prefix = manager->get_session_info().profile_url_prefix;
821 
822  if(!prefix.empty()) {
823  return prefix + std::to_string(user_id);
824  }
825  }
826 
827  return "";
828 }
829 
830 } // end namespace mp
void empty_playlist()
Definition: sound.cpp:611
#define MP_HASHING_PASSWORD_FAILED
An error occurred during when trying to communicate with the wesnothd server.
std::string format_timespan(std::time_t time)
Formats a timespan into human-readable text.
Dialog was closed with the CANCEL button.
Definition: retval.hpp:37
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:152
config & child(config_key_type key, int n=0)
Returns the nth child with the given key, or a reference to an invalid config if there is none...
Definition: config.cpp:417
Error used when the client is rejected by the MP server.
LEVEL_RESULT play_game()
void stop_music()
Definition: sound.cpp:556
std::map< std::string, t_string > string_map
unsigned current_turn
#define MP_NAME_AUTH_BAN_USER_ERROR
void add_log_data(const std::string &key, const std::string &var)
Definition: replay.cpp:310
static l_noret error(LoadState *S, const char *why)
Definition: lundump.cpp:40
#define MP_TOO_MANY_ATTEMPTS_ERROR
bool has_attribute(config_key_type key) const
Definition: config.cpp:209
This class represents the info a client has about a game on the server.
Definition: lobby_data.hpp:140
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:409
Shows an ok and cancel button.
Definition: message.hpp:75
static void progress(loading_stage stage=loading_stage::none)
void load_game_config_for_game(const game_classification &classification, const std::string &scenario_id)
void expand_random_scenario()
takes care of generate_map=, generate_scenario=, map= attributes This should be called before expandi...
Definition: saved_game.cpp:476
This class represents the collective information the client has about the players and games on the se...
Definition: lobby_info.hpp:30
bool multiplayer_ignore_map_settings
True if –ignore-map-settings was given at the command line.
int get_village_gold(const std::string &value, const game_classification *classification)
Gets the village gold.
std::optional< std::string > multiplayer_label
Non-empty if –label was given on the command line.
void fresh_stats()
Definition: statistics.cpp:782
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.
bool goto_mp_wait(bool observe)
Opens the MP Join Game screen and sets the game state according to the changes made.
#define MP_NAME_TOO_LONG_ERROR
Replay control code.
void clear()
Definition: config.cpp:899
Define the errors the server may send during the login procedure.
static std::string _(const char *str)
Definition: gettext.hpp:92
bool show(const unsigned auto_close_time=0)
Shows the window.
void call_in_main_thread(const std::function< void(void)> &f)
Definition: events.cpp:866
std::string get_scenario_id() const
Definition: saved_game.cpp:648
#define MP_INCORRECT_PASSWORD_ERROR
int get_village_support(const std::string &value)
Gets the village unit level support.
bool logged_in_as_moderator()
Gets whether the currently logged-in user is a moderator.
Main entry points of multiplayer mode.
Definition: lobby_data.cpp:51
std::string get_profile_link(int user_id)
Gets the forum profile link for the given user.
static game_config_manager * get()
#define MP_MUST_LOGIN
#define MP_NAME_UNREGISTERED_ERROR
std::string label
What to show in the filter&#39;s drop-down list.
Definition: manager.cpp:216
void start_game_commandline(const commandline_options &cmdline_opts, const game_config_view &game_config)
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:412
void start_local_game_commandline(const commandline_options &cmdline_opts)
Starts a multiplayer game in single-user mode using command line settings.
std::string dist_channel_id()
Return the distribution channel identifier, or "Default" if missing.
Definition: build_info.cpp:381
const game_config_view & game_config() const
bool blindfold_replay()
Definition: game.cpp:604
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:176
config & get_starting_point()
Definition: saved_game.cpp:582
Shows a yes and no button.
Definition: message.hpp:79
replay * recorder
Definition: resources.cpp:28
utils::optional_reference< config > optional_child(config_key_type key, int n=0)
Euivalent to child, but returns an empty optional if the nth child was not found. ...
Definition: config.cpp:460
void set_message_private(bool value)
Definition: game.cpp:851
General settings and defaults for scenarios.
std::set< std::string > connected_players
players and observers
bool allow_insecure
Definition: game_config.cpp:96
std::string era_define
If there is a define the era uses to customize data.
std::string login()
std::optional< std::string > multiplayer_era
Non-empty if –era was given on the command line.
#define DBG_MP
Definition: multiplayer.cpp:54
std::size_t i
Definition: function.cpp:940
void load_game_config_for_create(bool is_mp, bool is_test=false)
Game configuration data as global variables.
Definition: build_info.cpp:58
std::string scenario_define
If there is a define the scenario uses to customize data.
std::string password(const std::string &server, const std::string &login)
This shows the dialog to log in to the MP server.
Definition: mp_login.hpp:39
int get_xp_modifier(const std::string &value)
Gets the xp modifier.
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:371
#define MP_PASSWORD_REQUEST
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
bool skip_mp_replay()
Definition: game.cpp:594
static lg::log_domain log_mp("mp/main")
#define MP_NAME_INACTIVE_WARNING
const version_info wesnoth_version(VERSION)
config & add_child(config_key_type key)
Definition: config.cpp:503
std::optional< std::string > multiplayer_scenario
Non-empty if –scenario was given on the command line.
const config & find_child(config_key_type key, const std::string &name, const std::string &value) const
#define MP_PASSWORD_REQUEST_FOR_LOGGED_IN_NAME
#define MP_NAME_TAKEN_ERROR
int get_retval() const
Returns the cached window exit code.
static void display(std::function< void()> f)
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:704
game_classification & classification()
Definition: saved_game.hpp:55
void set_carryover_sides_start(config carryover_sides_start)
Definition: saved_game.cpp:161
void set_mp_info(mp_game_metadata *mp_info)
Standard logging facilities (interface).
std::string str() const
Serializes the version number into string form.
std::string message
Definition: exceptions.hpp:29
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition: message.cpp:205
bool goto_mp_staging(ng::connect_engine &engine)
Opens the MP Staging screen and sets the game state according to the changes made.
#define e
std::optional< unsigned int > multiplayer_repeat
Repeats specified by –multiplayer-repeat option.
#define MP_NAME_RESERVED_ERROR
void commit_music_changes()
Definition: sound.cpp:822
Dialog was closed with the OK button.
Definition: retval.hpp:34
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:59
#define MP_NAME_AUTH_BAN_EMAIL_ERROR
mp_game_settings & mp_settings()
Multiplayer parameters for this game.
Definition: saved_game.hpp:59
void start_client(const std::string &host)
Pubic entry points for the MP workflow.
int get_turns(const std::string &value)
Gets the number of turns.
bool skip_replay_blindfolded
#define MP_NAME_AUTH_BAN_IP_ERROR
#define MP_INVALID_CHARS_IN_NAME_ERROR
void start_local_game()
Starts a multiplayer game in single-user mode.