The Battle for Wesnoth  1.19.8+dev
connect_engine.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2013 - 2024
3  by Andrius Silinskas <silinskas.andrius@gmail.com>
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 "ai/configuration.hpp"
19 #include "formula/string_utils.hpp"
24 #include "gettext.hpp"
25 #include "log.hpp"
26 #include "map/map.hpp"
27 #include "mt_rng.hpp"
28 #include "side_controller.hpp"
29 #include "team.hpp"
30 
31 #include <array>
32 #include <cstdlib>
33 
34 static lg::log_domain log_config("config");
35 #define LOG_CF LOG_STREAM(info, log_config)
36 #define ERR_CF LOG_STREAM(err, log_config)
37 
38 static lg::log_domain log_mp_connect_engine("mp/connect/engine");
39 #define DBG_MP LOG_STREAM(debug, log_mp_connect_engine)
40 #define LOG_MP LOG_STREAM(info, log_mp_connect_engine)
41 #define WRN_MP LOG_STREAM(warn, log_mp_connect_engine)
42 #define ERR_MP LOG_STREAM(err, log_mp_connect_engine)
43 
44 static lg::log_domain log_network("network");
45 #define LOG_NW LOG_STREAM(info, log_network)
46 
47 namespace
48 {
49 const std::array controller_names {
50  side_controller::human,
51  side_controller::human,
52  side_controller::ai,
53  side_controller::none,
54  side_controller::reserved
55 };
56 
57 const std::set<std::string> children_to_swap {
58  "village",
59  "unit",
60  "ai"
61 };
62 } // end anon namespace
63 
64 namespace ng {
65 
66 connect_engine::connect_engine(saved_game& state, const bool first_scenario, mp_game_metadata* metadata)
67  : level_()
68  , state_(state)
69  , params_(state.mp_settings())
70  , default_controller_(metadata ? CNTR_NETWORK : CNTR_LOCAL)
71  , mp_metadata_(metadata)
72  , first_scenario_(first_scenario)
73  , force_lock_settings_()
74  , side_engines_()
75  , era_factions_()
76  , team_data_()
77 {
78  // Initial level config from the mp_game_settings.
80  if(level_.empty()) {
81  return;
82  }
83 
84  const bool is_mp = state_.classification().is_normal_mp_game();
85  force_lock_settings_ = (state.mp_settings().saved_game != saved_game_mode::type::midgame) && scenario()["force_lock_settings"].to_bool(!is_mp);
86 
87  // Original level sides.
89 
90  // AI algorithms.
91  if(auto era = level_.optional_child("era")) {
93  }
95 
96  // Set the team name lists and modify the original level sides if necessary.
97  std::vector<std::string> original_team_names;
98  std::string team_prefix(_("Team") + " ");
99 
100  int side_count = 1;
101  for(config& side : sides) {
102  const std::string side_str = std::to_string(side_count);
103 
104  config::attribute_value& team_name = side["team_name"];
105  config::attribute_value& user_team_name = side["user_team_name"];
106 
107  // Revert to default values if appropriate.
108  if(team_name.empty()) {
109  team_name = side_str;
110  }
111 
112  if(params_.use_map_settings && user_team_name.empty()) {
113  user_team_name = team_name;
114  }
115 
116  bool add_team = true;
118  // Only add a team if it is not found.
119  if(std::any_of(team_data_.begin(), team_data_.end(), [&team_name](const team_data_pod& data){
120  return data.team_name == team_name.str();
121  })) {
122  add_team = false;
123  }
124  } else {
125  // Always add a new team for every side, but leave the specified team assigned to a side if there is one.
126  auto name_itor = std::find(original_team_names.begin(), original_team_names.end(), team_name.str());
127 
128  // Note that the prefix "Team " is untranslatable, as team_name is not meant to be translated. This is needed
129  // so that the attribute is not interpretted as an int when reading from config, which causes bugs later.
130  if(name_itor == original_team_names.end()) {
131  original_team_names.push_back(team_name);
132 
133  team_name = "Team " + std::to_string(original_team_names.size());
134  } else {
135  team_name = "Team " + std::to_string(std::distance(original_team_names.begin(), name_itor) + 1);
136  }
137 
138  user_team_name = team_prefix + side_str;
139  }
140 
141  // Write the serialized translatable team name back to the config. Without this,
142  // the string can appear all messed up after leaving and rejoining a game (see
143  // issue #2040. This affected the mp_join_game dialog). I don't know why that issue
144  // didn't appear the first time you join a game, but whatever.
145  //
146  // The difference between that dialog and mp_staging is that the latter has access
147  // to connect_engine object, meaning it has access to serialized versions of the
148  // user_team_name string stored in the team_data_ vector. mp_join_game handled the
149  // raw side config instead. Again, I don't know why issues only cropped up on a
150  // subsequent join and not the first, but it doesn't really matter.
151  //
152  // This ensures both dialogs have access to the serialized form of the utn string.
153  // As for why this needs to be done in the first place, apparently the simple_wml
154  // format the server (wesnothd) uses doesn't preserve translatable strings (see
155  // issue #342).
156  //
157  // --vultraz, 2018-02-06
158  user_team_name = user_team_name.t_str().to_serialized();
159 
160  if(add_team) {
162  data.team_name = params_.use_map_settings ? team_name : "Team " + side_str;
163  data.user_team_name = user_team_name.str();
164  data.is_player_team = side["allow_player"].to_bool(true);
165 
166  team_data_.push_back(data);
167  }
168 
169  ++side_count;
170  }
171 
172  // Selected era's factions.
173  for(const config& era : level_.mandatory_child("era").child_range("multiplayer_side")) {
174  era_factions_.push_back(&era);
175  }
176 
177  // Sort alphabetically, but with the random faction options always first.
178  // Since some eras have multiple random options we can't just assume there is
179  // only one random faction on top of the list.
180  std::sort(era_factions_.begin(), era_factions_.end(), [](const config* c1, const config* c2) {
181  const config& lhs = *c1;
182  const config& rhs = *c2;
183 
184  // Random factions always first.
185  if(lhs["random_faction"].to_bool() && !rhs["random_faction"].to_bool()) {
186  return true;
187  }
188 
189  if(!lhs["random_faction"].to_bool() && rhs["random_faction"].to_bool()) {
190  return false;
191  }
192 
193  return translation::compare(lhs["name"].str(), rhs["name"].str()) < 0;
194  });
195 
197 
198  // Create side engines.
199  int index = 0;
200  for(const config& s : sides) {
201  side_engines_.emplace_back(new side_engine(s, *this, index++));
202  }
203 
204  if(first_scenario_) {
205  // Add host to the connected users list.
206  import_user(prefs::get().login(), false);
207  } else {
208  // Add host but don't assign a side to him.
209  import_user(prefs::get().login(), true);
210 
211  // Load reserved players information into the sides.
212  load_previous_sides_users();
213  }
214 
215  // Only updates the sides in the level.
216  update_level();
217 
218  // If we are connected, send data to the connected host.
219  send_level_data();
220 }
221 
222 
223 config* connect_engine::current_config() {
224  return &scenario();
225 }
226 
227 void connect_engine::import_user(const std::string& name, const bool observer, int side_taken)
228 {
230  user_data["name"] = name;
231  import_user(user_data, observer, side_taken);
232 }
233 
234 void connect_engine::import_user(const config& data, const bool observer, int side_taken)
235 {
236  const std::string& username = data["name"];
237  assert(!username.empty());
238  if(mp_metadata_) {
239  connected_users_rw().insert(username);
240  }
241 
242  update_side_controller_options();
243 
244  if(observer) {
245  return;
246  }
247 
248  bool side_assigned = false;
249  if(side_taken >= 0) {
250  side_engines_[side_taken]->place_user(data, true);
251  side_assigned = true;
252  }
253 
254  // Check if user has a side(s) reserved for him.
255  for(side_engine_ptr side : side_engines_) {
256  if(side->reserved_for() == username && side->player_id().empty() && side->controller() != CNTR_COMPUTER) {
257  side->place_user(data);
258 
259  side_assigned = true;
260  }
261  }
262 
263  // If no sides were assigned for a user,
264  // take a first available side.
265  if(side_taken < 0 && !side_assigned) {
266  for(side_engine_ptr side : side_engines_) {
267  if(side->available_for_user(username) ||
268  side->controller() == CNTR_LOCAL) {
269  side->place_user(data);
270 
271  side_assigned = true;
272  break;
273  }
274  }
275  }
276 
277  // Check if user has taken any sides, which should get control
278  // over any other sides.
279  for(side_engine_ptr user_side : side_engines_) {
280  if(user_side->player_id() == username && !user_side->previous_save_id().empty()) {
281  for(side_engine_ptr side : side_engines_){
282  if(side->player_id().empty() && side->previous_save_id() == user_side->previous_save_id()) {
283  side->place_user(data);
284  }
285  }
286  }
287  }
288 }
289 
290 bool connect_engine::sides_available() const
291 {
292  for(side_engine_ptr side : side_engines_) {
293  if(side->available_for_user()) {
294  return true;
295  }
296  }
297 
298  return false;
299 }
300 
301 void connect_engine::update_level()
302 {
303  DBG_MP << "updating level";
304 
305  scenario().clear_children("side");
306 
307  for(side_engine_ptr side : side_engines_) {
308  scenario().add_child("side", side->new_config());
309  }
310 }
311 
312 void connect_engine::update_and_send_diff()
313 {
314  config old_level = level_;
315  update_level();
316 
317  config diff = level_.get_diff(old_level);
318  if(!diff.empty()) {
319  config scenario_diff;
320  scenario_diff.add_child("scenario_diff", std::move(diff));
321  mp::send_to_server(scenario_diff);
322  }
323 }
324 
325 bool connect_engine::can_start_game() const
326 {
327  if(side_engines_.empty()) {
328  return true;
329  }
330 
331  // First check if all sides are ready to start the game.
332  for(side_engine_ptr side : side_engines_) {
333  if(!side->ready_for_start()) {
334  const int side_num = side->index() + 1;
335  DBG_MP << "not all sides are ready, side " <<
336  side_num << " not ready";
337 
338  return false;
339  }
340  }
341 
342  DBG_MP << "all sides are ready";
343 
344  /*
345  * If at least one human player is slotted with a player/ai we're allowed
346  * to start. Before used a more advanced test but it seems people are
347  * creative in what is used in multiplayer [1] so use a simpler test now.
348  * [1] http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=568029
349  */
350  for(side_engine_ptr side : side_engines_) {
351  if(side->controller() != CNTR_EMPTY && side->allow_player()) {
352  return true;
353  }
354  }
355 
356  return false;
357 }
358 
359 std::multimap<std::string, config> side_engine::get_side_children()
360 {
361  std::multimap<std::string, config> children;
362 
363  for(const std::string& to_swap : children_to_swap) {
364  for(const config& child : cfg_.child_range(to_swap)) {
365  children.emplace(to_swap, child);
366  }
367  }
368 
369  return children;
370 }
371 
372 void side_engine::set_side_children(const std::multimap<std::string, config>& children)
373 {
374  for(const std::string& children_to_remove : children_to_swap) {
375  cfg_.clear_children(children_to_remove);
376  }
377 
378  for(std::pair<std::string, config> child_map : children) {
379  cfg_.add_child(child_map.first, child_map.second);
380  }
381 }
382 
383 void connect_engine::start_game()
384 {
385  DBG_MP << "starting a new game";
386 
387  // Resolves the "random faction", "random gender" and "random message"
388  // Must be done before shuffle sides, or some cases will cause errors
389  randomness::mt_rng rng; // Make an RNG for all the shuffling and random faction operations
390  for(side_engine_ptr side : side_engines_) {
391  std::vector<std::string> avoid_faction_ids;
392 
393  // If we aren't resolving random factions independently at random, calculate which factions should not appear for this side.
394  if(params_.mode != random_faction_mode::type::independent) {
395  for(side_engine_ptr side2 : side_engines_) {
396  if(!side2->flg().is_random_faction()) {
397  switch(params_.mode) {
398  case random_faction_mode::type::no_mirror:
399  avoid_faction_ids.push_back(side2->flg().current_faction()["id"].str());
400  break;
401  case random_faction_mode::type::no_ally_mirror:
402  if(side2->team() == side->team()) {// TODO: When the connect engines are fixed to allow multiple teams, this should be changed to "if side1 and side2 are allied, i.e. their list of teams has nonempty intersection"
403  avoid_faction_ids.push_back(side2->flg().current_faction()["id"].str());
404  }
405  break;
406  default:
407  break; // assert(false);
408  }
409  }
410  }
411  }
412  side->resolve_random(rng, avoid_faction_ids);
413  }
414 
415  // Shuffle sides (check settings and if it is a re-loaded game).
416  // Must be done after resolve_random() or shuffle sides, or they won't work.
417  if(state_.mp_settings().shuffle_sides && !force_lock_settings_ && !(level_.has_child("snapshot") && level_.mandatory_child("snapshot").has_child("side"))) {
418 
419  // Only playable sides should be shuffled.
420  std::vector<int> playable_sides;
421  for(side_engine_ptr side : side_engines_) {
422  if(side->allow_player() && side->allow_shuffle()) {
423  playable_sides.push_back(side->index());
424  }
425  }
426 
427  // Fisher-Yates shuffle.
428  for(int i = playable_sides.size(); i > 1; i--) {
429  const int j_side = playable_sides[rng.get_next_random() % i];
430  const int i_side = playable_sides[i - 1];
431 
432  if(i_side == j_side) continue; //nothing to swap
433 
434  // First we swap everything about a side with another
435  std::swap(side_engines_[j_side], side_engines_[i_side]);
436 
437  // Some 'child' variables such as village ownership and initial side units need to be swapped over as well
438  std::multimap<std::string, config> tmp_side_children = side_engines_[j_side]->get_side_children();
439  side_engines_[j_side]->set_side_children(side_engines_[i_side]->get_side_children());
440  side_engines_[i_side]->set_side_children(tmp_side_children);
441 
442  // Then we revert the swap for fields that are unique to player control and the team they selected
443  std::swap(side_engines_[j_side]->index_, side_engines_[i_side]->index_);
444  std::swap(side_engines_[j_side]->team_, side_engines_[i_side]->team_);
445  }
446  }
447 
448  // Make other clients not show the results of resolve_random().
449  config lock("stop_updates");
450  mp::send_to_server(lock);
451 
452  update_and_send_diff();
453 
454  save_reserved_sides_information();
455 
456  // Build the gamestate object after updating the level.
457  mp::level_to_gamestate(level_, state_);
458 
459  mp::send_to_server(config("start_game"));
460 }
461 
462 void connect_engine::start_game_commandline(const commandline_options& cmdline_opts, const game_config_view& game_config)
463 {
464  DBG_MP << "starting a new game in commandline mode";
465 
466  randomness::mt_rng rng;
467 
468  unsigned num = 0;
469  for(side_engine_ptr side : side_engines_) {
470  num++;
471 
472  // Set the faction, if commandline option is given.
473  if(cmdline_opts.multiplayer_side) {
474  for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_side) {
475  if(side_num == num) {
476  if(std::find_if(era_factions_.begin(), era_factions_.end(),
477  [fid = faction_id](const config* faction) { return (*faction)["id"] == fid; })
478  != era_factions_.end()
479  ) {
480  DBG_MP << "\tsetting side " << side_num << "\tfaction: " << faction_id;
481  side->set_faction_commandline(faction_id);
482  } else {
483  ERR_MP << "failed to set side " << side_num << " to faction " << faction_id;
484  }
485  }
486  }
487  }
488 
489  // Set the controller, if commandline option is given.
490  if(cmdline_opts.multiplayer_controller) {
491  for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_controller) {
492  if(side_num == num) {
493  DBG_MP << "\tsetting side " << side_num << "\tfaction: " << faction_id;
494  side->set_controller_commandline(faction_id);
495  }
496  }
497  }
498 
499  // Set AI algorithm to default for all sides,
500  // then override if commandline option was given.
501  std::string ai_algorithm = game_config.mandatory_child("ais")["default_ai_algorithm"].str();
502  side->set_ai_algorithm(ai_algorithm);
503 
504  if(cmdline_opts.multiplayer_algorithm) {
505  for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_algorithm) {
506  if(side_num == num) {
507  DBG_MP << "\tsetting side " << side_num << "\tfaction: " << faction_id;
508  side->set_ai_algorithm(faction_id);
509  }
510  }
511  }
512 
513  // Finally, resolve "random faction",
514  // "random gender" and "random message", if any remains unresolved.
515  side->resolve_random(rng);
516  } // end top-level loop
517 
518  update_and_send_diff();
519 
520  // Update sides with commandline parameters.
521  if(cmdline_opts.multiplayer_turns) {
522  DBG_MP << "\tsetting turns: " << *cmdline_opts.multiplayer_turns;
523  scenario()["turns"] = *cmdline_opts.multiplayer_turns;
524  }
525 
526  for(config& side : scenario().child_range("side")) {
527  if(cmdline_opts.multiplayer_ai_config) {
528  for(const auto& [side_num, faction_id] : *cmdline_opts.multiplayer_ai_config) {
529  if(side_num == side["side"].to_unsigned()) {
530  DBG_MP << "\tsetting side " << side["side"] << "\tai_config: " << faction_id;
531  side["ai_config"] = faction_id;
532  }
533  }
534  }
535 
536  // Having hard-coded values here is undesirable,
537  // but that's how it is done in the MP lobby
538  // part of the code also.
539  // Should be replaced by settings/constants in both places
540  if(cmdline_opts.multiplayer_ignore_map_settings) {
541  side["gold"] = 100;
542  side["income"] = 1;
543  }
544 
545  if(cmdline_opts.multiplayer_parm) {
546  for(const auto& [side_num, pname, pvalue] : *cmdline_opts.multiplayer_parm) {
547  if(side_num == side["side"].to_unsigned()) {
548  DBG_MP << "\tsetting side " << side["side"] << " " << pname << ": " << pvalue;
549  side[pname] = pvalue;
550  }
551  }
552  }
553  }
554 
555  save_reserved_sides_information();
556 
557  // Build the gamestate object after updating the level
558  mp::level_to_gamestate(level_, state_);
559  mp::send_to_server(config("start_game"));
560 }
561 
562 void connect_engine::leave_game()
563 {
564  DBG_MP << "leaving the game";
565 
566  mp::send_to_server(config("leave_game"));
567 }
568 
569 std::pair<bool, bool> connect_engine::process_network_data(const config& data)
570 {
571  std::pair<bool, bool> result(false, true);
572 
573  if(data.has_child("leave_game")) {
574  result.first = true;
575  return result;
576  }
577 
578  // A side has been dropped.
579  if(auto side_drop = data.optional_child("side_drop")) {
580  unsigned side_index = side_drop["side_num"].to_int() - 1;
581 
582  if(side_index < side_engines_.size()) {
583  side_engine_ptr side_to_drop = side_engines_[side_index];
584 
585  // Remove user, whose side was dropped.
586  connected_users_rw().erase(side_to_drop->player_id());
587  update_side_controller_options();
588 
589  side_to_drop->reset();
590 
591  update_and_send_diff();
592 
593  return result;
594  }
595  }
596 
597  // A player is connecting to the game.
598  if(!data["side"].empty()) {
599  unsigned side_taken = data["side"].to_int() - 1;
600 
601  // Checks if the connecting user has a valid and unique name.
602  const std::string name = data["name"];
603  if(name.empty()) {
604  config response;
605  response["failed"] = true;
606  mp::send_to_server(response);
607 
608  ERR_CF << "ERROR: No username provided with the side.";
609 
610  return result;
611  }
612 
613  if(connected_users().find(name) != connected_users().end()) {
614  // TODO: Seems like a needless limitation
615  // to only allow one side per player.
616  if(find_user_side_index_by_id(name) != -1) {
617  config response;
618  response["failed"] = true;
619  response["message"] = "The nickname '" + name +
620  "' is already in use.";
621  mp::send_to_server(response);
622 
623  return result;
624  } else {
625  connected_users_rw().erase(name);
626  update_side_controller_options();
627  config observer_quit;
628  observer_quit.add_child("observer_quit")["name"] = name;
629  mp::send_to_server(observer_quit);
630  }
631  }
632 
633  // Assigns this user to a side.
634  if(side_taken < side_engines_.size()) {
635  if(!side_engines_[side_taken]->available_for_user(name)) {
636  // This side is already taken.
637  // Try to reassing the player to a different position.
638  side_taken = 0;
639  for(side_engine_ptr s : side_engines_) {
640  if(s->available_for_user()) {
641  break;
642  }
643 
644  side_taken++;
645  }
646 
647  if(side_taken >= side_engines_.size()) {
648  config response;
649  response["failed"] = true;
650  mp::send_to_server(response);
651 
652  config res;
653  config& kick = res.add_child("kick");
654  kick["username"] = data["name"];
655  mp::send_to_server(res);
656 
657  update_and_send_diff();
658 
659  ERR_CF << "ERROR: Couldn't assign a side to '" <<
660  name << "'";
661 
662  return result;
663  }
664  }
665 
666  LOG_CF << "client has taken a valid position";
667 
668  import_user(data, false, side_taken);
669  update_and_send_diff();
670 
671  // Wait for them to choose faction if allowed.
672  side_engines_[side_taken]->set_waiting_to_choose_status(side_engines_[side_taken]->allow_changes());
673  LOG_MP << "waiting to choose status = " << side_engines_[side_taken]->allow_changes();
674  result.second = false;
675 
676  LOG_NW << "sent player data";
677  } else {
678  ERR_CF << "tried to take illegal side: " << side_taken;
679 
680  config response;
681  response["failed"] = true;
682  mp::send_to_server(response);
683  }
684  }
685 
686  if(auto change_faction = data.optional_child("change_faction")) {
687  int side_taken = find_user_side_index_by_id(change_faction["name"]);
688  if(side_taken != -1 || !first_scenario_) {
689  import_user(*change_faction, false, side_taken);
690  update_and_send_diff();
691  }
692  }
693 
694  if(auto observer = data.optional_child("observer")) {
695  import_user(*observer, true);
696  update_and_send_diff();
697  }
698 
699  if(auto observer = data.optional_child("observer_quit")) {
700  const std::string& observer_name = observer["name"];
701 
702  if(connected_users().find(observer_name) != connected_users().end()) {
703  connected_users_rw().erase(observer_name);
704  update_side_controller_options();
705 
706  // If the observer was assigned a side, we need to send an update to other
707  // players so they no longer see the observer assigned to that side.
708  if(find_user_side_index_by_id(observer_name) != -1) {
709  update_and_send_diff();
710  }
711  }
712  }
713 
714  return result;
715 }
716 
717 int connect_engine::find_user_side_index_by_id(const std::string& id) const
718 {
719  std::size_t i = 0;
720  for(side_engine_ptr side : side_engines_) {
721  if(side->player_id() == id) {
722  break;
723  }
724 
725  i++;
726  }
727 
728  if(i >= side_engines_.size()) {
729  return -1;
730  }
731 
732  return i;
733 }
734 
735 void connect_engine::send_level_data() const
736 {
737  // Send initial information.
738  if(first_scenario_) {
740  "create_game", config {
741  "name", params_.name,
742  "password", params_.password,
743  "ignored", prefs::get().get_ignored_delim(),
744  "auto_hosted", false,
745  },
746  });
747  mp::send_to_server(level_);
748  } else {
749  config next_level;
750  next_level.add_child("store_next_scenario", level_);
751  mp::send_to_server(next_level);
752  }
753 }
754 
755 void connect_engine::save_reserved_sides_information()
756 {
757  // Add information about reserved sides to the level config.
758  // N.B. This information is needed only for a host player.
759  std::map<std::string, std::string> side_users = utils::map_split(level_.child_or_empty("multiplayer")["side_users"]);
760  for(side_engine_ptr side : side_engines_) {
761  const std::string& save_id = side->save_id();
762  const std::string& player_id = side->player_id();
763  if(!save_id.empty() && !player_id.empty()) {
764  side_users[save_id] = player_id;
765  }
766  }
767 
768  level_.mandatory_child("multiplayer")["side_users"] = utils::join_map(side_users);
769 }
770 
771 void connect_engine::load_previous_sides_users()
772 {
773  std::map<std::string, std::string> side_users = utils::map_split(level_.mandatory_child("multiplayer")["side_users"]);
774  std::set<std::string> names;
775  for(side_engine_ptr side : side_engines_) {
776  const std::string& save_id = side->previous_save_id();
777  if(side_users.find(save_id) != side_users.end()) {
778  side->set_reserved_for(side_users[save_id]);
779 
780  if(side->controller() != CNTR_COMPUTER) {
781  side->set_controller(CNTR_RESERVED);
782  names.insert(side_users[save_id]);
783  }
784 
785  side->update_controller_options();
786  }
787  }
788 
789  //Do this in an extra loop to make sure we import each user only once.
790  for(const std::string& name : names)
791  {
792  if(connected_users().find(name) != connected_users().end() || !mp_metadata_) {
793  import_user(name, false);
794  }
795  }
796 }
797 
798 void connect_engine::update_side_controller_options()
799 {
800  for(side_engine_ptr side : side_engines_) {
801  side->update_controller_options();
802  }
803 }
804 
805 const std::set<std::string>& connect_engine::connected_users() const
806 {
807  if(mp_metadata_) {
808  return mp_metadata_->connected_players;
809  }
810 
811  static std::set<std::string> empty;
812  return empty;
813 }
814 
815 std::set<std::string>& connect_engine::connected_users_rw()
816 {
817  assert(mp_metadata_);
818  return mp_metadata_->connected_players;
819 }
820 
821 side_engine::side_engine(const config& cfg, connect_engine& parent_engine, const int index)
822  : cfg_(cfg)
823  , parent_(parent_engine)
824  , controller_(CNTR_NETWORK)
825  , current_controller_index_(0)
826  , controller_options_()
827  , allow_player_(cfg["allow_player"].to_bool(true))
828  , controller_lock_(cfg["controller_lock"].to_bool(parent_.force_lock_settings_) && parent_.params_.use_map_settings)
829  , index_(index)
830  , team_(0)
831  , color_(std::min(index, gamemap::MAX_PLAYERS - 1))
832  , gold_(cfg["gold"].to_int(100))
833  , income_(cfg["income"].to_int())
834  , reserved_for_(cfg["current_player"])
835  , player_id_()
836  , ai_algorithm_()
837  , chose_random_(cfg["chose_random"].to_bool(false))
838  , disallow_shuffle_(cfg["disallow_shuffle"].to_bool(false))
839  , flg_(parent_.era_factions_, cfg_, parent_.force_lock_settings_, parent_.params_.use_map_settings, parent_.params_.saved_game == saved_game_mode::type::midgame)
840  , allow_changes_(parent_.params_.saved_game != saved_game_mode::type::midgame && !(flg_.choosable_factions().size() == 1 && flg_.choosable_leaders().size() == 1 && flg_.choosable_genders().size() == 1))
841  , waiting_to_choose_faction_(allow_changes_)
842  , color_options_(game_config::default_colors)
843  //TODO: what should we do if color_ is out of range?
844  , color_id_(color_options_.at(color_))
845 {
846 
847  // Save default attributes that could be overwritten by the faction, so that correct faction lists would be
848  // initialized by flg_manager when the new side config is sent over network.
849  cfg_.clear_children("default_faction");
850  cfg_.add_child("default_faction", config {
851  "faction", cfg_["faction"],
852  "recruit", cfg_["recruit"],
853  });
854  if(auto p_cfg = cfg_.optional_child("leader")) {
855  cfg_.mandatory_child("default_faction").add_child("leader", config {
856  "type", (p_cfg)["type"],
857  "gender", (p_cfg)["gender"],
858  });
859  }
860 
861 
862  if(cfg_["side"].to_int(index_ + 1) != index_ + 1) {
863  ERR_CF << "found invalid side=" << cfg_["side"].to_int(index_ + 1) << " in definition of side number " << index_ + 1;
864  }
865 
866  cfg_["side"] = index_ + 1;
867 
868  if(cfg_["controller"] != side_controller::human && cfg_["controller"] != side_controller::ai && cfg_["controller"] != side_controller::none) {
869  //an invalid controller type was specified. Remove it to prevent asertion failures later.
870  cfg_.remove_attribute("controller");
871  }
872 
874 
875  // Tweak the controllers.
876  if(parent_.state_.classification().is_scenario() && cfg_["controller"].blank()) {
877  cfg_["controller"] = side_controller::ai;
878  }
879 
880  if(cfg_["controller"] == side_controller::none) {
882  } else if(cfg_["controller"] == side_controller::ai) {
884  } else if(parent_.default_controller_ == CNTR_NETWORK && !reserved_for_.empty()) {
885  // Reserve a side for "current_player", unless the side
886  // is played by an AI.
888  } else if(allow_player_) {
890  } else {
891  // AI is the default.
893  }
894 
895  // Initialize team and color.
896  unsigned team_name_index = 0;
898  if(data.team_name == cfg["team_name"]) {
899  break;
900  }
901 
902  ++team_name_index;
903  }
904 
905  if(team_name_index >= parent_.team_data_.size()) {
906  assert(!parent_.team_data_.empty());
907  team_ = 0;
908  WRN_MP << "In side_engine constructor: Could not find my team_name " << cfg["team_name"] << " among the mp connect engine's list of team names. I am being assigned to the first team. This may indicate a bug!";
909  } else {
910  team_ = team_name_index;
911  }
912 
913  // Check the value of the config's color= key.
914  const std::string given_color = team::get_side_color_id_from_config(cfg_);
915 
916  if(!given_color.empty()) {
917  // If it's valid, save the color...
918  color_id_ = given_color;
919 
920  // ... and find the appropriate index for it.
921  const auto iter = std::find(color_options_.begin(), color_options_.end(), color_id_);
922 
923  if(iter != color_options_.end()) {
924  color_ = std::distance(color_options_.begin(), iter);
925  } else {
926  color_options_.push_back(color_id_);
927  color_ = color_options_.size() - 1;
928  }
929  }
930 
931  // Initialize ai algorithm.
932  if(auto ai = cfg.optional_child("ai")) {
933  ai_algorithm_ = ai["ai_algorithm"].str();
934  }
935 }
936 
937 std::string side_engine::user_description() const
938 {
939  switch(controller_) {
940  case CNTR_LOCAL:
941  return N_("Anonymous player");
942  case CNTR_COMPUTER:
943  if(allow_player_) {
945  } else {
946  return N_("Computer Player");
947  }
948  default:
949  return "";
950  }
951 }
952 
954 {
955  config res = cfg_;
956 
957  // In case of 'shuffle sides' the side index in cfg_ might be wrong which will confuse the team constructor later.
958  res["side"] = index_ + 1;
959 
960  // If the user is allowed to change type, faction, leader etc, then import their new values in the config.
961  if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
962  // Merge the faction data to res.
963  config faction = flg_.current_faction();
964  LOG_MP << "side_engine::new_config: side=" << index_ + 1 << " faction=" << faction["id"] << " recruit=" << faction["recruit"];
965  res["faction_name"] = faction["name"];
966  res["faction"] = faction["id"];
967  faction.remove_attributes("id", "name", "image", "gender", "type", "description");
968  res.append(faction);
969  }
970 
971  res["controller"] = controller_names[controller_];
972 
973  // The hosts receives the serversided controller tweaks after the start event, but
974  // for mp sync it's very important that the controller types are correct
975  // during the start/prestart event (otherwise random unit creation during prestart fails).
976  res["is_local"] = player_id_ == prefs::get().login() || controller_ == CNTR_COMPUTER || controller_ == CNTR_LOCAL;
977 
978  // This function (new_config) is only meant to be called by the host's machine, which is why this check
979  // works. It essentially certifies that whatever side has the player_id that matches the host's login
980  // will be flagged. The reason we cannot check mp_game_metadata::is_host is because that flag is *always*
981  // true on the host's machine, meaning this flag would be set to true for every side.
982  res["is_host"] = player_id_ == prefs::get().login();
983 
984  std::string desc = user_description();
985  if(!desc.empty()) {
986  res["user_description"] = t_string(desc, "wesnoth");
987 
988  desc = VGETTEXT("$playername $side", {
989  {"playername", _(desc.c_str())},
990  {"side", res["side"].str()}
991  });
992  } else if(!player_id_.empty()) {
993  desc = player_id_;
994  }
995 
996  if(res["name"].str().empty() && !desc.empty()) {
997  //TODO: maybe we should add this in to the leaders config instead of the side config?
998  res["name"] = desc;
999  }
1000 
1002  // If this is a saved game and "use_saved" (the default) was chosen for the
1003  // AI algorithm, we do nothing. Otherwise we add the chosen AI and if this
1004  // is a saved game, we also remove the old stages from the AI config.
1005  if(ai_algorithm_ != "use_saved") {
1006  if(parent_.params_.saved_game == saved_game_mode::type::midgame) {
1007  for (config &ai_config : res.child_range("ai")) {
1008  ai_config.clear_children("stage");
1009  }
1010  }
1011  res.add_child_at("ai", config {"ai_algorithm", ai_algorithm_}, 0);
1012  }
1013  }
1014 
1015  // A side's "current_player" is the player which has currently taken that side or the one for which it is reserved.
1016  // The "player_id" is the id of the client who controls that side. It's always the host for Local and AI players and
1017  // always empty for free/reserved sides or null controlled sides. You can use !res["player_id"].empty() to check
1018  // whether a side is already taken.
1019  assert(!prefs::get().login().empty());
1020  if(controller_ == CNTR_LOCAL) {
1021  res["player_id"] = prefs::get().login();
1022  res["current_player"] = prefs::get().login();
1023  } else if(controller_ == CNTR_RESERVED) {
1024  res.remove_attribute("player_id");
1025  res["current_player"] = reserved_for_;
1026  } else if(controller_ == CNTR_COMPUTER) {
1027  // TODO: what is the content of player_id_ here ?
1028  res["current_player"] = desc;
1029  res["player_id"] = prefs::get().login();
1030  } else if(!player_id_.empty()) {
1031  res["player_id"] = player_id_;
1032  res["current_player"] = player_id_;
1033  }
1034 
1035  res["allow_changes"] = allow_changes_;
1036  res["chose_random"] = chose_random_;
1037 
1038  if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
1039 
1040  if(!flg_.leader_lock()) {
1041  if(controller_ != CNTR_EMPTY) {
1042  auto& leader = res.child_or_add("leader");
1043  leader["type"] = flg_.current_leader();
1044  leader["gender"] = flg_.current_gender();
1045  LOG_MP << "side_engine::new_config: side=" << index_ + 1 << " type=" << leader["type"]
1046  << " gender=" << leader["gender"];
1047  } else if(!controller_lock_) {
1048  //if controller_lock_ == false and controller_ == CNTR_EMPTY, this means the user disalbles this side, so remove it's leader.
1049  res.remove_children("leader");
1050  }
1051  }
1052 
1053  const std::string& new_team_name = parent_.team_data_[team_].team_name;
1054 
1055  if(res["user_team_name"].empty() || !parent_.params_.use_map_settings || res["team_name"] != new_team_name) {
1056  res["team_name"] = new_team_name;
1057  res["user_team_name"] = parent_.team_data_[team_].user_team_name;
1058  }
1059 
1060  res["allow_player"] = allow_player_;
1061  res["color"] = color_id_;
1062  res["gold"] = gold_;
1063  res["income"] = income_;
1064  }
1065 
1066 
1067  if(parent_.params_.use_map_settings && parent_.params_.saved_game != saved_game_mode::type::midgame) {
1068  if(cfg_.has_attribute("name")){
1069  res["name"] = cfg_["name"];
1070  }
1071  if(cfg_.has_attribute("user_description") && controller_ == CNTR_COMPUTER){
1072  res["user_description"] = cfg_["user_description"];
1073  }
1074  }
1075 
1076  return res;
1077 }
1078 
1080 {
1081  if(!allow_player_) {
1082  // Sides without players are always ready.
1083  return true;
1084  }
1085 
1086  if((controller_ == CNTR_COMPUTER) ||
1087  (controller_ == CNTR_EMPTY) ||
1088  (controller_ == CNTR_LOCAL)) {
1089 
1090  return true;
1091  }
1092 
1093  if(available_for_user()) {
1094  // If controller_ == CNTR_NETWORK and player_id_.empty().
1095  return false;
1096  }
1097 
1098  if(controller_ == CNTR_NETWORK) {
1100  // The host is ready. A network player, who got a chance
1101  // to choose faction if allowed, is also ready.
1102  return true;
1103  }
1104  }
1105 
1106  return false;
1107 }
1108 
1109 bool side_engine::available_for_user(const std::string& name) const
1110 {
1111  if(controller_ == CNTR_NETWORK && player_id_.empty()) {
1112  // Side is free and waiting for user.
1113  return true;
1114  }
1115 
1116  if(controller_ == CNTR_RESERVED && name.empty()) {
1117  // Side is still available to someone.
1118  return true;
1119  }
1120 
1121  if(controller_ == CNTR_RESERVED && reserved_for_ == name) {
1122  // Side is available only for the player with specific name.
1123  return true;
1124  }
1125 
1126  return false;
1127 }
1128 
1129 void side_engine::resolve_random(randomness::mt_rng & rng, const std::vector<std::string> & avoid_faction_ids)
1130 {
1131  if(parent_.params_.saved_game == saved_game_mode::type::midgame) {
1132  return;
1133  }
1134 
1136 
1137  flg_.resolve_random(rng, avoid_faction_ids);
1138 
1139  LOG_MP << "side " << (index_ + 1) << ": faction=" <<
1140  (flg_.current_faction())["name"] << ", leader=" <<
1141  flg_.current_leader() << ", gender=" << flg_.current_gender();
1142 }
1143 
1145 {
1146  player_id_.clear();
1149 
1150  if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
1152  }
1153 }
1154 
1155 void side_engine::place_user(const std::string& name)
1156 {
1157  config data;
1158  data["name"] = name;
1159 
1160  place_user(data);
1161 }
1162 
1163 void side_engine::place_user(const config& data, bool contains_selection)
1164 {
1165  player_id_ = data["name"].str();
1167 
1168  if(data["change_faction"].to_bool() && contains_selection) {
1169  // Network user's data carry information about chosen
1170  // faction, leader and genders.
1171  flg_.set_current_faction(data["faction"].str());
1172  flg_.set_current_leader(data["leader"].str());
1173  flg_.set_current_gender(data["gender"].str());
1174  }
1175 
1177 }
1178 
1180 {
1181  controller_options_.clear();
1182 
1183  // Default options.
1184  if(parent_.mp_metadata_) {
1185  add_controller_option(CNTR_NETWORK, _("Network Player"), side_controller::human);
1186  }
1187 
1188  add_controller_option(CNTR_LOCAL, _("Local Player"), side_controller::human);
1189  add_controller_option(CNTR_COMPUTER, _("Computer Player"), side_controller::ai);
1190  add_controller_option(CNTR_EMPTY, _("Nobody"), side_controller::none);
1191 
1192  if(!reserved_for_.empty()) {
1193  add_controller_option(CNTR_RESERVED, _("Reserved"), side_controller::human);
1194  }
1195 
1196  // Connected users.
1197  for(const std::string& user : parent_.connected_users()) {
1198  add_controller_option(parent_.default_controller_, user, side_controller::human);
1199  }
1200 
1202 }
1203 
1205 {
1206  int i = 0;
1207  for(const controller_option& option : controller_options_) {
1208  if(option.first == controller_) {
1210 
1211  if(player_id_.empty() || player_id_ == option.second) {
1212  // Stop searching if no user is assigned to a side
1213  // or the selected user is found.
1214  break;
1215  }
1216  }
1217 
1218  i++;
1219  }
1220 
1222 }
1223 
1224 bool side_engine::controller_changed(const int selection)
1225 {
1226  const ng::controller selected_cntr = controller_options_[selection].first;
1227 
1228  // Check if user was selected. If so assign a side to him/her.
1229  // If not, make sure that no user is assigned to this side.
1230  if(selected_cntr == parent_.default_controller_ && selection != 0) {
1231  player_id_ = controller_options_[selection].second;
1233  } else {
1234  player_id_.clear();
1235  }
1236 
1237  set_controller(selected_cntr);
1238 
1239  return true;
1240 }
1241 
1243 {
1245 
1247 }
1248 
1249 void side_engine::set_faction_commandline(const std::string& faction_name)
1250 {
1251  flg_.set_current_faction(faction_name);
1252 }
1253 
1255 {
1257 
1258  if(controller_name == side_controller::ai) {
1260  }
1261 
1262  if(controller_name == side_controller::none) {
1264  }
1265 
1266  player_id_.clear();
1267 }
1268 
1270  const std::string& name, const std::string& controller_value)
1271 {
1272  if(controller_lock_ && !cfg_["controller"].empty() && cfg_["controller"] != controller_value) {
1273  return;
1274  }
1275 
1276  controller_options_.emplace_back(controller, name);
1277 }
1278 
1279 } // end namespace ng
std::vector< std::string > names
Definition: build_info.cpp:67
static const config & get_ai_config_for(const std::string &id)
Return the config for a specified ai.
static void add_era_ai_from_config(const config &game_config)
static void add_mod_ai_from_config(const config::const_child_itors &configs)
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_side
Non-empty if –side was given on the command line.
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_ai_config
Non-empty if –ai-config was given on the command line.
utils::optional< std::string > multiplayer_turns
Non-empty if –turns was given on the command line.
bool multiplayer_ignore_map_settings
True if –ignore-map-settings was given at the command line.
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_controller
Non-empty if –controller was given on the command line.
utils::optional< std::vector< std::pair< unsigned int, std::string > > > multiplayer_algorithm
Non-empty if –algorithm was given on the command line.
utils::optional< std::vector< std::tuple< unsigned int, std::string, std::string > > > multiplayer_parm
Non-empty if –parm was given on the command line.
Variant for storing WML attributes.
std::string str(const std::string &fallback="") const
bool empty() const
Tests for an attribute that either was never set or was set to "".
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:188
void remove_attributes(T... keys)
Definition: config.hpp:537
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:362
boost::iterator_range< child_iterator > child_itors
Definition: config.hpp:281
config & add_child_at(config_key_type key, const config &val, std::size_t index)
Definition: config.cpp:465
void clear_children(T... keys)
Definition: config.hpp:602
bool has_attribute(config_key_type key) const
Definition: config.cpp:157
void remove_children(config_key_type key, const std::function< bool(const config &)> &p={})
Removes all children with tag key for which p returns true.
Definition: config.cpp:650
child_itors child_range(config_key_type key)
Definition: config.cpp:268
config & child_or_add(config_key_type key)
Returns a reference to the first child with the given key.
Definition: config.cpp:401
void remove_attribute(config_key_type key)
Definition: config.cpp:162
config get_diff(const config &c) const
A function to get the differences between this object, and 'c', as another config object.
Definition: config.cpp:906
bool empty() const
Definition: config.cpp:845
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:380
config & add_child(config_key_type key)
Definition: config.cpp:436
A class grating read only view to a vector of config objects, viewed as one config with all children ...
static game_config_view wrap(const config &cfg)
Encapsulates the map of the game.
Definition: map.hpp:172
std::vector< const config * > era_factions_
std::vector< team_data_pod > team_data_
const ng::controller default_controller_
const std::set< std::string > & connected_users() const
const mp_game_settings & params_
mp_game_metadata * mp_metadata_
connect_engine(saved_game &state, const bool first_scenario, mp_game_metadata *metadata)
const std::string & current_gender() const
Definition: flg_manager.hpp:71
void set_current_faction(const unsigned index)
void resolve_random(randomness::mt_rng &rng, const std::vector< std::string > &avoid)
void set_current_leader(const unsigned index)
bool leader_lock() const
Definition: flg_manager.hpp:80
void set_current_gender(const unsigned index)
bool is_random_faction()
const config & current_faction() const
Definition: flg_manager.hpp:67
const std::string & current_leader() const
Definition: flg_manager.hpp:69
const config & cfg() const
unsigned current_controller_index_
void update_controller_options()
std::string player_id_
const bool controller_lock_
const bool allow_changes_
void set_controller_commandline(const std::string &controller_name)
void set_faction_commandline(const std::string &faction_name)
void place_user(const std::string &name)
void add_controller_option(ng::controller controller, const std::string &name, const std::string &controller_value)
void resolve_random(randomness::mt_rng &rng, const std::vector< std::string > &avoid_faction_ids=std::vector< std::string >())
void update_current_controller_index()
std::vector< controller_option > controller_options_
connect_engine & parent_
std::string ai_algorithm_
std::string reserved_for_
std::vector< std::string > color_options_
void set_controller(ng::controller controller)
void set_waiting_to_choose_status(bool status)
bool controller_changed(const int selection)
std::string color_id_
std::string user_description() const
ng::controller controller_
bool ready_for_start() const
config new_config() const
ng::controller controller() const
bool available_for_user(const std::string &name="") const
const bool allow_player_
static prefs & get()
const std::string get_ignored_delim()
std::string login()
uint32_t get_next_random()
Get a new random number.
Definition: mt_rng.cpp:63
game_classification & classification()
Definition: saved_game.hpp:56
mp_game_settings & mp_settings()
Multiplayer parameters for this game.
Definition: saved_game.hpp:60
std::string to_serialized() const
Definition: tstring.hpp:163
static std::string get_side_color_id_from_config(const config &cfg)
Definition: team.cpp:1000
void swap(config &lhs, config &rhs)
Implement non-member swap function for std::swap (calls config::swap).
Definition: config.cpp:1339
Managing the AIs configuration - headers.
#define WRN_MP
#define ERR_MP
#define DBG_MP
#define LOG_NW
static lg::log_domain log_network("network")
#define LOG_CF
#define ERR_CF
static lg::log_domain log_mp_connect_engine("mp/connect/engine")
#define LOG_MP
static lg::log_domain log_config("config")
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1029
#define N_(String)
Definition: gettext.hpp:101
static std::string _(const char *str)
Definition: gettext.hpp:93
std::vector< const mp::user_info * > user_data
The associated user data for each node, index-to-index.
Standard logging facilities (interface).
A small explanation about what's going on here: Each action has access to two game_info objects First...
Definition: actions.cpp:59
std::string observer
Game configuration data as global variables.
Definition: build_info.cpp:61
std::vector< std::string > default_colors
static void add_color_info(const game_config_view &v, bool build_defaults)
static std::string controller_name(const team &t)
Definition: game_stats.cpp:61
config initial_level_config(saved_game &state)
void level_to_gamestate(const config &level, saved_game &state)
void send_to_server(const config &data)
Attempts to send given data to server if a connection is open.
@ CNTR_RESERVED
@ CNTR_NETWORK
@ CNTR_COMPUTER
@ CNTR_EMPTY
@ CNTR_LOCAL
std::pair< ng::controller, std::string > controller_option
std::shared_ptr< side_engine > side_engine_ptr
static std::string at(const std::string &file, int line)
int compare(const std::string &s1, const std::string &s2)
Case-sensitive lexicographical comparison.
Definition: gettext.cpp:502
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
std::map< std::string, std::string > map_split(const std::string &val, char major, char minor, int flags, const std::string &default_value)
Splits a string based on two separators into a map.
std::string join_map(const T &v, const std::string &major=",", const std::string &minor=":")
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:140
std::string_view data
Definition: picture.cpp:178
saved_game_mode::type saved_game
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static map_location::direction s