The Battle for Wesnoth  1.19.8+dev
configuration.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2024
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  try {
186  read(cfg, *stream);
187  LOG_AI_CONFIGURATION << "Reading AI configuration from file '" << file << "'";
188  } catch(const config::error&) {
189  ERR_AI_CONFIGURATION << "Error while reading AI configuration from file '" << file << "'";
190  return false;
191  } catch(const std::exception&) {
192  //value() now throws on invalid paths.
193  ERR_AI_CONFIGURATION << "Error while reading AI configuration from file '" << file << "'";
194  return false;
195  }
196  LOG_AI_CONFIGURATION << "Successfully read AI configuration from file '" << file << "'";
197  return true;
198 }
199 
201 {
202  return default_config_;
203 }
204 
205 
206 bool configuration::parse_side_config(side_number side, const config& original_cfg, config& cfg )
207 {
208  LOG_AI_CONFIGURATION << "side "<< side <<": parsing AI configuration from config";
209 
210  //leave only the [ai] children
211  cfg.clear();
212  for (const config& aiparam : original_cfg.child_range("ai")) {
213  cfg.add_child("ai",aiparam);
214  }
215 
216  //backward-compatibility hack: put ai_algorithm if it is present
217  if (const config::attribute_value *v = original_cfg.get("ai_algorithm")) {
218  config ai_a;
219  ai_a["ai_algorithm"] = *v;
220  cfg.add_child("ai",ai_a);
221  }
222  DBG_AI_CONFIGURATION << "side " << side << ": config contains:"<< std::endl << cfg;
223 
224  //insert default config at the beginning
225  if (!default_config_.empty()) {
226  DBG_AI_CONFIGURATION << "side "<< side <<": applying default configuration";
227  cfg.add_child_at("ai",default_config_,0);
228  } else {
229  ERR_AI_CONFIGURATION << "side "<< side <<": default configuration is not available, not applying it";
230  }
231 
232  LOG_AI_CONFIGURATION << "side "<< side << ": expanding simplified aspects into full facets";
233  expand_simplified_aspects(side, cfg);
234 
235  //construct new-style integrated config
236  LOG_AI_CONFIGURATION << "side "<< side << ": doing final operations on AI config";
237  config parsed_cfg = config();
238 
239  LOG_AI_CONFIGURATION << "side "<< side <<": merging AI configurations";
240  for (const config& aiparam : cfg.child_range("ai")) {
241  parsed_cfg.append(aiparam);
242  }
243 
244 
245  LOG_AI_CONFIGURATION << "side "<< side <<": merging AI aspect with the same id";
246  parsed_cfg.merge_children_by_attribute("aspect","id");
247 
248  LOG_AI_CONFIGURATION << "side "<< side <<": removing duplicate [default] tags from aspects";
249  for (config& aspect_cfg : parsed_cfg.child_range("aspect")) {
250  if (aspect_cfg["name"] != "composite_aspect") {
251  // No point in warning about Lua or standard aspects lacking [default]
252  continue;
253  }
254  if (!aspect_cfg.has_child("default")) {
255  WRN_AI_CONFIGURATION << "side "<< side <<": aspect with id=["<<aspect_cfg["id"]<<"] lacks default config facet!";
256  continue;
257  }
258  aspect_cfg.merge_children("default");
259  config& dflt = aspect_cfg.mandatory_child("default");
260  if (dflt.has_child("value")) {
261  while (dflt.child_count("value") > 1) {
262  dflt.remove_child("value", 0);
263  }
264  }
265  }
266 
267  DBG_AI_CONFIGURATION << "side "<< side <<": done parsing side config, it contains:"<< std::endl << parsed_cfg;
268  LOG_AI_CONFIGURATION << "side "<< side <<": done parsing side config";
269 
270  cfg = parsed_cfg;
271  return true;
272 
273 }
274 
275 static const std::set<std::string> non_aspect_attributes {"turns", "time_of_day", "engine", "ai_algorithm", "id", "description", "hidden", "mp_rank"};
276 static const std::set<std::string> just_copy_tags {"engine", "stage", "aspect", "goal", "modify_ai", "micro_ai"};
277 static const std::set<std::string> old_goal_tags {"target", "target_location", "protect_unit", "protect_location"};
278 
280  std::string algorithm;
281  config base_config, parsed_config;
282  for (const config& aiparam : cfg.child_range("ai")) {
283  std::string turns, time_of_day, engine = "cpp";
284  if (aiparam.has_attribute("turns")) {
285  turns = aiparam["turns"].str();
286  }
287  if (aiparam.has_attribute("time_of_day")) {
288  time_of_day = aiparam["time_of_day"].str();
289  }
290  if (aiparam.has_attribute("engine")) {
291  engine = aiparam["engine"].str();
292  if(engine == "fai") {
293  deprecated_message("FormulaAI", DEP_LEVEL::FOR_REMOVAL, "1.17", "FormulaAI is slated to be removed. Use equivalent Lua AIs instead");
294  }
295  }
296  if (aiparam.has_attribute("ai_algorithm")) {
297  if (algorithm.empty()) {
298  algorithm = aiparam["ai_algorithm"].str();
299  base_config = get_ai_config_for(algorithm);
300  } else if(algorithm != aiparam["ai_algorithm"]) {
301  lg::log_to_chat() << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.\n";
302  ERR_WML << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.";
303  }
304  }
305  std::deque<std::pair<std::string, config>> facet_configs;
306  for(const auto& [key, value] : aiparam.attribute_range()) {
307  if (non_aspect_attributes.count(key)) {
308  continue;
309  }
310  config facet_config;
311  facet_config["engine"] = engine;
312  facet_config["name"] = "standard_aspect";
313  facet_config["turns"] = turns;
314  facet_config["time_of_day"] = time_of_day;
315  facet_config["value"] = value;
316  facet_configs.emplace_back(key, facet_config);
317  }
318  for(const auto [child_key, child_cfg] : aiparam.all_children_view()) {
319  if (just_copy_tags.count(child_key)) {
320  // These aren't simplified, so just copy over unchanged.
321  parsed_config.add_child(child_key, child_cfg);
322  if(
323  (child_key != "modify_ai" && child_cfg["engine"] == "fai") ||
324  (child_key == "modify_ai" && child_cfg.all_children_count() > 0 && child_cfg.all_children_range().front().cfg["engine"] == "fai")
325  ) {
326  deprecated_message("FormulaAI", DEP_LEVEL::FOR_REMOVAL, "1.17", "FormulaAI is slated to be removed. Use equivalent Lua AIs instead");
327  }
328  continue;
329  } else if(old_goal_tags.count(child_key)) {
330  // A simplified goal, mainly kept around just for backwards compatibility.
331  config goal_config, criteria_config = child_cfg;
332  goal_config["name"] = child_key;
333  goal_config["turns"] = turns;
334  goal_config["time_of_day"] = time_of_day;
335  if(child_key.substr(0,7) == "protect" && criteria_config.has_attribute("protect_radius")) {
336  goal_config["protect_radius"] = criteria_config["protect_radius"];
337  criteria_config.remove_attribute("protect_radius");
338  }
339  if(criteria_config.has_attribute("value")) {
340  goal_config["value"] = criteria_config["value"];
341  criteria_config.remove_attribute("value");
342  }
343  goal_config.add_child("criteria", criteria_config);
344  parsed_config.add_child("goal", std::move(goal_config));
345  continue;
346  }
347  // Now there's two possibilities. If the tag is [attacks] or contains either value= or [value],
348  // then it can be copied verbatim as a [facet] tag.
349  // Otherwise, it needs to be placed as a [value] within a [facet] tag.
350  if (child_key == "attacks" || child_cfg.has_attribute("value") || child_cfg.has_child("value")) {
351  facet_configs.emplace_back(child_key, child_cfg);
352  } else {
353  config facet_config;
354  facet_config["engine"] = engine;
355  facet_config["name"] = "standard_aspect";
356  facet_config["turns"] = turns;
357  facet_config["time_of_day"] = time_of_day;
358  facet_config.add_child("value", child_cfg);
359  if (child_key == "leader_goal" && !child_cfg["id"].empty()) {
360  // Use id= attribute (if present) as the facet ID
361  const std::string& id = child_cfg["id"];
362  if(id != "*" && id.find_first_not_of("0123456789") != std::string::npos) {
363  facet_config["id"] = child_cfg["id"];
364  }
365  }
366  facet_configs.emplace_back(child_key, facet_config);
367  }
368  }
369  std::map<std::string, config> aspect_configs;
370  while (!facet_configs.empty()) {
371  const std::string& aspect = facet_configs.front().first;
372  const config& facet_config = facet_configs.front().second;
373  aspect_configs[aspect]["id"] = aspect; // Will sometimes be redundant assignment
374  aspect_configs[aspect]["name"] = "composite_aspect";
375  aspect_configs[aspect].add_child("facet", facet_config);
376  facet_configs.pop_front();
377  }
378  typedef std::map<std::string, config>::value_type aspect_pair;
379  for (const aspect_pair& p : aspect_configs) {
380  parsed_config.add_child("aspect", p.second);
381  }
382  }
383  // Support old recruitment aspect syntax
384  for(auto& child : parsed_config.child_range("aspect")) {
385  if(child["id"] == "recruitment") {
386  deprecated_message("AI recruitment aspect", DEP_LEVEL::INDEFINITE, "", "Use the recruitment_instructions aspect instead");
387  child["id"] = "recruitment_instructions";
388  }
389  }
390  if (algorithm.empty() && !parsed_config.has_child("stage")) {
392  }
393  for(const auto [child_key, child_cfg] : parsed_config.all_children_view()) {
394  base_config.add_child(child_key, child_cfg);
395  }
396  cfg.clear_children("ai");
397  cfg.add_child("ai", std::move(base_config));
398 }
399 
400 } //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
std::stringstream & log_to_chat()
Use this to show WML errors in the ingame chat.
Definition: log.cpp:520
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.
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:629
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