The Battle for Wesnoth  1.19.0-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  }
192  LOG_AI_CONFIGURATION << "Successfully read AI configuration from file '" << file << "'";
193  return true;
194 }
195 
197 {
198  return default_config_;
199 }
200 
201 
202 bool configuration::parse_side_config(side_number side, const config& original_cfg, config &cfg )
203 {
204  LOG_AI_CONFIGURATION << "side "<< side <<": parsing AI configuration from config";
205 
206  //leave only the [ai] children
207  cfg.clear();
208  for (const config &aiparam : original_cfg.child_range("ai")) {
209  cfg.add_child("ai",aiparam);
210  }
211 
212  //backward-compatibility hack: put ai_algorithm if it is present
213  if (const config::attribute_value *v = original_cfg.get("ai_algorithm")) {
214  config ai_a;
215  ai_a["ai_algorithm"] = *v;
216  cfg.add_child("ai",ai_a);
217  }
218  DBG_AI_CONFIGURATION << "side " << side << ": config contains:"<< std::endl << cfg;
219 
220  //insert default config at the beginning
221  if (!default_config_.empty()) {
222  DBG_AI_CONFIGURATION << "side "<< side <<": applying default configuration";
223  cfg.add_child_at("ai",default_config_,0);
224  } else {
225  ERR_AI_CONFIGURATION << "side "<< side <<": default configuration is not available, not applying it";
226  }
227 
228  LOG_AI_CONFIGURATION << "side "<< side << ": expanding simplified aspects into full facets";
229  expand_simplified_aspects(side, cfg);
230 
231  //construct new-style integrated config
232  LOG_AI_CONFIGURATION << "side "<< side << ": doing final operations on AI config";
233  config parsed_cfg = config();
234 
235  LOG_AI_CONFIGURATION << "side "<< side <<": merging AI configurations";
236  for (const config &aiparam : cfg.child_range("ai")) {
237  parsed_cfg.append(aiparam);
238  }
239 
240 
241  LOG_AI_CONFIGURATION << "side "<< side <<": merging AI aspect with the same id";
242  parsed_cfg.merge_children_by_attribute("aspect","id");
243 
244  LOG_AI_CONFIGURATION << "side "<< side <<": removing duplicate [default] tags from aspects";
245  for (config &aspect_cfg : parsed_cfg.child_range("aspect")) {
246  if (aspect_cfg["name"] != "composite_aspect") {
247  // No point in warning about Lua or standard aspects lacking [default]
248  continue;
249  }
250  if (!aspect_cfg.has_child("default")) {
251  WRN_AI_CONFIGURATION << "side "<< side <<": aspect with id=["<<aspect_cfg["id"]<<"] lacks default config facet!";
252  continue;
253  }
254  aspect_cfg.merge_children("default");
255  config& dflt = aspect_cfg.mandatory_child("default");
256  if (dflt.has_child("value")) {
257  while (dflt.child_count("value") > 1) {
258  dflt.remove_child("value", 0);
259  }
260  }
261  }
262 
263  DBG_AI_CONFIGURATION << "side "<< side <<": done parsing side config, it contains:"<< std::endl << parsed_cfg;
264  LOG_AI_CONFIGURATION << "side "<< side <<": done parsing side config";
265 
266  cfg = parsed_cfg;
267  return true;
268 
269 }
270 
271 static const std::set<std::string> non_aspect_attributes {"turns", "time_of_day", "engine", "ai_algorithm", "id", "description", "hidden", "mp_rank"};
272 static const std::set<std::string> just_copy_tags {"engine", "stage", "aspect", "goal", "modify_ai", "micro_ai"};
273 static const std::set<std::string> old_goal_tags {"target", "target_location", "protect_unit", "protect_location"};
274 
276  std::string algorithm;
277  config base_config, parsed_config;
278  for (const config &aiparam : cfg.child_range("ai")) {
279  std::string turns, time_of_day, engine = "cpp";
280  if (aiparam.has_attribute("turns")) {
281  turns = aiparam["turns"].str();
282  }
283  if (aiparam.has_attribute("time_of_day")) {
284  time_of_day = aiparam["time_of_day"].str();
285  }
286  if (aiparam.has_attribute("engine")) {
287  engine = aiparam["engine"].str();
288  if(engine == "fai") {
289  deprecated_message("FormulaAI", DEP_LEVEL::FOR_REMOVAL, "1.17", "FormulaAI is slated to be removed. Use equivalent Lua AIs instead");
290  }
291  }
292  if (aiparam.has_attribute("ai_algorithm")) {
293  if (algorithm.empty()) {
294  algorithm = aiparam["ai_algorithm"].str();
295  base_config = get_ai_config_for(algorithm);
296  } else if(algorithm != aiparam["ai_algorithm"]) {
297  lg::log_to_chat() << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.\n";
298  ERR_WML << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.";
299  }
300  }
301  std::deque<std::pair<std::string, config>> facet_configs;
302  for (const config::attribute &attr : aiparam.attribute_range()) {
303  if (non_aspect_attributes.count(attr.first)) {
304  continue;
305  }
306  config facet_config;
307  facet_config["engine"] = engine;
308  facet_config["name"] = "standard_aspect";
309  facet_config["turns"] = turns;
310  facet_config["time_of_day"] = time_of_day;
311  facet_config["value"] = attr.second;
312  facet_configs.emplace_back(attr.first, facet_config);
313  }
314  for (const config::any_child child : aiparam.all_children_range()) {
315  if (just_copy_tags.count(child.key)) {
316  // These aren't simplified, so just copy over unchanged.
317  parsed_config.add_child(child.key, child.cfg);
318  if(
319  (child.key != "modify_ai" && child.cfg["engine"] == "fai") ||
320  (child.key == "modify_ai" && child.cfg.all_children_count() > 0 && child.cfg.all_children_range().front().cfg["engine"] == "fai")
321  ) {
322  deprecated_message("FormulaAI", DEP_LEVEL::FOR_REMOVAL, "1.17", "FormulaAI is slated to be removed. Use equivalent Lua AIs instead");
323  }
324  continue;
325  } else if(old_goal_tags.count(child.key)) {
326  // A simplified goal, mainly kept around just for backwards compatibility.
327  config goal_config, criteria_config = child.cfg;
328  goal_config["name"] = child.key;
329  goal_config["turns"] = turns;
330  goal_config["time_of_day"] = time_of_day;
331  if(child.key.substr(0,7) == "protect" && criteria_config.has_attribute("protect_radius")) {
332  goal_config["protect_radius"] = criteria_config["protect_radius"];
333  criteria_config.remove_attribute("protect_radius");
334  }
335  if(criteria_config.has_attribute("value")) {
336  goal_config["value"] = criteria_config["value"];
337  criteria_config.remove_attribute("value");
338  }
339  goal_config.add_child("criteria", criteria_config);
340  parsed_config.add_child("goal", std::move(goal_config));
341  continue;
342  }
343  // Now there's two possibilities. If the tag is [attacks] or contains either value= or [value],
344  // then it can be copied verbatim as a [facet] tag.
345  // Otherwise, it needs to be placed as a [value] within a [facet] tag.
346  if (child.key == "attacks" || child.cfg.has_attribute("value") || child.cfg.has_child("value")) {
347  facet_configs.emplace_back(child.key, child.cfg);
348  } else {
349  config facet_config;
350  facet_config["engine"] = engine;
351  facet_config["name"] = "standard_aspect";
352  facet_config["turns"] = turns;
353  facet_config["time_of_day"] = time_of_day;
354  facet_config.add_child("value", child.cfg);
355  if (child.key == "leader_goal" && !child.cfg["id"].empty()) {
356  // Use id= attribute (if present) as the facet ID
357  const std::string& id = child.cfg["id"];
358  if(id != "*" && id.find_first_not_of("0123456789") != std::string::npos) {
359  facet_config["id"] = child.cfg["id"];
360  }
361  }
362  facet_configs.emplace_back(child.key, facet_config);
363  }
364  }
365  std::map<std::string, config> aspect_configs;
366  while (!facet_configs.empty()) {
367  const std::string &aspect = facet_configs.front().first;
368  const config &facet_config = facet_configs.front().second;
369  aspect_configs[aspect]["id"] = aspect; // Will sometimes be redundant assignment
370  aspect_configs[aspect]["name"] = "composite_aspect";
371  aspect_configs[aspect].add_child("facet", facet_config);
372  facet_configs.pop_front();
373  }
374  typedef std::map<std::string, config>::value_type aspect_pair;
375  for (const aspect_pair& p : aspect_configs) {
376  parsed_config.add_child("aspect", p.second);
377  }
378  }
379  // Support old recruitment aspect syntax
380  for(auto& child : parsed_config.child_range("aspect")) {
381  if(child["id"] == "recruitment") {
382  deprecated_message("AI recruitment aspect", DEP_LEVEL::INDEFINITE, "", "Use the recruitment_instructions aspect instead");
383  child["id"] = "recruitment_instructions";
384  }
385  }
386  if (algorithm.empty() && !parsed_config.has_child("stage")) {
388  }
389  for (const config::any_child child : parsed_config.all_children_range()) {
390  base_config.add_child(child.key, child.cfg);
391  }
392  cfg.clear_children("ai");
393  cfg.add_child("ai", std::move(base_config));
394 }
395 
396 } //end of namespace ai
virtual bool add_child(const path_element &child, const config &cfg)
Definition: component.cpp:70
static void add_mod_ai_from_config(config::const_child_itors configs)
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_
Variant for storing WML attributes.
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
void append(const config &cfg)
Append data from another config object to this one.
Definition: config.cpp:204
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:367
void remove_child(config_key_type key, std::size_t index)
Definition: config.cpp:645
std::size_t child_count(config_key_type key) const
Definition: config.cpp:297
config & add_child_at(config_key_type key, const config &val, std::size_t index)
Definition: config.cpp:467
void merge_children_by_attribute(const std::string &key, const std::string &attribute)
All children with the given key and with equal values of the specified attribute will be merged into ...
Definition: config.cpp:255
void clear_children(T... keys)
Definition: config.hpp:642
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:317
bool has_attribute(config_key_type key) const
Definition: config.cpp:155
const_all_children_itors all_children_range() const
In-order iteration over all children.
Definition: config.cpp:887
child_itors child_range(config_key_type key)
Definition: config.cpp:273
void remove_attribute(config_key_type key)
Definition: config.cpp:160
attribute_map::value_type attribute
Definition: config.hpp:299
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:283
bool empty() const
Definition: config.cpp:852
void clear()
Definition: config.cpp:831
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:687
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Euivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:385
config & add_child(config_key_type key)
Definition: config.cpp:441
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:207
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")
std::string get_wml_location(const std::string &filename, const std::string &current_dir)
Returns a complete path to the actual WML file or directory or an empty string if the file isn't pres...
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:50
Game configuration data as global variables.
Definition: build_info.cpp:60
std::stringstream & log_to_chat()
Use this to show WML errors in the ingame chat.
Definition: log.cpp:544
std::string era()
Definition: game.cpp:678
int turns()
Definition: game.cpp:542
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:627
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 a
#define b