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