wesnoth/src/game_events/action_wml.cpp
JaMiT cab1c823b8 Refactor static_wml_actions as class wml_action.
It's a bit more C++-like, but more importantly puts related static
variables in the same file (so this should cure the static
initialization fiasco known as bug #21020).
2013-08-12 21:44:35 -05:00

2610 lines
77 KiB
C++

/*
Copyright (C) 2003 - 2013 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file
* Implementations of action WML tags, other than those implemented in Lua, and
* excluding conditional action WML.
*/
#include "global.hpp"
#include "action_wml.hpp"
#include "conditional_wml.hpp"
#include "handlers.hpp"
#include "pump.hpp"
#include "../actions/create.hpp"
#include "../actions/move.hpp"
#include "../actions/vision.hpp"
#include "../ai/manager.hpp"
#include "../dialogs.hpp"
#include "../game_display.hpp"
#include "../game_preferences.hpp"
#include "../gettext.hpp"
#include "../gui/dialogs/gamestate_inspector.hpp"
#include "../gui/dialogs/transient_message.hpp"
#include "../gui/dialogs/wml_message.hpp"
#include "../gui/widgets/window.hpp"
#include "../help.hpp"
#include "../log.hpp"
#include "../map.hpp"
#include "../map_exception.hpp"
#include "../map_label.hpp"
#include "../pathfind/teleport.hpp"
#include "../pathfind/pathfind.hpp"
#include "../persist_var.hpp"
#include "../play_controller.hpp"
#include "../replay.hpp"
#include "../resources.hpp"
#include "../side_filter.hpp"
#include "../sound.hpp"
#include "../soundsource.hpp"
#include "../terrain_filter.hpp"
#include "../unit_display.hpp"
#include "../unit_helper.hpp"
#include "../wml_exception.hpp"
#include "../utils/foreach.tpp"
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/scoped_array.hpp>
#include <boost/scoped_ptr.hpp>
static lg::log_domain log_engine("engine");
#define DBG_NG LOG_STREAM(debug, log_engine)
#define LOG_NG LOG_STREAM(info, log_engine)
#define WRN_NG LOG_STREAM(warn, log_engine)
#define ERR_NG LOG_STREAM(err, log_engine)
static lg::log_domain log_display("display");
#define DBG_DP LOG_STREAM(debug, log_display)
#define LOG_DP LOG_STREAM(info, log_display)
static lg::log_domain log_wml("wml");
#define LOG_WML LOG_STREAM(info, log_wml)
#define ERR_WML LOG_STREAM(err, log_wml)
static lg::log_domain log_config("config");
#define ERR_CF LOG_STREAM(err, log_config)
// This file is in the game_events namespace.
namespace game_events
{
// This must be defined before any WML actions are.
// (So keep it at the rop of this file?)
wml_action::map wml_action::registry_;
namespace { // advance declarations
std::string get_caption(const vconfig& cfg, unit_map::iterator speaker);
std::string get_image(const vconfig& cfg, unit_map::iterator speaker);
}
namespace { // Types
class recursion_preventer {
typedef std::map<map_location, int> t_counter;
static t_counter counter_;
static const int max_recursion = 10;
map_location loc_;
bool too_many_recursions_;
public:
recursion_preventer(map_location& loc) :
loc_(loc),
too_many_recursions_(false)
{
t_counter::iterator inserted = counter_.insert(std::make_pair(loc_, 0)).first;
++inserted->second;
too_many_recursions_ = inserted->second >= max_recursion;
}
~recursion_preventer()
{
t_counter::iterator itor = counter_.find(loc_);
if (--itor->second == 0)
{
counter_.erase(itor);
}
}
bool too_many_recursions() const
{
return too_many_recursions_;
}
};
recursion_preventer::t_counter recursion_preventer::counter_;
typedef boost::scoped_ptr<recursion_preventer> recursion_preventer_ptr;
struct message_user_choice : mp_sync::user_choice
{
vconfig cfg;
unit_map::iterator speaker;
vconfig text_input_element;
bool has_text_input;
const std::vector<std::string> &options;
message_user_choice(const vconfig &c, const unit_map::iterator &s,
const vconfig &t, bool ht, const std::vector<std::string> &o)
: cfg(c), speaker(s), text_input_element(t)
, has_text_input(ht), options(o)
{}
virtual config query_user() const
{
std::string image = get_image(cfg, speaker);
std::string caption = get_caption(cfg, speaker);
size_t right_offset = image.find("~RIGHT()");
bool left_side = right_offset == std::string::npos;
if (!left_side) {
image.erase(right_offset);
}
// Parse input text, if not available all fields are empty
std::string text_input_label = text_input_element["label"];
std::string text_input_content = text_input_element["text"];
unsigned input_max_size = text_input_element["max_length"].to_int(256);
if (input_max_size > 1024 || input_max_size < 1) {
lg::wml_error << "invalid maximum size for input "
<< input_max_size << '\n';
input_max_size = 256;
}
int option_chosen;
int dlg_result = gui2::show_wml_message(left_side,
resources::screen->video(), caption, cfg["message"],
image, false, has_text_input, text_input_label,
&text_input_content, input_max_size, options,
&option_chosen);
/* Since gui2::show_wml_message needs to do undrawing the
chatlines can get garbled and look dirty on screen. Force a
redraw to fix it. */
/** @todo This hack can be removed once gui2 is finished. */
resources::screen->invalidate_all();
resources::screen->draw(true,true);
if (dlg_result == gui2::twindow::CANCEL) {
context::skip_messages(true);
}
config cfg;
if (!options.empty()) cfg["value"] = option_chosen;
if (has_text_input) cfg["text"] = text_input_content;
return cfg;
}
virtual config random_choice(rand_rng::simple_rng &) const
{
return config();
}
};
struct unstore_unit_advance_choice: mp_sync::user_choice
{
int nb_options;
map_location loc;
bool use_dialog;
unstore_unit_advance_choice(int o, const map_location &l, bool d)
: nb_options(o), loc(l), use_dialog(d)
{}
virtual config query_user() const
{
int selected;
if (use_dialog) {
DBG_NG << "dialog requested\n";
selected = dialogs::advance_unit_dialog(loc);
} else {
// VITAL this is NOT done using the synced RNG
selected = rand() % nb_options;
}
config cfg;
cfg["value"] = selected;
return cfg;
}
virtual config random_choice(rand_rng::simple_rng &rng) const
{
config cfg;
cfg["value"] = rng.get_next_random() % nb_options;
return cfg;
}
};
} // end anonymous namespace (types)
namespace { // Variables
const gui::msecs prevent_misclick_duration = 10;
const gui::msecs average_frame_time = 30;
int floating_label = 0;
} // end anonymous namespace (variables)
namespace { // Support functions
/**
* Converts a vconfig to a location (based on x,y=).
* The default parameter values cause the default return value (if neither
* x nor y is specified) to equal map_location::null_location.
*/
map_location cfg_to_loc(const vconfig& cfg, int defaultx = -999, int defaulty = -999)
{
int x = cfg["x"].to_int(defaultx) - 1;
int y = cfg["y"].to_int(defaulty) - 1;
return map_location(x, y);
}
game_display::fake_unit *create_fake_unit(const vconfig& cfg)
{
std::string type = cfg["type"];
std::string variation = cfg["variation"];
std::string img_mods = cfg["image_mods"];
size_t side_num = cfg["side"].to_int(1);
if ( side_num == 0 || side_num > resources::teams->size() )
side_num = 1;
unit_race::GENDER gender = string_gender(cfg["gender"]);
const unit_type *ut = unit_types.find(type);
if (!ut) return NULL;
game_display::fake_unit * fake_unit = new game_display::fake_unit(*ut, side_num, gender);
if(!variation.empty()) {
config mod;
config &effect = mod.add_child("effect");
effect["apply_to"] = "variation";
effect["name"] = variation;
fake_unit->add_modification("variation",mod);
}
if(!img_mods.empty()) {
config mod;
config &effect = mod.add_child("effect");
effect["apply_to"] = "image_mod";
effect["add"] = img_mods;
fake_unit->add_modification("image_mod",mod);
}
return fake_unit;
}
std::vector<map_location> fake_unit_path(const unit& fake_unit, const std::vector<std::string>& xvals, const std::vector<std::string>& yvals)
{
gamemap *game_map = resources::game_map;
std::vector<map_location> path;
map_location src;
map_location dst;
for(size_t i = 0; i != std::min(xvals.size(),yvals.size()); ++i) {
if(i==0){
src.x = atoi(xvals[i].c_str())-1;
src.y = atoi(yvals[i].c_str())-1;
if (!game_map->on_board(src)) {
ERR_CF << "invalid move_unit_fake source: " << src << '\n';
break;
}
path.push_back(src);
continue;
}
pathfind::shortest_path_calculator calc(fake_unit,
(*resources::teams)[fake_unit.side()-1],
*resources::teams,
*game_map);
dst.x = atoi(xvals[i].c_str())-1;
dst.y = atoi(yvals[i].c_str())-1;
if (!game_map->on_board(dst)) {
ERR_CF << "invalid move_unit_fake destination: " << dst << '\n';
break;
}
pathfind::plain_route route = pathfind::a_star_search(src, dst, 10000, &calc,
game_map->w(), game_map->h());
if (route.steps.empty()) {
WRN_NG << "Could not find move_unit_fake route from " << src << " to " << dst << ": ignoring complexities\n";
pathfind::emergency_path_calculator calc(fake_unit, *game_map);
route = pathfind::a_star_search(src, dst, 10000, &calc,
game_map->w(), game_map->h());
if(route.steps.empty()) {
// This would occur when trying to do a MUF of a unit
// over locations which are unreachable to it (infinite movement
// costs). This really cannot fail.
WRN_NG << "Could not find move_unit_fake route from " << src << " to " << dst << ": ignoring terrain\n";
pathfind::dummy_path_calculator calc(fake_unit, *game_map);
route = a_star_search(src, dst, 10000, &calc, game_map->w(), game_map->h());
assert(!route.steps.empty());
}
}
// we add this section to the end of the complete path
// skipping section's head because already included
// by the previous iteration
path.insert(path.end(),
route.steps.begin()+1, route.steps.end());
src = dst;
}
return path;
}
/**
* Helper to handle the caption part of [message].
*
* @param cfg cfg of message.
* @param speaker The speaker of the message.
*
* @returns The caption to show.
*/
std::string get_caption(const vconfig& cfg, unit_map::iterator speaker)
{
std::string caption = cfg["caption"];
if (caption.empty() && speaker != resources::units->end()) {
caption = speaker->name();
if(caption.empty()) {
caption = speaker->type_name();
}
}
return caption;
}
/**
* Helper to handle the image part of [message].
*
* @param cfg cfg of message.
* @param speaker The speaker of the message.
*
* @returns The image to show.
*/
std::string get_image(const vconfig& cfg, unit_map::iterator speaker)
{
std::string image = cfg["image"];
if (image.empty() && speaker != resources::units->end())
{
image = speaker->big_profile();
#ifndef LOW_MEM
if(image == speaker->absolute_image()) {
image += speaker->image_mods();
}
#endif
}
return image;
}
/**
* Gets a vector of sides from side= attribute in a given config node.
* Promotes consistent behavior.
*/
std::vector<int> get_sides_vector(const vconfig& cfg)
{
const config::attribute_value sides = cfg["side"];
const vconfig &ssf = cfg.child("filter_side");
if (!ssf.null()) {
if(!sides.empty()) { WRN_NG << "ignoring duplicate side filter information (inline side=)\n"; }
side_filter filter(ssf);
return filter.get_teams();
}
side_filter filter(sides.str());
return filter.get_teams();
}
/**
* Helper to handle the speaker part of [message].
*
* @param event_info event_info of message.
* @param cfg cfg of message.
*
* @returns The unit who's the speaker or units->end().
*/
unit_map::iterator handle_speaker(const queued_event& event_info,
const vconfig& cfg, bool scroll)
{
unit_map *units = resources::units;
game_display &screen = *resources::screen;
unit_map::iterator speaker = units->end();
const std::string speaker_str = cfg["speaker"];
if(speaker_str == "unit") {
speaker = units->find(event_info.loc1);
} else if(speaker_str == "second_unit") {
speaker = units->find(event_info.loc2);
} else if(speaker_str != "narrator") {
for(speaker = units->begin(); speaker != units->end(); ++speaker){
if ( speaker->matches_filter(cfg) )
break;
}
}
if(speaker != units->end()) {
LOG_NG << "set speaker to '" << speaker->name() << "'\n";
const map_location &spl = speaker->get_location();
screen.highlight_hex(spl);
if(scroll) {
LOG_DP << "scrolling to speaker..\n";
const int offset_from_center = std::max<int>(0, spl.y - 1);
screen.scroll_to_tile(map_location(spl.x, offset_from_center));
}
screen.highlight_hex(spl);
} else if(speaker_str == "narrator") {
LOG_NG << "no speaker\n";
screen.highlight_hex(map_location::null_location);
} else {
return speaker;
}
screen.draw(false);
LOG_DP << "done scrolling to speaker...\n";
return speaker;
}
/**
* Implements the lifting and resetting of fog via WML.
* Keeping affect_normal_fog as false causes only the fog override to be affected.
* Otherwise, fog lifting will be implemented similar to normal sight (cannot be
* individually reset and ends at the end of the turn), and fog resetting will, in
* addition to removing overrides, extend the specified teams' normal fog to all
* hexes.
*/
void toggle_fog(const bool clear, const vconfig& cfg, const bool affect_normal_fog=false)
{
// Filter the sides.
const vconfig &ssf = cfg.child("filter_side");
const side_filter s_filter(ssf.null() ? vconfig::empty_vconfig() : ssf);
const std::vector<int> sides = s_filter.get_teams();
// Filter the locations.
std::set<map_location> locs;
const terrain_filter t_filter(cfg, *resources::units);
t_filter.get_locations(locs, true);
// Loop through sides.
BOOST_FOREACH(const int &side_num, sides)
{
team &t = (*resources::teams)[side_num-1];
if ( !clear )
{
// Extend fog.
t.remove_fog_override(locs);
if ( affect_normal_fog )
t.refog();
}
else if ( !affect_normal_fog )
// Force the locations clear of fog.
t.add_fog_override(locs);
else
// Simply clear fog from the locations.
BOOST_FOREACH(const map_location &hex, locs)
t.clear_fog(hex);
}
// Flag a screen update.
resources::screen->recalculate_minimap();
resources::screen->invalidate_all();
}
void toggle_shroud(const bool remove, const vconfig& cfg)
{
// Filter the sides.
std::vector<int> sides = get_sides_vector(cfg);
size_t index;
// Filter the locations.
std::set<map_location> locs;
const terrain_filter filter(cfg, *resources::units);
filter.get_locations(locs, true);
BOOST_FOREACH(const int &side_num, sides)
{
index = side_num - 1;
team &t = (*resources::teams)[index];
BOOST_FOREACH(map_location const &loc, locs)
{
if (remove) {
t.clear_shroud(loc);
} else {
t.place_shroud(loc);
}
}
}
resources::screen->labels().recalculate_shroud();
resources::screen->recalculate_minimap();
resources::screen->invalidate_all();
}
bool try_add_unit_to_recall_list(const map_location& loc, const unit& u)
{
if((*resources::teams)[u.side()-1].persistent()) {
(*resources::teams)[u.side()-1].recall_list().push_back(u);
return true;
} else {
ERR_NG << "unit with id " << u.id() << ": location (" << loc.x << "," << loc.y <<") is not on the map, and player "
<< u.side() << " has no recall list.\n";
return false;
}
}
} // end anonymous namespace (support functions)
void change_terrain(const map_location &loc, const t_translation::t_terrain &t,
gamemap::tmerge_mode mode, bool replace_if_failed)
{
/*
* When a hex changes from a village terrain to a non-village terrain, and
* a team owned that village it loses that village. When a hex changes from
* a non-village terrain to a village terrain and there is a unit on that
* hex it does not automatically capture the village. The reason for not
* capturing villages it that there are too many choices to make; should a
* unit loose its movement points, should capture events be fired. It is
* easier to do this as wanted by the author in WML.
*/
gamemap *game_map = resources::game_map;
t_translation::t_terrain
old_t = game_map->get_terrain(loc),
new_t = game_map->merge_terrains(old_t, t, mode, replace_if_failed);
if (new_t == t_translation::NONE_TERRAIN) return;
preferences::encountered_terrains().insert(new_t);
if (game_map->is_village(old_t) && !game_map->is_village(new_t)) {
int owner = village_owner(loc);
if (owner != -1)
(*resources::teams)[owner].lose_village(loc);
}
game_map->set_terrain(loc, new_t);
context::screen_needs_rebuild(true);
BOOST_FOREACH(const t_translation::t_terrain &ut, game_map->underlying_union_terrain(loc)) {
preferences::encountered_terrains().insert(ut);
}
}
void handle_deprecated_message(const config& cfg)
{
// Note: no need to translate the string, since only used for deprecated things.
const std::string& message = cfg["message"];
lg::wml_error << message << '\n';
}
void handle_wml_log_message(const config& cfg)
{
const std::string& logger = cfg["logger"];
const std::string& msg = cfg["message"];
put_wml_message(logger,msg);
}
/**
* Using this constructor for a static object outside action_wml.cpp
* will likely lead to a static initialization fiasco.
* @param[in] tag The WML tag for this action.
* @param[in] function The callback for this action.
*/
wml_action::wml_action(const std::string & tag, handler function)
{
registry_[tag] = function;
}
/**
* WML_HANDLER_FUNCTION macro handles auto registration for wml handlers
*
* @param pname wml tag name
* @param pei the variable name of the queued_event object inside the function
* @param pcfg the variable name of the config object inside the function
*
* You are warned! This is evil macro magic!
*
* The following code registers a [foo] tag:
* \code
* // comment out unused parameters to prevent compiler warnings
* WML_HANDLER_FUNCTION(foo, event_info, cfg)
* {
* // code for foo
* }
* \endcode
*
* Generated code looks like this:
* \code
* void wml_func_foo(...);
* static wml_action wml_action_foo("foo", &wml_func_foo);
* void wml_func_foo(...)
* {
* // code for foo
* }
* \endcode
*/
#define WML_HANDLER_FUNCTION(pname, pei, pcfg) \
static void wml_func_##pname(const queued_event &pei, const vconfig &pcfg); \
static wml_action wml_action_##pname(#pname, &wml_func_##pname); \
static void wml_func_##pname(const queued_event& pei, const vconfig& pcfg)
WML_HANDLER_FUNCTION(allow_end_turn, /*event_info*/, /*cfg*/)
{
resources::gamedata->set_allow_end_turn(true);
}
/// Allow undo sets the flag saying whether the event has mutated the game to false.
WML_HANDLER_FUNCTION(allow_undo,/*event_info*/,/*cfg*/)
{
context::mutated(false);
}
WML_HANDLER_FUNCTION(animate_unit, event_info, cfg)
{
const events::command_disabler disable_commands;
unit_display::wml_animation(cfg, event_info.loc1);
}
/// Experimental data persistence
/// @todo Finish experimenting.
WML_HANDLER_FUNCTION(clear_global_variable,/**/,pcfg)
{
if (get_replay_source().at_end() || (network::nconnections() != 0))
verify_and_clear_global_variable(pcfg);
}
WML_HANDLER_FUNCTION(clear_menu_item, /*event_info*/, cfg)
{
const std::string ids = cfg["id"].str();
BOOST_FOREACH(const std::string& id, utils::split(ids, ',', utils::STRIP_SPACES)) {
if(id.empty()) {
WRN_NG << "[clear_menu_item] has been given an empty id=, ignoring\n";
continue;
}
std::map<std::string, wml_menu_item*>& menu_items = resources::gamedata->get_wml_menu_items().get_menu_items();
if(menu_items.find(id) == menu_items.end()) {
WRN_NG << "trying to remove non-existent menu item '" << id << "', ignoring\n";
continue;
}
remove_wmi_change(id);
remove_event_handler(id);
wml_menu_item*& mi = menu_items[id];
delete mi;
mi = NULL;
menu_items.erase(id);
}
}
WML_HANDLER_FUNCTION(color_adjust, /*event_info*/, cfg)
{
game_display &screen = *resources::screen;
screen.adjust_color_overlay(cfg["red"], cfg["green"], cfg["blue"]);
screen.invalidate_all();
screen.draw(true,true);
}
WML_HANDLER_FUNCTION(deprecated_message, /*event_info*/, cfg)
{
handle_deprecated_message( cfg.get_parsed_config() );
}
WML_HANDLER_FUNCTION(disallow_end_turn, /*event_info*/, /*cfg*/)
{
resources::gamedata->set_allow_end_turn(false);
}
WML_HANDLER_FUNCTION(endlevel, /*event_info*/, cfg)
{
end_level_data &data = resources::controller->get_end_level_data();
if(data.transient.disabled) {
WRN_NG << "repeated [endlevel] execution, ignoring\n";
return;
}
data.transient.disabled = true;
game_state *state_of_game = resources::state_of_game;
unit_map *units = resources::units;
// Remove 0-hp units from the unit map to avoid the following problem:
// In case a die event triggers an endlevel the dead unit is still as a
// 'ghost' in linger mode. After save loading in linger mode the unit
// is fully visible again.
unit_map::iterator u = units->begin();
while (u != units->end()) {
if (u->hitpoints() <= 0) {
units->erase(u++);
} else {
++u;
}
}
std::string next_scenario = cfg["next_scenario"];
if (!next_scenario.empty()) {
resources::gamedata->set_next_scenario(next_scenario);
}
std::string end_of_campaign_text = cfg["end_text"];
if (!end_of_campaign_text.empty()) {
state_of_game->classification().end_text = end_of_campaign_text;
}
config::attribute_value end_of_campaign_text_delay = cfg["end_text_duration"];
if (!end_of_campaign_text_delay.empty()) {
state_of_game->classification().end_text_duration =
end_of_campaign_text_delay.to_int(state_of_game->classification().end_text_duration);
}
if(cfg.has_attribute("end_credits")) {
state_of_game->classification().end_credits = cfg["end_credits"].to_bool(true);
}
std::string result = cfg["result"];
VALIDATE_WITH_DEV_MESSAGE(
result.empty() || result == "victory" || result == "defeat"
, _("Invalid value in the result key for [end_level]")
, "result = '" + result + "'.");
data.transient.custom_endlevel_music = cfg["music"].str();
data.transient.carryover_report = cfg["carryover_report"].to_bool(true);
data.prescenario_save = cfg["save"].to_bool(true);
data.replay_save = cfg["replay_save"].to_bool(true);
data.transient.linger_mode = cfg["linger_mode"].to_bool(true)
&& !resources::teams->empty();
data.transient.reveal_map = cfg["reveal_map"].to_bool(true);
data.gold_bonus = cfg["bonus"].to_bool(true);
data.carryover_percentage = cfg["carryover_percentage"].to_int(game_config::gold_carryover_percentage);
data.carryover_add = cfg["carryover_add"].to_bool();
if(result == "defeat") {
data.carryover_add = false;
resources::controller->force_end_level(DEFEAT);
} else {
resources::controller->force_end_level(VICTORY);
}
}
WML_HANDLER_FUNCTION(end_turn, /*event_info*/, /*cfg*/)
{
resources::controller->force_end_turn();
}
/// Adding new events
WML_HANDLER_FUNCTION(event, /*event_info*/, cfg)
{
if (cfg["remove"].to_bool(false)) {
remove_event_handler(cfg["id"]);
} else if (!cfg["delayed_variable_substitution"].to_bool(true)) {
add_event_handler(cfg.get_parsed_config());
} else {
add_event_handler(cfg.get_config());
}
}
/// Experimental data persistence
/// @todo Finish experimenting.
WML_HANDLER_FUNCTION(get_global_variable,/**/,pcfg)
{
verify_and_get_global_variable(pcfg);
}
WML_HANDLER_FUNCTION(heal_unit, event_info, cfg)
{
unit_map* units = resources::units;
const vconfig healers_filter = cfg.child("filter_second");
std::vector<unit*> healers;
if (!healers_filter.null()) {
BOOST_FOREACH(unit& u, *units) {
if ( u.matches_filter(healers_filter) && u.has_ability_type("heals") ) {
healers.push_back(&u);
}
}
}
const config::attribute_value amount = cfg["amount"];
const config::attribute_value moves = cfg["moves"];
const bool restore_attacks = cfg["restore_attacks"].to_bool(false);
const bool restore_statuses = cfg["restore_statuses"].to_bool(true);
const bool animate = cfg["animate"].to_bool(false);
const vconfig healed_filter = cfg.child("filter");
bool only_unit_at_loc1 = healed_filter.null();
bool heal_amount_to_set = true;
for(unit_map::unit_iterator u = units->begin(); u != units->end(); ++u) {
if (only_unit_at_loc1)
{
u = units->find(event_info.loc1);
if(!u.valid()) return;
}
else if ( !u->matches_filter(healed_filter) ) continue;
int heal_amount = u->max_hitpoints() - u->hitpoints();
if(amount.blank() || amount == "full") u->set_hitpoints(u->max_hitpoints());
else {
heal_amount = lexical_cast_default<int, config::attribute_value> (amount, heal_amount);
const int new_hitpoints = std::max(1, std::min(u->max_hitpoints(), u->hitpoints() + heal_amount));
heal_amount = new_hitpoints - u->hitpoints();
u->set_hitpoints(new_hitpoints);
}
if(!moves.blank()) {
if(moves == "full") u->set_movement(u->total_movement());
else {
// set_movement doesn't set below 0
u->set_movement(std::min<int>(
u->total_movement(),
u->movement_left() + lexical_cast_default<int, config::attribute_value> (moves, 0)
));
}
}
if(restore_attacks) u->set_attacks(u->max_attacks());
if(restore_statuses)
{
u->set_state(unit::STATE_POISONED, false);
u->set_state(unit::STATE_SLOWED, false);
u->set_state(unit::STATE_PETRIFIED, false);
u->set_state(unit::STATE_UNHEALABLE, false);
}
if (heal_amount_to_set)
{
heal_amount_to_set = false;
resources::gamedata->get_variable("heal_amount") = heal_amount;
}
if(animate) unit_display::unit_healing(*u, healers, heal_amount);
if(only_unit_at_loc1) return;
}
}
WML_HANDLER_FUNCTION(inspect, /*event_info*/, cfg)
{
gui2::tgamestate_inspector inspect_dialog(cfg);
inspect_dialog.show(resources::screen->video());
}
WML_HANDLER_FUNCTION(kill, event_info, cfg)
{
bool secondary_unit = cfg.has_child("secondary_unit");
entity_location killer_loc(map_location(0, 0));
if(cfg["fire_event"].to_bool() && secondary_unit)
{
secondary_unit = false;
for(unit_map::const_unit_iterator unit = resources::units->begin();
unit != resources::units->end(); ++unit) {
if ( unit->matches_filter(cfg.child("secondary_unit")) )
{
killer_loc = entity_location(*unit);
secondary_unit = true;
break;
}
}
if(!secondary_unit) {
WRN_NG << "failed to match [secondary_unit] in [kill] with a single on-board unit\n";
}
}
//Find all the dead units first, because firing events ruins unit_map iteration
std::vector<unit *> dead_men_walking;
BOOST_FOREACH(unit & u, *resources::units){
if ( u.matches_filter(cfg) ) {
dead_men_walking.push_back(&u);
}
}
BOOST_FOREACH(unit * un, dead_men_walking) {
map_location loc(un->get_location());
bool fire_event = false;
entity_location death_loc(*un);
if(!secondary_unit) {
killer_loc = entity_location(*un);
}
if (cfg["fire_event"].to_bool())
{
// Prevent infinite recursion of 'die' events
fire_event = true;
recursion_preventer_ptr recursion_prevent;
if (event_info.loc1 == death_loc && (event_info.name == "die" || event_info.name == "last breath"))
{
recursion_prevent.reset(new recursion_preventer(death_loc));
if(recursion_prevent->too_many_recursions())
{
fire_event = false;
ERR_NG << "tried to fire 'die' or 'last breath' event on primary_unit inside its own 'die' or 'last breath' event with 'first_time_only' set to false!\n";
}
}
}
if (fire_event) {
fire("last breath", death_loc, killer_loc);
}
// Visual consequences of the kill.
if (cfg["animate"].to_bool()) {
resources::screen->scroll_to_tile(loc);
unit_map::iterator iun = resources::units->find(loc);
if (iun != resources::units->end() && iun.valid()) {
unit_display::unit_die(loc, *iun);
}
} else {
// Make sure the unit gets (fully) cleaned off the screen.
resources::screen->invalidate(loc);
unit_map::iterator iun = resources::units->find(loc);
if ( iun != resources::units->end() && iun.valid() )
iun->invalidate(loc);
}
resources::screen->redraw_minimap();
if (fire_event) {
fire("die", death_loc, killer_loc);
unit_map::iterator iun = resources::units->find(death_loc);
if ( death_loc.matches_unit(iun) ) {
resources::units->erase(iun);
}
}
else resources::units->erase(loc);
}
// If the filter doesn't contain positional information,
// then it may match units on all recall lists.
t_string const& cfg_x = cfg["x"];
t_string const& cfg_y = cfg["y"];
if((cfg_x.empty() || cfg_x == "recall")
&& (cfg_y.empty() || cfg_y == "recall"))
{
//remove the unit from the corresponding team's recall list
for(std::vector<team>::iterator pi = resources::teams->begin();
pi!=resources::teams->end(); ++pi)
{
std::vector<unit>& avail_units = pi->recall_list();
for(std::vector<unit>::iterator j = avail_units.begin(); j != avail_units.end();) {
scoped_recall_unit auto_store("this_unit", pi->save_id(), j - avail_units.begin());
if (j->matches_filter(cfg, map_location())) {
j = avail_units.erase(j);
} else {
++j;
}
}
}
}
}
WML_HANDLER_FUNCTION(label, /*event_info*/, cfg)
{
game_display &screen = *resources::screen;
terrain_label label(screen.labels(), cfg.get_config());
screen.labels().set_label(label.location(), label.text(),
label.team_name(), label.color(), label.visible_in_fog(), label.visible_in_shroud(), label.immutable());
}
WML_HANDLER_FUNCTION(lift_fog, /*event_info*/, cfg)
{
toggle_fog(true, cfg, !cfg["multiturn"].to_bool(false));
}
/// Display a message dialog
WML_HANDLER_FUNCTION(message, event_info, cfg)
{
// Check if there is any input to be made, if not the message may be skipped
const vconfig::child_list menu_items = cfg.get_children("option");
const vconfig::child_list text_input_elements = cfg.get_children("text_input");
const bool has_text_input = (text_input_elements.size() == 1);
bool has_input= (has_text_input || !menu_items.empty() );
// skip messages during quick replay
play_controller *controller = resources::controller;
if(!has_input && (
controller->is_skipping_replay() ||
context::skip_messages()
))
{
return;
}
// Check if this message is for this side
std::string side_for_raw = cfg["side_for"];
if (!side_for_raw.empty())
{
/* Always ignore side_for when the message has some input
boxes, but display the error message only if side_for is
used for an inactive side. */
bool side_for_show = has_input;
if (has_input && side_for_raw != str_cast(resources::controller->current_side()))
lg::wml_error << "[message]side_for= cannot query any user input out of turn.\n";
std::vector<std::string> side_for =
utils::split(side_for_raw, ',', utils::STRIP_SPACES | utils::REMOVE_EMPTY);
std::vector<std::string>::iterator itSide;
size_t side;
// Check if any of side numbers are human controlled
for (itSide = side_for.begin(); itSide != side_for.end(); ++itSide)
{
side = lexical_cast_default<size_t>(*itSide);
// Make sanity check that side number is good
// then check if this side is human controlled.
if (side > 0 && side <= resources::teams->size() &&
(*resources::teams)[side-1].is_human())
{
side_for_show = true;
break;
}
}
if (!side_for_show)
{
DBG_NG << "player isn't controlling side which should get message\n";
return;
}
}
unit_map::iterator speaker = handle_speaker(event_info, cfg, cfg["scroll"].to_bool(true));
if (speaker == resources::units->end() && cfg["speaker"] != "narrator") {
// No matching unit found, so the dialog can't come up.
// Continue onto the next message.
WRN_NG << "cannot show message\n";
return;
}
std::vector<std::string> options;
std::vector<vconfig::child_list> option_events;
for(vconfig::child_list::const_iterator mi = menu_items.begin();
mi != menu_items.end(); ++mi) {
std::string msg_str = (*mi)["message"];
if (!mi->has_child("show_if")
|| conditional_passed(mi->child("show_if")))
{
options.push_back(msg_str);
option_events.push_back((*mi).get_children("command"));
}
}
has_input = !options.empty() || has_text_input;
if (!has_input && get_replay_source().is_skipping()) {
// No input to get and the user is not interested either.
return;
}
if (cfg.has_attribute("sound")) {
sound::play_sound(cfg["sound"]);
}
if(text_input_elements.size()>1) {
lg::wml_error << "too many text_input tags, only one accepted\n";
}
const vconfig text_input_element = has_text_input ?
text_input_elements.front() : vconfig::empty_vconfig();
int option_chosen = 0;
std::string text_input_result;
DBG_DP << "showing dialog...\n";
message_user_choice msg(cfg, speaker, text_input_element, has_text_input,
options);
if (!has_input)
{
/* Always show the dialog if it has no input, whether we are
replaying or not. */
msg.query_user();
}
else
{
config choice = mp_sync::get_user_choice("input", msg, 0, true);
option_chosen = choice["value"];
text_input_result = choice["text"].str();
}
// Implement the consequences of the choice
if(options.empty() == false) {
if(size_t(option_chosen) >= menu_items.size()) {
std::stringstream errbuf;
errbuf << "invalid choice (" << option_chosen
<< ") was specified, choice 0 to " << (menu_items.size() - 1)
<< " was expected.\n";
replay::process_error(errbuf.str());
return;
}
BOOST_FOREACH(const vconfig &cmd, option_events[option_chosen]) {
handle_event_commands(event_info, cmd);
}
}
if(has_text_input) {
std::string variable_name=text_input_element["variable"];
if(variable_name.empty())
variable_name="input";
resources::gamedata->set_variable(variable_name, text_input_result);
}
}
WML_HANDLER_FUNCTION(modify_ai, /*event_info*/, cfg)
{
const vconfig& filter_side = cfg.child("filter_side");
std::vector<int> sides;
if(!filter_side.null()) {
// TODO: since 1.11.0-dev it seems
WRN_NG << "[modify_ai][filter_side] is deprecated, use only an inline SSF\n";
if(!cfg["side"].str().empty()) {
ERR_NG << "duplicate side information in [modify_ai]\n";
return;
}
side_filter ssf(filter_side);
sides = ssf.get_teams();
} else {
side_filter ssf(cfg);
sides = ssf.get_teams();
}
BOOST_FOREACH(const int &side_num, sides)
{
ai::manager::modify_active_ai_for_side(side_num,cfg.get_parsed_config());
}
}
WML_HANDLER_FUNCTION(modify_side, /*event_info*/, cfg)
{
std::vector<team> &teams = *resources::teams;
bool invalidate_screen = false;
std::string team_name = cfg["team_name"];
std::string user_team_name = cfg["user_team_name"];
std::string controller = cfg["controller"];
std::string recruit_str = cfg["recruit"];
std::string shroud_data = cfg["shroud_data"];
std::string village_support = cfg["village_support"];
const config& parsed = cfg.get_parsed_config();
const config::const_child_itors &ai = parsed.child_range("ai");
std::string switch_ai = cfg["switch_ai"];
std::vector<int> sides = get_sides_vector(cfg);
size_t team_index;
BOOST_FOREACH(const int &side_num, sides)
{
team_index = side_num - 1;
LOG_NG << "modifying side: " << side_num << "\n";
if(!team_name.empty()) {
LOG_NG << "change side's team to team_name '" << team_name << "'\n";
teams[team_index].change_team(team_name,
user_team_name);
} else if(!user_team_name.empty()) {
LOG_NG << "change side's user_team_name to '" << user_team_name << "'\n";
teams[team_index].change_team(teams[team_index].team_name(),
user_team_name);
}
// Modify recruit list (override)
if (!recruit_str.empty()) {
teams[team_index].set_recruits(utils::set_split(recruit_str));
}
// Modify income
config::attribute_value income = cfg["income"];
if (!income.empty()) {
teams[team_index].set_base_income(income.to_int() + game_config::base_income);
}
// Modify total gold
config::attribute_value gold = cfg["gold"];
if (!gold.empty()) {
teams[team_index].set_gold(gold);
}
// Set controller
if (!controller.empty()) {
teams[team_index].change_controller(controller);
}
// Set shroud
config::attribute_value shroud = cfg["shroud"];
if (!shroud.empty()) {
teams[team_index].set_shroud(shroud.to_bool(true));
invalidate_screen = true;
}
// Reset shroud
if ( cfg["reset_maps"].to_bool(false) ) {
teams[team_index].reshroud();
invalidate_screen = true;
}
// Merge shroud data
if (!shroud_data.empty()) {
teams[team_index].merge_shroud_map_data(shroud_data);
invalidate_screen = true;
}
// Set whether team is hidden in status table
config::attribute_value hidden = cfg["hidden"];
if (!hidden.empty()) {
teams[team_index].set_hidden(hidden.to_bool(true));
}
// Set fog
config::attribute_value fog = cfg["fog"];
if (!fog.empty()) {
teams[team_index].set_fog(fog.to_bool(true));
invalidate_screen = true;
}
// Reset fog
if ( cfg["reset_view"].to_bool(false) ) {
teams[team_index].refog();
invalidate_screen = true;
}
// Set income per village
config::attribute_value village_gold = cfg["village_gold"];
if (!village_gold.empty()) {
teams[team_index].set_village_gold(village_gold);
}
// Set support (unit levels supported per village, for upkeep purposes)
if (!village_support.empty()) {
teams[team_index].set_village_support(lexical_cast_default<int>(village_support, game_config::village_support));
}
// Redeploy ai from location (this ignores current AI parameters)
if (!switch_ai.empty()) {
ai::manager::add_ai_for_side_from_file(side_num,switch_ai,true);
}
// Override AI parameters
if (ai.first != ai.second) {
ai::manager::modify_active_ai_config_old_for_side(side_num,ai);
}
// Change team color
config::attribute_value color = cfg["color"];
if(!color.empty()) {
teams[team_index].set_color(color);
invalidate_screen = true;
}
// Add shared view to current team
config::attribute_value share_view = cfg["share_view"];
if (!share_view.empty()){
teams[team_index].set_share_view(share_view.to_bool(true));
team::clear_caches();
invalidate_screen = true;
}
// Add shared maps to current team
// IMPORTANT: this MUST happen *after* share_view is changed
config::attribute_value share_maps = cfg["share_maps"];
if (!share_maps.empty()){
teams[team_index].set_share_maps(share_maps.to_bool(true));
team::clear_caches();
invalidate_screen = true;
}
// Suppress end turn confirmations?
config::attribute_value setc = cfg["suppress_end_turn_confirmation"];
if ( !setc.empty() ) {
teams[team_index].set_no_turn_confirmation(setc.to_bool());
}
}
// Flag an update of the screen, if needed.
if ( invalidate_screen ) {
resources::screen->recalculate_minimap();
resources::screen->invalidate_all();
}
}
WML_HANDLER_FUNCTION(modify_turns, /*event_info*/, cfg)
{
config::attribute_value value = cfg["value"];
std::string add = cfg["add"];
config::attribute_value current = cfg["current"];
tod_manager& tod_man = *resources::tod_manager;
if(!add.empty()) {
tod_man.modify_turns(add);
} else if(!value.empty()) {
tod_man.set_number_of_turns(value.to_int(-1));
}
// change current turn only after applying mods
if(!current.empty()) {
const unsigned int current_turn_number = tod_man.turn();
int new_turn_number = current.to_int(current_turn_number);
const unsigned int new_turn_number_u = static_cast<unsigned int>(new_turn_number);
if(new_turn_number_u < 1 || (new_turn_number > tod_man.number_of_turns() && tod_man.number_of_turns() != -1)) {
ERR_NG << "attempted to change current turn number to one out of range (" << new_turn_number << ")\n";
} else if(new_turn_number_u != current_turn_number) {
tod_man.set_turn(new_turn_number_u);
resources::screen->new_turn();
}
}
}
/// Moving a 'unit' - i.e. a dummy unit
/// that is just moving for the visual effect
WML_HANDLER_FUNCTION(move_unit_fake, /*event_info*/, cfg)
{
util::unique_ptr<unit> dummy_unit(create_fake_unit(cfg));
if(!dummy_unit.get())
return;
const bool force_scroll = cfg["force_scroll"].to_bool(true);
const std::string x = cfg["x"];
const std::string y = cfg["y"];
const std::vector<std::string> xvals = utils::split(x);
const std::vector<std::string> yvals = utils::split(y);
const std::vector<map_location>& path = fake_unit_path(*dummy_unit, xvals, yvals);
if (!path.empty()) {
// Always scroll.
unit_display::move_unit(path, *dummy_unit, true, map_location::NDIRECTIONS, force_scroll);
}
}
WML_HANDLER_FUNCTION(move_units_fake, /*event_info*/, cfg)
{
LOG_NG << "Processing [move_units_fake]\n";
const vconfig::child_list unit_cfgs = cfg.get_children("fake_unit");
size_t num_units = unit_cfgs.size();
boost::scoped_array<util::unique_ptr<game_display::fake_unit> > units(
new util::unique_ptr<game_display::fake_unit>[num_units]);
std::vector<std::vector<map_location> > paths;
paths.reserve(num_units);
game_display* disp = game_display::get_singleton();
LOG_NG << "Moving " << num_units << " units\n";
size_t longest_path = 0;
BOOST_FOREACH(const vconfig& config, unit_cfgs) {
const std::vector<std::string> xvals = utils::split(config["x"]);
const std::vector<std::string> yvals = utils::split(config["y"]);
int skip_steps = config["skip_steps"];
game_display::fake_unit *u = create_fake_unit(config);
units[paths.size()].reset(u);
paths.push_back(fake_unit_path(*u, xvals, yvals));
if(skip_steps > 0)
paths.back().insert(paths.back().begin(), skip_steps, paths.back().front());
longest_path = std::max(longest_path, paths.back().size());
DBG_NG << "Path " << paths.size() - 1 << " has length " << paths.back().size() << '\n';
u->set_location(paths.back().front());
u->place_on_game_display(disp);
}
LOG_NG << "Units placed, longest path is " << longest_path << " long\n";
std::vector<map_location> path_step(2);
path_step.resize(2);
for(size_t step = 1; step < longest_path; ++step) {
DBG_NG << "Doing step " << step << "...\n";
for(size_t un = 0; un < num_units; ++un) {
if(step >= paths[un].size() || paths[un][step - 1] == paths[un][step])
continue;
DBG_NG << "Moving unit " << un << ", doing step " << step << '\n';
path_step[0] = paths[un][step - 1];
path_step[1] = paths[un][step];
unit_display::move_unit(path_step, *units[un]);
units[un]->set_location(path_step[1]);
units[un]->set_standing();
}
}
LOG_NG << "Units moved\n";
for(size_t un = 0; un < num_units; ++un) {
units[un]->remove_from_game_display();
}
LOG_NG << "Units removed\n";
}
WML_HANDLER_FUNCTION(object, event_info, cfg)
{
const vconfig filter = cfg.child("filter");
std::string id = cfg["id"];
// If this item has already been used
if ( item_used(id) )
return;
std::string image = cfg["image"];
std::string caption = cfg["name"];
std::string text;
map_location loc;
if(!filter.null()) {
BOOST_FOREACH(const unit &u, *resources::units) {
if ( u.matches_filter(filter) ) {
loc = u.get_location();
break;
}
}
}
if(loc.valid() == false) {
loc = event_info.loc1;
}
const unit_map::iterator u = resources::units->find(loc);
std::string command_type = "then";
if ( u != resources::units->end() && (filter.null() || u->matches_filter(filter)) )
{
///@deprecated This can be removed (and a proper duration=level implemented) after 1.11.2
/// Don't forget to remove it from wmllint too!
const std::string& duration = cfg["duration"];
if (duration == "level") {
lg::wml_error << "[object]duration=level is deprecated. Use duration=scenario instead.\n";
}
text = cfg["description"].str();
if(cfg["delayed_variable_substitution"].to_bool(false))
u->add_modification("object", cfg.get_config());
else
u->add_modification("object", cfg.get_parsed_config());
resources::screen->select_hex(event_info.loc1);
resources::screen->invalidate_unit();
// Mark this item as used up.
item_used(id, true);
} else {
text = cfg["cannot_use_message"].str();
command_type = "else";
}
if (!cfg["silent"].to_bool())
{
// Redraw the unit, with its new stats
resources::screen->draw();
try {
gui2::show_transient_message(resources::screen->video(), caption, text, image, true);
} catch(utils::invalid_utf8_exception&) {
// we already had a warning so do nothing.
}
}
BOOST_FOREACH(const vconfig &cmd, cfg.get_children(command_type)) {
handle_event_commands(event_info, cmd);
}
}
WML_HANDLER_FUNCTION(open_help, /*event_info*/, cfg)
{
game_display &screen = *resources::screen;
t_string topic_id = cfg["topic"];
help::show_help(screen, topic_id.to_serialized());
}
WML_HANDLER_FUNCTION(place_shroud, /*event_info*/,cfg)
{
toggle_shroud(false,cfg );
}
WML_HANDLER_FUNCTION(print, /*event_info*/, cfg)
{
// Remove any old message.
if (floating_label)
font::remove_floating_label(floating_label);
// Display a message on-screen
std::string text = cfg["text"];
if(text.empty())
return;
int size = cfg["size"].to_int(font::SIZE_SMALL);
int lifetime = cfg["duration"].to_int(50);
SDL_Color color = create_color(cfg["red"], cfg["green"], cfg["blue"]);
const SDL_Rect& rect = resources::screen->map_outside_area();
font::floating_label flabel(text);
flabel.set_font_size(size);
flabel.set_color(color);
flabel.set_position(rect.w/2,rect.h/2);
flabel.set_lifetime(lifetime);
flabel.set_clip_rect(rect);
floating_label = font::add_floating_label(flabel);
}
/// If we should recall units that match a certain description.
WML_HANDLER_FUNCTION(recall, /*event_info*/, cfg)
{
LOG_NG << "recalling unit...\n";
config temp_config(cfg.get_config());
// Prevent the recall unit filter from using the location as a criterion
/**
* @todo FIXME: we should design the WML to avoid these types of
* collisions; filters should be named consistently and always have a
* distinct scope.
*/
temp_config["x"] = "recall";
temp_config["y"] = "recall";
vconfig unit_filter(temp_config);
const vconfig leader_filter = cfg.child("secondary_unit");
for(int index = 0; index < int(resources::teams->size()); ++index) {
LOG_NG << "for side " << index + 1 << "...\n";
const std::string player_id = (*resources::teams)[index].save_id();
if((*resources::teams)[index].recall_list().size() < 1) {
DBG_NG << "recall list is empty when trying to recall!\n"
<< "player_id: " << player_id << " side: " << index+1 << "\n";
continue;
}
std::vector<unit>& avail = (*resources::teams)[index].recall_list();
std::vector<unit_map::unit_iterator> leaders = resources::units->find_leaders(index + 1);
for(std::vector<unit>::iterator u = avail.begin(); u != avail.end(); ++u) {
DBG_NG << "checking unit against filter...\n";
scoped_recall_unit auto_store("this_unit", player_id, u - avail.begin());
if (u->matches_filter(unit_filter, map_location())) {
DBG_NG << u->id() << " matched the filter...\n";
const unit to_recruit(*u);
const unit* pass_check = &to_recruit;
if(!cfg["check_passability"].to_bool(true)) pass_check = NULL;
const map_location cfg_loc = cfg_to_loc(cfg);
/// @todo fendrin: comment this monster
BOOST_FOREACH(unit_map::const_unit_iterator leader, leaders) {
DBG_NG << "...considering " + leader->id() + " as the recalling leader...\n";
map_location loc = cfg_loc;
if ( (leader_filter.null() || leader->matches_filter(leader_filter)) &&
u->matches_filter(vconfig(leader->recall_filter()), map_location()) ) {
DBG_NG << "...matched the leader filter and is able to recall the unit.\n";
if(!resources::game_map->on_board(loc))
loc = leader->get_location();
if(pass_check || (resources::units->count(loc) > 0))
loc = pathfind::find_vacant_tile(loc, pathfind::VACANT_ANY, pass_check);
if(resources::game_map->on_board(loc)) {
DBG_NG << "...valid location for the recall found. Recalling.\n";
avail.erase(u); // Erase before recruiting, since recruiting can fire more events
actions::place_recruit(to_recruit, loc, leader->get_location(), 0, true,
cfg["show"].to_bool(true), cfg["fire_event"].to_bool(false),
true, true);
return;
}
}
}
if (resources::game_map->on_board(cfg_loc)) {
map_location loc = cfg_loc;
if(pass_check || (resources::units->count(loc) > 0))
loc = pathfind::find_vacant_tile(loc, pathfind::VACANT_ANY, pass_check);
// Check if we still have a valid location
if (resources::game_map->on_board(loc)) {
DBG_NG << "No usable leader found, but found usable location. Recalling.\n";
avail.erase(u); // Erase before recruiting, since recruiting can fire more events
map_location null_location = map_location::null_location;
actions::place_recruit(to_recruit, loc, null_location, 0, true, cfg["show"].to_bool(true),
cfg["fire_event"].to_bool(false), true, true);
return;
}
}
}
}
}
LOG_WML << "A [recall] tag with the following content failed:\n" << cfg.get_config().debug();
}
WML_HANDLER_FUNCTION(redraw, /*event_info*/, cfg)
{
game_display &screen = *resources::screen;
const config::attribute_value clear_shroud_av = cfg["clear_shroud"];
const config::attribute_value side = cfg["side"];
bool clear_shroud_bool = clear_shroud_av.to_bool(false);
if(clear_shroud_av.blank() && !side.blank()) {
//Backwards compat, behavior of the tag was to clear shroud in case that side= is given.
clear_shroud_bool = true;
}
if (clear_shroud_bool) {
side_filter filter(cfg);
BOOST_FOREACH(const int side, filter.get_teams()){
actions::clear_shroud(side);
}
screen.recalculate_minimap();
}
if ( context::screen_needs_rebuild() ) {
context::screen_needs_rebuild(false);
screen.recalculate_minimap();
screen.rebuild_all();
}
screen.invalidate_all();
screen.draw(true,true);
}
WML_HANDLER_FUNCTION(remove_shroud, /*event_info*/, cfg)
{
toggle_shroud(true,cfg);
}
WML_HANDLER_FUNCTION(remove_sound_source, /*event_info*/, cfg)
{
resources::soundsources->remove(cfg["id"]);
}
/// Experimental map replace
/// @todo Finish experimenting.
WML_HANDLER_FUNCTION(replace_map, /*event_info*/, cfg)
{
/*
* When a hex changes from a village terrain to a non-village terrain, and
* a team owned that village it loses that village. When a hex changes from
* a non-village terrain to a village terrain and there is a unit on that
* hex it does not automatically capture the village. The reason for not
* capturing villages it that there are too many choices to make; should a
* unit loose its movement points, should capture events be fired. It is
* easier to do this as wanted by the author in WML.
*/
gamemap *game_map = resources::game_map;
gamemap map(*game_map);
/* Remember the locations where a village is owned by a side. */
std::map<map_location, int> villages;
FOREACH(const AUTO& village, map.villages()) {
const int owner = village_owner(village);
if(owner != -1) {
villages[village] = owner;
}
}
try {
if (cfg["map"].empty()) {
const vconfig& map_cfg = cfg.child("map");
map.read(map_cfg["data"], false, map_cfg["border_size"].to_int(), map_cfg["usage"].str());
}
else map.read(cfg["map"], false);
} catch(incorrect_map_format_error&) {
lg::wml_error << "replace_map: Unable to load map " << cfg["map"] << "\n";
return;
} catch(twml_exception& e) {
e.show(*resources::screen);
return;
}
if (map.total_width() > game_map->total_width()
|| map.total_height() > game_map->total_height()) {
if (!cfg["expand"].to_bool()) {
lg::wml_error << "replace_map: Map dimension(s) increase but expand is not set\n";
return;
}
}
if (map.total_width() < game_map->total_width()
|| map.total_height() < game_map->total_height()) {
if (!cfg["shrink"].to_bool()) {
lg::wml_error << "replace_map: Map dimension(s) decrease but shrink is not set\n";
return;
}
unit_map *units = resources::units;
unit_map::iterator itor;
for (itor = units->begin(); itor != units->end(); ) {
if (!map.on_board(itor->get_location())) {
if (!try_add_unit_to_recall_list(itor->get_location(), *itor)) {
lg::wml_error << "replace_map: Cannot add a unit that would become off-map to the recall list\n";
}
units->erase(itor++);
} else {
++itor;
}
}
}
/* Disown villages that are no longer villages. */
FOREACH(const AUTO& village, villages) {
if(!map.is_village(village.first)) {
(*resources::teams)[village.second].lose_village(village.first);
}
}
*game_map = map;
resources::screen->reload_map();
context::screen_needs_rebuild(true);
ai::manager::raise_map_changed();
}
/// Replacing the current time of day schedule.
WML_HANDLER_FUNCTION(replace_schedule, /*event_info*/, cfg)
{
if(cfg.get_children("time").empty()) {
ERR_NG << "attempted to to replace ToD schedule with empty schedule\n";
} else {
resources::tod_manager->replace_schedule(cfg.get_parsed_config());
resources::screen->new_turn();
LOG_NG << "replaced ToD schedule\n";
}
}
WML_HANDLER_FUNCTION(reset_fog, /*event_info*/, cfg)
{
toggle_fog(false, cfg, cfg["reset_view"].to_bool(false));
}
WML_HANDLER_FUNCTION(role, /*event_info*/, cfg)
{
bool found = false;
// role= represents the instruction, so we can't filter on it
config item = cfg.get_config();
item.remove_attribute("role");
vconfig filter(item);
// try to match units on the gamemap before the recall lists
std::vector<std::string> types = utils::split(filter["type"]);
const bool has_any_types = !types.empty();
std::vector<std::string>::iterator ti = types.begin(),
ti_end = types.end();
// loop to give precedence based on type order
do {
if (has_any_types) {
item["type"] = *ti;
}
unit_map::iterator itor;
BOOST_FOREACH(unit &u, *resources::units) {
if ( u.matches_filter(filter) ) {
u.set_role(cfg["role"]);
found = true;
break;
}
}
} while(!found && has_any_types && ++ti != ti_end);
if(!found) {
// next try to match units on the recall lists
std::set<std::string> player_ids;
std::vector<std::string> sides = utils::split(cfg["side"]);
const bool has_any_sides = !sides.empty();
BOOST_FOREACH(std::string const& side_str, sides) {
size_t side_num = lexical_cast_default<size_t>(side_str,0);
if(side_num > 0 && side_num <= resources::teams->size()) {
player_ids.insert((resources::teams->begin() + (side_num - 1))->save_id());
}
}
// loop to give precedence based on type order
std::vector<std::string>::iterator ti = types.begin();
do {
if (has_any_types) {
item["type"] = *ti;
}
std::vector<team>::iterator pi,
pi_end = resources::teams->end();
for (pi = resources::teams->begin(); pi != pi_end; ++pi)
{
std::string const& player_id = pi->save_id();
// Verify the filter's side= includes this player
if(has_any_sides && !player_ids.count(player_id)) {
continue;
}
// Iterate over the player's recall list to find a match
for(size_t i=0; i < pi->recall_list().size(); ++i) {
unit& u = pi->recall_list()[i];
scoped_recall_unit auto_store("this_unit", player_id, i);
if (u.matches_filter(filter, map_location())) {
u.set_role(cfg["role"]);
found=true;
break;
}
}
}
} while(!found && has_any_types && ++ti != ti_end);
}
}
WML_HANDLER_FUNCTION(scroll, /*event_info*/, cfg)
{
game_display &screen = *resources::screen;
screen.scroll(cfg["x"], cfg["y"], true);
screen.draw(true,true);
}
/// Experimental data persistence
/// @todo Finish experimenting.
WML_HANDLER_FUNCTION(set_global_variable,/**/,pcfg)
{
if (get_replay_source().at_end() || (network::nconnections() != 0))
verify_and_set_global_variable(pcfg);
}
/// Setting of menu items
WML_HANDLER_FUNCTION(set_menu_item, /*event_info*/, cfg)
{
/*
[set_menu_item]
id=test1
image="buttons/group_all.png"
description="Summon Troll"
[show_if]
[not]
[have_unit]
x,y=$x1,$y1
[/have_unit]
[/not]
[/show_if]
[filter_location]
[/filter_location]
[command]
{LOYAL_UNIT $side_number (Troll) $x1 $y1 (Myname) ( _ "Myname")}
[/command]
[/set_menu_item]
*/
std::string id = cfg["id"];
wml_menu_item*& mref = resources::gamedata->get_wml_menu_items().get_item(id);
if(mref == NULL) {
mref = new wml_menu_item(id);
}
if(cfg.has_attribute("image")) {
mref->image = cfg["image"].str();
}
if(cfg.has_attribute("description")) {
mref->description = cfg["description"];
}
if(cfg.has_attribute("needs_select")) {
mref->needs_select = cfg["needs_select"].to_bool();
}
if(cfg.has_child("show_if")) {
mref->show_if = cfg.child("show_if").get_config();
}
if(cfg.has_child("filter_location")) {
mref->filter_location = cfg.child("filter_location").get_config();
}
if(cfg.has_child("command")) {
const vconfig& cmd = cfg.child("command");
const bool delayed = cmd["delayed_variable_substitution"].to_bool(true);
add_wmi_change(id, delayed ? cmd.get_config() : cmd.get_parsed_config());
}
}
WML_HANDLER_FUNCTION(set_variable, /*event_info*/, cfg)
{
game_data *gameinfo = resources::gamedata;
const std::string name = cfg["name"];
if(name.empty()) {
ERR_NG << "trying to set a variable with an empty name:\n" << cfg.get_config().debug();
return;
}
config::attribute_value &var = gameinfo->get_variable(name);
config::attribute_value literal = cfg.get_config()["literal"]; // no $var substitution
if (!literal.blank()) {
var = literal;
}
config::attribute_value value = cfg["value"];
if (!value.blank()) {
var = value;
}
const std::string to_variable = cfg["to_variable"];
if(to_variable.empty() == false) {
var = gameinfo->get_variable(to_variable);
}
config::attribute_value add = cfg["add"];
if (!add.empty()) {
var = var.to_double() + add.to_double();
}
config::attribute_value sub = cfg["sub"];
if (!sub.empty()) {
var = var.to_double() - sub.to_double();
}
config::attribute_value multiply = cfg["multiply"];
if (!multiply.empty()) {
var = var.to_double() * multiply.to_double();
}
config::attribute_value divide = cfg["divide"];
if (!divide.empty()) {
if (divide.to_double() == 0) {
ERR_NG << "division by zero on variable " << name << "\n";
return;
}
var = var.to_double() / divide.to_double();
}
config::attribute_value modulo = cfg["modulo"];
if (!modulo.empty()) {
if (modulo.to_double() == 0) {
ERR_NG << "division by zero on variable " << name << "\n";
return;
}
var = std::fmod(var.to_double(), modulo.to_double());
}
config::attribute_value round_val = cfg["round"];
if (!round_val.empty()) {
double value = var.to_double();
if (round_val == "ceil") {
var = int(std::ceil(value));
} else if (round_val == "floor") {
var = int(std::floor(value));
} else {
// We assume the value is an integer.
// Any non-numerical values will be interpreted as 0
// Which is probably what was intended anyway
int decimals = round_val.to_int();
value *= std::pow(10.0, decimals); //add $decimals zeroes
value = round_portable(value); // round() isn't implemented everywhere
value *= std::pow(10.0, -decimals); //and remove them
var = value;
}
}
config::attribute_value ipart = cfg["ipart"];
if (!ipart.empty()) {
double result;
std::modf(ipart.to_double(), &result);
var = int(result);
}
config::attribute_value fpart = cfg["fpart"];
if (!fpart.empty()) {
double ignore;
var = std::modf(fpart.to_double(), &ignore);
}
config::attribute_value string_length_target = cfg["string_length"];
if (!string_length_target.blank()) {
var = int(string_length_target.str().size());
}
// Note: maybe we add more options later, eg. strftime formatting.
// For now make the stamp mandatory.
const std::string time = cfg["time"];
if(time == "stamp") {
var = int(SDL_GetTicks());
}
// Random generation works as follows:
// rand=[comma delimited list]
// Each element in the list will be considered a separate choice,
// unless it contains "..". In this case, it must be a numerical
// range (i.e. -1..-10, 0..100, -10..10, etc).
const std::string rand = cfg["rand"];
// The new random generator, the logic is a copy paste of the old random.
if(rand.empty() == false) {
assert(gameinfo);
// A default value in case something goes really wrong.
var = "";
std::string word;
std::vector<std::string> words;
std::vector<std::pair<long,long> > ranges;
long num_choices = 0;
std::string::size_type pos = 0, pos2 = std::string::npos;
std::stringstream ss(std::stringstream::in|std::stringstream::out);
while (pos2 != rand.length()) {
pos = pos2+1;
pos2 = rand.find(",", pos);
if (pos2 == std::string::npos)
pos2 = rand.length();
word = rand.substr(pos, pos2-pos);
words.push_back(word);
std::string::size_type tmp = word.find("..");
if (tmp == std::string::npos) {
// Treat this element as a string
ranges.push_back(std::pair<long, long>(0,0));
num_choices += 1;
}
else {
// Treat as a numerical range
const std::string first = word.substr(0, tmp);
const std::string second = word.substr(tmp+2,
rand.length());
long low, high;
ss << first + " " + second;
if ( !(ss >> low) || !(ss >> high) ) {
ERR_NG << "Malformed range: rand = \"" << rand << "\"\n";
// Treat this element as a string?
ranges.push_back(std::pair<long, long>(0,0));
num_choices += 1;
}
else {
if (low > high) {
std::swap(low, high);
}
ranges.push_back(std::pair<long, long>(low,high));
num_choices += (high - low) + 1;
// Make 0..0 ranges work
if (high == 0 && low == 0) {
words.pop_back();
words.push_back("0");
}
}
ss.clear();
}
}
/*
* Choice gets a value in the range [0..32768).
* So need to add a second set of random values when a value
* outside the range is requested.
*/
if(num_choices > 0x3fffffff) {
WRN_NG << "Requested random number with an upper bound of "
<< num_choices
<< " however the maximum number generated will be "
<< 0x3fffffff
<< ".\n";
}
long choice = gameinfo->rng().get_next_random();
if(num_choices >= 32768) {
choice <<= 15;
choice += gameinfo->rng().get_next_random();
}
choice %= num_choices;
long tmp = 0;
for(size_t i = 0; i < ranges.size(); ++i) {
tmp += (ranges[i].second - ranges[i].first) + 1;
if (tmp > choice) {
if (ranges[i].first == 0 && ranges[i].second == 0) {
var = words[i];
}
else {
var = (ranges[i].second - (tmp - choice)) + 1;
}
break;
}
}
}
const vconfig::child_list join_elements = cfg.get_children("join");
if(!join_elements.empty())
{
const vconfig join_element=join_elements.front();
std::string array_name=join_element["variable"];
std::string separator=join_element["separator"];
std::string key_name=join_element["key"];
if(key_name.empty())
{
key_name="value";
}
bool remove_empty = join_element["remove_empty"].to_bool();
std::string joined_string;
variable_info vi(array_name, true, variable_info::TYPE_ARRAY);
bool first = true;
BOOST_FOREACH(const config &cfg, vi.as_array())
{
std::string current_string = cfg[key_name];
if (remove_empty && current_string.empty()) continue;
if (first) first = false;
else joined_string += separator;
joined_string += current_string;
}
var=joined_string;
}
}
WML_HANDLER_FUNCTION(set_variables, /*event_info*/, cfg)
{
const t_string& name = cfg["name"];
variable_info dest(name, true, variable_info::TYPE_CONTAINER);
if(name.empty()) {
ERR_NG << "trying to set a variable with an empty name:\n" << cfg.get_config().debug();
return;
}
std::string mode = cfg["mode"]; // replace, append, merge, or insert
if(mode == "extend") {
mode = "append";
} else if(mode != "append" && mode != "merge") {
if(mode == "insert") {
size_t child_count = dest.vars->child_count(dest.key);
if(dest.index >= child_count) {
while(dest.index >= ++child_count) {
//inserting past the end requires empty data
dest.vars->append(config(dest.key));
}
//inserting at the end is handled by an append
mode = "append";
}
} else {
mode = "replace";
}
}
const vconfig::child_list values = cfg.get_children("value");
const vconfig::child_list literals = cfg.get_children("literal");
const vconfig::child_list split_elements = cfg.get_children("split");
config data;
if(cfg.has_attribute("to_variable"))
{
variable_info tovar(cfg["to_variable"], false, variable_info::TYPE_CONTAINER);
if(tovar.is_valid) {
if(tovar.explicit_index) {
data.add_child(dest.key, tovar.as_container());
} else {
variable_info::array_range range = tovar.as_array();
while(range.first != range.second)
{
data.add_child(dest.key, *range.first++);
}
}
}
} else if(!values.empty()) {
for(vconfig::child_list::const_iterator i=values.begin(); i!=values.end(); ++i)
{
data.add_child(dest.key, (*i).get_parsed_config());
}
} else if(!literals.empty()) {
for(vconfig::child_list::const_iterator i=literals.begin(); i!=literals.end(); ++i)
{
data.add_child(dest.key, i->get_config());
}
} else if(!split_elements.empty()) {
const vconfig split_element=split_elements.front();
std::string split_string=split_element["list"];
std::string separator_string=split_element["separator"];
std::string key_name=split_element["key"];
if(key_name.empty())
{
key_name="value";
}
bool remove_empty = split_element["remove_empty"].to_bool();
char* separator = separator_string.empty() ? NULL : &separator_string[0];
std::vector<std::string> split_vector;
//if no separator is specified, explode the string
if(separator == NULL)
{
for(std::string::iterator i=split_string.begin(); i!=split_string.end(); ++i)
{
split_vector.push_back(std::string(1, *i));
}
}
else {
split_vector=utils::split(split_string, *separator, remove_empty ? utils::REMOVE_EMPTY | utils::STRIP_SPACES : utils::STRIP_SPACES);
}
for(std::vector<std::string>::iterator i=split_vector.begin(); i!=split_vector.end(); ++i)
{
data.add_child(dest.key)[key_name]=*i;
}
}
if(mode == "replace")
{
if(dest.explicit_index) {
dest.vars->remove_child(dest.key, dest.index);
} else {
dest.vars->clear_children(dest.key);
}
}
if(!data.empty())
{
if(mode == "merge")
{
if(dest.explicit_index) {
// merging multiple children into a single explicit index
// requires that they first be merged with each other
data.merge_children(dest.key);
dest.as_container().merge_with(data.child(dest.key));
} else {
dest.vars->merge_with(data);
}
} else if(mode == "insert" || dest.explicit_index) {
BOOST_FOREACH(const config &child, data.child_range(dest.key))
{
dest.vars->add_child_at(dest.key, child, dest.index++);
}
} else {
dest.vars->append(data);
}
}
}
WML_HANDLER_FUNCTION(sound_source, /*event_info*/, cfg)
{
soundsource::sourcespec spec(cfg.get_parsed_config());
resources::soundsources->add(spec);
}
/// Store time of day config in a WML variable. This is useful for those who
/// are too lazy to calculate the corresponding time of day for a given turn,
/// or if the turn / time-of-day sequence mutates in a scenario.
WML_HANDLER_FUNCTION(store_time_of_day, /*event_info*/, cfg)
{
const map_location loc = cfg_to_loc(cfg);
int turn = cfg["turn"];
// using 0 will use the current turn
const time_of_day& tod = resources::tod_manager->get_time_of_day(loc,turn);
std::string variable = cfg["variable"];
if(variable.empty()) {
variable = "time_of_day";
}
variable_info store(variable, true, variable_info::TYPE_CONTAINER);
tod.write(store.as_container());
}
WML_HANDLER_FUNCTION(teleport, event_info, cfg)
{
unit_map::iterator u = resources::units->find(event_info.loc1);
// Search for a valid unit filter, and if we have one, look for the matching unit
const vconfig filter = cfg.child("filter");
if(!filter.null()) {
for (u = resources::units->begin(); u != resources::units->end(); ++u){
if ( u->matches_filter(filter) )
break;
}
}
if (u == resources::units->end()) return;
// We have found a unit that matches the filter
const map_location dst = cfg_to_loc(cfg);
if (dst == u->get_location() || !resources::game_map->on_board(dst)) return;
const unit* pass_check = NULL;
if (cfg["check_passability"].to_bool(true))
pass_check = &*u;
const map_location vacant_dst = find_vacant_tile(dst, pathfind::VACANT_ANY, pass_check);
if (!resources::game_map->on_board(vacant_dst)) return;
// Clear the destination hex before the move (so the animation can be seen).
bool clear_shroud = cfg["clear_shroud"].to_bool(true);
actions::shroud_clearer clearer;
if ( clear_shroud ) {
clearer.clear_dest(vacant_dst, *u);
}
map_location src_loc = u->get_location();
std::vector<map_location> teleport_path;
teleport_path.push_back(src_loc);
teleport_path.push_back(vacant_dst);
bool animate = cfg["animate"].to_bool();
unit_display::move_unit(teleport_path, *u, animate);
resources::units->move(src_loc, vacant_dst);
unit::clear_status_caches();
u = resources::units->find(vacant_dst);
u->set_standing();
if ( clear_shroud ) {
// Now that the unit is visibly in position, clear the shroud.
clearer.clear_unit(vacant_dst, *u);
}
if (resources::game_map->is_village(vacant_dst)) {
actions::get_village(vacant_dst, u->side());
}
resources::screen->invalidate_unit_after_move(src_loc, vacant_dst);
resources::screen->draw();
// Sighted events.
clearer.fire_events();
}
/// Creating a mask of the terrain
WML_HANDLER_FUNCTION(terrain_mask, /*event_info*/, cfg)
{
map_location loc = cfg_to_loc(cfg, 1, 1);
gamemap mask_map(*resources::game_map);
//config level;
std::string mask = cfg["mask"];
std::string usage = "mask";
int border_size = 0;
if (mask.empty()) {
usage = cfg["usage"].str();
border_size = cfg["border_size"];
mask = cfg["data"].str();
}
try {
mask_map.read(mask, false, border_size, usage);
} catch(incorrect_map_format_error&) {
ERR_NG << "terrain mask is in the incorrect format, and couldn't be applied\n";
return;
} catch(twml_exception& e) {
e.show(*resources::screen);
return;
}
bool border = cfg["border"].to_bool();
resources::game_map->overlay(mask_map, cfg.get_parsed_config(), loc.x, loc.y, border);
context::screen_needs_rebuild(true);
}
/// Adding/removing new time_areas dynamically with Standard Location Filters.
WML_HANDLER_FUNCTION(time_area, /*event_info*/, cfg)
{
log_scope("time_area");
bool remove = cfg["remove"].to_bool();
std::string ids = cfg["id"];
if(remove) {
const std::vector<std::string> id_list =
utils::split(ids, ',', utils::STRIP_SPACES | utils::REMOVE_EMPTY);
BOOST_FOREACH(const std::string& id, id_list) {
resources::tod_manager->remove_time_area(id);
LOG_NG << "event WML removed time_area '" << id << "'\n";
}
}
else {
std::string id;
if(ids.find(',') != std::string::npos) {
id = utils::split(ids,',',utils::STRIP_SPACES | utils::REMOVE_EMPTY).front();
ERR_NG << "multiple ids for inserting a new time_area; will use only the first\n";
} else {
id = ids;
}
std::set<map_location> locs;
const terrain_filter filter(cfg, *resources::units);
filter.get_locations(locs, true);
config parsed_cfg = cfg.get_parsed_config();
resources::tod_manager->add_time_area(id, locs, parsed_cfg);
LOG_NG << "event WML inserted time_area '" << id << "'\n";
}
}
WML_HANDLER_FUNCTION(tunnel, /*event_info*/, cfg)
{
const bool remove = cfg["remove"].to_bool(false);
if (remove) {
const std::vector<std::string> ids = utils::split(cfg["id"]);
BOOST_FOREACH(const std::string &id, ids) {
resources::tunnels->remove(id);
}
} else if (cfg.get_children("source").empty() ||
cfg.get_children("target").empty() ||
cfg.get_children("filter").empty()) {
ERR_WML << "[tunnel] is missing a mandatory tag:\n"
<< cfg.get_config().debug();
} else {
pathfind::teleport_group tunnel(cfg, false);
resources::tunnels->add(tunnel);
if(cfg["bidirectional"].to_bool(true)) {
tunnel = pathfind::teleport_group(cfg, true);
resources::tunnels->add(tunnel);
}
}
}
/// If we should spawn a new unit on the map somewhere
WML_HANDLER_FUNCTION(unit, /*event_info*/, cfg)
{
config parsed_cfg = cfg.get_parsed_config();
config::attribute_value to_variable = cfg["to_variable"];
if (!to_variable.blank())
{
parsed_cfg.remove_attribute("to_variable");
unit new_unit(parsed_cfg, true, resources::state_of_game);
config &var = resources::gamedata->get_variable_cfg(to_variable);
var.clear();
new_unit.write(var);
if (const config::attribute_value *v = parsed_cfg.get("x")) var["x"] = *v;
if (const config::attribute_value *v = parsed_cfg.get("y")) var["y"] = *v;
return;
}
int side = parsed_cfg["side"].to_int(1);
if ((side<1)||(side > static_cast<int>(resources::teams->size()))) {
ERR_NG << "wrong side in [unit] tag - no such side: "<<side<<" ( number of teams :"<<resources::teams->size()<<")"<<std::endl;
DBG_NG << parsed_cfg.debug();
return;
}
team &tm = resources::teams->at(side-1);
unit_creator uc(tm,resources::game_map->starting_position(side));
uc
.allow_add_to_recall(true)
.allow_discover(true)
.allow_get_village(true)
.allow_invalidate(true)
.allow_rename_side(true)
.allow_show(true);
uc.add_unit(parsed_cfg, &cfg);
}
/// Unit serialization from variables
WML_HANDLER_FUNCTION(unstore_unit, /*event_info*/, cfg)
{
const config &var = resources::gamedata->get_variable_cfg(cfg["variable"]);
try {
config tmp_cfg(var);
const unit u(tmp_cfg, false);
preferences::encountered_units().insert(u.type_id());
map_location loc = cfg_to_loc(
(cfg.has_attribute("x") && cfg.has_attribute("y")) ? cfg : vconfig(var));
const bool advance = cfg["advance"].to_bool(true);
if(resources::game_map->on_board(loc)) {
if (cfg["find_vacant"].to_bool()) {
const unit* pass_check = NULL;
if (cfg["check_passability"].to_bool(true)) pass_check = &u;
loc = pathfind::find_vacant_tile(
loc,
pathfind::VACANT_ANY,
pass_check);
}
resources::units->erase(loc);
resources::units->add(loc, u);
std::string text = cfg["text"];
play_controller *controller = resources::controller;
if(!text.empty() && !controller->is_skipping_replay())
{
// Print floating label
resources::screen->float_label(loc, text, cfg["red"], cfg["green"], cfg["blue"]);
}
const int side = controller->current_side();
unsigned loop_limit = 5;
while ( advance
&& unit_helper::will_certainly_advance(resources::units->find(loc))
&& (loop_limit > 0) )
{
loop_limit--;
int total_opt = unit_helper::number_of_possible_advances(u);
bool use_dialog = side == u.side() &&
(*resources::teams)[side - 1].is_human();
config selected = mp_sync::get_user_choice("choose",
unstore_unit_advance_choice(total_opt, loc, use_dialog));
dialogs::animate_unit_advancement(loc, selected["value"], cfg["fire_event"].to_bool(false), cfg["animate"].to_bool(true));
}
} else {
if(advance && u.advances()) {
WRN_NG << "Cannot advance units when unstoring to the recall list.\n";
}
team& t = (*resources::teams)[u.side()-1];
if(t.persistent()) {
// Test whether the recall list has duplicates if so warn.
// This might be removed at some point but the uniqueness of
// the description is needed to avoid the recall duplication
// bugs. Duplicates here might cause the wrong unit being
// replaced by the wrong unit.
if(t.recall_list().size() > 1) {
std::vector<size_t> desciptions;
for(std::vector<unit>::const_iterator citor =
t.recall_list().begin();
citor != t.recall_list().end(); ++citor) {
const size_t desciption =
citor->underlying_id();
if(std::find(desciptions.begin(), desciptions.end(),
desciption) != desciptions.end()) {
lg::wml_error << "Recall list has duplicate unit "
"underlying_ids '" << desciption
<< "' unstore_unit may not work as expected.\n";
} else {
desciptions.push_back(desciption);
}
}
}
// Avoid duplicates in the list.
/**
* @todo it would be better to change recall_list() from
* a vector to a map and use the underlying_id as key.
*/
const size_t key = u.underlying_id();
for(std::vector<unit>::iterator itor =
t.recall_list().begin();
itor != t.recall_list().end(); ++itor) {
LOG_NG << "Replaced unit '"
<< key << "' on the recall list\n";
if(itor->underlying_id() == key) {
t.recall_list().erase(itor);
break;
}
}
t.recall_list().push_back(u);
} else {
ERR_NG << "Cannot unstore unit: recall list is empty for player " << u.side()
<< " and the map location is invalid.\n";
}
}
// If we unstore a leader make sure the team gets a leader if not the loading
// in MP might abort since a side without a leader has a recall list.
if(u.can_recruit()) {
(*resources::teams)[u.side() - 1].have_leader();
}
} catch (game::game_error &e) {
ERR_NG << "could not de-serialize unit: '" << e.message << "'\n";
}
}
WML_HANDLER_FUNCTION(volume, /*event_info*/, cfg)
{
int vol;
float rel;
std::string music = cfg["music"];
std::string sound = cfg["sound"];
if(!music.empty()) {
vol = preferences::music_volume();
rel = atof(music.c_str());
if (rel >= 0.0f && rel < 100.0f) {
vol = static_cast<int>(rel*vol/100.0f);
}
sound::set_music_volume(vol);
}
if(!sound.empty()) {
vol = preferences::sound_volume();
rel = atof(sound.c_str());
if (rel >= 0.0f && rel < 100.0f) {
vol = static_cast<int>(rel*vol/100.0f);
}
sound::set_sound_volume(vol);
}
}
WML_HANDLER_FUNCTION(wml_message, /*event_info*/, cfg)
{
handle_wml_log_message( cfg.get_parsed_config() );
}
} // end namespace game_events