1 /*
2 Copyright (C) 2009 - 2018 by Yurii Chernyi <terraninfo@terraninfo.net>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14
15
16 /**
17 * Managing the AI configuration
18 * @file
19 */
20
21 #include "ai/configuration.hpp"
22
23 #include "filesystem.hpp"
24 #include "log.hpp"
25 #include "serialization/parser.hpp"
26 #include "serialization/preprocessor.hpp"
27
28 #include <vector>
29 #include <deque>
30 #include <set>
31
32 namespace ai {
33
34 static lg::log_domain log_ai_configuration("ai/config");
35 #define DBG_AI_CONFIGURATION LOG_STREAM(debug, log_ai_configuration)
36 #define LOG_AI_CONFIGURATION LOG_STREAM(info, log_ai_configuration)
37 #define WRN_AI_CONFIGURATION LOG_STREAM(warn, log_ai_configuration)
38 #define ERR_AI_CONFIGURATION LOG_STREAM(err, log_ai_configuration)
39
init(const config & game_config)40 void configuration::init(const config &game_config)
41 {
42 ai_configurations_.clear();
43 era_ai_configurations_.clear();
44 mod_ai_configurations_.clear();
45
46 const config &ais = game_config.child("ais");
47 default_config_ = ais.child("default_config");
48 if (!default_config_) {
49 ERR_AI_CONFIGURATION << "Missing AI [default_config]. Therefore, default_config_ set to empty." << std::endl;
50 default_config_.clear();
51 }
52 default_ai_algorithm_ = ais["default_ai_algorithm"].str();
53 if (default_ai_algorithm_.empty()) {
54 ERR_AI_CONFIGURATION << "Missing default_ai_algorithm. This will result in no AI being loaded by default." << std::endl;
55 }
56
57
58 for (const config &ai_configuration : ais.child_range("ai")) {
59 const std::string &id = ai_configuration["id"];
60 if (id.empty()){
61
62 ERR_AI_CONFIGURATION << "skipped AI config due to missing id" << ". Config contains:"<< std::endl << ai_configuration << std::endl;
63 continue;
64 }
65 if (ai_configurations_.count(id)>0){
66 ERR_AI_CONFIGURATION << "skipped AI config due to duplicate id [" << id << "]. Config contains:"<< std::endl << ai_configuration << std::endl;
67 continue;
68 }
69
70 description desc;
71 desc.id=id;
72 desc.mp_rank=ai_configuration["mp_rank"].to_int(std::numeric_limits<int>::max());
73 desc.text = ai_configuration["description"].t_str();
74 desc.cfg=ai_configuration;
75
76 ai_configurations_.emplace(id, desc);
77 LOG_AI_CONFIGURATION << "loaded AI config: " << ai_configuration["description"] << std::endl;
78 }
79 }
80
81 namespace {
extract_ai_configurations(std::map<std::string,description> & storage,const config & input)82 void extract_ai_configurations(std::map<std::string, description> &storage, const config &input)
83 {
84 for (const config &ai_configuration : input.child_range("ai")) {
85 const std::string &id = ai_configuration["id"];
86 if (id.empty()){
87
88 ERR_AI_CONFIGURATION << "skipped AI config due to missing id" << ". Config contains:"<< std::endl << ai_configuration << std::endl;
89 continue;
90 }
91 if (storage.count(id)>0){
92 ERR_AI_CONFIGURATION << "skipped AI config due to duplicate id [" << id << "]. Config contains:"<< std::endl << ai_configuration << std::endl;
93 continue;
94 }
95
96 description desc;
97 desc.id=id;
98 desc.text = ai_configuration["description"].t_str();
99 desc.mp_rank = ai_configuration["mp_rank"].to_int(std::numeric_limits<int>::max());
100 desc.cfg=ai_configuration;
101
102 storage.emplace(id, desc);
103 LOG_AI_CONFIGURATION << "loaded AI config: " << ai_configuration["description"] << std::endl;
104 }
105 }
106 }
107
add_era_ai_from_config(const config & era)108 void configuration::add_era_ai_from_config(const config &era)
109 {
110 era_ai_configurations_.clear();
111 extract_ai_configurations(era_ai_configurations_, era);
112 }
113
add_mod_ai_from_config(config::const_child_itors mods)114 void configuration::add_mod_ai_from_config(config::const_child_itors mods)
115 {
116 mod_ai_configurations_.clear();
117 for (const config &mod : mods) {
118 extract_ai_configurations(mod_ai_configurations_, mod);
119 }
120 }
121
get_available_ais()122 std::vector<description*> configuration::get_available_ais()
123 {
124 std::vector<description*> ais_list;
125
126 const auto add_if_not_hidden = [&ais_list](description* d) {
127 const config& cfg = d->cfg;
128
129 if(!cfg["hidden"].to_bool(false)) {
130 ais_list.push_back(d);
131
132 DBG_AI_CONFIGURATION << "has ai with config: " << std::endl << cfg << std::endl;
133 }
134 };
135
136 for(auto& a_config : ai_configurations_) {
137 add_if_not_hidden(&a_config.second);
138 }
139
140 for(auto& e_config : era_ai_configurations_) {
141 add_if_not_hidden(&e_config.second);
142 }
143
144 for(auto& m_config : mod_ai_configurations_) {
145 add_if_not_hidden(&m_config.second);
146 }
147
148 // Sort by mp_rank. For same mp_rank, keep alphabetical order.
149 std::stable_sort(ais_list.begin(), ais_list.end(),
150 [](const description* a, const description* b) {
151 return a->mp_rank < b->mp_rank;
152 }
153 );
154
155 return ais_list;
156 }
157
get_ai_config_for(const std::string & id)158 const config& configuration::get_ai_config_for(const std::string &id)
159 {
160 description_map::iterator cfg_it = ai_configurations_.find(id);
161 if (cfg_it==ai_configurations_.end()){
162 description_map::iterator era_cfg_it = era_ai_configurations_.find(id);
163 if (era_cfg_it==era_ai_configurations_.end()){
164 description_map::iterator mod_cfg_it = mod_ai_configurations_.find(id);
165 if (mod_cfg_it==mod_ai_configurations_.end()) {
166 return default_config_;
167 } else {
168 return mod_cfg_it->second.cfg;
169 }
170 } else {
171 return era_cfg_it->second.cfg;
172 }
173 }
174 return cfg_it->second.cfg;
175 }
176
177
178 configuration::description_map configuration::ai_configurations_ = configuration::description_map();
179 configuration::description_map configuration::era_ai_configurations_ = configuration::description_map();
180 configuration::description_map configuration::mod_ai_configurations_ = configuration::description_map();
181 config configuration::default_config_ = config();
182 std::string configuration::default_ai_algorithm_;
183
get_side_config_from_file(const std::string & file,config & cfg)184 bool configuration::get_side_config_from_file(const std::string& file, config& cfg ){
185 try {
186 filesystem::scoped_istream stream = preprocess_file(filesystem::get_wml_location(file));
187 read(cfg, *stream);
188 LOG_AI_CONFIGURATION << "Reading AI configuration from file '" << file << "'" << std::endl;
189 } catch(const config::error &) {
190 ERR_AI_CONFIGURATION << "Error while reading AI configuration from file '" << file << "'" << std::endl;
191 return false;
192 }
193 LOG_AI_CONFIGURATION << "Successfully read AI configuration from file '" << file << "'" << std::endl;
194 return true;
195 }
196
get_default_ai_parameters()197 const config& configuration::get_default_ai_parameters()
198 {
199 return default_config_;
200 }
201
202
parse_side_config(side_number side,const config & original_cfg,config & cfg)203 bool configuration::parse_side_config(side_number side, const config& original_cfg, config &cfg )
204 {
205 LOG_AI_CONFIGURATION << "side "<< side <<": parsing AI configuration from config" << std::endl;
206
207 //leave only the [ai] children
208 cfg.clear();
209 for (const config &aiparam : original_cfg.child_range("ai")) {
210 cfg.add_child("ai",aiparam);
211 }
212
213 //backward-compatibility hack: put ai_algorithm if it is present
214 if (const config::attribute_value *v = original_cfg.get("ai_algorithm")) {
215 config ai_a;
216 ai_a["ai_algorithm"] = *v;
217 cfg.add_child("ai",ai_a);
218 }
219 DBG_AI_CONFIGURATION << "side " << side << ": config contains:"<< std::endl << cfg << std::endl;
220
221 //insert default config at the beginning
222 if (default_config_) {
223 DBG_AI_CONFIGURATION << "side "<< side <<": applying default configuration" << std::endl;
224 cfg.add_child_at("ai",default_config_,0);
225 } else {
226 ERR_AI_CONFIGURATION << "side "<< side <<": default configuration is not available, not applying it" << std::endl;
227 }
228
229 LOG_AI_CONFIGURATION << "side "<< side << ": expanding simplified aspects into full facets"<< std::endl;
230 expand_simplified_aspects(side, cfg);
231
232 //construct new-style integrated config
233 LOG_AI_CONFIGURATION << "side "<< side << ": doing final operations on AI config"<< std::endl;
234 config parsed_cfg = config();
235
236 LOG_AI_CONFIGURATION << "side "<< side <<": merging AI configurations"<< std::endl;
237 for (const config &aiparam : cfg.child_range("ai")) {
238 parsed_cfg.append(aiparam);
239 }
240
241
242 LOG_AI_CONFIGURATION << "side "<< side <<": merging AI aspect with the same id"<< std::endl;
243 parsed_cfg.merge_children_by_attribute("aspect","id");
244
245 LOG_AI_CONFIGURATION << "side "<< side <<": removing duplicate [default] tags from aspects"<< std::endl;
246 for (config &aspect_cfg : parsed_cfg.child_range("aspect")) {
247 if (aspect_cfg["name"] != "composite_aspect") {
248 // No point in warning about Lua or standard aspects lacking [default]
249 continue;
250 }
251 if (!aspect_cfg.child("default")) {
252 WRN_AI_CONFIGURATION << "side "<< side <<": aspect with id=["<<aspect_cfg["id"]<<"] lacks default config facet!" <<std::endl;
253 continue;
254 }
255 aspect_cfg.merge_children("default");
256 config& dflt = aspect_cfg.child("default");
257 if (dflt.has_child("value")) {
258 while (dflt.child_count("value") > 1) {
259 dflt.remove_child("value", 0);
260 }
261 }
262 }
263
264 DBG_AI_CONFIGURATION << "side "<< side <<": done parsing side config, it contains:"<< std::endl << parsed_cfg << std::endl;
265 LOG_AI_CONFIGURATION << "side "<< side <<": done parsing side config"<< std::endl;
266
267 cfg = parsed_cfg;
268 return true;
269
270 }
271
272 static const std::set<std::string> non_aspect_attributes {"turns", "time_of_day", "engine", "ai_algorithm", "id", "description", "hidden", "mp_rank"};
273 static const std::set<std::string> just_copy_tags {"engine", "stage", "aspect", "goal", "modify_ai"};
274 static const std::set<std::string> old_goal_tags {"target", "target_location", "protect_unit", "protect_location"};
275
expand_simplified_aspects(side_number side,config & cfg)276 void configuration::expand_simplified_aspects(side_number side, config &cfg) {
277 std::string algorithm;
278 config base_config, parsed_config;
279 for (const config &aiparam : cfg.child_range("ai")) {
280 std::string turns, time_of_day, engine = "cpp";
281 if (aiparam.has_attribute("turns")) {
282 turns = aiparam["turns"].str();
283 }
284 if (aiparam.has_attribute("time_of_day")) {
285 time_of_day = aiparam["time_of_day"].str();
286 }
287 if (aiparam.has_attribute("engine")) {
288 engine = aiparam["engine"].str();
289 }
290 if (aiparam.has_attribute("ai_algorithm")) {
291 if (algorithm.empty()) {
292 algorithm = aiparam["ai_algorithm"].str();
293 base_config = get_ai_config_for(algorithm);
294 } else if(algorithm != aiparam["ai_algorithm"]) {
295 lg::wml_error() << "side " << side << " has two [ai] tags with contradictory ai_algorithm - the first one will take precedence.\n";
296 }
297 }
298 std::deque<std::pair<std::string, config>> facet_configs;
299 for (const config::attribute &attr : aiparam.attribute_range()) {
300 if (non_aspect_attributes.count(attr.first)) {
301 continue;
302 }
303 config facet_config;
304 facet_config["engine"] = engine;
305 facet_config["name"] = "standard_aspect";
306 facet_config["turns"] = turns;
307 facet_config["time_of_day"] = time_of_day;
308 facet_config["value"] = attr.second;
309 facet_configs.emplace_back(attr.first, facet_config);
310 }
311 for (const config::any_child &child : aiparam.all_children_range()) {
312 if (just_copy_tags.count(child.key)) {
313 // These aren't simplified, so just copy over unchanged.
314 parsed_config.add_child(child.key, child.cfg);
315 continue;
316 } else if(old_goal_tags.count(child.key)) {
317 // A simplified goal, mainly kept around just for backwards compatibility.
318 config goal_config, criteria_config = child.cfg;
319 goal_config["name"] = child.key;
320 goal_config["turns"] = turns;
321 goal_config["time_of_day"] = time_of_day;
322 if(child.key.substr(0,7) == "protect" && criteria_config.has_attribute("protect_radius")) {
323 goal_config["protect_radius"] = criteria_config["protect_radius"];
324 criteria_config.remove_attribute("protect_radius");
325 }
326 if(criteria_config.has_attribute("value")) {
327 goal_config["value"] = criteria_config["value"];
328 criteria_config.remove_attribute("value");
329 }
330 parsed_config.add_child("goal", std::move(goal_config));
331 continue;
332 }
333 // Now there's two possibilities. If the tag is [attacks] or contains either value= or [value],
334 // then it can be copied verbatim as a [facet] tag.
335 // Otherwise, it needs to be placed as a [value] within a [facet] tag.
336 if (child.key == "attacks" || child.cfg.has_attribute("value") || child.cfg.has_child("value")) {
337 facet_configs.emplace_back(child.key, child.cfg);
338 } else {
339 config facet_config;
340 facet_config["engine"] = engine;
341 facet_config["name"] = "standard_aspect";
342 facet_config["turns"] = turns;
343 facet_config["time_of_day"] = time_of_day;
344 facet_config.add_child("value", child.cfg);
345 if (child.key == "leader_goal" && !child.cfg["id"].empty()) {
346 // Use id= attribute (if present) as the facet ID
347 const std::string& id = child.cfg["id"];
348 if(id != "*" && id.find_first_not_of("0123456789") != std::string::npos) {
349 facet_config["id"] = child.cfg["id"];
350 }
351 }
352 facet_configs.emplace_back(child.key, facet_config);
353 }
354 }
355 std::map<std::string, config> aspect_configs;
356 while (!facet_configs.empty()) {
357 const std::string &aspect = facet_configs.front().first;
358 const config &facet_config = facet_configs.front().second;
359 aspect_configs[aspect]["id"] = aspect; // Will sometimes be redundant assignment
360 aspect_configs[aspect]["name"] = "composite_aspect";
361 aspect_configs[aspect].add_child("facet", facet_config);
362 facet_configs.pop_front();
363 }
364 typedef std::map<std::string, config>::value_type aspect_pair;
365 for (const aspect_pair& p : aspect_configs) {
366 parsed_config.add_child("aspect", p.second);
367 }
368 }
369 // Support old recruitment aspect syntax
370 for(auto& child : parsed_config.child_range("aspect")) {
371 if(child["id"] == "recruitment") {
372 child["id"] = "recruitment_instructions";
373 }
374 }
375 if (algorithm.empty() && !parsed_config.has_child("stage")) {
376 base_config = get_ai_config_for(default_ai_algorithm_);
377 }
378 for (const config::any_child &child : parsed_config.all_children_range()) {
379 base_config.add_child(child.key, child.cfg);
380 }
381 cfg.clear_children("ai");
382 cfg.add_child("ai", std::move(base_config));
383 }
384
385 } //end of namespace ai
386