mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-09 18:16:28 +00:00
3688 lines
108 KiB
C++
3688 lines
108 KiB
C++
/* $Id$ */
|
|
/*
|
|
Copyright (C) 2003 - 2012 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
|
|
* Processing of WML-events.
|
|
*/
|
|
|
|
#include "global.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_events.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_label.hpp"
|
|
#include "map_exception.hpp"
|
|
#include "pathfind/teleport.hpp"
|
|
#include "replay.hpp"
|
|
#include "reports.hpp"
|
|
#include "resources.hpp"
|
|
#include "scripting/lua.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 "play_controller.hpp"
|
|
#include "persist_var.hpp"
|
|
#include "whiteboard/manager.hpp"
|
|
|
|
#include <boost/scoped_array.hpp>
|
|
#include <boost/scoped_ptr.hpp>
|
|
#include <boost/lexical_cast.hpp>
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
|
|
#include <boost/foreach.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 DBG_WML LOG_STREAM(debug, log_wml)
|
|
#define LOG_WML LOG_STREAM(info, log_wml)
|
|
#define WRN_WML LOG_STREAM(warn, 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)
|
|
|
|
static lg::log_domain log_event_handler("event_handler");
|
|
#define DBG_EH LOG_STREAM(debug, log_event_handler)
|
|
|
|
/**
|
|
* State when processing a flight of events or commands.
|
|
*/
|
|
struct event_context
|
|
{
|
|
bool mutated;
|
|
bool skip_messages;
|
|
event_context(bool s): mutated(true), skip_messages(s) {}
|
|
};
|
|
|
|
static event_context default_context(false);
|
|
static event_context *current_context = &default_context;
|
|
|
|
/**
|
|
* Context state with automatic lifetime handling.
|
|
*/
|
|
struct scoped_context
|
|
{
|
|
event_context *old_context;
|
|
event_context new_context;
|
|
|
|
scoped_context()
|
|
: old_context(current_context)
|
|
, new_context(old_context != &default_context && old_context->skip_messages)
|
|
{
|
|
current_context = &new_context;
|
|
}
|
|
|
|
~scoped_context()
|
|
{
|
|
old_context->mutated |= new_context.mutated;
|
|
current_context = old_context;
|
|
}
|
|
};
|
|
|
|
static bool screen_needs_rebuild;
|
|
// The value returned by wml_tracking();
|
|
static size_t internal_wml_tracking = 0;
|
|
|
|
namespace {
|
|
|
|
std::stringstream wml_messages_stream;
|
|
|
|
bool manager_running = false;
|
|
int floating_label = 0;
|
|
|
|
typedef std::pair< std::string, config* > wmi_command_change;
|
|
std::vector< wmi_command_change > wmi_command_changes;
|
|
|
|
const gui::msecs prevent_misclick_duration = 10;
|
|
const gui::msecs average_frame_time = 30;
|
|
|
|
class pump_manager {
|
|
public:
|
|
pump_manager() :
|
|
x1_(resources::gamedata->get_variable("x1")),
|
|
x2_(resources::gamedata->get_variable("x2")),
|
|
y1_(resources::gamedata->get_variable("y1")),
|
|
y2_(resources::gamedata->get_variable("y2"))
|
|
{
|
|
++instance_count;
|
|
}
|
|
~pump_manager() {
|
|
resources::gamedata->get_variable("x1") = x1_;
|
|
resources::gamedata->get_variable("x2") = x2_;
|
|
resources::gamedata->get_variable("y1") = y1_;
|
|
resources::gamedata->get_variable("y2") = y2_;
|
|
--instance_count;
|
|
}
|
|
static unsigned count() {
|
|
return instance_count;
|
|
}
|
|
private:
|
|
static unsigned instance_count;
|
|
int x1_, x2_, y1_, y2_;
|
|
};
|
|
unsigned pump_manager::instance_count=0;
|
|
|
|
} // end anonymous namespace (1)
|
|
|
|
#ifdef _MSC_VER
|
|
// std::getline might be broken in Visual Studio so show a warning
|
|
#if _MSC_VER < 1300
|
|
#ifndef GETLINE_PATCHED
|
|
#pragma message("warning: the std::getline implementation in your compiler might be broken.")
|
|
#pragma message(" http://support.microsoft.com/default.aspx?scid=kb;EN-US;q240015")
|
|
#endif
|
|
#endif
|
|
#endif
|
|
|
|
/**
|
|
* Helper function which determines whether a wml_message text can
|
|
* really be pushed into the wml_messages_stream, and does it.
|
|
*/
|
|
static void put_wml_message(const std::string& logger, const std::string& message)
|
|
{
|
|
if (logger == "err" || logger == "error") {
|
|
ERR_WML << message << "\n";
|
|
wml_messages_stream << _("Error: ") << message << "\n";
|
|
} else if (logger == "warn" || logger == "wrn" || logger == "warning") {
|
|
WRN_WML << message << "\n";
|
|
wml_messages_stream << _("Warning: ") << message << "\n";
|
|
} else if ((logger == "debug" || logger == "dbg") && !lg::debug.dont_log(log_wml)) {
|
|
DBG_WML << message << "\n";
|
|
wml_messages_stream << _("Debug: ") << message << "\n";
|
|
} else if (!lg::info.dont_log(log_wml)) {
|
|
LOG_WML << message << "\n";
|
|
wml_messages_stream << _("Info: ") << message << "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function for show_wml_errors(), which gathers
|
|
* the messages from a stringstream.
|
|
*/
|
|
static void fill_wml_messages_map(std::map<std::string, int>& msg_map, std::stringstream& source)
|
|
{
|
|
while(true) {
|
|
std::string msg;
|
|
std::getline(source, msg);
|
|
|
|
if(source.eof()) {
|
|
break;
|
|
}
|
|
|
|
if(msg == "") {
|
|
continue;
|
|
}
|
|
|
|
if(msg_map.find(msg) == msg_map.end()) {
|
|
msg_map[msg] = 1;
|
|
} else {
|
|
msg_map[msg]++;
|
|
}
|
|
}
|
|
// Make sure the eof flag is cleared otherwise no new messages are shown
|
|
source.clear();
|
|
}
|
|
|
|
/**
|
|
* Shows a summary of the errors encountered in WML thusfar,
|
|
* to avoid a lot of the same messages to be shown.
|
|
* Identical messages are shown once, with (between braces)
|
|
* the number of times that message was encountered.
|
|
* The order in which the messages are shown does not need
|
|
* to be the order in which these messages are encountered.
|
|
* Messages are always written to std::cerr.
|
|
*/
|
|
static void show_wml_errors()
|
|
{
|
|
// Get all unique messages in messages,
|
|
// with the number of encounters for these messages
|
|
std::map<std::string, int> messages;
|
|
fill_wml_messages_map(messages, lg::wml_error);
|
|
|
|
// Show the messages collected
|
|
const std::string caption = "Invalid WML found";
|
|
for(std::map<std::string, int>::const_iterator itor = messages.begin();
|
|
itor != messages.end(); ++itor) {
|
|
|
|
std::stringstream msg;
|
|
msg << itor->first;
|
|
if(itor->second > 1) {
|
|
msg << " (" << itor->second << ")";
|
|
}
|
|
|
|
resources::screen->add_chat_message(time(NULL), caption, 0, msg.str(),
|
|
events::chat_handler::MESSAGE_PUBLIC, false);
|
|
std::cerr << caption << ": " << msg.str() << '\n';
|
|
}
|
|
}
|
|
|
|
static void show_wml_messages()
|
|
{
|
|
// Get all unique messages in messages,
|
|
// with the number of encounters for these messages
|
|
std::map<std::string, int> messages;
|
|
fill_wml_messages_map(messages, wml_messages_stream);
|
|
|
|
// Show the messages collected
|
|
const std::string caption = "WML";
|
|
for(std::map<std::string, int>::const_iterator itor = messages.begin();
|
|
itor != messages.end(); ++itor) {
|
|
|
|
std::stringstream msg;
|
|
msg << itor->first;
|
|
if(itor->second > 1) {
|
|
msg << " (" << itor->second << ")";
|
|
}
|
|
|
|
resources::screen->add_chat_message(time(NULL), caption, 0, msg.str(),
|
|
events::chat_handler::MESSAGE_PUBLIC, false);
|
|
}
|
|
}
|
|
|
|
typedef void (*wml_handler_function)(
|
|
const game_events::queued_event &event_info, const vconfig &cfg);
|
|
|
|
typedef std::map<std::string, wml_handler_function> static_wml_action_map;
|
|
/** Map of the default action handlers known of the engine. */
|
|
static static_wml_action_map static_wml_actions;
|
|
|
|
/**
|
|
* WML_HANDLER_FUNCTION macro handles auto registeration for wml handlers
|
|
*
|
|
* @param pname wml tag name
|
|
* @param pei the variable name of game_events::queued_event object inside function
|
|
* @param pcfg the variable name of config object inside 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_action_foo(...);
|
|
* struct wml_func_register_foo {
|
|
* wml_func_register_foo() {
|
|
* static_wml_actions["foo"] = &wml_func_foo;
|
|
* } wml_func_register_foo;
|
|
* void wml_func_foo(...)
|
|
* {
|
|
* // code for foo
|
|
* }
|
|
* \endcode
|
|
*/
|
|
#define WML_HANDLER_FUNCTION(pname, pei, pcfg) \
|
|
static void wml_func_##pname(const game_events::queued_event &pei, const vconfig &pcfg); \
|
|
struct wml_func_register_##pname \
|
|
{ \
|
|
wml_func_register_##pname() \
|
|
{ static_wml_actions[#pname] = &wml_func_##pname; } \
|
|
}; \
|
|
static wml_func_register_##pname wml_func_register_##pname##_aux; \
|
|
static void wml_func_##pname(const game_events::queued_event& pei, const vconfig& pcfg)
|
|
|
|
namespace game_events {
|
|
|
|
static bool matches_special_filter(const config &cfg, const vconfig& filter);
|
|
|
|
static bool internal_conditional_passed(const vconfig& cond, bool& backwards_compat)
|
|
{
|
|
static std::vector<std::pair<int,int> > default_counts = utils::parse_ranges("1-99999");
|
|
|
|
// If the if statement requires we have a certain unit,
|
|
// then check for that.
|
|
const vconfig::child_list& have_unit = cond.get_children("have_unit");
|
|
backwards_compat = backwards_compat && have_unit.empty();
|
|
for(vconfig::child_list::const_iterator u = have_unit.begin(); u != have_unit.end(); ++u) {
|
|
if(resources::units == NULL)
|
|
return false;
|
|
std::vector<std::pair<int,int> > counts = (*u).has_attribute("count")
|
|
? utils::parse_ranges((*u)["count"]) : default_counts;
|
|
int match_count = 0;
|
|
BOOST_FOREACH(const unit &i, *resources::units)
|
|
{
|
|
if(i.hitpoints() > 0 && unit_matches_filter(i, *u)) {
|
|
++match_count;
|
|
if(counts == default_counts) {
|
|
// by default a single match is enough, so avoid extra work
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ((*u)["search_recall_list"].to_bool())
|
|
{
|
|
for(std::vector<team>::iterator team = resources::teams->begin();
|
|
team!=resources::teams->end(); ++team)
|
|
{
|
|
if(counts == default_counts && match_count) {
|
|
break;
|
|
}
|
|
const std::vector<unit>& avail_units = team->recall_list();
|
|
for(std::vector<unit>::const_iterator unit = avail_units.begin(); unit!=avail_units.end(); ++unit) {
|
|
if(counts == default_counts && match_count) {
|
|
break;
|
|
}
|
|
scoped_recall_unit auto_store("this_unit", team->save_id(), unit - avail_units.begin());
|
|
if (unit_matches_filter(*unit, *u)) {
|
|
++match_count;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(!in_ranges(match_count, counts)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If the if statement requires we have a certain location,
|
|
// then check for that.
|
|
const vconfig::child_list& have_location = cond.get_children("have_location");
|
|
backwards_compat = backwards_compat && have_location.empty();
|
|
for(vconfig::child_list::const_iterator v = have_location.begin(); v != have_location.end(); ++v) {
|
|
std::set<map_location> res;
|
|
terrain_filter(*v, *resources::units).get_locations(res);
|
|
|
|
std::vector<std::pair<int,int> > counts = (*v).has_attribute("count")
|
|
? utils::parse_ranges((*v)["count"]) : default_counts;
|
|
if(!in_ranges<int>(res.size(), counts)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check against each variable statement,
|
|
// to see if the variable matches the conditions or not.
|
|
const vconfig::child_list& variables = cond.get_children("variable");
|
|
backwards_compat = backwards_compat && variables.empty();
|
|
|
|
BOOST_FOREACH(const vconfig &values, variables)
|
|
{
|
|
const std::string name = values["name"];
|
|
config::attribute_value value = resources::gamedata->get_variable_const(name);
|
|
std::string str_value = value.str();
|
|
double num_value = value.to_double();
|
|
|
|
#define TEST_ATTR(name, test) do { \
|
|
if (values.has_attribute(name)) { \
|
|
config::attribute_value attr = values[name]; \
|
|
if (!(test)) return false; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define TEST_STR_ATTR(name, test) do { \
|
|
if (values.has_attribute(name)) { \
|
|
std::string attr_str = values[name]; \
|
|
if (!(test)) return false; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define TEST_NUM_ATTR(name, test) do { \
|
|
if (values.has_attribute(name)) { \
|
|
double attr_num = values[name].to_double(); \
|
|
if (!(test)) return false; \
|
|
} \
|
|
} while (0)
|
|
|
|
TEST_STR_ATTR("equals", str_value == attr_str);
|
|
TEST_STR_ATTR("not_equals", str_value != attr_str);
|
|
TEST_NUM_ATTR("numerical_equals", num_value == attr_num);
|
|
TEST_NUM_ATTR("numerical_not_equals", num_value != attr_num);
|
|
TEST_NUM_ATTR("greater_than", num_value > attr_num);
|
|
TEST_NUM_ATTR("less_than", num_value < attr_num);
|
|
TEST_NUM_ATTR("greater_than_equal_to", num_value >= attr_num);
|
|
TEST_NUM_ATTR("less_than_equal_to", num_value <= attr_num);
|
|
TEST_ATTR("boolean_equals", value.to_bool() == attr.to_bool());
|
|
TEST_ATTR("boolean_not_equals", value.to_bool() != attr.to_bool());
|
|
TEST_STR_ATTR("contains", value.str().find(attr_str) != std::string::npos);
|
|
|
|
#undef TEST_ATTR
|
|
#undef TEST_STR_ATTR
|
|
#undef TEST_NUM_ATTR
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool conditional_passed(const vconfig& cond, bool backwards_compat)
|
|
{
|
|
bool allow_backwards_compat = backwards_compat = backwards_compat &&
|
|
cond["backwards_compat"].to_bool(true);
|
|
bool matches = internal_conditional_passed(cond, allow_backwards_compat);
|
|
|
|
// Handle [and], [or], and [not] with in-order precedence
|
|
int or_count = 0;
|
|
vconfig::all_children_iterator cond_i = cond.ordered_begin();
|
|
vconfig::all_children_iterator cond_end = cond.ordered_end();
|
|
while(cond_i != cond_end)
|
|
{
|
|
const std::string& cond_name = cond_i.get_key();
|
|
const vconfig& cond_filter = cond_i.get_child();
|
|
|
|
// Handle [and]
|
|
if(cond_name == "and")
|
|
{
|
|
matches = matches && conditional_passed(cond_filter, backwards_compat);
|
|
backwards_compat = false;
|
|
}
|
|
// Handle [or]
|
|
else if(cond_name == "or")
|
|
{
|
|
matches = matches || conditional_passed(cond_filter, backwards_compat);
|
|
++or_count;
|
|
}
|
|
// Handle [not]
|
|
else if(cond_name == "not")
|
|
{
|
|
matches = matches && !conditional_passed(cond_filter, backwards_compat);
|
|
backwards_compat = false;
|
|
}
|
|
++cond_i;
|
|
}
|
|
// Check for deprecated [or] syntax
|
|
if(matches && or_count > 1 && allow_backwards_compat)
|
|
{
|
|
///@deprecated r18803 [or] syntax
|
|
lg::wml_error << "possible deprecated [or] syntax: now forcing re-interpretation\n";
|
|
/**
|
|
* @todo For now we will re-interpret it according to the old
|
|
* rules, but this should be later to prevent re-interpretation
|
|
* errors.
|
|
*/
|
|
const vconfig::child_list& orcfgs = cond.get_children("or");
|
|
for(unsigned int i=0; i < orcfgs.size(); ++i) {
|
|
if(conditional_passed(orcfgs[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
} // end namespace game_events (1)
|
|
|
|
namespace {
|
|
|
|
std::set<std::string> used_items;
|
|
|
|
} // end anonymous namespace (2)
|
|
|
|
static bool events_init() { return resources::screen != 0; }
|
|
|
|
namespace {
|
|
|
|
std::deque<game_events::queued_event> events_queue;
|
|
|
|
|
|
} // end anonymous namespace (3)
|
|
|
|
static map_location cfg_to_loc(const vconfig& cfg,int defaultx = 0, int defaulty = 0)
|
|
{
|
|
int x = cfg["x"].to_int(defaultx) - 1;
|
|
int y = cfg["y"].to_int(defaulty) - 1;
|
|
|
|
return map_location(x, y);
|
|
}
|
|
|
|
namespace {
|
|
|
|
class t_event_handlers {
|
|
typedef std::vector<game_events::event_handler> t_active;
|
|
public:
|
|
typedef t_active::iterator iterator;
|
|
typedef t_active::const_iterator const_iterator;
|
|
private:
|
|
|
|
t_active active_; ///Active event handlers
|
|
t_active insert_buffer_; ///Event handlers added while pumping events
|
|
std::set<std::string> remove_buffer_; ///Event handlers removed while pumping events
|
|
bool buffering_;
|
|
|
|
|
|
void log_handler(std::stringstream& ss,
|
|
const std::vector<game_events::event_handler>& handlers,
|
|
const std::string& msg) {
|
|
|
|
BOOST_FOREACH(const game_events::event_handler& h, handlers){
|
|
const config& cfg = h.get_config();
|
|
ss << "name=" << cfg["name"] << ", with id=" << cfg["id"] << "; ";
|
|
}
|
|
DBG_EH << msg << " handlers are now " << ss.str() << "\n";
|
|
ss.str(std::string());
|
|
}
|
|
|
|
void log_handlers() {
|
|
if(lg::debug.dont_log("event_handler")) return;
|
|
|
|
std::stringstream ss;
|
|
log_handler(ss, active_, "active");
|
|
log_handler(ss, insert_buffer_, "insert buffered");
|
|
BOOST_FOREACH(const std::string& h, remove_buffer_){
|
|
ss << "id=" << h << "; ";
|
|
}
|
|
DBG_EH << "remove buffered handlers are now " << ss.str() << "\n";
|
|
}
|
|
|
|
public:
|
|
|
|
t_event_handlers()
|
|
: active_() , insert_buffer_() , remove_buffer_() , buffering_(false) { }
|
|
|
|
/**
|
|
* Adds an event handler. An event with a nonempty ID will not
|
|
* be added if an event with that ID already exists. This method
|
|
* respects this class's buffering functionality.
|
|
*/
|
|
void add_event_handler(game_events::event_handler const & new_handler) {
|
|
if(buffering_) {
|
|
DBG_EH << "buffering event handler for name=" << new_handler.get_config()["name"] <<
|
|
" with id " << new_handler.get_config()["id"] << "\n";
|
|
insert_buffer_.push_back(new_handler);
|
|
log_handlers();
|
|
}
|
|
|
|
else {
|
|
const config & cfg = new_handler.get_config();
|
|
std::string id = cfg["id"];
|
|
if(!id.empty()) {
|
|
BOOST_FOREACH( game_events::event_handler const & eh, active_) {
|
|
config const & temp_config( eh.get_config());
|
|
if(id == temp_config["id"]) {
|
|
DBG_EH << "ignoring event handler for name=" << cfg["name"] <<
|
|
" with id " << id << "\n";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
DBG_EH << "inserting event handler for name=" << cfg["name"] <<
|
|
" with id=" << id << "\n";
|
|
active_.push_back(new_handler);
|
|
log_handlers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes an event handler, identified by its ID. Events with
|
|
* empty IDs cannot be removed. This method respects this class's
|
|
* buffering functionality.
|
|
*/
|
|
void remove_event_handler(std::string const & id) {
|
|
if(id == "") { return; }
|
|
|
|
DBG_EH << "removing event handler with id " << id << "\n";
|
|
|
|
if(buffering_) { remove_buffer_.insert(id); }
|
|
|
|
t_active &temp = buffering_ ? insert_buffer_ : active_;
|
|
|
|
t_active::iterator i = temp.begin();
|
|
while(i < temp.end()) {
|
|
config const & temp_config = (*i).get_config();
|
|
std::string event_id = temp_config["id"];
|
|
if(event_id != "" && event_id == id) {
|
|
i = temp.erase(i); }
|
|
else {
|
|
++i; }
|
|
}
|
|
log_handlers();
|
|
}
|
|
|
|
/**
|
|
* Starts buffering. While buffering, any calls to add_event_handler
|
|
* and remove_event_handler will not take effect until commit_buffer
|
|
* is called. This function is idempotent - starting a buffer
|
|
* when already buffering will not start a second buffer.
|
|
*/
|
|
void start_buffering() {
|
|
buffering_ = true;
|
|
DBG_EH << "starting buffering...\n";
|
|
}
|
|
|
|
void stop_buffering() {
|
|
DBG_EH << "stopping buffering...\n";
|
|
buffering_ = false;
|
|
}
|
|
|
|
/**
|
|
* Commits all buffered events
|
|
*/
|
|
void commit_buffer() {
|
|
DBG_EH << "committing buffered event handlers, buffering: " << buffering_ << "\n";
|
|
if(buffering_)
|
|
return;
|
|
|
|
// Commit any event removals
|
|
BOOST_FOREACH(std::string const & i , remove_buffer_ ){
|
|
remove_event_handler( i ); }
|
|
remove_buffer_.clear();
|
|
|
|
// Commit any spawned events-within-events
|
|
BOOST_FOREACH( game_events::event_handler const & i , insert_buffer_ ){
|
|
add_event_handler( i ); }
|
|
insert_buffer_.clear();
|
|
|
|
log_handlers();
|
|
}
|
|
|
|
void clear(){
|
|
active_.clear();
|
|
insert_buffer_.clear();
|
|
remove_buffer_.clear();
|
|
buffering_ = false;
|
|
}
|
|
|
|
|
|
iterator begin() { return active_.begin(); }
|
|
const_iterator begin() const { return active_.begin(); }
|
|
|
|
iterator end() { return active_.end(); }
|
|
const_iterator end() const { return active_.end(); }
|
|
|
|
};
|
|
|
|
t_event_handlers event_handlers;
|
|
|
|
} // end anonymous namespace (4)
|
|
|
|
static void toggle_shroud(const bool remove, const vconfig& cfg)
|
|
{
|
|
// Filter the sides.
|
|
std::vector<int> sides = game_events::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();
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(remove_shroud, /*event_info*/, cfg)
|
|
{
|
|
toggle_shroud(true,cfg);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(place_shroud, /*event_info*/,cfg)
|
|
{
|
|
toggle_shroud(false,cfg );
|
|
}
|
|
|
|
/* 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.
|
|
*/
|
|
static 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();
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(lift_fog, /*event_info*/, cfg)
|
|
{
|
|
toggle_fog(true, cfg, !cfg["multiturn"].to_bool(false));
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(reset_fog, /*event_info*/, cfg)
|
|
{
|
|
toggle_fog(false, cfg, cfg["reset_view"].to_bool(false));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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(game_events::unit_matches_filter(*u, 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;
|
|
//@deprecated ignore_passability 1.9.10
|
|
const config::attribute_value ignore_passability = cfg["ignore_passability"];
|
|
if (!ignore_passability.blank()) {
|
|
WRN_NG << "[teleport]ignore_passability= is deprecated, use check_passability=\n";
|
|
if (!ignore_passability.to_bool(false))
|
|
pass_check = &*u;
|
|
}
|
|
else 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;
|
|
|
|
actions::shroud_clearer clearer;
|
|
if (cfg["clear_shroud"].to_bool(true)) {
|
|
clearer.clear_unit(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 (resources::game_map->is_village(vacant_dst)) {
|
|
get_village(vacant_dst, u->side());
|
|
}
|
|
|
|
resources::screen->invalidate_unit_after_move(src_loc, dst);
|
|
|
|
resources::screen->draw();
|
|
|
|
// Sighted events.
|
|
clearer.fire_events();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
}
|
|
|
|
static void color_adjust(const vconfig& 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(color_adjust, /*event_info*/, cfg)
|
|
{
|
|
color_adjust(cfg);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(scroll, /*event_info*/, cfg)
|
|
{
|
|
game_display &screen = *resources::screen;
|
|
screen.scroll(cfg["x"], cfg["y"], true);
|
|
screen.draw(true,true);
|
|
}
|
|
|
|
// store time of day config in a WML variable; 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, -999, -999);
|
|
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);
|
|
|
|
config tod_cfg;
|
|
tod.write(tod_cfg);
|
|
|
|
(*store.vars).add_child(store.key, tod_cfg);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(inspect, /*event_info*/, cfg)
|
|
{
|
|
gui2::tgamestate_inspector inspect_dialog(cfg);
|
|
inspect_dialog.show(resources::screen->video());
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(modify_ai, /*event_info*/, cfg)
|
|
{
|
|
const vconfig& filter_side = cfg.child("filter_side");
|
|
std::vector<int> sides;
|
|
if(!filter_side.null()) {
|
|
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 = game_events::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()) {
|
|
std::vector<std::string> recruit = utils::split(recruit_str);
|
|
if (recruit.size() == 1 && recruit.back() == "")
|
|
recruit.clear();
|
|
|
|
teams[team_index].set_recruits(std::set<std::string>(recruit.begin(),recruit.end()));
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
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::units,
|
|
*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;
|
|
}
|
|
|
|
} //end of anonymous namespace
|
|
|
|
// 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 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())
|
|
unit_display::move_unit(path, *dummy_unit);
|
|
}
|
|
|
|
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(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);
|
|
|
|
std::string random_value;
|
|
|
|
std::string word;
|
|
std::vector<std::string> words;
|
|
std::vector<std::pair<long,long> > ranges;
|
|
int 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<int, int>(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;
|
|
ss >> low;
|
|
ss >> high;
|
|
ss.clear();
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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";
|
|
}
|
|
int choice = gameinfo->rng().get_next_random();
|
|
if(num_choices >= 32768) {
|
|
choice <<= 15;
|
|
choice += gameinfo->rng().get_next_random();
|
|
}
|
|
choice %= num_choices;
|
|
int 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) {
|
|
random_value = words[i];
|
|
}
|
|
else {
|
|
tmp = (ranges[i].second - (tmp - choice)) + 1;
|
|
ss << tmp;
|
|
ss >> random_value;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
var = random_value;
|
|
}
|
|
|
|
|
|
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(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 precendence based on type order
|
|
do {
|
|
if (has_any_types) {
|
|
item["type"] = *ti;
|
|
}
|
|
unit_map::iterator itor;
|
|
BOOST_FOREACH(unit &u, *resources::units) {
|
|
if (game_events::unit_matches_filter(u, 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 precendence 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(sound_source, /*event_info*/, cfg)
|
|
{
|
|
soundsource::sourcespec spec(cfg.get_parsed_config());
|
|
resources::soundsources->add(spec);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(remove_sound_source, /*event_info*/, cfg)
|
|
{
|
|
resources::soundsources->remove(cfg["id"]);
|
|
}
|
|
|
|
void change_terrain(const map_location &loc, const t_translation::t_terrain &t,
|
|
gamemap::tmerge_mode mode, bool replace_if_failed)
|
|
{
|
|
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);
|
|
screen_needs_rebuild = true;
|
|
|
|
BOOST_FOREACH(const t_translation::t_terrain &ut, game_map->underlying_union_terrain(loc)) {
|
|
preferences::encountered_terrains().insert(ut);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
screen_needs_rebuild = true;
|
|
}
|
|
|
|
static 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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
// 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, leader->get_location())) &&
|
|
(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
|
|
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;
|
|
place_recruit(to_recruit, loc, null_location, 0, true,
|
|
cfg["show"].to_bool(true), cfg["fire_event"].to_bool(false), true, true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//TODO I don't know about that error throwing. Sometimes a unit is just not available,
|
|
//the designer needs to check with [have_unit] or fetch the recall event.
|
|
ERR_NG << "A [recall] tag with the following content failed:\n" << cfg.get_config().debug();
|
|
}
|
|
|
|
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(id != "" && used_items.count(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 (game_events::unit_matches_filter(u, 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() || game_events::unit_matches_filter(*u, 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.
|
|
used_items.insert(id);
|
|
} 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(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);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(deprecated_message, /*event_info*/, cfg)
|
|
{
|
|
game_events::handle_deprecated_message( cfg.get_parsed_config() );
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(wml_message, /*event_info*/, cfg)
|
|
{
|
|
game_events::handle_wml_log_message( cfg.get_parsed_config() );
|
|
}
|
|
|
|
|
|
typedef std::map<map_location, int> recursion_counter;
|
|
|
|
class recursion_preventer {
|
|
static recursion_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)
|
|
{
|
|
recursion_counter::iterator inserted = counter_.insert(std::make_pair(loc_, 0)).first;
|
|
++inserted->second;
|
|
too_many_recursions_ = inserted->second >= max_recursion;
|
|
}
|
|
~recursion_preventer()
|
|
{
|
|
recursion_counter::iterator itor = counter_.find(loc_);
|
|
if (--itor->second == 0)
|
|
{
|
|
counter_.erase(itor);
|
|
}
|
|
}
|
|
bool too_many_recursions() const
|
|
{
|
|
return too_many_recursions_;
|
|
}
|
|
};
|
|
|
|
recursion_counter recursion_preventer::counter_ = recursion_counter();
|
|
|
|
typedef boost::scoped_ptr<recursion_preventer> recursion_preventer_ptr;
|
|
|
|
WML_HANDLER_FUNCTION(kill, event_info, cfg)
|
|
{
|
|
bool secondary_unit = cfg.has_child("secondary_unit");
|
|
game_events::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(game_events::unit_matches_filter(*unit, cfg.child("secondary_unit")))
|
|
{
|
|
killer_loc = game_events::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(game_events::unit_matches_filter(u, cfg)){
|
|
dead_men_walking.push_back(&u);
|
|
}
|
|
}
|
|
|
|
BOOST_FOREACH(unit * un, dead_men_walking) {
|
|
map_location loc(un->get_location());
|
|
bool fire_event = false;
|
|
game_events::entity_location death_loc(*un);
|
|
if(!secondary_unit) {
|
|
killer_loc = game_events::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) {
|
|
game_events::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) {
|
|
game_events::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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
config* new_command = new config(delayed ? cmd.get_config() : cmd.get_parsed_config());
|
|
wmi_command_changes.push_back(wmi_command_change(id, new_command));
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
std::vector<wmi_command_change>::iterator wcc = wmi_command_changes.begin();
|
|
while(wcc != wmi_command_changes.end()) {
|
|
if(wcc->first != id) {
|
|
++wcc;
|
|
continue;
|
|
}
|
|
delete wcc->second;
|
|
wcc->second = NULL;
|
|
wcc = wmi_command_changes.erase(wcc);
|
|
}
|
|
event_handlers.remove_event_handler(id);
|
|
|
|
wml_menu_item*& mi = menu_items[id];
|
|
delete mi;
|
|
mi = NULL;
|
|
menu_items.erase(id);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
// Unit serialization to 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();
|
|
if (advance &&
|
|
unit_helper::will_certainly_advance(resources::units->find(loc)))
|
|
{
|
|
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(end_turn, /*event_info*/, /*cfg*/)
|
|
{
|
|
resources::controller->force_end_turn();
|
|
}
|
|
|
|
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(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()){
|
|
clear_shroud(side);
|
|
}
|
|
screen.recalculate_minimap();
|
|
}
|
|
if (screen_needs_rebuild) {
|
|
screen_needs_rebuild = false;
|
|
screen.recalculate_minimap();
|
|
screen.rebuild_all();
|
|
}
|
|
screen.invalidate_all();
|
|
screen.draw(true,true);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(animate_unit, event_info, cfg)
|
|
{
|
|
const events::command_disabler disable_commands;
|
|
unit_display::wml_animation(cfg, event_info.loc1);
|
|
}
|
|
|
|
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(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 (game_events::unit_matches_filter(u, 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 (!game_events::unit_matches_filter(*u, 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;
|
|
}
|
|
}
|
|
|
|
// Allow undo sets the flag saying whether the event has mutated the game to false
|
|
WML_HANDLER_FUNCTION(allow_undo,/*event_info*/,/*cfg*/)
|
|
{
|
|
current_context->mutated = false;
|
|
}
|
|
|
|
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());
|
|
}
|
|
// Helper namespace to do some subparts for message function
|
|
namespace {
|
|
|
|
/**
|
|
* Helper to handle the speaker part of the 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 game_events::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 (game_events::unit_matches_filter(*speaker, 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;
|
|
}
|
|
|
|
/**
|
|
* Helper to handle the image part of the 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;
|
|
}
|
|
|
|
/**
|
|
* Helper to handle the caption part of the 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;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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) {
|
|
current_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();
|
|
}
|
|
};
|
|
|
|
// 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() ||
|
|
current_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")
|
|
|| game_events::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);
|
|
}
|
|
}
|
|
|
|
// 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";
|
|
}
|
|
}
|
|
|
|
//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(allow_end_turn, /*event_info*/, /*cfg*/)
|
|
{
|
|
resources::gamedata->set_allow_end_turn(true);
|
|
}
|
|
|
|
WML_HANDLER_FUNCTION(disallow_end_turn, /*event_info*/, /*cfg*/)
|
|
{
|
|
resources::gamedata->set_allow_end_turn(false);
|
|
}
|
|
|
|
// Adding new events
|
|
WML_HANDLER_FUNCTION(event, /*event_info*/, cfg)
|
|
{
|
|
if (cfg["remove"].to_bool(false)) {
|
|
event_handlers.remove_event_handler(cfg["id"]);
|
|
} else if (!cfg["delayed_variable_substitution"].to_bool(true)) {
|
|
event_handlers.add_event_handler(game_events::event_handler(cfg.get_parsed_config()));
|
|
} else {
|
|
event_handlers.add_event_handler(game_events::event_handler(cfg.get_config()));
|
|
}
|
|
}
|
|
|
|
// Experimental map replace
|
|
WML_HANDLER_FUNCTION(replace_map, /*event_info*/, cfg)
|
|
{
|
|
gamemap *game_map = resources::game_map;
|
|
|
|
gamemap map(*game_map);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
*game_map = map;
|
|
resources::screen->reload_map();
|
|
screen_needs_rebuild = true;
|
|
ai::manager::raise_map_changed();
|
|
}
|
|
|
|
// Experimental data persistence
|
|
WML_HANDLER_FUNCTION(set_global_variable,/**/,pcfg)
|
|
{
|
|
if (get_replay_source().at_end() || (network::nconnections() != 0))
|
|
verify_and_set_global_variable(pcfg);
|
|
}
|
|
WML_HANDLER_FUNCTION(get_global_variable,/**/,pcfg)
|
|
{
|
|
verify_and_get_global_variable(pcfg);
|
|
}
|
|
WML_HANDLER_FUNCTION(clear_global_variable,/**/,pcfg)
|
|
{
|
|
if (get_replay_source().at_end() || (network::nconnections() != 0))
|
|
verify_and_clear_global_variable(pcfg);
|
|
}
|
|
|
|
/** Handles all the different types of actions that can be triggered by an event. */
|
|
|
|
static void commit_wmi_commands() {
|
|
// Commit WML Menu Item command changes
|
|
while(wmi_command_changes.size() > 0) {
|
|
wmi_command_change wcc = wmi_command_changes.front();
|
|
const bool is_empty_command = wcc.second->empty();
|
|
|
|
wml_menu_item*& mref = resources::gamedata->get_wml_menu_items().get_item(wcc.first);
|
|
const bool has_current_handler = !mref->command.empty();
|
|
|
|
config::attribute_value event_id = (*wcc.second)["id"];
|
|
if(event_id.empty()) {
|
|
event_id = mref->event_id;
|
|
if(!event_id.empty())
|
|
(*wcc.second)["id"] = event_id;
|
|
}
|
|
mref->command = *(wcc.second);
|
|
mref->command["name"] = mref->name;
|
|
mref->command["first_time_only"] = false;
|
|
|
|
if(has_current_handler) {
|
|
BOOST_FOREACH(game_events::event_handler& hand, event_handlers) {
|
|
if(hand.is_menu_item() && hand.matches_name(mref->name)) {
|
|
LOG_NG << "changing command for " << mref->name << " to:\n" << *wcc.second;
|
|
hand = game_events::event_handler(mref->command, true);
|
|
}
|
|
}
|
|
} else if(!is_empty_command) {
|
|
LOG_NG << "setting command for " << mref->name << " to:\n" << *wcc.second;
|
|
event_handlers.add_event_handler(game_events::event_handler(mref->command, true));
|
|
}
|
|
|
|
delete wcc.second;
|
|
wmi_command_changes.erase(wmi_command_changes.begin());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true iff the given event passes all its filters.
|
|
*/
|
|
static bool filter_event(const game_events::event_handler& handler,
|
|
const game_events::queued_event& ev)
|
|
{
|
|
const unit_map *units = resources::units;
|
|
unit_map::const_iterator unit1 = units->find(ev.loc1);
|
|
unit_map::const_iterator unit2 = units->find(ev.loc2);
|
|
vconfig filters(handler.get_config());
|
|
|
|
BOOST_FOREACH(const vconfig &condition, filters.get_children("filter_condition"))
|
|
{
|
|
if (!game_events::conditional_passed(condition)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
BOOST_FOREACH(const vconfig &f, filters.get_children("filter_side"))
|
|
{
|
|
side_filter ssf(f);
|
|
const int current_side = resources::controller->current_side();
|
|
if(!ssf.match(current_side)) return false;
|
|
}
|
|
|
|
BOOST_FOREACH(const vconfig &f, filters.get_children("filter"))
|
|
{
|
|
if ( !ev.loc1.matches_unit_filter(unit1, f) ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
vconfig::child_list special_filters = filters.get_children("filter_attack");
|
|
bool special_matches = special_filters.empty();
|
|
if ( !special_matches && unit1 != units->end() )
|
|
{
|
|
const bool matches_unit = ev.loc1.matches_unit(unit1);
|
|
const config & attack = ev.data.child("first");
|
|
BOOST_FOREACH(const vconfig &f, special_filters)
|
|
{
|
|
if ( f.empty() )
|
|
special_matches = true;
|
|
else if ( !matches_unit )
|
|
return false;
|
|
|
|
special_matches = special_matches ||
|
|
game_events::matches_special_filter(attack, f);
|
|
}
|
|
}
|
|
if(!special_matches) {
|
|
return false;
|
|
}
|
|
|
|
BOOST_FOREACH(const vconfig &f, filters.get_children("filter_second"))
|
|
{
|
|
if ( !ev.loc2.matches_unit_filter(unit2, f) ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
special_filters = filters.get_children("filter_second_attack");
|
|
special_matches = special_filters.empty();
|
|
if ( !special_matches && unit2 != units->end() )
|
|
{
|
|
const bool matches_unit = ev.loc2.matches_unit(unit2);
|
|
const config & attack = ev.data.child("second");
|
|
BOOST_FOREACH(const vconfig &f, special_filters)
|
|
{
|
|
if ( f.empty() )
|
|
special_matches = true;
|
|
else if ( !matches_unit )
|
|
return false;
|
|
|
|
special_matches = special_matches ||
|
|
game_events::matches_special_filter(attack, f);
|
|
}
|
|
}
|
|
if(!special_matches) {
|
|
return false;
|
|
}
|
|
|
|
// All filters passed.
|
|
return true;
|
|
}
|
|
|
|
static bool process_event(game_events::event_handler& handler, const game_events::queued_event& ev)
|
|
{
|
|
if(handler.disabled())
|
|
return false;
|
|
|
|
unit_map *units = resources::units;
|
|
scoped_xy_unit first_unit("unit", ev.loc1.x, ev.loc1.y, *units);
|
|
scoped_xy_unit second_unit("second_unit", ev.loc2.x, ev.loc2.y, *units);
|
|
scoped_weapon_info first_weapon("weapon", ev.data.child("first"));
|
|
scoped_weapon_info second_weapon("second_weapon", ev.data.child("second"));
|
|
|
|
if ( !filter_event(handler, ev) )
|
|
return false;
|
|
|
|
// The event hasn't been filtered out, so execute the handler.
|
|
++internal_wml_tracking;
|
|
scoped_context evc;
|
|
handler.handle_event(ev);
|
|
|
|
if(ev.name == "select") {
|
|
resources::gamedata->last_selected = ev.loc1;
|
|
}
|
|
|
|
if (screen_needs_rebuild) {
|
|
screen_needs_rebuild = false;
|
|
game_display *screen = resources::screen;
|
|
screen->recalculate_minimap();
|
|
screen->invalidate_all();
|
|
screen->rebuild_all();
|
|
}
|
|
|
|
return current_context->mutated;
|
|
}
|
|
|
|
namespace game_events {
|
|
|
|
event_handler::event_handler(const config &cfg, bool imi) :
|
|
first_time_only_(cfg["first_time_only"].to_bool(true)),
|
|
disabled_(false), is_menu_item_(imi), cfg_(cfg)
|
|
{}
|
|
|
|
void event_handler::handle_event(const game_events::queued_event& event_info)
|
|
{
|
|
if (first_time_only_)
|
|
{
|
|
disabled_ = true;
|
|
}
|
|
|
|
if (is_menu_item_) {
|
|
DBG_NG << cfg_["name"] << " will now invoke the following command(s):\n" << cfg_;
|
|
}
|
|
|
|
handle_event_commands(event_info, vconfig(cfg_));
|
|
}
|
|
|
|
void handle_event_commands(const game_events::queued_event& event_info, const vconfig &cfg)
|
|
{
|
|
resources::lua_kernel->run_wml_action("command", cfg, event_info);
|
|
}
|
|
|
|
void handle_event_command(const std::string &cmd,
|
|
const game_events::queued_event &event_info, const vconfig &cfg)
|
|
{
|
|
log_scope2(log_engine, "handle_event_command");
|
|
LOG_NG << "handling command '" << cmd << "' from "
|
|
<< (cfg.is_volatile()?"volatile ":"") << "cfg 0x"
|
|
<< std::hex << std::setiosflags(std::ios::uppercase)
|
|
<< reinterpret_cast<uintptr_t>(&cfg.get_config()) << std::dec << "\n";
|
|
|
|
if (!resources::lua_kernel->run_wml_action(cmd, cfg, event_info))
|
|
{
|
|
ERR_NG << "Couldn't find function for wml tag: "<< cmd <<"\n";
|
|
}
|
|
|
|
DBG_NG << "done handling command...\n";
|
|
}
|
|
|
|
bool event_handler::matches_name(const std::string &name) const
|
|
{
|
|
const t_string& t_my_names = cfg_["name"];
|
|
const std::string& my_names = t_my_names;
|
|
std::string::const_iterator itor,
|
|
it_begin = my_names.begin(),
|
|
it_end = my_names.end(),
|
|
match_it = name.begin(),
|
|
match_begin = name.begin(),
|
|
match_end = name.end();
|
|
int skip_count = 0;
|
|
for(itor = it_begin; itor != it_end; ++itor) {
|
|
bool do_eat = false,
|
|
do_skip = false;
|
|
switch(*itor) {
|
|
case ',':
|
|
if(itor - it_begin - skip_count == match_it - match_begin && match_it == match_end) {
|
|
return true;
|
|
}
|
|
it_begin = itor + 1;
|
|
match_it = match_begin;
|
|
skip_count = 0;
|
|
continue;
|
|
case '\f':
|
|
case '\n':
|
|
case '\r':
|
|
case '\t':
|
|
case '\v':
|
|
do_skip = (match_it == match_begin || match_it == match_end);
|
|
break;
|
|
case ' ':
|
|
do_skip = (match_it == match_begin || match_it == match_end);
|
|
// fall through to case '_'
|
|
case '_':
|
|
do_eat = (match_it != match_end && (*match_it == ' ' || *match_it == '_'));
|
|
break;
|
|
default:
|
|
do_eat = (match_it != match_end && *match_it == *itor);
|
|
break;
|
|
}
|
|
if(do_eat) {
|
|
++match_it;
|
|
} else if(do_skip) {
|
|
++skip_count;
|
|
} else {
|
|
itor = std::find(itor, it_end, ',');
|
|
if(itor == it_end) {
|
|
return false;
|
|
}
|
|
it_begin = itor + 1;
|
|
match_it = match_begin;
|
|
skip_count = 0;
|
|
}
|
|
}
|
|
if(itor - it_begin - skip_count == match_it - match_begin && match_it == match_end) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool matches_special_filter(const config &cfg, const vconfig& filter)
|
|
{
|
|
if (!cfg) {
|
|
WRN_NG << "attempt to filter attack for an event with no attack data.\n";
|
|
// better to not execute the event (so the problem is more obvious)
|
|
return false;
|
|
}
|
|
const attack_type attack(cfg);
|
|
return attack.matches_filter(filter.get_parsed_config());
|
|
}
|
|
|
|
bool unit_matches_filter(const unit &u, const vconfig& filter)
|
|
{
|
|
return u.matches_filter(filter, u.get_location());
|
|
}
|
|
|
|
static std::set<std::string> unit_wml_ids;
|
|
|
|
manager::manager(const config& cfg)
|
|
: variable_manager()
|
|
{
|
|
assert(!manager_running);
|
|
BOOST_FOREACH(const config &ev, cfg.child_range("event")) {
|
|
event_handlers.add_event_handler(game_events::event_handler(ev));
|
|
}
|
|
BOOST_FOREACH(const std::string &id, utils::split(cfg["unit_wml_ids"])) {
|
|
unit_wml_ids.insert(id);
|
|
}
|
|
|
|
resources::lua_kernel = new LuaKernel(cfg);
|
|
manager_running = true;
|
|
|
|
BOOST_FOREACH(static_wml_action_map::value_type &action, static_wml_actions) {
|
|
resources::lua_kernel->set_wml_action(action.first, action.second);
|
|
}
|
|
|
|
const std::string used = cfg["used_items"];
|
|
if(!used.empty()) {
|
|
const std::vector<std::string>& v = utils::split(used);
|
|
for(std::vector<std::string>::const_iterator i = v.begin(); i != v.end(); ++i) {
|
|
used_items.insert(*i);
|
|
}
|
|
}
|
|
int wmi_count = 0;
|
|
typedef std::pair<std::string, wml_menu_item *> item;
|
|
BOOST_FOREACH(const item &itor, resources::gamedata->get_wml_menu_items().get_menu_items()) {
|
|
if (!itor.second->command.empty()) {
|
|
event_handlers.add_event_handler(game_events::event_handler(itor.second->command, true));
|
|
}
|
|
++wmi_count;
|
|
}
|
|
if(wmi_count > 0) {
|
|
LOG_NG << wmi_count << " WML menu items found, loaded." << std::endl;
|
|
}
|
|
}
|
|
|
|
void write_events(config& cfg)
|
|
{
|
|
assert(manager_running);
|
|
BOOST_FOREACH(const game_events::event_handler &eh, event_handlers) {
|
|
if (eh.disabled() || eh.is_menu_item()) continue;
|
|
cfg.add_child("event", eh.get_config());
|
|
}
|
|
|
|
std::stringstream used;
|
|
std::set<std::string>::const_iterator u;
|
|
for(u = used_items.begin(); u != used_items.end(); ++u) {
|
|
if(u != used_items.begin())
|
|
used << ",";
|
|
|
|
used << *u;
|
|
}
|
|
|
|
cfg["used_items"] = used.str();
|
|
std::stringstream ids;
|
|
for(u = unit_wml_ids.begin(); u != unit_wml_ids.end(); ++u) {
|
|
if(u != unit_wml_ids.begin())
|
|
ids << ",";
|
|
|
|
ids << *u;
|
|
}
|
|
|
|
cfg["unit_wml_ids"] = ids.str();
|
|
|
|
if (resources::soundsources)
|
|
resources::soundsources->write_sourcespecs(cfg);
|
|
|
|
resources::lua_kernel->save_game(cfg);
|
|
}
|
|
|
|
manager::~manager() {
|
|
assert(manager_running);
|
|
manager_running = false;
|
|
events_queue.clear();
|
|
event_handlers.clear();
|
|
reports::reset_generators();
|
|
delete resources::lua_kernel;
|
|
resources::lua_kernel = NULL;
|
|
unit_wml_ids.clear();
|
|
used_items.clear();
|
|
}
|
|
|
|
void raise(const std::string& event,
|
|
const entity_location& loc1,
|
|
const entity_location& loc2,
|
|
const config& data)
|
|
{
|
|
assert(manager_running);
|
|
if(!events_init())
|
|
return;
|
|
|
|
DBG_EH << "raising event: " << event << "\n";
|
|
|
|
events_queue.push_back(game_events::queued_event(event,loc1,loc2,data));
|
|
}
|
|
|
|
bool fire(const std::string& event,
|
|
const entity_location& loc1,
|
|
const entity_location& loc2,
|
|
const config& data)
|
|
{
|
|
assert(manager_running);
|
|
raise(event,loc1,loc2,data);
|
|
return pump();
|
|
}
|
|
|
|
void add_events(const config::const_child_itors &cfgs, const std::string& type)
|
|
{
|
|
if(!type.empty()) {
|
|
if(std::find(unit_wml_ids.begin(),unit_wml_ids.end(),type) != unit_wml_ids.end()) return;
|
|
unit_wml_ids.insert(type);
|
|
}
|
|
BOOST_FOREACH(const config &new_ev, cfgs) {
|
|
if(type.empty() && new_ev["id"].empty())
|
|
{
|
|
WRN_NG << "attempt to add an [event] with empty id=, ignoring \n";
|
|
continue;
|
|
}
|
|
event_handlers.add_event_handler(game_events::event_handler(new_ev));
|
|
}
|
|
}
|
|
|
|
void commit()
|
|
{
|
|
DBG_EH << "committing new event handlers, number of pump_instances: " <<
|
|
pump_manager::count() << "\n";
|
|
event_handlers.commit_buffer();
|
|
commit_wmi_commands();
|
|
// Dialogs can only be shown if the display is not locked
|
|
if (!resources::screen->video().update_locked()) {
|
|
show_wml_errors();
|
|
show_wml_messages();
|
|
}
|
|
}
|
|
|
|
bool pump()
|
|
{
|
|
//ensure the whiteboard doesn't attempt to build its future unit map
|
|
//for the duration of this method
|
|
wb::real_map real_unit_map;
|
|
|
|
assert(manager_running);
|
|
if(!events_init())
|
|
return false;
|
|
|
|
pump_manager pump_instance;
|
|
if(pump_manager::count() >= game_config::max_loop) {
|
|
ERR_NG << "game_events::pump() waiting to process new events because "
|
|
<< "recursion level would exceed maximum " << game_config::max_loop << '\n';
|
|
return false;
|
|
}
|
|
|
|
if(!lg::debug.dont_log("event_handler")) {
|
|
std::stringstream ss;
|
|
BOOST_FOREACH(const game_events::queued_event& ev, events_queue) {
|
|
ss << "name=" << ev.name << "; ";
|
|
}
|
|
DBG_EH << "processing queued events: " << ss.str() << "\n";
|
|
}
|
|
|
|
const size_t old_wml_track = internal_wml_tracking;
|
|
|
|
bool result = false;
|
|
while(events_queue.empty() == false) {
|
|
if(pump_manager::count() <= 1)
|
|
event_handlers.start_buffering();
|
|
game_events::queued_event ev = events_queue.front();
|
|
events_queue.pop_front(); // pop now for exception safety
|
|
const std::string& event_name = ev.name;
|
|
|
|
// Clear the unit cache, since the best clearing time is hard to figure out
|
|
// due to status changes by WML. Every event will flush the cache.
|
|
unit::clear_status_caches();
|
|
|
|
if ( resources::lua_kernel->run_event(ev) )
|
|
++internal_wml_tracking;
|
|
|
|
bool init_event_vars = true;
|
|
|
|
BOOST_FOREACH(game_events::event_handler& handler, event_handlers) {
|
|
if(!handler.matches_name(event_name))
|
|
continue;
|
|
// Set the variables for the event
|
|
if (init_event_vars) {
|
|
resources::gamedata->get_variable("x1") = ev.loc1.filter_x() + 1;
|
|
resources::gamedata->get_variable("y1") = ev.loc1.filter_y() + 1;
|
|
resources::gamedata->get_variable("x2") = ev.loc2.filter_x() + 1;
|
|
resources::gamedata->get_variable("y2") = ev.loc2.filter_y() + 1;
|
|
init_event_vars = false;
|
|
}
|
|
|
|
DBG_EH << "processing event " << event_name << " with id="<<
|
|
handler.get_config()["id"] << "\n";
|
|
if(process_event(handler, ev))
|
|
{
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
if(pump_manager::count() <= 1)
|
|
event_handlers.stop_buffering();
|
|
// Only commit new handlers when finished iterating over event_handlers.
|
|
commit();
|
|
}
|
|
|
|
if ( old_wml_track != internal_wml_tracking )
|
|
// Notify the whiteboard of any event.
|
|
// This is used to track when moves, recruits, etc. happen.
|
|
resources::whiteboard->on_gamestate_change();
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* This function can be used to detect when no WML/Lua has been executed.
|
|
*
|
|
* If two calls to this function return the same value, then one can
|
|
* assume that the usual game mechanics have been followed, and code does
|
|
* not have to account for all the things WML/Lua can do. If the return
|
|
* values are different, then something unusual might have happened between
|
|
* those calls.
|
|
*
|
|
* This is not intended as a precise metric. Rather, it is motivated by
|
|
* how large the number of fired WML events is, compared to the (typical)
|
|
* number of WML event handlers. It is intended for code that can benefit
|
|
* from caching some aspect of the game state and that cannot rely on
|
|
* [allow_undo] not being used when that state changes.
|
|
*/
|
|
size_t wml_tracking()
|
|
{
|
|
return internal_wml_tracking;
|
|
}
|
|
|
|
|
|
const entity_location entity_location::null_entity(map_location::null_location);
|
|
|
|
/**
|
|
* Constructor for when an event has a location but not necessarily a unit.
|
|
* Can also be used if the event has a unit and the caller already has the
|
|
* unit's location and underlying ID.
|
|
*/
|
|
entity_location::entity_location(const map_location &loc, size_t id)
|
|
: map_location(loc), id_(id), filter_loc_(loc)
|
|
{}
|
|
|
|
/**
|
|
* Constructor for when an event has a unit that needs to be filtered as if
|
|
* it was in a different location.
|
|
*/
|
|
entity_location::entity_location(const map_location &loc, size_t id,
|
|
const map_location & filter_loc)
|
|
: map_location(loc), id_(id), filter_loc_(filter_loc)
|
|
{}
|
|
|
|
/**
|
|
* Convenience constructor for when an event has a unit, saving the caller
|
|
* the need to explicitly get the location and underlying ID.
|
|
*/
|
|
entity_location::entity_location(const unit &u)
|
|
: map_location(u.get_location())
|
|
, id_(u.underlying_id())
|
|
, filter_loc_(*this)
|
|
{}
|
|
|
|
/**
|
|
* Convenience constructor for when an event has a unit that needs to be
|
|
* filtered as if it was in a different location, and the caller does not
|
|
* want to explicitly get the unit's location and underlying ID.
|
|
*/
|
|
entity_location::entity_location(const unit &u, const map_location & filter_loc)
|
|
: map_location(u.get_location())
|
|
, id_(u.underlying_id())
|
|
, filter_loc_(filter_loc)
|
|
{}
|
|
|
|
/**
|
|
* Determines if @a un_it matches (using underlying ID) the unit that was
|
|
* supplied when this was constructed.
|
|
* If no unit was supplied, then all units (including non-existent units)
|
|
* match.
|
|
*/
|
|
bool entity_location::matches_unit(const unit_map::const_iterator & un_it) const
|
|
{
|
|
return id_ == 0 || ( un_it.valid() && id_ == un_it->underlying_id() );
|
|
}
|
|
|
|
/**
|
|
* Determines if @a un_it matches @a filter. If the filter is not empty,
|
|
* the unit is required to additionally match the unit that was supplied
|
|
* when this was constructed.
|
|
*/
|
|
bool entity_location::matches_unit_filter(const unit_map::const_iterator & un_it,
|
|
const vconfig & filter) const
|
|
{
|
|
if ( !un_it.valid() )
|
|
return false;
|
|
|
|
if ( filter.empty() )
|
|
// Skip the check for un_it matching *this.
|
|
return true;
|
|
|
|
// Filter the unit at the filter location (should be the unit's
|
|
// location if no special filter location was specified).
|
|
return un_it->matches_filter(filter, filter_loc_) &&
|
|
matches_unit(un_it);
|
|
}
|
|
|
|
} // end namespace game_events (2)
|