Port [message] ActionWML tag from C++ to Lua

Lua API additions:
- wesnoth.skipping_replay()
- wesnoth.deselect_hex()

Note: vultraz and aetheryn deserve partial credit for the Lua implementation
This commit is contained in:
Celtic Minstrel 2015-09-17 01:52:59 -04:00
parent 6200f60c79
commit d4835b0157
7 changed files with 258 additions and 334 deletions

View file

@ -13,14 +13,13 @@ end
wesnoth.require "lua/wml/objectives.lua"
wesnoth.require "lua/wml/items.lua"
wesnoth.require "lua/wml/message.lua"
local helper = wesnoth.require "lua/helper.lua"
local location_set = wesnoth.require "lua/location_set.lua"
local utils = wesnoth.require "lua/wml-utils.lua"
local wml_actions = wesnoth.wml_actions
local engine_message = wml_actions.message
function wml_actions.sync_variable(cfg)
local names = cfg.name or helper.wml_error "[sync_variable] missing required name= attribute."
local result = wesnoth.synchronize_choice(
@ -59,13 +58,6 @@ function wml_actions.sync_variable(cfg)
end
end
function wml_actions.message(cfg)
local show_if = helper.get_child(cfg, "show_if")
if not show_if or wesnoth.eval_conditional(show_if) then
engine_message(cfg)
end
end
function wml_actions.chat(cfg)
local side_list = wesnoth.get_sides(cfg)
local speaker = tostring(cfg.speaker or "WML")

227
data/lua/wml/message.lua Normal file
View file

@ -0,0 +1,227 @@
local helper = wesnoth.require "lua/helper.lua"
local utils = wesnoth.require "lua/wml-utils.lua"
local location_set = wesnoth.require "lua/location_set.lua"
local skip_messages = false
local function log(msg, level)
wesnoth.wml_actions.wml_message({
message = msg,
logger = level,
})
end
local function get_image(cfg, speaker)
local image = cfg.image
if speaker and image == nil then
image = speaker.__cfg.profile
end
if image == "none" or image == nil then
return ""
end
return image
end
local function get_caption(cfg, speaker)
local caption = cfg.caption
if not caption and speaker ~= nil then
caption = speaker.name or speaker.type_name
end
return caption
end
local function get_speaker(cfg)
local speaker
local context = wesnoth.current.event_context
if cfg.speaker == "narrator" then
speaker = "narrator"
elseif cfg.speaker == "unit" then
speaker = wesnoth.get_unit(context.x1, context.y1)
elseif cfg.speaker == "second_unit" then
speaker = wesnoth.get_unit(context.x2, context.y2)
else
speaker = wesnoth.get_units(cfg)[1]
end
return speaker
end
local function message_user_choice(cfg, speaker, options, text_input)
local image = get_image(cfg, speaker)
local caption = get_caption(cfg, speaker)
local left_side = true
-- If this doesn't work, might need tostring()
if image:find("~RIGHT()") then
left_side = false
-- The percent signs escape the parentheses for a literal match
image = image:gsub("~RIGHT%(%)", "")
end
local msg_cfg = {
left_side = left_side,
title = caption,
message = cfg.message,
portrait = image,
}
-- Parse input text, if not available all fields are empty
if text_input then
local input_max_size = tonumber(text_input.max_length) or 256
if input_max_size > 1024 or input_max_size < 1 then
log("Invalid maximum size for input " .. input_max_size, "warning")
input_max_size = 256
end
-- This roundabout method is because text_input starts out
-- as an immutable userdata value
text_input = {
label = text_input.label or "",
text = text_input.text or "",
max_length = input_max_size,
}
end
return function()
local option_chosen, ti_content = wesnoth.show_message_dialog(msg_cfg, options, text_input)
if option_chosen == -2 then -- Pressed Escape (only if no input)
skip_messages = true
end
local result_cfg = {}
if #options > 0 then
result_cfg.value = option_chosen
end
if text_input ~= nil then
result_cfg.text = ti_content
end
return result_cfg
end
end
function wesnoth.wml_actions.message(cfg)
local show_if = helper.get_child(cfg, "show_if") or {}
if not wesnoth.eval_conditional(show_if) then
log("[message] skipped because [show_if] did not pass", "debug")
return
end
-- Only the first text_input tag is considered
local text_input
for cfg in helper.child_range(cfg, "text_input") do
if text_input ~= nil then
log("Too many [text_input] tags, only one accepted", "warning")
break
end
text_input = cfg
end
local options, option_events = {}, {}
for option in helper.child_range(cfg, "option") do
local condition = helper.get_child(cfg, "show_if") or {}
if wesnoth.eval_conditional(condition) then
table.insert(options, option.message)
table.insert(option_events, {})
for cmd in helper.child_range(option, "command") do
table.insert(option_events[#option_events], cmd)
end
end
end
-- Check if there is any input to be made, if not the message may be skipped
local has_input = text_input ~= nil or #options > 0
if not has_input and (wesnoth.skipping_replay() or skip_messages) then
-- No input to get and the user is not interested either
log("Skipping [message] because user not interested", "debug")
return
end
local sides_for = cfg.side_for
if sides_for and not has_input then
local show_for_side = false
-- Sanity checks on side number and controller
for side in utils.split(sides_for) do
side = tonumber(side)
if side > 0 and side < #wesnoth.sides and wesnoth.sides[side].controller == "human" then
show_for_side = true
break
end
end
if not show_for_side then
-- Player isn't controlling side which should see the message
log("Player isn't controlling side that should see [message]", "debug")
return
end
end
local speaker = get_speaker(cfg)
if not speaker then
-- No matching unit found, continue onto the next message
log("No speaker found for [message]", "debug")
return
elseif speaker == "narrator" then
-- Narrator, so deselect units
wesnoth.deselect_hex()
-- The speaker is expected to be either nil or a unit later
speaker = nil
else
-- Check ~= false, because the default if omitted should be true
if cfg.scroll ~= false then
wesnoth.scroll_to_tile(speaker.x, speaker.y)
end
wesnoth.select_hex(speaker.x, speaker.y, false)
end
if cfg.sound then wesnoth.play_sound(cfg.sound) end
local msg_dlg = message_user_choice(cfg, speaker, options, text_input)
local option_chosen
if not has_input then
-- Always show the dialog if it has no input, whether we are replaying or not
msg_dlg()
else
local choice = wesnoth.synchronize_choice(msg_dlg)
option_chosen = tonumber(choice.value)
if text_input ~= nil then
-- Implement the consequences of the choice
wesnoth.set_variable(text_input.variable or "input", choice.text)
end
end
if #options > 0 then
if option_chosen > #options then
log("invalid choice (" .. option_chosen .. ") was specified, choice 1 to " ..
#options .. " was expected", "debug")
return
end
for i, cmd in ipairs(option_events[option_chosen]) do
utils.handle_event_commands(cmd)
end
end
end
local on_event = wesnoth.game_events.on_event
function wesnoth.game_events.on_event(...)
if type(on_event) == "function" then on_event(...) end
skip_messages = false
end

View file

@ -96,79 +96,6 @@ namespace game_events
// (So keep it at the rop of this file?)
wml_action::map wml_action::registry_;
namespace { // advance declarations
std::string get_caption(const vconfig& cfg, unit_map::iterator speaker);
std::string get_image(const vconfig& cfg, unit_map::iterator speaker);
}
namespace { // Types
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(int /*side*/) 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 = -1;
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) {
resources::game_events->pump().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(int /*side*/) const
{
return config();
}
};
} // end anonymous namespace (types)
namespace { // Support functions
/**
@ -277,103 +204,6 @@ namespace { // Support functions
return path;
}
/**
* Helper to handle the caption part of [message].
*
* @param cfg cfg of message.
* @param speaker The speaker of the message.
*
* @returns The caption to show.
*/
std::string get_caption(const vconfig& cfg, unit_map::iterator speaker)
{
std::string caption = cfg["caption"];
if (caption.empty() && speaker != resources::units->end()) {
caption = speaker->name();
if(caption.empty()) {
caption = speaker->type_name();
}
}
return caption;
}
/**
* Helper to handle the image part of [message].
*
* @param cfg cfg of message.
* @param speaker The speaker of the message.
*
* @returns The image to show.
*/
std::string get_image(const vconfig& cfg, unit_map::iterator speaker)
{
std::string image = cfg["image"];
if (image == "none") {
return "";
}
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 speaker part of [message].
*
* @param event_info event_info of message.
* @param cfg cfg of message.
*
* @returns The unit who's the speaker or units->end().
*/
unit_map::iterator handle_speaker(const queued_event& event_info,
const vconfig& cfg, bool scroll)
{
unit_map *units = resources::units;
game_display &screen = *resources::screen;
unit_map::iterator speaker = units->end();
const std::string speaker_str = cfg["speaker"];
if(speaker_str == "unit") {
speaker = units->find(event_info.loc1);
} else if(speaker_str == "second_unit") {
speaker = units->find(event_info.loc2);
} else if(speaker_str != "narrator") {
const unit_filter ufilt(cfg, resources::filter_con);
for(speaker = units->begin(); speaker != units->end(); ++speaker){
if ( ufilt(*speaker) )
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";
screen.scroll_to_tile(spl);
}
screen.highlight_hex(spl);
} else if(speaker_str == "narrator") {
LOG_NG << "no speaker\n";
screen.highlight_hex(map_location::null_location());
} else {
return speaker;
}
screen.draw(false);
LOG_DP << "done scrolling to speaker...\n";
return speaker;
}
/**
* Implements the lifting and resetting of fog via WML.
* Keeping affect_normal_fog as false causes only the fog override to be affected.
@ -562,141 +392,6 @@ WML_HANDLER_FUNCTION(lift_fog, /*event_info*/, cfg)
toggle_fog(true, cfg, !cfg["multiturn"].to_bool(false));
}
/// Display a message dialog
WML_HANDLER_FUNCTION(message, event_info, cfg)
{
// Check if there is any input to be made, if not the message may be skipped
const vconfig::child_list menu_items = cfg.get_children("option");
const vconfig::child_list text_input_elements = cfg.get_children("text_input");
const bool has_text_input = (text_input_elements.size() == 1);
bool has_input= (has_text_input || !menu_items.empty() );
// skip messages during quick replay
play_controller *controller = resources::controller;
if(!has_input && (
controller->is_skipping_replay() ||
resources::game_events->pump().context_skip_messages()
))
{
return;
}
// Check if this message is for this side
// handeling of side_for for messages with input is done below in the get_user_choice call
std::string side_for_raw = cfg["side_for"];
if (!side_for_raw.empty() && !has_input)
{
bool side_for_show = false;
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_local_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" << std::endl;
return;
}
std::vector<std::string> options;
std::vector<vconfig::child_list> option_events;
for(vconfig::child_list::const_iterator mi = menu_items.begin();
mi != menu_items.end(); ++mi) {
std::string msg_str = (*mi)["message"];
if (!mi->has_child("show_if")
|| conditional_passed(mi->child("show_if")))
{
options.push_back(msg_str);
option_events.push_back((*mi).get_children("command"));
}
}
has_input = !options.empty() || has_text_input;
if (!has_input && resources::controller->is_skipping_replay()) {
// 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(resources::controller->current_side());
}
else
{
config choice = mp_sync::get_user_choice("input", msg, cfg["side_for"].to_int(0));
option_chosen = choice["value"];
text_input_result = choice["text"].str();
}
// Implement the consequences of the choice
if(options.empty() == false) {
if(size_t(option_chosen) >= menu_items.size()) {
std::stringstream errbuf;
errbuf << "invalid choice (" << option_chosen
<< ") was specified, choice 0 to " << (menu_items.size() - 1)
<< " was expected.\n";
replay::process_error(errbuf.str());
return;
}
BOOST_FOREACH(const vconfig &cmd, option_events[option_chosen]) {
handle_event_commands(event_info, cmd);
}
}
if(has_text_input) {
std::string variable_name=text_input_element["variable"];
if(variable_name.empty())
variable_name="input";
resources::gamedata->set_variable(variable_name, text_input_result);
}
}
WML_HANDLER_FUNCTION(modify_turns, /*event_info*/, cfg)
{
config::attribute_value value = cfg["value"];

View file

@ -75,9 +75,8 @@ namespace context {
/// State when processing a particular flight of events or commands.
struct state {
bool mutated;
bool skip_messages;
explicit state(bool s, bool m = true) : mutated(m), skip_messages(s) {}
explicit state(bool m = true) : mutated(m) {}
};
class scoped {
@ -417,8 +416,7 @@ context::scoped::scoped(std::stack<context::state> & contexts, bool m)
//The default context at least should always be on the stack
assert(contexts_.size() > 0);
bool skip_messages = (contexts_.size() > 1) && contexts_.top().skip_messages;
contexts_.push(context::state(skip_messages, m));
contexts_.push(context::state(m));
}
context::scoped::~scoped()
@ -441,18 +439,6 @@ void t_pump::context_mutated(bool b)
impl_->contexts_.top().mutated = b;
}
bool t_pump::context_skip_messages()
{
assert(impl_->contexts_.size() > 0);
return impl_->contexts_.top().skip_messages;
}
void t_pump::context_skip_messages(bool b)
{
assert(impl_->contexts_.size() > 0);
impl_->contexts_.top().skip_messages = b;
}
/**
* Helper function which determines whether a wml_message text can
* really be pushed into the wml_messages_stream, and does it.

View file

@ -71,10 +71,6 @@ namespace game_events
bool context_mutated();
/// Sets whether or not we believe WML might have changed something.
void context_mutated(bool mutated);
/// Returns whether or not we are skipping messages.
bool context_skip_messages();
/// Sets whether or not we are skipping messages.
void context_skip_messages(bool skip);
/// Helper function which determines whether a wml_message text can
/// really be pushed into the wml_messages_stream, and does it.

View file

@ -2850,6 +2850,30 @@ int game_lua_kernel::intf_select_hex(lua_State *L)
return 0;
}
/**
* Deselects any highlighted hex on the map.
* No arguments or return values
*/
int game_lua_kernel::intf_deselect_hex(lua_State*)
{
const map_location loc;
play_controller_.get_mouse_handler_base().select_hex(
loc, false, false, false);
if (game_display_) {
game_display_->highlight_hex(loc);
}
return 0;
}
/**
* Return true if a replay is in progress but the player has chosen to skip it
*/
int game_lua_kernel::intf_skipping_replay(lua_State *L)
{
lua_pushboolean(L, play_controller_.is_skipping_replay());
return 1;
}
namespace {
struct lua_synchronize : mp_sync::user_choice
{
@ -4154,6 +4178,8 @@ game_lua_kernel::game_lua_kernel(CVideo * video, game_state & gs, play_controlle
{ "scroll", &dispatch<&game_lua_kernel::intf_scroll > },
{ "scroll_to_tile", &dispatch<&game_lua_kernel::intf_scroll_to_tile > },
{ "select_hex", &dispatch<&game_lua_kernel::intf_select_hex > },
{ "deselect_hex", &dispatch<&game_lua_kernel::intf_deselect_hex > },
{ "skipping_replay", &dispatch<&game_lua_kernel::intf_skipping_replay > },
{ "set_end_campaign_credits", &dispatch<&game_lua_kernel::intf_set_end_campaign_credits > },
{ "set_end_campaign_text", &dispatch<&game_lua_kernel::intf_set_end_campaign_text > },
{ "set_menu_item", &dispatch<&game_lua_kernel::intf_set_menu_item > },

View file

@ -129,6 +129,8 @@ class game_lua_kernel : public lua_kernel_base
int intf_simulate_combat(lua_State *L);
int intf_scroll_to_tile(lua_State *L);
int intf_select_hex(lua_State *L);
int intf_deselect_hex(lua_State *L);
int intf_skipping_replay(lua_State *L);
int intf_synchronize_choice(lua_State *L);
int intf_get_locations(lua_State *L);
int intf_get_villages(lua_State *L);