The Battle for Wesnoth  1.19.13+dev
flg_manager.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 "config.hpp"
19 #include "formula/string_utils.hpp"
20 #include "gettext.hpp"
21 #include "log.hpp"
22 #include "mt_rng.hpp"
23 #include "units/types.hpp"
24 #include "utils/general.hpp"
25 
26 #include <algorithm>
27 
28 static lg::log_domain log_mp_connect_engine("mp/connect/engine");
29 #define LOG_MP LOG_STREAM(info, log_mp_connect_engine)
30 #define ERR_MP LOG_STREAM(err, log_mp_connect_engine)
31 
32 namespace ng
33 {
35  : faction_sort_order(sort_order::get_enum(cfg["auto_sort"].str()).value_or(sort_order::type::ascending))
36 {
37 }
38 
39 flg_manager::flg_manager(const era_metadata& era_info, const std::vector<const config*>& era_factions,
40  const config& side, const bool lock_settings, const bool use_map_settings, const bool saved_game)
41  : era_info_(era_info)
42  , era_factions_(era_factions)
43  , side_num_(side["side"].to_int())
44  , faction_from_recruit_(side["faction_from_recruit"].to_bool())
45  , original_faction_(get_default_faction(side)["faction"].str())
46  , original_recruit_(utils::split(get_default_faction(side)["recruit"].str()))
47  , saved_game_(saved_game)
48  , has_no_recruits_(original_recruit_.empty() && side["previous_recruits"].empty())
49  , faction_lock_(side["faction_lock"].to_bool(lock_settings))
50  , leader_lock_(side["leader_lock"].to_bool(lock_settings))
51  , available_factions_()
52  , available_leaders_()
53  , available_genders_()
54  , choosable_factions_()
55  , choosable_leaders_()
56  , choosable_genders_()
57  , current_faction_(nullptr)
58  , current_leader_("null")
59  , current_gender_("null")
60  , default_leader_type_("")
61  , default_leader_gender_("")
62 {
63  std::string leader_id = side["id"];
64  bool found_leader;
65 
66  leader_lock_ = leader_lock_ && (use_map_settings || lock_settings || default_leader_type_.empty());
67  faction_lock_ = faction_lock_ && (use_map_settings || lock_settings);
68 
69  auto set_leader = [&](const config& cfg) {
70  found_leader = true;
71  leader_id = cfg["id"];
72  default_leader_type_ = cfg["type"];
73  default_leader_gender_ = cfg["gender"];
74  };
75 
76  if(auto p_cfg = side.optional_child("leader")) {
77  set_leader(*p_cfg);
78  // If we are not the host of the game it can happen that the code overwrote
79  // the [leaders] type/gender and the original values are found in [default_faction]
80  // we still need the id from p_cfg tho.
81  if(auto p_ocfg = get_default_faction(side).optional_child("leader")) {
82  default_leader_type_ = (*p_ocfg)["type"];
83  default_leader_gender_ = (*p_ocfg)["gender"];
84  }
85  }
86 
87  if(!leader_id.empty()) {
88  // Check if leader was carried over and now is in [unit] tag.
89  // (in this case we dont allow changing it, but we still want to show the corect unit type in the dialogs)
90  if(auto p_cfg = side.find_child("unit", "id", leader_id)) {
91  set_leader(*p_cfg);
92  leader_lock_ = true;
93  }
94  }
95 
96  if(!found_leader) {
97  // Find a unit which can recruit.
98  if(auto p_cfg = side.find_child("unit", "canrecruit", "yes")) {
99  set_leader(*p_cfg);
100  leader_lock_ = true;
101  }
102  }
103 
104 
105  if(!default_leader_type_.empty() && default_leader_type_ != "random") {
106  if(unit_types.find(default_leader_type_) == nullptr) {
107  default_leader_type_.clear();
108  default_leader_gender_.clear();
109  }
110  }
111 
113 
115 
116 }
117 
119 {
120  assert(index < choosable_factions_.size());
122 
125 }
126 
127 void flg_manager::set_current_faction(const std::string& id)
128 {
129  unsigned index = 0;
130  for(const config* faction : choosable_factions_) {
131  if((*faction)["id"] == id) {
133  return;
134  }
135  index++;
136  }
137 
138  ERR_MP << "Faction '" << id << "' is not available for side " << side_num_ << " Ignoring";
139 }
140 
142 {
143  assert(index < choosable_leaders_.size());
145 
148 }
149 
151 {
152  assert(index < choosable_genders_.size());
154 }
155 
157 {
158  return (*current_faction_)["random_faction"].to_bool();
159 }
160 
161 // When we use a random mode like "no mirror", "no ally mirror", the list of faction ids to avoid is passed
162 // as an argument. It may be that for some scenario configuration, a strict no mirror assignment is not possible,
163 // because there are too many sides, or some users have forced their faction choices to be matching, etc.
164 // In that case we gracefully continue by ignoring the no mirror rule and assigning as we would have if it were off.
165 // If there is still no options we throw a config error because it means the era is misconfigured.
166 void flg_manager::resolve_random(randomness::mt_rng& rng, const std::vector<std::string>& avoid)
167 {
168  if(is_random_faction()) {
169  std::vector<std::string> faction_choices, faction_excepts;
170 
171  faction_choices = utils::split((*current_faction_)["choices"]);
172  if(faction_choices.size() == 1 && faction_choices.front().empty()) {
173  faction_choices.clear();
174  }
175 
176  faction_excepts = utils::split((*current_faction_)["except"]);
177  if(faction_excepts.size() == 1 && faction_excepts.front().empty()) {
178  faction_excepts.clear();
179  }
180 
181  // Builds the list of factions eligible for choice (non-random factions).
182  std::vector<int> nonrandom_sides;
183  std::vector<int> fallback_nonrandom_sides;
184  for(unsigned int i = 0; i < available_factions_.size(); ++i) {
185  const config& faction = *available_factions_[i];
186 
187  if(faction["random_faction"].to_bool()) {
188  continue;
189  }
190 
191  const std::string& faction_id = faction["id"];
192 
193  if(!faction_choices.empty() && std::find(faction_choices.begin(), faction_choices.end(),
194  faction_id) == faction_choices.end()) {
195  continue;
196  }
197 
198  if(!faction_excepts.empty() && std::find(faction_excepts.begin(), faction_excepts.end(),
199  faction_id) != faction_excepts.end()) {
200  continue;
201  }
202 
203  // This side is consistent with this random faction, remember as a fallback.
204  fallback_nonrandom_sides.push_back(i);
205 
206  if(!avoid.empty() && std::find(avoid.begin(), avoid.end(),
207  faction_id) != avoid.end()) {
208  continue;
209  }
210 
211  // This side is consistent with this random faction, and the avoid factions argument.
212  nonrandom_sides.push_back(i);
213  }
214 
215  if(nonrandom_sides.empty()) {
216  // There was no way to succeed consistently with the avoid factions argument, so ignore it as a fallback.
217  nonrandom_sides = fallback_nonrandom_sides;
218  }
219 
220  if(nonrandom_sides.empty()) {
221  throw config::error(_("Only random sides in the current era."));
222  }
223 
224  const int faction_index = nonrandom_sides[rng.get_next_random() % nonrandom_sides.size()];
226 
229  }
230 
231  if(current_leader_ == "random") {
232  std::vector<std::string> nonrandom_leaders = utils::split((*current_faction_)["random_leader"]);
233  if(nonrandom_leaders.empty()) {
234  for(const std::string& leader : available_leaders_) {
235  if(leader != "random") {
236  nonrandom_leaders.push_back(leader);
237  }
238  }
239  }
240 
241  if(nonrandom_leaders.empty()) {
242  throw config::error(VGETTEXT(
243  "Unable to find a leader type for faction $faction", {{"faction", (*current_faction_)["name"].str()}}));
244  } else {
245  const int lchoice = rng.get_next_random() % nonrandom_leaders.size();
246  current_leader_ = nonrandom_leaders[lchoice];
247 
250  }
251  }
252 
253  // Resolve random genders "very much" like standard unit code.
254  if(current_gender_ == "random") {
256  std::vector<std::string> nonrandom_genders;
257  for(const std::string& gender : available_genders_) {
258  if(gender != "random") {
259  nonrandom_genders.push_back(gender);
260  }
261  }
262 
263  const int gchoice = rng.get_next_random() % nonrandom_genders.size();
264  current_gender_ = nonrandom_genders[gchoice];
265  } else {
266  throw config::error(VGETTEXT("Cannot obtain genders for invalid leader $leader", {{"leader", current_leader_}}));
267  }
268  }
269 }
270 
272 {
273  const config* custom_faction = nullptr;
274  const bool show_custom_faction = original_faction_ == "Custom" || !has_no_recruits_ || faction_lock_;
275 
276  for(const config* faction : era_factions_) {
277  if((*faction)["id"] == "Custom" && !show_custom_faction) {
278 
279  // "Custom" faction should not be available if both
280  // "recruit" and "previous_recruits" lists are empty.
281  // However, it should be available if it was explicitly stated so.
282  custom_faction = faction;
283  continue;
284  }
285 
286  // Add default faction to the top of the list.
287  if(original_faction_ == (*faction)["id"]) {
288  available_factions_.insert(available_factions_.begin(), faction);
289  } else {
290  available_factions_.push_back(faction);
291  }
292  }
293 
294  if(available_factions_.empty() && custom_faction) {
295  available_factions_.push_back(custom_faction);
296  }
297 
298  assert(!available_factions_.empty());
299 
301 }
302 
304 {
305  available_leaders_.clear();
306 
307  if(!default_leader_type_.empty() || !leader_lock_) {
308 
309  int random_pos = 0;
310  // Add a default leader if there is one.
311  if(!default_leader_type_.empty()) {
313  random_pos = 1;
314  }
315 
316  if(!saved_game_ && !is_random_faction()) {
317  if((*current_faction_)["id"] == "Custom") {
318  // Allow user to choose a leader from any faction.
319  for(const config* f : available_factions_) {
320  if((*f)["id"] != "Random") {
322  }
323  }
324  } else {
326  }
327 
328  // Remove duplicate leaders.
329  std::set<std::string> seen;
330  utils::erase_if(available_leaders_, [&seen](const std::string& s) { return !seen.insert(s).second; });
331 
332  if(available_leaders_.size() > 1) {
333  available_leaders_.insert(available_leaders_.begin() + random_pos, "random");
334  }
335  }
336  }
337 
338  // If none of the possible leaders could be determined,
339  // use "null" as an indicator for empty leaders list.
340  if(available_leaders_.empty()) {
341  available_leaders_.push_back("null");
342  }
343 
345 }
346 
348 {
349  available_genders_.clear();
350 
351  if(saved_game_) {
352  if(!default_leader_gender_.empty()) {
354  }
355  } else {
357  if(unit->genders().size() > 1 && !leader_lock_) {
358  available_genders_.push_back("random");
359  }
360 
361  for(unit_race::GENDER gender : unit->genders()) {
362  const std::string gender_str = gender == unit_race::FEMALE
365 
366  // Add default gender to the top of the list.
367  if(default_leader_gender_ == gender_str) {
368  available_genders_.insert(available_genders_.begin(), gender_str);
369  } else {
370  available_genders_.push_back(gender_str);
371  }
372  }
373  }
374  }
375 
376  // If none of the possible genders could be determined,
377  // use "null" as an indicator for empty genders list.
378  if(available_genders_.empty()) {
379  available_genders_.push_back("null");
380  }
381 
383 }
384 
386 {
388 
389  if(faction_lock_) {;
390  const int faction_index = find_suitable_faction();
391  if(faction_index >= 0) {
392  const config* faction = choosable_factions_[faction_index];
393  choosable_factions_.clear();
394  choosable_factions_.push_back(faction);
395  }
396  }
397 }
398 
400 {
402 
403  if(!default_leader_type_.empty() && leader_lock_) {
406 
407  choosable_leaders_.clear();
409  }
410  }
411 
412  // Sort alphabetically, but with the 'random' option always first
413  std::sort(choosable_leaders_.begin() + 1, choosable_leaders_.end(), [](const std::string& str1, const std::string& str2) {
414  return str1 < str2;
415  });
416 }
417 
419 {
421 
422  if(leader_lock_) {
423  std::string default_gender = default_leader_gender_;
424  if(default_gender.empty()) {
425  default_gender = choosable_genders_.front();
426  }
427 
428  if(std::find(available_genders_.begin(), available_genders_.end(), default_gender) != available_genders_.end()) {
429  choosable_genders_.clear();
430  choosable_genders_.push_back(default_gender);
431  }
432  }
433 }
434 
436 {
437  const std::string& default_faction = original_faction_;
438  auto default_faction_it = std::find_if(choosable_factions_.begin(), choosable_factions_.end(),
439  [&default_faction](const config* faction) {
440  return (*faction)["id"] == default_faction;
441  });
442 
443  if(default_faction_it != choosable_factions_.end()) {
444  set_current_faction(std::distance(choosable_factions_.begin(), default_faction_it));
445  } else {
447  }
448 }
449 
451 {
452  std::vector<std::string> find;
453  std::string search_field;
454 
455  if(!original_faction_.empty()) {
456  // Choose based on faction.
457  find.push_back(original_faction_);
458  search_field = "id";
459  } else if(faction_from_recruit_) {
460  // Choose based on recruit.
462  search_field = "recruit";
463  } else {
464  find.push_back("Custom");
465  search_field = "id";
466  }
467 
468  int res = -1, index = 0, best_score = 0;
469  for(const config* faction : choosable_factions_) {
470  int faction_score = 0;
471  for(const std::string& search : find) {
472  for(const std::string& r : utils::split((*faction)[search_field])) {
473  if(r == search) {
474  ++faction_score;
475  break;
476  }
477  }
478  }
479 
480  if(faction_score > best_score) {
481  best_score = faction_score;
482  res = index;
483  }
484 
485  ++index;
486  }
487 
488  return res;
489 }
490 
492 {
493  assert(current_faction_);
494 
496 }
497 
499 {
500  std::vector<std::string> leaders_to_append = utils::split((*faction)["leader"]);
501 
502  available_leaders_.insert(available_leaders_.end(), leaders_to_append.begin(),
503  leaders_to_append.end());
504 }
505 
506 int flg_manager::faction_index(const config& faction) const
507 {
508  const auto it = std::find(choosable_factions_.begin(), choosable_factions_.end(), &faction);
509 
510  assert(it != choosable_factions_.end());
511  return std::distance(choosable_factions_.begin(), it);
512 }
513 
514 int flg_manager::leader_index(const std::string& leader) const
515 {
516  const auto it = std::find(choosable_leaders_.begin(), choosable_leaders_.end(), leader);
517 
518  return it != choosable_leaders_.end() ? std::distance(choosable_leaders_.begin(), it) : -1;
519 }
520 
521 int flg_manager::gender_index(const std::string& gender) const
522 {
523  const auto it = std::find(choosable_genders_.begin(), choosable_genders_.end(), gender);
524 
525  return it != choosable_genders_.end() ? std::distance(choosable_genders_.begin(), it) : -1;
526 }
527 
528 void flg_manager::set_current_leader(const std::string& leader)
529 {
530  int index = leader_index(leader);
531  if(index < 0) {
532  ERR_MP << "Leader '" << leader << "' is not available for side " << side_num_ << " Ignoring";
533  } else {
535  }
536 }
537 
538 void flg_manager::set_current_gender(const std::string& gender)
539 {
540  int index = gender_index(gender);
541  if(index < 0) {
542  ERR_MP << "Gender '" << gender << "' is not available for side " << side_num_ << " Ignoring";
543  } else {
545  }
546 }
547 
549 {
550  if(auto df = cfg.optional_child("default_faction")) {
551  return *df;
552  } else {
553  return cfg;
554  }
555 }
556 
557 } // end namespace ng
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
optional_config_impl< config > find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:780
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
void update_available_factions()
const bool faction_from_recruit_
void update_choosable_leaders()
void update_choosable_factions()
flg_manager(const era_metadata &era_info, const std::vector< const config * > &era_factions, const config &side, bool lock_settings, bool use_map_settings, bool saved_game)
Definition: flg_manager.cpp:39
std::string default_leader_type_
void update_available_leaders()
const std::vector< std::string > original_recruit_
std::string current_leader_
const bool has_no_recruits_
void update_available_genders()
void set_current_faction(const unsigned index)
void select_default_faction()
void resolve_random(randomness::mt_rng &rng, const std::vector< std::string > &avoid)
std::vector< std::string > choosable_leaders_
std::vector< std::string > choosable_genders_
std::vector< std::string > available_genders_
void update_choosable_genders()
const config * current_faction_
int faction_index(const config &faction) const
const std::string original_faction_
void set_current_leader(const unsigned index)
int leader_index(const std::string &leader) const
returns -1 if no leader with that name was found
int gender_index(const std::string &gender) const
returns -1 if no gender with that name was found
int current_faction_index() const
static const config & get_default_faction(const config &cfg)
const bool saved_game_
std::string default_leader_gender_
std::vector< const config * > available_factions_
std::vector< const config * > choosable_factions_
void set_current_gender(const unsigned index)
bool is_random_faction()
void append_leaders_from_faction(const config *faction)
const std::vector< const config * > & era_factions_
std::string current_gender_
int find_suitable_faction() const
const int side_num_
std::vector< std::string > available_leaders_
uint32_t get_next_random()
Get a new random number.
Definition: mt_rng.cpp:63
static const std::string s_female
Standard string id (not translatable) for FEMALE.
Definition: race.hpp:29
static const std::string s_male
Standard string id (not translatable) for MALE.
Definition: race.hpp:30
@ FEMALE
Definition: race.hpp:28
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition: types.cpp:1224
A single unit type that the player may recruit.
Definition: types.hpp:43
This class represents a single unit of a specific type.
Definition: unit.hpp:133
Definitions for the interface to Wesnoth Markup Language (WML).
const config * cfg
#define ERR_MP
Definition: flg_manager.cpp:30
static lg::log_domain log_mp_connect_engine("mp/connect/engine")
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
Standard logging facilities (interface).
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
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition: general.hpp:106
std::vector< std::string > split(const config_attribute_value &val)
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
era_metadata(const config &cfg)
Parses an [era] tag.
Definition: flg_manager.cpp:34
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static map_location::direction s
unit_type_data unit_types
Definition: types.cpp:1463
#define f