1 /*
2 Copyright (C) 2003 - 2018 by David White <dave@whitevine.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 * @file
17 * Implementations of action WML tags, other than those implemented in Lua, and
18 * excluding conditional action WML.
19 */
20
21 #include "game_events/action_wml.hpp"
22 #include "game_events/conditional_wml.hpp"
23 #include "game_events/pump.hpp"
24
25 #include "actions/attack.hpp"
26 #include "actions/create.hpp"
27 #include "actions/move.hpp"
28 #include "actions/vision.hpp"
29 #include "ai/manager.hpp"
30 #include "fake_unit_ptr.hpp"
31 #include "filesystem.hpp"
32 #include "game_classification.hpp"
33 #include "game_display.hpp"
34 #include "preferences/game.hpp"
35 #include "gettext.hpp"
36 #include "gui/dialogs/transient_message.hpp"
37 #include "gui/widgets/retval.hpp"
38 #include "log.hpp"
39 #include "map/map.hpp"
40 #include "map/exception.hpp"
41 #include "map/label.hpp"
42 #include "pathfind/teleport.hpp"
43 #include "pathfind/pathfind.hpp"
44 #include "persist_var.hpp"
45 #include "play_controller.hpp"
46 #include "recall_list_manager.hpp"
47 #include "replay.hpp"
48 #include "random.hpp"
49 #include "mouse_handler_base.hpp" // for events::commands_disabled
50 #include "resources.hpp"
51 #include "scripting/game_lua_kernel.hpp"
52 #include "side_filter.hpp"
53 #include "soundsource.hpp"
54 #include "synced_context.hpp"
55 #include "synced_user_choice.hpp"
56 #include "team.hpp"
57 #include "terrain/filter.hpp"
58 #include "units/unit.hpp"
59 #include "units/animation_component.hpp"
60 #include "units/udisplay.hpp"
61 #include "units/filter.hpp"
62 #include "wml_exception.hpp"
63 #include "whiteboard/manager.hpp"
64
65 #include <boost/regex.hpp>
66
67 static lg::log_domain log_engine("engine");
68 #define DBG_NG LOG_STREAM(debug, log_engine)
69 #define LOG_NG LOG_STREAM(info, log_engine)
70 #define WRN_NG LOG_STREAM(warn, log_engine)
71 #define ERR_NG LOG_STREAM(err, log_engine)
72
73 static lg::log_domain log_display("display");
74 #define DBG_DP LOG_STREAM(debug, log_display)
75 #define LOG_DP LOG_STREAM(info, log_display)
76
77 static lg::log_domain log_wml("wml");
78 #define LOG_WML LOG_STREAM(info, log_wml)
79 #define WRN_WML LOG_STREAM(warn, log_wml)
80 #define ERR_WML LOG_STREAM(err, log_wml)
81
82 static lg::log_domain log_config("config");
83 #define ERR_CF LOG_STREAM(err, log_config)
84
85
86 // This file is in the game_events namespace.
87 namespace game_events
88 {
89
90 // This must be defined before any WML actions are.
91 // (So keep it at the rop of this file?)
92 wml_action::map wml_action::registry_;
93
94 namespace { // Support functions
95
96 /**
97 * Converts a vconfig to a location (based on x,y=).
98 * The default parameter values cause the default return value (if neither
99 * x nor y is specified) to equal map_location::null_location().
100 */
cfg_to_loc(const vconfig & cfg,int defaultx=-999,int defaulty=-999)101 map_location cfg_to_loc(const vconfig& cfg, int defaultx = -999, int defaulty = -999)
102 {
103 return map_location(cfg["x"].to_int(defaultx), cfg["y"].to_int(defaulty), wml_loc());
104 }
105
create_fake_unit(const vconfig & cfg)106 fake_unit_ptr create_fake_unit(const vconfig& cfg)
107 {
108 std::string type = cfg["type"];
109 std::string variation = cfg["variation"];
110 std::string img_mods = cfg["image_mods"];
111
112 size_t side_num = cfg["side"].to_int(1);
113 if (!resources::gameboard->has_team(side_num)) {
114 side_num = 1;
115 }
116
117 unit_race::GENDER gender = string_gender(cfg["gender"]);
118 const unit_type *ut = unit_types.find(type);
119 if (!ut) return fake_unit_ptr();
120 fake_unit_ptr fake = fake_unit_ptr(unit::create(*ut, side_num, false, gender));
121
122 if(!variation.empty()) {
123 config mod;
124 config &effect = mod.add_child("effect");
125 effect["apply_to"] = "variation";
126 effect["name"] = variation;
127 fake->add_modification("variation",mod);
128 }
129
130 if(!img_mods.empty()) {
131 config mod;
132 config &effect = mod.add_child("effect");
133 effect["apply_to"] = "image_mod";
134 effect["add"] = img_mods;
135 fake->add_modification("image_mod",mod);
136 }
137
138 return fake;
139 }
140
fake_unit_path(const unit & fake_unit,const std::vector<std::string> & xvals,const std::vector<std::string> & yvals)141 std::vector<map_location> fake_unit_path(const unit& fake_unit, const std::vector<std::string>& xvals, const std::vector<std::string>& yvals)
142 {
143 const gamemap *game_map = & resources::gameboard->map();
144 std::vector<map_location> path;
145 map_location src;
146 map_location dst;
147 for(size_t i = 0; i != std::min(xvals.size(),yvals.size()); ++i) {
148 if(i==0){
149 try {
150 src.set_wml_x(std::stoi(xvals[i]));
151 src.set_wml_y(std::stoi(yvals[i]));
152 } catch(std::invalid_argument&) {
153 ERR_CF << "Invalid move_unit_fake source: " << xvals[i] << ", " << yvals[i] << '\n';
154 continue;
155 }
156 if (!game_map->on_board(src)) {
157 ERR_CF << "Invalid move_unit_fake source: " << src << '\n';
158 break;
159 }
160 path.push_back(src);
161 continue;
162 }
163 pathfind::shortest_path_calculator calc(fake_unit,
164 resources::gameboard->get_team(fake_unit.side()),
165 resources::gameboard->teams(),
166 *game_map);
167
168 try {
169 dst.set_wml_x(std::stoi(xvals[i]));
170 dst.set_wml_y(std::stoi(yvals[i]));
171 } catch(std::invalid_argument&) {
172 ERR_CF << "Invalid move_unit_fake destination: " << xvals[i] << ", " << yvals[i] << '\n';
173 }
174 if (!game_map->on_board(dst)) {
175 ERR_CF << "Invalid move_unit_fake destination: " << dst << '\n';
176 break;
177 }
178
179 pathfind::plain_route route = pathfind::a_star_search(src, dst, 10000, calc,
180 game_map->w(), game_map->h());
181
182 if (route.steps.empty()) {
183 WRN_NG << "Could not find move_unit_fake route from " << src << " to " << dst << ": ignoring complexities" << std::endl;
184 pathfind::emergency_path_calculator emergency_calc(fake_unit, *game_map);
185
186 route = pathfind::a_star_search(src, dst, 10000, emergency_calc,
187 game_map->w(), game_map->h());
188 if(route.steps.empty()) {
189 // This would occur when trying to do a MUF of a unit
190 // over locations which are unreachable to it (infinite movement
191 // costs). This really cannot fail.
192 WRN_NG << "Could not find move_unit_fake route from " << src << " to " << dst << ": ignoring terrain" << std::endl;
193 pathfind::dummy_path_calculator dummy_calc(fake_unit, *game_map);
194 route = a_star_search(src, dst, 10000, dummy_calc, game_map->w(), game_map->h());
195 assert(!route.steps.empty());
196 }
197 }
198 // we add this section to the end of the complete path
199 // skipping section's head because already included
200 // by the previous iteration
201 path.insert(path.end(),
202 route.steps.begin()+1, route.steps.end());
203
204 src = dst;
205 }
206 return path;
207 }
208 } // end anonymous namespace (support functions)
209
210 /**
211 * Using this constructor for a static object outside action_wml.cpp
212 * will likely lead to a static initialization fiasco.
213 * @param[in] tag The WML tag for this action.
214 * @param[in] function The callback for this action.
215 */
wml_action(const std::string & tag,handler function)216 wml_action::wml_action(const std::string & tag, handler function)
217 {
218 registry_[tag] = function;
219 }
220
221
222 /**
223 * WML_HANDLER_FUNCTION macro handles auto registration for wml handlers
224 *
225 * @param pname wml tag name
226 * @param pei the variable name of the queued_event object inside the function
227 * @param pcfg the variable name of the config object inside the function
228 *
229 * You are warned! This is evil macro magic!
230 *
231 * The following code registers a [foo] tag:
232 * \code
233 * // comment out unused parameters to prevent compiler warnings
234 * WML_HANDLER_FUNCTION(foo, event_info, cfg)
235 * {
236 * // code for foo
237 * }
238 * \endcode
239 *
240 * Generated code looks like this:
241 * \code
242 * void wml_func_foo(...);
243 * static wml_action wml_action_foo("foo", &wml_func_foo);
244 * void wml_func_foo(...)
245 * {
246 * // code for foo
247 * }
248 * \endcode
249 */
250 #define WML_HANDLER_FUNCTION(pname, pei, pcfg) \
251 static void wml_func_##pname(const queued_event &pei, const vconfig &pcfg); \
252 static wml_action wml_action_##pname(#pname, &wml_func_##pname); \
253 static void wml_func_##pname(const queued_event& pei, const vconfig& pcfg)
254
255
256 /// Experimental data persistence
257 /// @todo Finish experimenting.
258 WML_HANDLER_FUNCTION(clear_global_variable,,pcfg)
259 {
260 if (!resources::controller->is_replay())
261 verify_and_clear_global_variable(pcfg);
262 }
263
on_replay_error(const std::string & message,bool)264 static void on_replay_error(const std::string& message, bool /*b*/)
265 {
266 ERR_NG << "Error via [do_command]:" << std::endl;
267 ERR_NG << message << std::endl;
268 }
269
270 // This tag exposes part of the code path used to handle [command]'s in replays
271 // This allows to perform scripting in WML that will use the same code path as player actions, for example.
272 WML_HANDLER_FUNCTION(do_command,, cfg)
273 {
274 // Doing this in a whiteboard applied context will cause bugs
275 // Note that even though game_events::wml_event_pump() will always apply the real unit map
276 // It is still possible get a wml commands to run in a whiteboard applied context
277 // With the theme_items lua callbacks
278 if(resources::whiteboard->has_planned_unit_map())
279 {
280 ERR_NG << "[do_command] called while whiteboard is applied, ignoring" << std::endl;
281 return;
282 }
283
284 static const std::set<std::string> allowed_tags {"attack", "move", "recruit", "recall", "disband", "fire_event", "custom_command"};
285
286 const bool is_too_early = resources::gamedata->phase() != game_data::START && resources::gamedata->phase() != game_data::PLAY;
287 const bool is_unsynced_too_early = resources::gamedata->phase() != game_data::PLAY;
288 const bool is_unsynced = synced_context::get_synced_state() == synced_context::UNSYNCED;
289 if(is_too_early)
290 {
291 ERR_NG << "[do_command] called too early, only allowed at START or later" << std::endl;
292 return;
293 }
294 if(is_unsynced && resources::controller->is_lingering())
295 {
296 ERR_NG << "[do_command] cannot be used in linger mode" << std::endl;
297 return;
298 }
299 if(is_unsynced && !resources::controller->gamestate().init_side_done())
300 {
301 ERR_NG << "[do_command] cannot be used before the turn has started" << std::endl;
302 return;
303 }
304 if(is_unsynced && is_unsynced_too_early)
305 {
306 ERR_NG << "[do_command] called too early" << std::endl;
307 return;
308 }
309 if(is_unsynced && events::commands_disabled)
310 {
311 ERR_NG << "[do_command] cannot invoke synced commands while commands are blocked" << std::endl;
312 return;
313 }
314 if(is_unsynced && !resources::controller->current_team().is_local())
315 {
316 ERR_NG << "[do_command] can only be used from clients that control the currently playing side" << std::endl;
317 return;
318 }
319 for(vconfig::all_children_iterator i = cfg.ordered_begin(); i != cfg.ordered_end(); ++i)
320 {
321 if(allowed_tags.find( i.get_key()) == allowed_tags.end()) {
322 ERR_NG << "unsupported tag [" << i.get_key() << "] in [do_command]" << std::endl;
323 std::stringstream o;
324 std::copy(allowed_tags.begin(), allowed_tags.end(), std::ostream_iterator<std::string>(o, " "));
325 ERR_NG << "allowed tags: " << o.str() << std::endl;
326 continue;
327 }
328 // TODO: afaik run_in_synced_context_if_not_already thows exceptions when the executed action end the scenario or the turn.
329 // This could cause problems, specially when its unclear whether that exception is caught by lua or not...
330
331 //Note that this fires related events and everything else that also happens normally.
332 //have to watch out with the undo stack, therefore forbid [auto_shroud] and [update_shroud] here...
333 synced_context::run_in_synced_context_if_not_already(
334 /*commandname*/ i.get_key(),
335 /*data*/ i.get_child().get_parsed_config(),
336 /*use_undo*/ true,
337 /*show*/ true,
338 /*error_handler*/ &on_replay_error
339 );
340 }
341 }
342
343 /// Experimental data persistence
344 /// @todo Finish experimenting.
345 WML_HANDLER_FUNCTION(get_global_variable,,pcfg)
346 {
347 verify_and_get_global_variable(pcfg);
348 }
349
350 WML_HANDLER_FUNCTION(modify_turns,, cfg)
351 {
352 config::attribute_value value = cfg["value"];
353 std::string add = cfg["add"];
354 config::attribute_value current = cfg["current"];
355 tod_manager& tod_man = *resources::tod_manager;
356 if(!add.empty()) {
357 tod_man.modify_turns_by_wml(add);
358 } else if(!value.empty()) {
359 tod_man.set_number_of_turns_by_wml(value.to_int(-1));
360 }
361 // change current turn only after applying mods
362 if(!current.empty()) {
363 const unsigned int current_turn_number = tod_man.turn();
364 int new_turn_number = current.to_int(current_turn_number);
365 const unsigned int new_turn_number_u = static_cast<unsigned int>(new_turn_number);
366 if(new_turn_number_u < 1 || (new_turn_number > tod_man.number_of_turns() && tod_man.number_of_turns() != -1)) {
367 ERR_NG << "attempted to change current turn number to one out of range (" << new_turn_number << ")" << std::endl;
368 } else if(new_turn_number_u != current_turn_number) {
369 tod_man.set_turn_by_wml(new_turn_number_u, resources::gamedata);
370 game_display::get_singleton()->new_turn();
371 }
372 }
373 }
374
375 /// Moving a 'unit' - i.e. a dummy unit
376 /// that is just moving for the visual effect
377 WML_HANDLER_FUNCTION(move_unit_fake,, cfg)
378 {
379 events::command_disabler command_disabler;
380 fake_unit_ptr dummy_unit(create_fake_unit(cfg));
381 if(!dummy_unit.get())
382 return;
383
384 const bool force_scroll = cfg["force_scroll"].to_bool(true);
385
386 const std::string x = cfg["x"];
387 const std::string y = cfg["y"];
388
389 const std::vector<std::string> xvals = utils::split(x);
390 const std::vector<std::string> yvals = utils::split(y);
391
392 const std::vector<map_location>& path = fake_unit_path(*dummy_unit, xvals, yvals);
393 if (!path.empty()) {
394 // Always scroll.
395 unit_display::move_unit(path, dummy_unit.get_unit_ptr(), true, map_location::NDIRECTIONS, force_scroll);
396 }
397 }
398
399 WML_HANDLER_FUNCTION(move_units_fake,, cfg)
400 {
401 events::command_disabler command_disabler;
402 LOG_NG << "Processing [move_units_fake]\n";
403
404 const vconfig::child_list unit_cfgs = cfg.get_children("fake_unit");
405 size_t num_units = unit_cfgs.size();
406 std::vector<fake_unit_ptr > units;
407 units.reserve(num_units);
408 std::vector<std::vector<map_location>> paths;
409 paths.reserve(num_units);
410
411 LOG_NG << "Moving " << num_units << " units\n";
412
413 size_t longest_path = 0;
414
415 for (const vconfig& config : unit_cfgs) {
416 const std::vector<std::string> xvals = utils::split(config["x"]);
417 const std::vector<std::string> yvals = utils::split(config["y"]);
418 int skip_steps = config["skip_steps"];
419 fake_unit_ptr u = create_fake_unit(config);
420 units.push_back(u);
421 paths.push_back(fake_unit_path(*u, xvals, yvals));
422 if(skip_steps > 0)
423 paths.back().insert(paths.back().begin(), skip_steps, paths.back().front());
424 longest_path = std::max(longest_path, paths.back().size());
425 DBG_NG << "Path " << paths.size() - 1 << " has length " << paths.back().size() << '\n';
426
427 u->set_location(paths.back().front());
428 units.back().place_on_fake_unit_manager(resources::fake_units);
429 }
430
431 LOG_NG << "Units placed, longest path is " << longest_path << " long\n";
432
433 std::vector<map_location> path_step(2);
434 path_step.resize(2);
435 for(size_t step = 1; step < longest_path; ++step) {
436 DBG_NG << "Doing step " << step << "...\n";
437 for(size_t un = 0; un < num_units; ++un) {
438 if(step >= paths[un].size() || paths[un][step - 1] == paths[un][step])
439 continue;
440 DBG_NG << "Moving unit " << un << ", doing step " << step << '\n';
441 path_step[0] = paths[un][step - 1];
442 path_step[1] = paths[un][step];
443 unit_display::move_unit(path_step, units[un].get_unit_ptr());
444 units[un]->set_location(path_step[1]);
445 units[un]->anim_comp().set_standing(false);
446 }
447 }
448
449 LOG_NG << "Units moved\n";
450 }
451
452 /// If we should recall units that match a certain description.
453 // If you change attributes specific to [recall] (that is, not a Standard Unit Filter)
454 // be sure to update data/lua/wml_tag, auto_recall feature for [role] to reflect your changes.
455 WML_HANDLER_FUNCTION(recall,, cfg)
456 {
457 events::command_disabler command_disabler;
458 LOG_NG << "recalling unit...\n";
459 config temp_config(cfg.get_config());
460 // Prevent the recall unit filter from using the location as a criterion
461
462 /**
463 * @todo FIXME: we should design the WML to avoid these types of
464 * collisions; filters should be named consistently and always have a
465 * distinct scope.
466 */
467 temp_config["x"] = "recall";
468 temp_config["y"] = "recall";
469 vconfig unit_filter_cfg(temp_config);
470 const vconfig & leader_filter = cfg.child("secondary_unit");
471
472 for(int index = 0; index < static_cast<int>(resources::gameboard->teams().size()); ++index) {
473 LOG_NG << "for side " << index + 1 << "...\n";
474 const std::string player_id = resources::gameboard->teams()[index].save_id_or_number();
475
476 if(resources::gameboard->teams()[index].recall_list().size() < 1) {
477 DBG_NG << "recall list is empty when trying to recall!\n"
478 << "player_id: " << player_id << " side: " << index+1 << "\n";
479 continue;
480 }
481
482 recall_list_manager & avail = resources::gameboard->teams()[index].recall_list();
483 std::vector<unit_map::unit_iterator> leaders = resources::gameboard->units().find_leaders(index + 1);
484
485 const unit_filter ufilt(unit_filter_cfg);
486 const unit_filter lfilt(leader_filter); // Note that if leader_filter is null, this correctly gives a null filter that matches all units.
487 for(std::vector<unit_ptr>::iterator u = avail.begin(); u != avail.end(); ++u) {
488 DBG_NG << "checking unit against filter...\n";
489 scoped_recall_unit auto_store("this_unit", player_id, std::distance(avail.begin(), u));
490 if (ufilt(*(*u), map_location())) {
491 DBG_NG << (*u)->id() << " matched the filter...\n";
492 const unit_ptr to_recruit = *u;
493 const unit* pass_check = to_recruit.get();
494 if(!cfg["check_passability"].to_bool(true)) pass_check = nullptr;
495 const map_location cfg_loc = cfg_to_loc(cfg);
496
497 /// @todo fendrin: comment this monster
498 for (unit_map::const_unit_iterator leader : leaders) {
499 DBG_NG << "...considering " + leader->id() + " as the recalling leader...\n";
500 map_location loc = cfg_loc;
501 if ( lfilt(*leader) &&
502 unit_filter(vconfig(leader->recall_filter())).matches( *(*u),map_location() ) ) {
503 DBG_NG << "...matched the leader filter and is able to recall the unit.\n";
504 if(!resources::gameboard->map().on_board(loc))
505 loc = leader->get_location();
506 if(pass_check || (resources::gameboard->units().count(loc) > 0))
507 loc = pathfind::find_vacant_tile(loc, pathfind::VACANT_ANY, pass_check);
508 if(resources::gameboard->map().on_board(loc)) {
509 DBG_NG << "...valid location for the recall found. Recalling.\n";
510 avail.erase(u); // Erase before recruiting, since recruiting can fire more events
511 actions::place_recruit(to_recruit, loc, leader->get_location(), 0, true,
512 map_location::parse_direction(cfg["facing"]),
513 cfg["show"].to_bool(true), cfg["fire_event"].to_bool(false),
514 true, true);
515 return;
516 }
517 }
518 }
519 if (resources::gameboard->map().on_board(cfg_loc)) {
520 map_location loc = cfg_loc;
521 if(pass_check || (resources::gameboard->units().count(loc) > 0))
522 loc = pathfind::find_vacant_tile(loc, pathfind::VACANT_ANY, pass_check);
523 // Check if we still have a valid location
524 if (resources::gameboard->map().on_board(loc)) {
525 DBG_NG << "No usable leader found, but found usable location. Recalling.\n";
526 avail.erase(u); // Erase before recruiting, since recruiting can fire more events
527 map_location null_location = map_location::null_location();
528 actions::place_recruit(to_recruit, loc, null_location, 0, true,
529 map_location::parse_direction(cfg["facing"]),
530 cfg["show"].to_bool(true), cfg["fire_event"].to_bool(false),
531 true, true);
532 return;
533 }
534 }
535 }
536 }
537 }
538 LOG_WML << "A [recall] tag with the following content failed:\n" << cfg.get_config().debug();
539 }
540
541 namespace {
542 struct map_choice : public mp_sync::user_choice
543 {
map_choicegame_events::__anona619c90d0211::map_choice544 map_choice(const std::string& filename) : filename_(filename) {}
545 std::string filename_;
query_usergame_events::__anona619c90d0211::map_choice546 virtual config query_user(int /*side*/) const
547 {
548 //Do a regex check for the file format to prevent sending arbitrary files to other clients.
549 //Note: this allows only the new format.
550 static const std::string s_simple_terrain = R"""([A-Za-z\\|/]{1,4})""";
551 static const std::string s_terrain = s_simple_terrain + R"""((\^)""" + s_simple_terrain + ")?";
552 static const std::string s_sep = "(, |\\n)";
553 static const std::string s_prefix = R"""((\d+ )?)""";
554 static const std::string s_all = "(" + s_prefix + s_terrain + s_sep + ")+";
555 static const boost::regex r_all(s_all);
556
557 const std::string& mapfile = filesystem::get_wml_location(filename_);
558 std::string res = "";
559 if(filesystem::file_exists(mapfile)) {
560 res = filesystem::read_file(mapfile);
561 }
562 config retv;
563 if(boost::regex_match(res, r_all))
564 {
565 retv["map_data"] = res;
566 }
567 return retv;
568 }
random_choicegame_events::__anona619c90d0211::map_choice569 virtual config random_choice(int /*side*/) const
570 {
571 return config();
572 }
descriptiongame_events::__anona619c90d0211::map_choice573 virtual std::string description() const
574 {
575 return "Map Data";
576 }
577
578 };
579 }
580
581 /// Experimental map replace
582 /// @todo Finish experimenting.
583 WML_HANDLER_FUNCTION(replace_map,, cfg)
584 {
585 /*
586 * When a hex changes from a village terrain to a non-village terrain, and
587 * a team owned that village it loses that village. When a hex changes from
588 * a non-village terrain to a village terrain and there is a unit on that
589 * hex it does not automatically capture the village. The reason for not
590 * capturing villages it that there are too many choices to make; should a
591 * unit loose its movement points, should capture events be fired. It is
592 * easier to do this as wanted by the author in WML.
593 */
594
595 const gamemap * game_map = & resources::gameboard->map();
596 gamemap map(*game_map);
597
598 try {
599 if(!cfg["map_file"].empty()) {
600 config file_cfg = mp_sync::get_user_choice("map_data", map_choice(cfg["map_file"].str()));
601 map.read(file_cfg["map_data"].str(), false);
602 } else {
603 map.read(cfg["map"], false);
604 }
605 } catch(const incorrect_map_format_error&) {
606 const std::string log_map_name = cfg["map"].empty() ? cfg["file"] : std::string("from inline data");
607 lg::wml_error() << "replace_map: Unable to load map " << log_map_name << std::endl;
608 return;
609 } catch(const wml_exception& e) {
610 e.show();
611 return;
612 }
613
614 if (map.total_width() > game_map->total_width()
615 || map.total_height() > game_map->total_height()) {
616 if (!cfg["expand"].to_bool()) {
617 lg::wml_error() << "replace_map: Map dimension(s) increase but expand is not set" << std::endl;
618 return;
619 }
620 }
621
622 if (map.total_width() < game_map->total_width()
623 || map.total_height() < game_map->total_height()) {
624 if (!cfg["shrink"].to_bool()) {
625 lg::wml_error() << "replace_map: Map dimension(s) decrease but shrink is not set" << std::endl;
626 return;
627 }
628 }
629
630 boost::optional<std::string> errmsg = resources::gameboard->replace_map(map);
631
632 if (errmsg) {
633 lg::wml_error() << *errmsg << std::endl;
634 }
635
636 display::get_singleton()->reload_map();
637 game_display::get_singleton()->needs_rebuild(true);
638 ai::manager::get_singleton().raise_map_changed();
639 }
640
641 /// Experimental data persistence
642 /// @todo Finish experimenting.
643 WML_HANDLER_FUNCTION(set_global_variable,,pcfg)
644 {
645 if (!resources::controller->is_replay())
646 verify_and_set_global_variable(pcfg);
647 }
648
649 WML_HANDLER_FUNCTION(set_variables,, cfg)
650 {
651 const t_string& name = cfg["name"];
652 variable_access_create dest = resources::gamedata->get_variable_access_write(name);
653 if(name.empty()) {
654 ERR_NG << "trying to set a variable with an empty name:\n" << cfg.get_config().debug();
655 return;
656 }
657
658 std::vector<config> data;
659 if(cfg.has_attribute("to_variable"))
660 {
661 try
662 {
663 variable_access_const tovar = resources::gamedata->get_variable_access_read(cfg["to_variable"]);
664 for (const config& c : tovar.as_array())
665 {
666 data.push_back(c);
667 }
668 }
669 catch(const invalid_variablename_exception&)
670 {
671 ERR_NG << "Cannot do [set_variables] with invalid to_variable variable: " << cfg["to_variable"] << " with " << cfg.get_config().debug() << std::endl;
672 }
673 } else {
674 typedef std::pair<std::string, vconfig> vchild;
675 for (const vchild& p : cfg.all_ordered()) {
676 if(p.first == "value") {
677 data.push_back(p.second.get_parsed_config());
678 } else if(p.first == "literal") {
679 data.push_back(p.second.get_config());
680 } else if(p.first == "split") {
681 const vconfig & split_element = p.second;
682
683 std::string split_string=split_element["list"];
684 std::string separator_string=split_element["separator"];
685 std::string key_name=split_element["key"];
686 if(key_name.empty())
687 {
688 key_name="value";
689 }
690
691 bool remove_empty = split_element["remove_empty"].to_bool();
692
693 char* separator = separator_string.empty() ? nullptr : &separator_string[0];
694 if(separator_string.size() > 1){
695 ERR_NG << "[set_variables] [split] separator only supports 1 character, multiple passed: " << split_element["separator"] << " with " << cfg.get_config().debug() << std::endl;
696 }
697
698 std::vector<std::string> split_vector;
699
700 //if no separator is specified, explode the string
701 if(separator == nullptr)
702 {
703 for(std::string::iterator i=split_string.begin(); i!=split_string.end(); ++i)
704 {
705 split_vector.push_back(std::string(1, *i));
706 }
707 }
708 else {
709 split_vector=utils::split(split_string, *separator, remove_empty ? utils::REMOVE_EMPTY | utils::STRIP_SPACES : utils::STRIP_SPACES);
710 }
711
712 for(std::vector<std::string>::iterator i=split_vector.begin(); i!=split_vector.end(); ++i)
713 {
714 data.emplace_back(key_name, *i);
715 }
716 }
717 }
718 }
719 try
720 {
721 const std::string& mode = cfg["mode"];
722 if(mode == "merge")
723 {
724 if(dest.explicit_index() && data.size() > 1)
725 {
726 //merge children into one
727 config merged_children;
728 for (const config &ch : data) {
729 merged_children.append(ch);
730 }
731 data = {merged_children};
732 }
733 dest.merge_array(data);
734 }
735 else if(mode == "insert")
736 {
737 dest.insert_array(data);
738 }
739 else if(mode == "append")
740 {
741 dest.append_array(data);
742 }
743 else /*default if(mode == "replace")*/
744 {
745 dest.replace_array(data);
746 }
747 }
748 catch(const invalid_variablename_exception&)
749 {
750 ERR_NG << "Cannot do [set_variables] with invalid destination variable: " << name << " with " << cfg.get_config().debug() << std::endl;
751 }
752 }
753
754 /// Store the relative direction from one hex to another in a WML variable.
755 /// This is mainly useful as a diagnostic tool, but could be useful
756 /// for some kind of scenario.
757 WML_HANDLER_FUNCTION(store_relative_direction,, cfg)
758 {
759 if (!cfg.child("source")) {
760 WRN_NG << "No source in [store_relative_direction]" << std::endl;
761 return;
762 }
763 if (!cfg.child("destination")) {
764 WRN_NG << "No destination in [store_relative_direction]" << std::endl;
765 return;
766 }
767 if (!cfg.has_attribute("variable")) {
768 WRN_NG << "No variable in [store_relative_direction]" << std::endl;
769 return;
770 }
771
772 const map_location src = cfg_to_loc(cfg.child("source"));
773 const map_location dst = cfg_to_loc(cfg.child("destination"));
774
775 std::string variable = cfg["variable"];
776 map_location::RELATIVE_DIR_MODE mode = static_cast<map_location::RELATIVE_DIR_MODE> (cfg["mode"].to_int(0));
777 try
778 {
779 variable_access_create store = resources::gamedata->get_variable_access_write(variable);
780
781 store.as_scalar() = map_location::write_direction(src.get_relative_dir(dst,mode));
782 }
783 catch(const invalid_variablename_exception&)
784 {
785 ERR_NG << "Cannot do [store_relative_direction] with invalid destination variable: " << variable << " with " << cfg.get_config().debug() << std::endl;
786 }
787 }
788
789 /// Store the rotation of one hex around another in a WML variable.
790 /// In increments of 60 degrees, clockwise.
791 /// This is mainly useful as a diagnostic tool, but could be useful
792 /// for some kind of scenario.
793 WML_HANDLER_FUNCTION(store_rotate_map_location,, cfg)
794 {
795 if (!cfg.child("source")) {
796 WRN_NG << "No source in [store_rotate_map_location]" << std::endl;
797 return;
798 }
799 if (!cfg.child("destination")) {
800 WRN_NG << "No destination in [store_rotate_map_location]" << std::endl;
801 return;
802 }
803 if (!cfg.has_attribute("variable")) {
804 WRN_NG << "No variable in [store_rotate_map_location]" << std::endl;
805 return;
806 }
807
808 const map_location src = cfg_to_loc(cfg.child("source"));
809 const map_location dst = cfg_to_loc(cfg.child("destination"));
810
811 std::string variable = cfg["variable"];
812 int angle = cfg["angle"].to_int(1);
813
814 try
815 {
816 variable_access_create store = resources::gamedata->get_variable_access_write(variable);
817
818 dst.rotate_right_around_center(src,angle).write(store.as_container());
819 }
820 catch(const invalid_variablename_exception&)
821 {
822 ERR_NG << "Cannot do [store_rotate_map_location] with invalid destination variable: " << variable << " with " << cfg.get_config().debug() << std::endl;
823 }
824 }
825
826
827 /// Store time of day config in a WML variable. This is useful for those who
828 /// are too lazy to calculate the corresponding time of day for a given turn,
829 /// or if the turn / time-of-day sequence mutates in a scenario.
830 WML_HANDLER_FUNCTION(store_time_of_day,, cfg)
831 {
832 const map_location loc = cfg_to_loc(cfg);
833 int turn = cfg["turn"];
834 // using 0 will use the current turn
835 const time_of_day& tod = resources::tod_manager->get_time_of_day(loc,turn);
836
837 std::string variable = cfg["variable"];
838 if(variable.empty()) {
839 variable = "time_of_day";
840 }
841 try
842 {
843 variable_access_create store = resources::gamedata->get_variable_access_write(variable);
844 tod.write(store.as_container());
845 }
846 catch(const invalid_variablename_exception&)
847 {
848 ERR_NG << "Found invalid variablename " << variable << " in [store_time_of_day] with " << cfg.get_config().debug() << "\n";
849 }
850 }
851
852 /// Creating a mask of the terrain
853 WML_HANDLER_FUNCTION(terrain_mask,, cfg)
854 {
855 map_location loc = cfg_to_loc(cfg, 1, 1);
856
857 gamemap mask_map(resources::gameboard->map().tdata(), "");
858
859 try {
860 if(!cfg["mask_file"].empty()) {
861 const std::string& maskfile = filesystem::get_wml_location(cfg["mask_file"].str());
862
863 if(filesystem::file_exists(maskfile)) {
864 mask_map.read(filesystem::read_file(maskfile), false);
865 } else {
866 throw incorrect_map_format_error("Invalid file path");
867 }
868 } else {
869 mask_map.read(cfg["mask"], false);
870 }
871 } catch(const incorrect_map_format_error&) {
872 ERR_NG << "terrain mask is in the incorrect format, and couldn't be applied" << std::endl;
873 return;
874 } catch(const wml_exception& e) {
875 e.show();
876 return;
877 }
878
879 if (!cfg["border"].to_bool(true)) {
880 mask_map.add_fog_border();
881 }
882
883 resources::gameboard->overlay_map(mask_map, cfg.get_parsed_config(), loc);
884 game_display::get_singleton()->needs_rebuild(true);
885 }
886
887 WML_HANDLER_FUNCTION(tunnel,, cfg)
888 {
889 const bool remove = cfg["remove"].to_bool(false);
890 const bool delay = cfg["delayed_variable_substitution"].to_bool(true);
891 if (remove) {
892 const std::vector<std::string> ids = utils::split(cfg["id"]);
893 for (const std::string &id : ids) {
894 resources::tunnels->remove(id);
895 }
896 } else if (cfg.get_children("source").empty() ||
897 cfg.get_children("target").empty() ||
898 cfg.get_children("filter").empty()) {
899 ERR_WML << "[tunnel] is missing a mandatory tag:\n"
900 << cfg.get_config().debug();
901 } else {
902 pathfind::teleport_group tunnel(delay ? cfg : vconfig(cfg.get_parsed_config()), false);
903 resources::tunnels->add(tunnel);
904
905 if(cfg["bidirectional"].to_bool(true)) {
906 tunnel = pathfind::teleport_group(delay ? cfg : vconfig(cfg.get_parsed_config()), true);
907 resources::tunnels->add(tunnel);
908 }
909 }
910 }
911
912 /// If we should spawn a new unit on the map somewhere
913 WML_HANDLER_FUNCTION(unit,, cfg)
914 {
915 events::command_disabler command_disabler;
916 config parsed_cfg = cfg.get_parsed_config();
917
918 config::attribute_value to_variable = cfg["to_variable"];
919 if (!to_variable.blank())
920 {
921 parsed_cfg.remove_attribute("to_variable");
922 unit_ptr new_unit = unit::create(parsed_cfg, true, &cfg);
923 try
924 {
925 config &var = resources::gamedata->get_variable_cfg(to_variable);
926 var.clear();
927 new_unit->write(var);
928 if (const config::attribute_value *v = parsed_cfg.get("x")) var["x"] = *v;
929 if (const config::attribute_value *v = parsed_cfg.get("y")) var["y"] = *v;
930 }
931 catch(const invalid_variablename_exception&)
932 {
933 ERR_NG << "Cannot do [unit] with invalid to_variable: " << to_variable << " with " << cfg.get_config().debug() << std::endl;
934 }
935 return;
936
937 }
938
939 int side = parsed_cfg["side"].to_int(1);
940
941
942 if ((side<1)||(side > static_cast<int>(resources::gameboard->teams().size()))) {
943 ERR_NG << "wrong side in [unit] tag - no such side: "<<side<<" ( number of teams :"<<resources::gameboard->teams().size()<<")"<<std::endl;
944 DBG_NG << parsed_cfg.debug();
945 return;
946 }
947 team &tm = resources::gameboard->get_team(side);
948
949 unit_creator uc(tm,resources::gameboard->map().starting_position(side));
950
951 uc
952 .allow_add_to_recall(true)
953 .allow_discover(true)
954 .allow_get_village(true)
955 .allow_invalidate(true)
956 .allow_rename_side(true)
957 .allow_show(true);
958
959 uc.add_unit(parsed_cfg, &cfg);
960
961 }
962
WML_HANDLER_FUNCTION(on_undo,event_info,cfg)963 WML_HANDLER_FUNCTION(on_undo, event_info, cfg)
964 {
965 if(cfg["delayed_variable_substitution"].to_bool(false)) {
966 synced_context::add_undo_commands(cfg.get_config(), event_info);
967 } else {
968 synced_context::add_undo_commands(cfg.get_parsed_config(), event_info);
969 }
970 }
971
972 } // end namespace game_events
973