The Battle for Wesnoth  1.19.10+dev
configuration.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2025
3  by Yurii Chernyi <terraninfo@terraninfo.net>
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 
16 
17 /**
18  * Managing the AI configuration
19  * @file
20  */
21 
22 #include "ai/configuration.hpp"
23 
24 #include "filesystem.hpp"
25 #include "log.hpp"
26 #include "serialization/parser.hpp"
28 #include "game_config_view.hpp"
29 #include "deprecation.hpp"
30 #include <vector>
31 #include <deque>
32 #include <set>
33 
34 namespace ai {
35 
37 #define DBG_AI_CONFIGURATION LOG_STREAM(debug, log_ai_configuration)
38 #define LOG_AI_CONFIGURATION LOG_STREAM(info, log_ai_configuration)
39 #define WRN_AI_CONFIGURATION LOG_STREAM(warn, log_ai_configuration)
40 #define ERR_AI_CONFIGURATION LOG_STREAM(err, log_ai_configuration)
41 
42 static lg::log_domain log_wml("wml");
43 #define ERR_WML LOG_STREAM(err, log_wml)
44 
46 {
47  ai_configurations_.clear();
48  era_ai_configurations_.clear();
49  mod_ai_configurations_.clear();
50 
51  const config& ais = game_config.mandatory_child("ais");
52  if (auto default_config = ais.optional_child("default_config")) {
53  default_config_ = *default_config;
54  } else {
55  ERR_AI_CONFIGURATION << "Missing AI [default_config]. Therefore, default_config_ set to empty.";
57  }
58  default_ai_algorithm_ = ais["default_ai_algorithm"].str();
59  if (default_ai_algorithm_.empty()) {
60  ERR_AI_CONFIGURATION << "Missing default_ai_algorithm. This will result in no AI being loaded by default.";
61  }
62 
63 
64  for (const config& ai_configuration : ais.child_range("ai")) {
65  const std::string& id = ai_configuration["id"];
66  if (id.empty()){
67 
68  ERR_AI_CONFIGURATION << "skipped AI config due to missing id" << ". Config contains:"<< std::endl << ai_configuration;
69  continue;
70  }
71  if (ai_configurations_.count(id)>0){
72  ERR_AI_CONFIGURATION << "skipped AI config due to duplicate id [" << id << "]. Config contains:"<< std::endl << ai_configuration;
73  continue;
74  }
75 
76  description desc;
77  desc.id=id;
78  desc.mp_rank=ai_configuration["mp_rank"].to_int(std::numeric_limits<int>::max());
79  desc.text = ai_configuration["description"].t_str();
80  desc.cfg=ai_configuration;
81 
82  ai_configurations_.emplace(id, desc);
83  LOG_AI_CONFIGURATION << "loaded AI config: " << ai_configuration["description"];
84  }
85 }
86 
87 namespace {
88 void extract_ai_configurations(std::map<std::string, description>& storage, const config& input)
89 {
90  for (const config& ai_configuration : input.child_range("ai")) {
91  const std::string& id = ai_configuration["id"];
92  if (id.empty()){
93 
94  ERR_AI_CONFIGURATION << "skipped AI config due to missing id" << ". Config contains:"<< std::endl << ai_configuration;
95  continue;
96  }
97  if (storage.count(id)>0){
98  ERR_AI_CONFIGURATION << "skipped AI config due to duplicate id [" << id << "]. Config contains:"<< std::endl << ai_configuration;
99  continue;
100  }
101 
102  description desc;
103  desc.id=id;
104  desc.text = ai_configuration["description"].t_str();
105  desc.mp_rank = ai_configuration["mp_rank"].to_int(std::numeric_limits<int>::max());
106  desc.cfg=ai_configuration;
107 
108  storage.emplace(id, desc);
109  LOG_AI_CONFIGURATION << "loaded AI config: " << ai_configuration["description"];
110  }
111 }
112 }
113 
115 {
116  era_ai_configurations_.clear();
117  extract_ai_configurations(era_ai_configurations_, era);
118 }
119 
121 {
122  mod_ai_configurations_.clear();
123  for (const config& mod : mods) {
124  extract_ai_configurations(mod_ai_configurations_, mod);
125  }
126 }
127 
128 std::vector<description*> configuration::get_available_ais()
129 {
130  std::vector<description*> ais_list;
131 
132  const auto add_if_not_hidden = [&ais_list](description* d) {
133  const config& cfg = d->cfg;
134 
135  if(!cfg["hidden"].to_bool(false)) {
136  ais_list.push_back(d);
137 
138  DBG_AI_CONFIGURATION << "has ai with config: " << std::endl << cfg;
139  }
140  };
141 
142  for(auto& a_config : ai_configurations_) {
143  add_if_not_hidden(&a_config.second);
144  }
145 
146  for(auto& e_config : era_ai_configurations_) {
147  add_if_not_hidden(&e_config.second);
148  }
149 
150  for(auto& m_config : mod_ai_configurations_) {
151  add_if_not_hidden(&m_config.second);
152  }
153 
154  // Sort by mp_rank. For same mp_rank, keep alphabetical order.
155  std::stable_sort(ais_list.begin(), ais_list.end(),
156  [](const description* a, const description* b) {
157  return a->mp_rank < b->mp_rank;
158  }
159  );
160 
161  return ais_list;
162 }
163 
164 const config& configuration::get_ai_config_for(const std::string& id)
165 {
167  if (cfg_it==ai_configurations_.end()){
169  if (era_cfg_it==era_ai_configurations_.end()){
171  if (mod_cfg_it==mod_ai_configurations_.end()) {
172  return default_config_;
173  } else {
174  return mod_cfg_it->second.cfg;
175  }
176  } else {
177  return era_cfg_it->second.cfg;
178  }
179  }
180  return cfg_it->second.cfg;
181 }
182 
183 bool configuration::get_side_config_from_file(const std::string& file, config& cfg)
184 {
185  try {
187  cfg = io::read(*stream);
188  LOG_AI_CONFIGURATION << "Reading AI configuration from file '" << file << "'";
189  } catch(const config::error&) {
190  ERR_AI_CONFIGURATION << "Error while reading AI configuration from file '" << file << "'";
191  return false;
192  } catch(const std::exception&) {
193  //value() now throws on invalid paths.
194  ERR_AI_CONFIGURATION << "Error while reading AI configuration from file '" << file << "'";
195  return false;
196  }
197  LOG_AI_CONFIGURATION << "Successfully read AI configuration from file '" << file << "'";
198  return true;
199 }
200 
202 {
203  return default_config_;
204 }
205 
206 
207 bool configuration::parse_side_config(side_number side, const config& original_cfg, config& cfg )
208 {
209  LOG_AI_CONFIGURATION << "side "<< side <<": parsing AI configuration from config";
210 
211  //leave only the [ai] children
212  cfg.clear();
213  for (const config& aiparam : original_cfg.child_range("ai")) {
214  cfg.add_child("ai",aiparam);
215  }
216 
217  //backward-compatibility hack: put ai_algorithm if it is present
218  if (const config::attribute_value *v = original_cfg.get("ai_algorithm")) {
219  config ai_a;
220  ai_a["ai_algorithm"] = *v;
221  cfg.add_child("ai",ai_a);
222  }
223  DBG_AI_CONFIGURATION << "side " << side << ": config contains:"<< std::endl << cfg;
224 
225  //insert default config at the beginning
226  if (!default_config_.empty()) {
227  DBG_AI_CONFIGURATION << "side "<< side <<": applying default configuration";
228  cfg.add_child_at("ai",default_config_,0);
229  } else {
230  ERR_AI_CONFIGURATION << "side "<< side <<": default configuration is not available, not applying it";
231  }
232 
233  LOG_AI_CONFIGURATION << "side "<< side << ": expanding simplified aspects into full facets";
234  expand_simplified_aspects(side, cfg);
235 
236  //construct new-style integrated config
237  LOG_AI_CONFIGURATION << "side "<< side << ": doing final operations on AI config";
238  config parsed_cfg = config();
239 
240  LOG_AI_CONFIGURATION << "side "<< side <<": merging AI configurations";
241  for (const config& aiparam : cfg.child_range("ai")) {
242  parsed_cfg.append(aiparam);
243  }
244 
245 
246  LOG_AI_CONFIGURATION << "side "<< side <<": merging AI aspect with the same id";
247  parsed_cfg.merge_children_by_attribute("aspect","id");
248 
249  LOG_AI_CONFIGURATION << "side "<< side <<": removing duplicate [default] tags from aspects";
250  for (config& aspect_cfg : parsed_cfg.child_range("aspect")) {
251  if (aspect_cfg["name"] != "composite_aspect") {
252  // No point in warning about Lua or standard aspects lacking [default]
253  continue;
254  }
255  if (!aspect_cfg.has_child("default")) {
256  WRN_AI_CONFIGURATION << "side "<< side <<": aspect with id=["<<aspect_cfg["id"]<<"] lacks default config facet!";
257  continue;
258  }
259  aspect_cfg.merge_children("default");
260  config& dflt = aspect_cfg.mandatory_child("default");
261  if (dflt.has_child("value")) {
262  while (dflt.child_count("value") > 1) {
263  dflt.remove_child("value", 0);
264  }
265  }
266  }
267 
268  DBG_AI_CONFIGURATION << "side "<< side <<": done parsing side config, it contains:"<< std::endl << parsed_cfg;
269  LOG_AI_CONFIGURATION << "side "<< side <<": done parsing side config";
270 
271  cfg = parsed_cfg;
272  return true;
273 
274 }
275 
276 static const std::set<std::string> non_aspect_attributes {"turns", "time_of_day", "engine", "ai_algorithm", "id", "description", "hidden", "mp_rank"};
277 static const std::set<std::string> just_copy_tags {"engine", "stage", "aspect", "goal", "modify_ai", "micro_ai"};
278 static const std::set<std::string> old_goal_tags {"target", "target_location", "protect_unit", "protect_location"};
279 
281  std::string algorithm;
282  config base_config, parsed_config;
283  for (const config& aiparam : cfg.child_range("ai")) {
284  std::string turns, time_of_day, engine = "cpp";
285  if (aiparam.has_attribute("turns")) {
286  turns = aiparam["turns"].str();
287  }
288  if (aiparam.has_attribute("time_of_day")) {
289  time_of_day = aiparam["time_of_day"].str();
290  }
291  if (aiparam.has_attribute("engine")) {
292  engine = aiparam["engine"].str();
293  if(engine == "fai") {
294  deprecated_message("FormulaAI", DEP_LEVEL::FOR_REMOVAL, "1.17", "FormulaAI is slated to be removed. Use equivalent Lua AIs instead");
295  }
296  }
297  if (aiparam.has_attribute("ai_algorithm")) {
298  if (algorithm.empty()) {
299  algorithm = aiparam["ai_algorithm"].str();
300  base_config = get_ai_config_for(algorithm);
301  } else if(algorithm != aiparam["ai_algorithm"]) {
302  lg::log_to_chat() << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.\n";
303  ERR_WML << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.";
304  }
305  }
306  std::deque<std::pair<std::string, config>> facet_configs;
307  for(const auto& [key, value] : aiparam.attribute_range()) {
308  if (non_aspect_attributes.count(key)) {
309  continue;
310  }
311  config facet_config;
312  facet_config["engine"] = engine;
313  facet_config["name"] = "standard_aspect";
314  facet_config["turns"] = turns;
315  facet_config["time_of_day"] = time_of_day;
316  facet_config["value"] = value;
317  facet_configs.emplace_back(key, facet_config);
318  }
319  for(const auto [child_key, child_cfg] : aiparam.all_children_view()) {
320  if (just_copy_tags.count(child_key)) {
321  // These aren't simplified, so just copy over unchanged.
322  parsed_config.add_child(child_key, child_cfg);
323  if(
324  (child_key != "modify_ai" && child_cfg["engine"] == "fai") ||
325  (child_key == "modify_ai" && child_cfg.all_children_count() > 0 && child_cfg.all_children_range().front().cfg["engine"] == "fai")
326  ) {
327  deprecated_message("FormulaAI", DEP_LEVEL::FOR_REMOVAL, "1.17", "FormulaAI is slated to be removed. Use equivalent Lua AIs instead");
328  }
329  continue;
330  } else if(old_goal_tags.count(child_key)) {
331  // A simplified goal, mainly kept around just for backwards compatibility.
332  config goal_config, criteria_config = child_cfg;
333  goal_config["name"] = child_key;
334  goal_config["turns"] = turns;
335  goal_config["time_of_day"] = time_of_day;
336  if(child_key.substr(0,7) == "protect" && criteria_config.has_attribute("protect_radius")) {
337  goal_config["protect_radius"] = criteria_config["protect_radius"];
338  criteria_config.remove_attribute("protect_radius");
339  }
340  if(criteria_config.has_attribute("value")) {
341  goal_config["value"] = criteria_config["value"];
342  criteria_config.remove_attribute("value");
343  }
344  goal_config.add_child("criteria", criteria_config);
345  parsed_config.add_child("goal", std::move(goal_config));
346  continue;
347  }
348  // Now there's two possibilities. If the tag is [attacks] or contains either value= or [value],
349  // then it can be copied verbatim as a [facet] tag.
350  // Otherwise, it needs to be placed as a [value] within a [facet] tag.
351  if (child_key == "attacks" || child_cfg.has_attribute("value") || child_cfg.has_child("value")) {
352  facet_configs.emplace_back(child_key, child_cfg);
353  } else {
354  config facet_config;
355  facet_config["engine"] = engine;
356  facet_config["name"] = "standard_aspect";
357  facet_config["turns"] = turns;
358  facet_config["time_of_day"] = time_of_day;
359  facet_config.add_child("value", child_cfg);
360  if (child_key == "leader_goal" && !child_cfg["id"].empty()) {
361  // Use id= attribute (if present) as the facet ID
362  const std::string& id = child_cfg["id"];
363  if(id != "*" && id.find_first_not_of("0123456789") != std::string::npos) {
364  facet_config["id"] = child_cfg["id"];
365  }
366  }
367  facet_configs.emplace_back(child_key, facet_config);
368  }
369  }
370  std::map<std::string, config> aspect_configs;
371  while (!facet_configs.empty()) {
372  const std::string& aspect = facet_configs.front().first;
373  const config& facet_config = facet_configs.front().second;
374  aspect_configs[aspect]["id"] = aspect; // Will sometimes be redundant assignment
375  aspect_configs[aspect]["name"] = "composite_aspect";
376  aspect_configs[aspect].add_child("facet", facet_config);
377  facet_configs.pop_front();
378  }
379  typedef std::map<std::string, config>::value_type aspect_pair;
380  for (const aspect_pair& p : aspect_configs) {
381  parsed_config.add_child("aspect", p.second);
382  }
383  }
384  // Support old recruitment aspect syntax
385  for(auto& child : parsed_config.child_range("aspect")) {
386  if(child["id"] == "recruitment") {
387  deprecated_message("AI recruitment aspect", DEP_LEVEL::INDEFINITE, "", "Use the recruitment_instructions aspect instead");
388  child["id"] = "recruitment_instructions";
389  }
390  }
391  if (algorithm.empty() && !parsed_config.has_child("stage")) {
393  }
394  for(const auto [child_key, child_cfg] : parsed_config.all_children_view()) {
395  base_config.add_child(child_key, child_cfg);
396  }
397  cfg.clear_children("ai");
398  cfg.add_child("ai", std::move(base_config));
399 }
400 
401 } //end of namespace ai
virtual bool add_child(const path_element &child, const config &cfg)
Definition: component.cpp:70
static description_map ai_configurations_
static config default_config_
static std::vector< description * > get_available_ais()
Returns a list of available AIs.
static description_map era_ai_configurations_
static bool get_side_config_from_file(const std::string &file, config &cfg)
get side config from file
static std::string default_ai_algorithm_
static void init(const game_config_view &game_config)
Init the parameters of ai configuration parser.
static const config & get_ai_config_for(const std::string &id)
Return the config for a specified ai.
static bool parse_side_config(side_number side, const config &original_cfg, config &cfg)
static void add_era_ai_from_config(const config &game_config)
static void expand_simplified_aspects(side_number side, config &cfg)
Expand simplified aspects, similar to the change from 1.7.2 to 1.7.3 but with some additional syntax ...
static const config & get_default_ai_parameters()
get default AI parameters
static description_map mod_ai_configurations_
static void add_mod_ai_from_config(const config::const_child_itors &configs)
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
void merge_children_by_attribute(config_key_type key, config_key_type attribute)
All children with the given key and with equal values of the specified attribute will be merged into ...
Definition: config.cpp:250
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:188
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
void remove_child(config_key_type key, std::size_t index)
Definition: config.cpp:639
std::size_t child_count(config_key_type key) const
Definition: config.cpp:292
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:796
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_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:312
bool has_attribute(config_key_type key) const
Definition: config.cpp:157
child_itors child_range(config_key_type key)
Definition: config.cpp:268
void remove_attribute(config_key_type key)
Definition: config.cpp:162
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:282
bool empty() const
Definition: config.cpp:845
void clear()
Definition: config.cpp:824
const attribute_value * get(config_key_type key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition: config.cpp:681
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 ...
#define DBG_AI_CONFIGURATION
#define ERR_AI_CONFIGURATION
#define LOG_AI_CONFIGURATION
#define ERR_WML
#define WRN_AI_CONFIGURATION
Managing the AIs configuration - headers.
std::string deprecated_message(const std::string &elem_name, DEP_LEVEL level, const version_info &version, const std::string &detail)
Definition: deprecation.cpp:29
Declarations for File-IO.
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
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
static const std::set< std::string > non_aspect_attributes
static const std::set< std::string > old_goal_tags
static lg::log_domain log_wml("wml")
int side_number
Definition: game_info.hpp:40
static const std::set< std::string > just_copy_tags
static lg::log_domain log_ai_configuration("ai/config")
utils::optional< std::string > get_wml_location(const std::string &path, const utils::optional< std::string > &current_dir)
Returns a translated path to the actual file or directory, if it exists.
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:53
Game configuration data as global variables.
Definition: build_info.cpp:61
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:627
std::stringstream & log_to_chat()
Use this to show WML errors in the ingame chat.
Definition: log.cpp:517
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map *defines)
Function to use the WML preprocessor on a file.
AI parameters.
std::string id
Object which defines a time of day with associated bonuses, image, sounds etc.
Definition: time_of_day.hpp:57
mock_party p
#define d
#define b