The Battle for Wesnoth  1.19.12+dev
connect_engine.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2013 - 2025
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  // all queue games count as auto hosted, but not all auto hosted games are queue games
745  "auto_hosted", mp_metadata_ ? mp_metadata_->queue_type != queue_type::normal : false,
746  "queue_type", mp_metadata_ ? mp_metadata_->queue_type : queue_type::normal,
747  "queue_id", mp_metadata_ ? mp_metadata_->queue_id : 0,
748  },
749  });
750  mp::send_to_server(level_);
751  } else {
752  config next_level;
753  next_level.add_child("store_next_scenario", level_);
754  mp::send_to_server(next_level);
755  }
756 }
757 
758 void connect_engine::save_reserved_sides_information()
759 {
760  // Add information about reserved sides to the level config.
761  // N.B. This information is needed only for a host player.
762  std::map<std::string, std::string> side_users = utils::map_split(level_.child_or_empty("multiplayer")["side_users"]);
763  for(side_engine_ptr side : side_engines_) {
764  const std::string& save_id = side->save_id();
765  const std::string& player_id = side->player_id();
766  if(!save_id.empty() && !player_id.empty()) {
767  side_users[save_id] = player_id;
768  }
769  }
770 
771  level_.mandatory_child("multiplayer")["side_users"] = utils::join_map(side_users);
772 }
773 
774 void connect_engine::load_previous_sides_users()
775 {
776  std::map<std::string, std::string> side_users = utils::map_split(level_.mandatory_child("multiplayer")["side_users"]);
777  std::set<std::string> names;
778  for(side_engine_ptr side : side_engines_) {
779  const std::string& save_id = side->previous_save_id();
780  if(side_users.find(save_id) != side_users.end()) {
781  side->set_reserved_for(side_users[save_id]);
782 
783  if(side->controller() != CNTR_COMPUTER) {
784  side->set_controller(CNTR_RESERVED);
785  names.insert(side_users[save_id]);
786  }
787 
788  side->update_controller_options();
789  }
790  }
791 
792  //Do this in an extra loop to make sure we import each user only once.
793  for(const std::string& name : names)
794  {
795  if(connected_users().find(name) != connected_users().end() || !mp_metadata_) {
796  import_user(name, false);
797  }
798  }
799 }
800 
801 void connect_engine::update_side_controller_options()
802 {
803  for(side_engine_ptr side : side_engines_) {
804  side->update_controller_options();
805  }
806 }
807 
808 const std::set<std::string>& connect_engine::connected_users() const
809 {
810  if(mp_metadata_) {
811  return mp_metadata_->connected_players;
812  }
813 
814  static std::set<std::string> empty;
815  return empty;
816 }
817 
818 std::set<std::string>& connect_engine::connected_users_rw()
819 {
820  assert(mp_metadata_);
821  return mp_metadata_->connected_players;
822 }
823 
824 side_engine::side_engine(const config& cfg, connect_engine& parent_engine, const int index)
825  : cfg_(cfg)
826  , parent_(parent_engine)
827  , controller_(CNTR_NETWORK)
828  , current_controller_index_(0)
829  , controller_options_()
830  , allow_player_(cfg["allow_player"].to_bool(true))
831  , controller_lock_(cfg["controller_lock"].to_bool(parent_.force_lock_settings_) && parent_.params_.use_map_settings)
832  , index_(index)
833  , team_(0)
834  , color_(std::min(index, gamemap::MAX_PLAYERS - 1))
835  , gold_(cfg["gold"].to_int(100))
836  , income_(cfg["income"].to_int())
837  , reserved_for_(cfg["current_player"])
838  , player_id_()
839  , ai_algorithm_()
840  , chose_random_(cfg["chose_random"].to_bool(false))
841  , disallow_shuffle_(cfg["disallow_shuffle"].to_bool(false))
842  , flg_(parent_.era_factions_, cfg_, parent_.force_lock_settings_, parent_.params_.use_map_settings, parent_.params_.saved_game == saved_game_mode::type::midgame)
843  , 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))
844  , waiting_to_choose_faction_(allow_changes_)
845  , color_options_(game_config::default_colors)
846  //TODO: what should we do if color_ is out of range?
847  , color_id_(color_options_.at(color_))
848 {
849 
850  // Save default attributes that could be overwritten by the faction, so that correct faction lists would be
851  // initialized by flg_manager when the new side config is sent over network.
852  cfg_.clear_children("default_faction");
853  cfg_.add_child("default_faction", config {
854  "faction", cfg_["faction"],
855  "recruit", cfg_["recruit"],
856  });
857  if(auto p_cfg = cfg_.optional_child("leader")) {
858  cfg_.mandatory_child("default_faction").add_child("leader", config {
859  "type", (p_cfg)["type"],
860  "gender", (p_cfg)["gender"],
861  });
862  }
863 
864 
865  if(cfg_["side"].to_int(index_ + 1) != index_ + 1) {
866  ERR_CF << "found invalid side=" << cfg_["side"].to_int(index_ + 1) << " in definition of side number " << index_ + 1;
867  }
868 
869  cfg_["side"] = index_ + 1;
870 
871  if(cfg_["controller"] != side_controller::human && cfg_["controller"] != side_controller::ai && cfg_["controller"] != side_controller::none) {
872  //an invalid controller type was specified. Remove it to prevent asertion failures later.
873  cfg_.remove_attribute("controller");
874  }
875 
877 
878  // Tweak the controllers.
879  if(parent_.state_.classification().is_scenario() && cfg_["controller"].blank()) {
880  cfg_["controller"] = side_controller::ai;
881  }
882 
883  if(cfg_["controller"] == side_controller::none) {
885  } else if(cfg_["controller"] == side_controller::ai) {
887  } else if(parent_.default_controller_ == CNTR_NETWORK && !reserved_for_.empty()) {
888  // Reserve a side for "current_player", unless the side
889  // is played by an AI.
891  } else if(allow_player_) {
893  } else {
894  // AI is the default.
896  }
897 
898  // Initialize team and color.
899  unsigned team_name_index = 0;
901  if(data.team_name == cfg["team_name"]) {
902  break;
903  }
904 
905  ++team_name_index;
906  }
907 
908  if(team_name_index >= parent_.team_data_.size()) {
909  assert(!parent_.team_data_.empty());
910  team_ = 0;
911  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!";
912  } else {
913  team_ = team_name_index;
914  }
915 
916  // Check the value of the config's color= key.
917  const std::string given_color = team::get_side_color_id_from_config(cfg_);
918 
919  if(!given_color.empty()) {
920  // If it's valid, save the color...
921  color_id_ = given_color;
922 
923  // ... and find the appropriate index for it.
924  const auto iter = std::find(color_options_.begin(), color_options_.end(), color_id_);
925 
926  if(iter != color_options_.end()) {
927  color_ = std::distance(color_options_.begin(), iter);
928  } else {
929  color_options_.push_back(color_id_);
930  color_ = color_options_.size() - 1;
931  }
932  }
933 
934  // Initialize ai algorithm.
935  if(auto ai = cfg.optional_child("ai")) {
936  ai_algorithm_ = ai["ai_algorithm"].str();
937  }
938 }
939 
940 std::string side_engine::user_description() const
941 {
942  switch(controller_) {
943  case CNTR_LOCAL:
944  return N_("Anonymous player");
945  case CNTR_COMPUTER:
946  if(allow_player_) {
948  } else {
949  return N_("Computer Player");
950  }
951  default:
952  return "";
953  }
954 }
955 
957 {
958  config res = cfg_;
959 
960  // In case of 'shuffle sides' the side index in cfg_ might be wrong which will confuse the team constructor later.
961  res["side"] = index_ + 1;
962 
963  // If the user is allowed to change type, faction, leader etc, then import their new values in the config.
964  if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
965  // Merge the faction data to res.
966  config faction = flg_.current_faction();
967  LOG_MP << "side_engine::new_config: side=" << index_ + 1 << " faction=" << faction["id"] << " recruit=" << faction["recruit"];
968  res["faction_name"] = faction["name"];
969  res["faction"] = faction["id"];
970  faction.remove_attributes("id", "name", "image", "gender", "type", "description");
971  res.append(faction);
972  }
973 
974  res["controller"] = controller_names[controller_];
975 
976  // The hosts receives the serversided controller tweaks after the start event, but
977  // for mp sync it's very important that the controller types are correct
978  // during the start/prestart event (otherwise random unit creation during prestart fails).
979  res["is_local"] = player_id_ == prefs::get().login() || controller_ == CNTR_COMPUTER || controller_ == CNTR_LOCAL;
980 
981  // This function (new_config) is only meant to be called by the host's machine, which is why this check
982  // works. It essentially certifies that whatever side has the player_id that matches the host's login
983  // will be flagged. The reason we cannot check mp_game_metadata::is_host is because that flag is *always*
984  // true on the host's machine, meaning this flag would be set to true for every side.
985  res["is_host"] = player_id_ == prefs::get().login();
986 
987  std::string desc = user_description();
988  if(!desc.empty()) {
989  res["user_description"] = t_string(desc, "wesnoth");
990 
991  desc = VGETTEXT("$playername $side", {
992  {"playername", _(desc.c_str())},
993  {"side", res["side"].str()}
994  });
995  } else if(!player_id_.empty()) {
996  desc = player_id_;
997  }
998 
999  if(res["name"].str().empty() && !desc.empty()) {
1000  //TODO: maybe we should add this in to the leaders config instead of the side config?
1001  res["name"] = desc;
1002  }
1003 
1005  // If this is a saved game and "use_saved" (the default) was chosen for the
1006  // AI algorithm, we do nothing. Otherwise we add the chosen AI and if this
1007  // is a saved game, we also remove the old stages from the AI config.
1008  if(ai_algorithm_ != "use_saved") {
1009  if(parent_.params_.saved_game == saved_game_mode::type::midgame) {
1010  for (config &ai_config : res.child_range("ai")) {
1011  ai_config.clear_children("stage");
1012  }
1013  }
1014  res.add_child_at("ai", config {"ai_algorithm", ai_algorithm_}, 0);
1015  }
1016  }
1017 
1018  // A side's "current_player" is the player which has currently taken that side or the one for which it is reserved.
1019  // The "player_id" is the id of the client who controls that side. It's always the host for Local and AI players and
1020  // always empty for free/reserved sides or null controlled sides. You can use !res["player_id"].empty() to check
1021  // whether a side is already taken.
1022  assert(!prefs::get().login().empty());
1023  if(controller_ == CNTR_LOCAL) {
1024  res["player_id"] = prefs::get().login();
1025  res["current_player"] = prefs::get().login();
1026  } else if(controller_ == CNTR_RESERVED) {
1027  res.remove_attribute("player_id");
1028  res["current_player"] = reserved_for_;
1029  } else if(controller_ == CNTR_COMPUTER) {
1030  // TODO: what is the content of player_id_ here ?
1031  res["current_player"] = desc;
1032  res["player_id"] = prefs::get().login();
1033  } else if(!player_id_.empty()) {
1034  res["player_id"] = player_id_;
1035  res["current_player"] = player_id_;
1036  }
1037 
1038  res["allow_changes"] = allow_changes_;
1039  res["chose_random"] = chose_random_;
1040 
1041  if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
1042 
1043  if(!flg_.leader_lock()) {
1044  if(controller_ != CNTR_EMPTY) {
1045  auto& leader = res.child_or_add("leader");
1046  leader["type"] = flg_.current_leader();
1047  leader["gender"] = flg_.current_gender();
1048  LOG_MP << "side_engine::new_config: side=" << index_ + 1 << " type=" << leader["type"]
1049  << " gender=" << leader["gender"];
1050  } else if(!controller_lock_) {
1051  //if controller_lock_ == false and controller_ == CNTR_EMPTY, this means the user disalbles this side, so remove it's leader.
1052  res.remove_children("leader");
1053  }
1054  }
1055 
1056  const std::string& new_team_name = parent_.team_data_[team_].team_name;
1057 
1058  if(res["user_team_name"].empty() || !parent_.params_.use_map_settings || res["team_name"] != new_team_name) {
1059  res["team_name"] = new_team_name;
1060  res["user_team_name"] = parent_.team_data_[team_].user_team_name;
1061  }
1062 
1063  res["allow_player"] = allow_player_;
1064  res["color"] = color_id_;
1065  res["gold"] = gold_;
1066  res["income"] = income_;
1067  }
1068 
1069 
1070  if(parent_.params_.use_map_settings && parent_.params_.saved_game != saved_game_mode::type::midgame) {
1071  if(cfg_.has_attribute("name")){
1072  res["name"] = cfg_["name"];
1073  }
1074  if(cfg_.has_attribute("user_description") && controller_ == CNTR_COMPUTER){
1075  res["user_description"] = cfg_["user_description"];
1076  }
1077  }
1078 
1079  return res;
1080 }
1081 
1083 {
1084  if(!allow_player_) {
1085  // Sides without players are always ready.
1086  return true;
1087  }
1088 
1089  if((controller_ == CNTR_COMPUTER) ||
1090  (controller_ == CNTR_EMPTY) ||
1091  (controller_ == CNTR_LOCAL)) {
1092 
1093  return true;
1094  }
1095 
1096  if(available_for_user()) {
1097  // If controller_ == CNTR_NETWORK and player_id_.empty().
1098  return false;
1099  }
1100 
1101  if(controller_ == CNTR_NETWORK) {
1103  // The host is ready. A network player, who got a chance
1104  // to choose faction if allowed, is also ready.
1105  return true;
1106  }
1107  }
1108 
1109  return false;
1110 }
1111 
1112 bool side_engine::available_for_user(const std::string& name) const
1113 {
1114  if(controller_ == CNTR_NETWORK && player_id_.empty()) {
1115  // Side is free and waiting for user.
1116  return true;
1117  }
1118 
1119  if(controller_ == CNTR_RESERVED && name.empty()) {
1120  // Side is still available to someone.
1121  return true;
1122  }
1123 
1124  if(controller_ == CNTR_RESERVED && reserved_for_ == name) {
1125  // Side is available only for the player with specific name.
1126  return true;
1127  }
1128 
1129  return false;
1130 }
1131 
1132 void side_engine::resolve_random(randomness::mt_rng & rng, const std::vector<std::string> & avoid_faction_ids)
1133 {
1134  if(parent_.params_.saved_game == saved_game_mode::type::midgame) {
1135  return;
1136  }
1137 
1139 
1140  flg_.resolve_random(rng, avoid_faction_ids);
1141 
1142  LOG_MP << "side " << (index_ + 1) << ": faction=" <<
1143  (flg_.current_faction())["name"] << ", leader=" <<
1144  flg_.current_leader() << ", gender=" << flg_.current_gender();
1145 }
1146 
1148 {
1149  player_id_.clear();
1152 
1153  if(parent_.params_.saved_game != saved_game_mode::type::midgame) {
1155  }
1156 }
1157 
1158 void side_engine::place_user(const std::string& name)
1159 {
1160  config data;
1161  data["name"] = name;
1162 
1163  place_user(data);
1164 }
1165 
1166 void side_engine::place_user(const config& data, bool contains_selection)
1167 {
1168  player_id_ = data["name"].str();
1170 
1171  if(data["change_faction"].to_bool() && contains_selection) {
1172  // Network user's data carry information about chosen
1173  // faction, leader and genders.
1174  flg_.set_current_faction(data["faction"].str());
1175  flg_.set_current_leader(data["leader"].str());
1176  flg_.set_current_gender(data["gender"].str());
1177  }
1178 
1180 }
1181 
1183 {
1184  controller_options_.clear();
1185 
1186  // Default options.
1187  if(parent_.mp_metadata_) {
1188  add_controller_option(CNTR_NETWORK, _("Network Player"), side_controller::human);
1189  }
1190 
1191  add_controller_option(CNTR_LOCAL, _("Local Player"), side_controller::human);
1192  add_controller_option(CNTR_COMPUTER, _("Computer Player"), side_controller::ai);
1193  add_controller_option(CNTR_EMPTY, _("Nobody"), side_controller::none);
1194 
1195  if(!reserved_for_.empty()) {
1196  add_controller_option(CNTR_RESERVED, _("Reserved"), side_controller::human);
1197  }
1198 
1199  // Connected users.
1200  for(const std::string& user : parent_.connected_users()) {
1201  add_controller_option(parent_.default_controller_, user, side_controller::human);
1202  }
1203 
1205 }
1206 
1208 {
1209  int i = 0;
1210  for(const controller_option& option : controller_options_) {
1211  if(option.first == controller_) {
1213 
1214  if(player_id_.empty() || player_id_ == option.second) {
1215  // Stop searching if no user is assigned to a side
1216  // or the selected user is found.
1217  break;
1218  }
1219  }
1220 
1221  i++;
1222  }
1223 
1225 }
1226 
1227 bool side_engine::controller_changed(const int selection)
1228 {
1229  const ng::controller selected_cntr = controller_options_[selection].first;
1230 
1231  // Check if user was selected. If so assign a side to him/her.
1232  // If not, make sure that no user is assigned to this side.
1233  if(selected_cntr == parent_.default_controller_ && selection != 0) {
1234  player_id_ = controller_options_[selection].second;
1236  } else {
1237  player_id_.clear();
1238  }
1239 
1240  set_controller(selected_cntr);
1241 
1242  return true;
1243 }
1244 
1246 {
1248 
1250 }
1251 
1252 void side_engine::set_faction_commandline(const std::string& faction_name)
1253 {
1254  flg_.set_current_faction(faction_name);
1255 }
1256 
1258 {
1260 
1261  if(controller_name == side_controller::ai) {
1263  }
1264 
1265  if(controller_name == side_controller::none) {
1267  }
1268 
1269  player_id_.clear();
1270 }
1271 
1273  const std::string& name, const std::string& controller_value)
1274 {
1275  if(controller_lock_ && !cfg_["controller"].empty() && cfg_["controller"] != controller_value) {
1276  return;
1277  }
1278 
1279  controller_options_.emplace_back(controller, name);
1280 }
1281 
1282 } // 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:165
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:1030
#define N_(String)
Definition: gettext.hpp:105
static std::string _(const char *str)
Definition: gettext.hpp:97
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:188
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