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