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