The Battle for Wesnoth  1.19.14+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() && !utils::contains(faction_choices, faction_id)) {
194  continue;
195  }
196 
197  if(!faction_excepts.empty() && utils::contains(faction_excepts, faction_id)) {
198  continue;
199  }
200 
201  // This side is consistent with this random faction, remember as a fallback.
202  fallback_nonrandom_sides.push_back(i);
203 
204  if(!avoid.empty() && utils::contains(avoid, faction_id)) {
205  continue;
206  }
207 
208  // This side is consistent with this random faction, and the avoid factions argument.
209  nonrandom_sides.push_back(i);
210  }
211 
212  if(nonrandom_sides.empty()) {
213  // There was no way to succeed consistently with the avoid factions argument, so ignore it as a fallback.
214  nonrandom_sides = fallback_nonrandom_sides;
215  }
216 
217  if(nonrandom_sides.empty()) {
218  throw config::error(_("Only random sides in the current era."));
219  }
220 
221  const int faction_index = nonrandom_sides[rng.get_next_random() % nonrandom_sides.size()];
223 
226  }
227 
228  if(current_leader_ == "random") {
229  std::vector<std::string> nonrandom_leaders = utils::split((*current_faction_)["random_leader"]);
230  if(nonrandom_leaders.empty()) {
231  for(const std::string& leader : available_leaders_) {
232  if(leader != "random") {
233  nonrandom_leaders.push_back(leader);
234  }
235  }
236  }
237 
238  if(nonrandom_leaders.empty()) {
239  throw config::error(VGETTEXT(
240  "Unable to find a leader type for faction $faction", {{"faction", (*current_faction_)["name"].str()}}));
241  } else {
242  const int lchoice = rng.get_next_random() % nonrandom_leaders.size();
243  current_leader_ = nonrandom_leaders[lchoice];
244 
247  }
248  }
249 
250  // Resolve random genders "very much" like standard unit code.
251  if(current_gender_ == "random") {
253  std::vector<std::string> nonrandom_genders;
254  for(const std::string& gender : available_genders_) {
255  if(gender != "random") {
256  nonrandom_genders.push_back(gender);
257  }
258  }
259 
260  const int gchoice = rng.get_next_random() % nonrandom_genders.size();
261  current_gender_ = nonrandom_genders[gchoice];
262  } else {
263  throw config::error(VGETTEXT("Cannot obtain genders for invalid leader $leader", {{"leader", current_leader_}}));
264  }
265  }
266 }
267 
269 {
270  const config* custom_faction = nullptr;
271  const bool show_custom_faction = original_faction_ == "Custom" || !has_no_recruits_ || faction_lock_;
272 
273  for(const config* faction : era_factions_) {
274  if((*faction)["id"] == "Custom" && !show_custom_faction) {
275 
276  // "Custom" faction should not be available if both
277  // "recruit" and "previous_recruits" lists are empty.
278  // However, it should be available if it was explicitly stated so.
279  custom_faction = faction;
280  continue;
281  }
282 
283  // Add default faction to the top of the list.
284  if(original_faction_ == (*faction)["id"]) {
285  available_factions_.insert(available_factions_.begin(), faction);
286  } else {
287  available_factions_.push_back(faction);
288  }
289  }
290 
291  if(available_factions_.empty() && custom_faction) {
292  available_factions_.push_back(custom_faction);
293  }
294 
295  assert(!available_factions_.empty());
296 
298 }
299 
301 {
302  available_leaders_.clear();
303 
304  if(!default_leader_type_.empty() || !leader_lock_) {
305 
306  int random_pos = 0;
307  // Add a default leader if there is one.
308  if(!default_leader_type_.empty()) {
310  random_pos = 1;
311  }
312 
313  if(!saved_game_ && !is_random_faction()) {
314  if((*current_faction_)["id"] == "Custom") {
315  // Allow user to choose a leader from any faction.
316  for(const config* f : available_factions_) {
317  if((*f)["id"] != "Random") {
319  }
320  }
321  } else {
323  }
324 
325  // Remove duplicate leaders.
326  std::set<std::string> seen;
327  utils::erase_if(available_leaders_, [&seen](const std::string& s) { return !seen.insert(s).second; });
328 
329  if(available_leaders_.size() > 1) {
330  available_leaders_.insert(available_leaders_.begin() + random_pos, "random");
331  }
332  }
333  }
334 
335  // If none of the possible leaders could be determined,
336  // use "null" as an indicator for empty leaders list.
337  if(available_leaders_.empty()) {
338  available_leaders_.push_back("null");
339  }
340 
342 }
343 
345 {
346  available_genders_.clear();
347 
348  if(saved_game_) {
349  if(!default_leader_gender_.empty()) {
351  }
352  } else {
354  if(unit->genders().size() > 1 && !leader_lock_) {
355  available_genders_.push_back("random");
356  }
357 
358  for(unit_race::GENDER gender : unit->genders()) {
359  const std::string gender_str = gender == unit_race::FEMALE
362 
363  // Add default gender to the top of the list.
364  if(default_leader_gender_ == gender_str) {
365  available_genders_.insert(available_genders_.begin(), gender_str);
366  } else {
367  available_genders_.push_back(gender_str);
368  }
369  }
370  }
371  }
372 
373  // If none of the possible genders could be determined,
374  // use "null" as an indicator for empty genders list.
375  if(available_genders_.empty()) {
376  available_genders_.push_back("null");
377  }
378 
380 }
381 
383 {
385 
386  if(faction_lock_) {;
387  const int faction_index = find_suitable_faction();
388  if(faction_index >= 0) {
389  const config* faction = choosable_factions_[faction_index];
390  choosable_factions_.clear();
391  choosable_factions_.push_back(faction);
392  }
393  }
394 }
395 
397 {
399 
400  if(!default_leader_type_.empty() && leader_lock_) {
402  choosable_leaders_.clear();
404  }
405  }
406 
407  // Sort alphabetically, but with the 'random' option always first
408  std::sort(choosable_leaders_.begin() + 1, choosable_leaders_.end(), [](const std::string& str1, const std::string& str2) {
409  return str1 < str2;
410  });
411 }
412 
414 {
416 
417  if(leader_lock_) {
418  std::string default_gender = default_leader_gender_;
419  if(default_gender.empty()) {
420  default_gender = choosable_genders_.front();
421  }
422 
423  if(utils::contains(available_genders_, default_gender)) {
424  choosable_genders_.clear();
425  choosable_genders_.push_back(default_gender);
426  }
427  }
428 }
429 
431 {
432  const std::string& default_faction = original_faction_;
433  auto default_faction_it = utils::ranges::find(choosable_factions_, default_faction,
434  [](const config* faction) { return (*faction)["id"]; });
435 
436  if(default_faction_it != choosable_factions_.end()) {
437  set_current_faction(std::distance(choosable_factions_.begin(), default_faction_it));
438  } else {
440  }
441 }
442 
444 {
445  std::vector<std::string> find;
446  std::string search_field;
447 
448  if(!original_faction_.empty()) {
449  // Choose based on faction.
450  find.push_back(original_faction_);
451  search_field = "id";
452  } else if(faction_from_recruit_) {
453  // Choose based on recruit.
455  search_field = "recruit";
456  } else {
457  find.push_back("Custom");
458  search_field = "id";
459  }
460 
461  int res = -1, index = 0, best_score = 0;
462  for(const config* faction : choosable_factions_) {
463  int faction_score = 0;
464  for(const std::string& search : find) {
465  for(const std::string& r : utils::split((*faction)[search_field])) {
466  if(r == search) {
467  ++faction_score;
468  break;
469  }
470  }
471  }
472 
473  if(faction_score > best_score) {
474  best_score = faction_score;
475  res = index;
476  }
477 
478  ++index;
479  }
480 
481  return res;
482 }
483 
485 {
486  assert(current_faction_);
487 
489 }
490 
492 {
493  std::vector<std::string> leaders_to_append = utils::split((*faction)["leader"]);
494 
495  available_leaders_.insert(available_leaders_.end(), leaders_to_append.begin(),
496  leaders_to_append.end());
497 }
498 
499 int flg_manager::faction_index(const config& faction) const
500 {
501  const auto it = std::find(choosable_factions_.begin(), choosable_factions_.end(), &faction);
502 
503  assert(it != choosable_factions_.end());
504  return std::distance(choosable_factions_.begin(), it);
505 }
506 
507 int flg_manager::leader_index(const std::string& leader) const
508 {
509  const auto it = std::find(choosable_leaders_.begin(), choosable_leaders_.end(), leader);
510 
511  return it != choosable_leaders_.end() ? std::distance(choosable_leaders_.begin(), it) : -1;
512 }
513 
514 int flg_manager::gender_index(const std::string& gender) const
515 {
516  const auto it = std::find(choosable_genders_.begin(), choosable_genders_.end(), gender);
517 
518  return it != choosable_genders_.end() ? std::distance(choosable_genders_.begin(), it) : -1;
519 }
520 
521 void flg_manager::set_current_leader(const std::string& leader)
522 {
523  int index = leader_index(leader);
524  if(index < 0) {
525  ERR_MP << "Leader '" << leader << "' is not available for side " << side_num_ << " Ignoring";
526  } else {
528  }
529 }
530 
531 void flg_manager::set_current_gender(const std::string& gender)
532 {
533  int index = gender_index(gender);
534  if(index < 0) {
535  ERR_MP << "Gender '" << gender << "' is not available for side " << side_num_ << " Ignoring";
536  } else {
538  }
539 }
540 
542 {
543  if(auto df = cfg.optional_child("default_faction")) {
544  return *df;
545  } else {
546  return cfg;
547  }
548 }
549 
550 } // 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:132
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
auto find(Container &container, const Value &value, const Projection &projection={})
Definition: general.hpp:179
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition: general.hpp:87
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition: general.hpp:107
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:141
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