Implement most of the confirm plugin in Lua

This should make resolving future issues easier, although implementing
new confirmations in lua isn't possible yet (each one requires a line
in confirm.cpp).

This also resolves an issue with note-delete and route-delete, with
dfhack/df-structures@1bc4f61
develop
lethosor 2015-11-21 18:59:30 -05:00
parent 67aed56183
commit b7dd93b6e8
4 changed files with 375 additions and 202 deletions

@ -1 +1 @@
Subproject commit 378a580f7e333607a64a301d598e3885954a5d9d
Subproject commit 1bc4f6133c8a5bbd397001f97647c15724317e07

@ -108,7 +108,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(cleanowned cleanowned.cpp)
DFHACK_PLUGIN(colonies colonies.cpp)
DFHACK_PLUGIN(command-prompt command-prompt.cpp)
DFHACK_PLUGIN(confirm confirm.cpp)
DFHACK_PLUGIN(confirm confirm.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(createitem createitem.cpp)
DFHACK_PLUGIN(cursecheck cursecheck.cpp)
DFHACK_PLUGIN(deramp deramp.cpp)

@ -6,6 +6,8 @@
#include "Export.h"
#include "PluginManager.h"
#include "VTableInterpose.h"
#include "LuaTools.h"
#include "LuaWrapper.h"
#include "uicommon.h"
#include "modules/Gui.h"
@ -38,6 +40,16 @@ inline bool in_vector (std::vector<VT> &vec, FT item)
return std::find(vec.begin(), vec.end(), item) != vec.end();
}
string char_replace (string s, char a, char b)
{
string res = s;
size_t i = res.size();
while (i--)
if (res[i] == a)
res[i] = b;
return res;
}
namespace trade {
static bool goods_selected (const std::vector<char> &selected)
{
@ -89,6 +101,70 @@ namespace trade {
}
}
namespace conf_lua {
static color_ostream_proxy *out;
static lua_State *l_state;
bool init (color_ostream &dfout)
{
out = new color_ostream_proxy(Core::getInstance().getConsole());
l_state = Lua::Open(*out);
return l_state;
}
void cleanup()
{
if (out)
{
delete out;
out = NULL;
}
lua_close(l_state);
}
bool call (const char *func, int nargs = 0, int nres = 0)
{
if (!Lua::PushModulePublic(*out, l_state, "plugins.confirm", func))
return false;
if (nargs > 0)
lua_insert(l_state, lua_gettop(l_state) - nargs);
return Lua::SafeCall(*out, l_state, nargs, nres);
}
template <typename T>
void push (T val)
{
Lua::Push(l_state, val);
}
template <typename KeyType, typename ValueType>
void table_set (lua_State *L, KeyType k, ValueType v)
{
Lua::Push(L, k);
Lua::Push(L, v);
lua_settable(L, -3);
}
namespace api {
int get_ids (lua_State *L)
{
lua_newtable(L);
for (auto it = confirmations.begin(); it != confirmations.end(); ++it)
table_set(L, it->first, true);
return 1;
}
}
}
#define CONF_LUA_FUNC(ns, name) {#name, df::wrap_function(ns::name, true)}
DFHACK_PLUGIN_LUA_FUNCTIONS {
CONF_LUA_FUNC(trade, broker_goods_selected),
CONF_LUA_FUNC(trade, broker_goods_all_selected),
CONF_LUA_FUNC(trade, trader_goods_selected),
CONF_LUA_FUNC(trade, trader_goods_all_selected),
DFHACK_LUA_END
};
#define CONF_LUA_CMD(name) {#name, conf_lua::api::name}
DFHACK_PLUGIN_LUA_COMMANDS {
CONF_LUA_CMD(get_ids),
DFHACK_LUA_END
};
template <class T>
class confirmation {
public:
@ -184,11 +260,42 @@ public:
state = INACTIVE;
}
}
virtual bool intercept_key (df::interface_key key) = 0;
virtual string get_id() = 0;
virtual string get_title() { return "Confirm"; }
virtual string get_message() = 0;
virtual UIColor get_color() { return COLOR_YELLOW; }
#define CONF_LUA_START using namespace conf_lua; Lua::StackUnwinder unwind(l_state); push(screen); push(get_id());
bool intercept_key (df::interface_key key)
{
CONF_LUA_START;
push(key);
if (call("intercept_key", 3, 1))
return lua_toboolean(l_state, -1);
else
return false;
};
string get_title()
{
CONF_LUA_START;
if (call("get_title", 2, 1) && lua_isstring(l_state, -1))
return lua_tostring(l_state, -1);
else
return "Confirm";
}
string get_message()
{
CONF_LUA_START;
if (call("get_message", 2, 1) && lua_isstring(l_state, -1))
return lua_tostring(l_state, -1);
else
return "<Message generation failed>";
};
UIColor get_color()
{
CONF_LUA_START;
if (call("get_color", 2, 1) && lua_isnumber(l_state, -1))
return lua_tointeger(l_state, -1) % 16;
else
return COLOR_YELLOW;
}
#undef CONF_LUA_START
protected:
cstate state;
df::interface_key last_key;
@ -234,8 +341,7 @@ int conf_register(confirmation<T> *c, ...)
return 0;
}
#define IMPLEMENT_CONFIRMATION_HOOKS(cls) IMPLEMENT_CONFIRMATION_HOOKS_PRIO(cls, 0)
#define IMPLEMENT_CONFIRMATION_HOOKS_PRIO(cls, prio) \
#define IMPLEMENT_CONFIRMATION_HOOKS(cls, prio) \
static cls cls##_instance; \
struct cls##_hooks : cls::screen_type { \
typedef cls::screen_type interpose_base; \
@ -265,198 +371,33 @@ static int conf_register_##cls = conf_register(&cls##_instance, \
&INTERPOSE_HOOK(cls##_hooks, key_conflict), \
NULL);
class trade_confirmation : public confirmation<df::viewscreen_tradegoodsst> {
public:
virtual bool intercept_key (df::interface_key key)
{
return key == df::interface_key::TRADE_TRADE;
}
virtual string get_id() { return "trade"; }
virtual string get_title() { return "Confirm trade"; }
virtual string get_message()
{
if (trade::trader_goods_selected(screen) && trade::broker_goods_selected(screen))
return "Are you sure you want to trade the selected goods?";
else if (trade::trader_goods_selected(screen))
return "You are not giving any items. This is likely\n"
"to irritate the merchants.\n"
"Attempt to trade anyway?";
else if (trade::broker_goods_selected(screen))
return "You are not receiving any items. You may want to\n"
"offer these items instead or choose items to receive.\n"
"Attempt to trade anyway?";
else
return "No items are selected. This is likely\n"
"to irritate the merchants.\n"
"Attempt to trade anyway?";
}
};
IMPLEMENT_CONFIRMATION_HOOKS(trade_confirmation);
class trade_cancel_confirmation : public confirmation<df::viewscreen_tradegoodsst> {
public:
virtual bool intercept_key (df::interface_key key)
{
return key == df::interface_key::LEAVESCREEN &&
(trade::trader_goods_selected(screen) || trade::broker_goods_selected(screen));
}
virtual string get_id() { return "trade-cancel"; }
virtual string get_title() { return "Cancel trade"; }
virtual string get_message() { return "Are you sure you want leave this screen?\nSelected items will not be saved."; }
};
IMPLEMENT_CONFIRMATION_HOOKS_PRIO(trade_cancel_confirmation, -1);
class trade_seize_confirmation : public confirmation<df::viewscreen_tradegoodsst> {
public:
virtual bool intercept_key (df::interface_key key)
{
return trade::trader_goods_selected(screen) && key == df::interface_key::TRADE_SEIZE;
}
virtual string get_id() { return "trade-seize"; }
virtual string get_title() { return "Confirm seize"; }
virtual string get_message() { return "Are you sure you want to sieze these goods?"; }
};
IMPLEMENT_CONFIRMATION_HOOKS(trade_seize_confirmation);
class trade_offer_confirmation : public confirmation<df::viewscreen_tradegoodsst> {
public:
virtual bool intercept_key (df::interface_key key)
{
return trade::broker_goods_selected(screen) && key == df::interface_key::TRADE_OFFER;
}
virtual string get_id() { return "trade-offer"; }
virtual string get_title() { return "Confirm offer"; }
virtual string get_message() { return "Are you sure you want to offer these goods?\nYou will receive no payment."; }
};
IMPLEMENT_CONFIRMATION_HOOKS(trade_offer_confirmation);
class trade_select_all_confirmation : public confirmation<df::viewscreen_tradegoodsst> {
public:
virtual bool intercept_key (df::interface_key key)
{
if (key == df::interface_key::SEC_SELECT)
{
if (screen->in_right_pane && trade::broker_goods_selected(screen) && !trade::broker_goods_all_selected(screen))
return true;
else if (!screen->in_right_pane && trade::trader_goods_selected(screen) && !trade::trader_goods_all_selected(screen))
return true;
}
return false;
}
virtual string get_id() { return "trade-select-all"; }
virtual string get_title() { return "Confirm selection"; }
virtual string get_message()
{
return "Selecting all goods will overwrite your current selection\n"
"and cannot be undone. Continue?";
}
};
IMPLEMENT_CONFIRMATION_HOOKS(trade_select_all_confirmation);
#define DEFINE_CONFIRMATION(cls, screen, prio) \
class confirmation_##cls : public confirmation<df::screen> { \
virtual string get_id() { static string id = char_replace(#cls, '_', '-'); return id; } \
}; \
IMPLEMENT_CONFIRMATION_HOOKS(confirmation_##cls, prio);
class hauling_route_delete_confirmation : public confirmation<df::viewscreen_dwarfmodest> {
public:
virtual bool intercept_key (df::interface_key key)
{
if (ui->main.mode == ui_sidebar_mode::Hauling &&
ui->hauling.view_routes.size() &&
!ui->hauling.in_name &&
!ui->hauling.in_stop &&
!ui->hauling.in_assign_vehicle
)
return key == df::interface_key::D_HAULING_REMOVE;
return false;
}
virtual string get_id() { return "haul-delete"; }
virtual string get_title() { return "Confirm deletion"; }
virtual string get_message()
{
std::string type = (ui->hauling.view_stops[ui->hauling.cursor_top]) ? "stop" : "route";
return std::string("Are you sure you want to delete this ") + type + "?";
}
};
IMPLEMENT_CONFIRMATION_HOOKS(hauling_route_delete_confirmation);
class depot_remove_confirmation : public confirmation<df::viewscreen_dwarfmodest> {
public:
virtual bool intercept_key (df::interface_key key)
{
df::building_tradedepotst *depot = virtual_cast<df::building_tradedepotst>(Gui::getAnyBuilding(screen));
if (depot && key == df::interface_key::DESTROYBUILDING)
{
for (auto it = ui->caravans.begin(); it != ui->caravans.end(); ++it)
{
if ((**it).time_remaining)
return true;
}
}
return false;
}
virtual string get_id() { return "depot-remove"; }
virtual string get_title() { return "Confirm depot removal"; }
virtual string get_message()
{
return "Are you sure you want to remove this depot?\n"
"Merchants are present and will lose profits.";
}
};
IMPLEMENT_CONFIRMATION_HOOKS(depot_remove_confirmation);
class squad_disband_confirmation : public confirmation<df::viewscreen_layer_militaryst> {
public:
virtual bool intercept_key (df::interface_key key)
{
return screen->page == df::viewscreen_layer_militaryst::T_page::Positions &&
screen->num_squads &&
!screen->in_rename_alert &&
key == df::interface_key::D_MILITARY_DISBAND_SQUAD;
}
virtual string get_id() { return "squad-disband"; }
virtual string get_title() { return "Disband squad"; }
virtual string get_message() { return "Are you sure you want to disband this squad?"; }
};
IMPLEMENT_CONFIRMATION_HOOKS(squad_disband_confirmation);
class uniform_delete_confirmation : public confirmation<df::viewscreen_layer_militaryst> {
public:
virtual bool intercept_key (df::interface_key key)
{
return screen->page == df::viewscreen_layer_militaryst::T_page::Uniforms &&
!screen->equip.uniforms.empty() &&
!screen->equip.in_name_uniform &&
key == df::interface_key::D_MILITARY_DELETE_UNIFORM;
}
virtual string get_id() { return "uniform-delete"; }
virtual string get_title() { return "Delete uniform"; }
virtual string get_message() { return "Are you sure you want to delete this uniform?"; }
};
IMPLEMENT_CONFIRMATION_HOOKS(uniform_delete_confirmation);
class note_delete_confirmation : public confirmation<df::viewscreen_dwarfmodest> {
public:
virtual bool intercept_key (df::interface_key key)
{
return ui->main.mode == ui_sidebar_mode::NotesPoints && key == df::interface_key::D_NOTE_DELETE;
}
virtual string get_id() { return "note-delete"; }
virtual string get_title() { return "Delete note"; }
virtual string get_message() { return "Are you sure you want to delete this note?"; }
};
IMPLEMENT_CONFIRMATION_HOOKS(note_delete_confirmation);
class route_delete_confirmation : public confirmation<df::viewscreen_dwarfmodest> {
public:
virtual bool intercept_key (df::interface_key key)
{
return ui->main.mode == ui_sidebar_mode::NotesRoutes && key == df::interface_key::D_NOTE_ROUTE_DELETE;
}
virtual string get_id() { return "route-delete"; }
virtual string get_title() { return "Delete route"; }
virtual string get_message() { return "Are you sure you want to delete this route?"; }
};
IMPLEMENT_CONFIRMATION_HOOKS(route_delete_confirmation);
/* This section defines stubs for all confirmation dialogs, with methods
implemented in plugins/lua/confirm.lua.
IDs (used in the "confirm enable/disable" command, by Lua, and in the docs)
are obtained by replacing '_' with '-' in the first argument to DEFINE_CONFIRMATION
*/
DEFINE_CONFIRMATION(trade, viewscreen_tradegoodsst, 0);
DEFINE_CONFIRMATION(trade_cancel, viewscreen_tradegoodsst, -1);
DEFINE_CONFIRMATION(trade_seize, viewscreen_tradegoodsst, 0);
DEFINE_CONFIRMATION(trade_offer, viewscreen_tradegoodsst, 0);
DEFINE_CONFIRMATION(trade_select_all, viewscreen_tradegoodsst, 0);
DEFINE_CONFIRMATION(haul_delete, viewscreen_dwarfmodest, 0);
DEFINE_CONFIRMATION(depot_remove, viewscreen_dwarfmodest, 0);
DEFINE_CONFIRMATION(squad_disband, viewscreen_layer_militaryst, 0);
DEFINE_CONFIRMATION(uniform_delete, viewscreen_layer_militaryst, 0);
DEFINE_CONFIRMATION(note_delete, viewscreen_dwarfmodest, 0);
DEFINE_CONFIRMATION(route_delete, viewscreen_dwarfmodest, 0);
DFhackCExport command_result plugin_init (color_ostream &out, vector <PluginCommand> &commands)
{
if (!conf_lua::init(out))
return CR_FAILURE;
commands.push_back(PluginCommand(
"confirm",
"Confirmation dialogs",
@ -480,6 +421,12 @@ DFhackCExport command_result plugin_enable (color_ostream &out, bool enable)
}
is_enabled = enable;
}
if (is_enabled)
{
using namespace conf_lua;
Lua::StackUnwinder unwind(l_state);
call("check");
}
return CR_OK;
}
@ -487,6 +434,7 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
{
if (plugin_enable(out, false) != CR_OK)
return CR_FAILURE;
conf_lua::cleanup();
return CR_OK;
}
@ -509,13 +457,11 @@ command_result df_confirm (color_ostream &out, vector <string> & parameters)
{
CoreSuspender suspend;
bool state = true;
if (in_vector(parameters, "help") || in_vector(parameters, "status"))
if (parameters.empty() || in_vector(parameters, "help") || in_vector(parameters, "status"))
{
out << "Available options: \n";
for (auto it = confirmations.begin(); it != confirmations.end(); ++it)
{
out << " " << it->first << ": " << (it->second->is_enabled() ? "enabled" : "disabled") << std::endl;
}
out.print(" %20s: %s\n", it->first.c_str(), it->second->is_enabled() ? "enabled" : "disabled");
return CR_OK;
}
for (auto it = parameters.begin(); it != parameters.end(); ++it)
@ -527,9 +473,7 @@ command_result df_confirm (color_ostream &out, vector <string> & parameters)
else if (*it == "all")
{
for (auto it = confirmations.begin(); it != confirmations.end(); ++it)
{
it->second->apply(state);
}
}
else
enable_conf(out, *it, state);

@ -0,0 +1,229 @@
local _ENV = mkmodule('plugins.confirm')
local ui = df.global.ui
local confs = {}
-- Wraps df.interface_key[foo] functionality but fails with invalid keys
keys = {}
setmetatable(keys, {
__index = function(self, k)
return df.interface_key[k] and df.interface_key[k] or error('Invalid key: ' .. tostring(k))
end,
__newindex = function() error('Table is read-only') end
})
--[[ The screen where a confirmation has been triggered
Note that this is *not* necessarily the topmost viewscreen, so do not use
gui.getCurViewscreen() or related functions. ]]
screen = nil
function if_nil(obj, default)
if obj == nil then
return default
else
return obj
end
end
function defconf(id)
if not get_ids()[id] then
error('Bad confirmation ID (not defined in plugin): ' .. id)
end
local cls = {}
cls.intercept_key = function(key) return false end
cls.get_title = function() return if_nil(cls.title, '<No title>') end
cls.get_message = function() return if_nil(cls.message, '<No message>') end
cls.get_color = function() return if_nil(cls.color, COLOR_YELLOW) end
confs[id] = cls
return cls
end
--[[ Beginning of confirmation definitions
All confirmations declared in confirm.cpp must have a corresponding call to
defconf() here, and should implement intercept_key(), get_title(), and
get_message(). get_color() can also be implemented here, but the default should
be sufficient.
In cases where getter functions always return the same value (e.g. get_title()),
they can be replaced with a field named after the method without the "get_"
prefix:
trade.title = "Confirm trade"
is equivalent to:
function trade.get_title() return "Confirm trade" end
]]
trade = defconf('trade')
function trade.intercept_key(key)
return screen.in_edit_count == 0 and
key == keys.TRADE_TRADE
end
trade.title = "Confirm trade"
function trade.get_message()
if trader_goods_selected(screen) and broker_goods_selected(screen) then
return "Are you sure you want to trade the selected goods?"
elseif trader_goods_selected(screen) then
return "You are not giving any items. This is likely\n" ..
"to irritate the merchants.\n" ..
"Attempt to trade anyway?"
elseif broker_goods_selected(screen) then
return "You are not receiving any items. You may want to\n" ..
"offer these items instead or choose items to receive.\n" ..
"Attempt to trade anyway?"
else
return "No items are selected. This is likely\n" ..
"to irritate the merchants.\n" ..
"Attempt to trade anyway?"
end
end
trade_cancel = defconf('trade-cancel')
function trade_cancel.intercept_key(key)
return screen.in_edit_count == 0 and
key == keys.LEAVESCREEN and
(trader_goods_selected(screen) or broker_goods_selected(screen))
end
trade_cancel.title = "Cancel trade"
trade_cancel.message = "Are you sure you want leave this screen?\nSelected items will not be saved."
trade_seize = defconf('trade-seize')
function trade_seize.intercept_key(key)
return screen.in_edit_count == 0 and
trader_goods_selected(screen) and
key == keys.TRADE_SEIZE
end
trade_seize.title = "Confirm seize"
trade_seize.message = "Are you sure you want to sieze these goods?"
trade_offer = defconf('trade-offer')
function trade_offer.intercept_key(key)
return screen.in_edit_count == 0 and
broker_goods_selected(screen) and
key == keys.TRADE_OFFER
end
trade_offer.title = "Confirm offer"
trade_offer.message = "Are you sure you want to offer these goods?\nYou will receive no payment."
trade_select_all = defconf('trade-select-all')
function trade_select_all.intercept_key(key)
if screen.in_edit_count == 0 and key == keys.SEC_SELECT then
if screen.in_right_pane and broker_goods_selected(screen) and not broker_goods_all_selected(screen) then
return true
elseif not screen.in_right_pane and trader_goods_selected(screen) and not trader_goods_all_selected(screen) then
return true
end
end
return false
end
trade_select_all.title = "Confirm selection"
trade_select_all.message = "Selecting all goods will overwrite your current selection\n" ..
"and cannot be undone. Continue?"
haul_delete = defconf('haul-delete')
function haul_delete.intercept_key(key)
if ui.main.mode == df.ui_sidebar_mode.Hauling and
#ui.hauling.view_routes > 0 and
not ui.hauling.in_name and
not ui.hauling.in_stop and
not ui.hauling.in_assign_vehicle then
return key == keys.D_HAULING_REMOVE
end
return false
end
haul_delete.title = "Confirm deletion"
function haul_delete.get_message()
local t = ui.hauling.view_stops[ui.hauling.cursor_top] and "stop" or "route"
return "Are you sure you want to delete this " ..
(ui.hauling.view_stops[ui.hauling.cursor_top] and "stop" or "route") .. "?"
end
depot_remove = defconf('depot-remove')
function depot_remove.intercept_key(key)
if df.building_tradedepotst:is_instance(dfhack.gui.getSelectedBuilding(true)) and
key == keys.DESTROYBUILDING then
for _, caravan in pairs(ui.caravans) do
if caravan.time_remaining > 0 then
return true
end
end
end
end
depot_remove.title = "Confirm depot removal"
depot_remove.message = "Are you sure you want to remove this depot?\n" ..
"Merchants are present and will lose profits."
squad_disband = defconf('squad-disband')
function squad_disband.intercept_key(key)
return key == keys.D_MILITARY_DISBAND_SQUAD and
screen.page == screen._type.T_page.Positions and
screen.num_squads > 0 and
not screen.in_rename_alert
end
squad_disband.title = "Disband squad"
squad_disband.message = "Are you sure you want to disband this squad?"
uniform_delete = defconf('uniform-delete')
function uniform_delete.intercept_key(key)
return key == keys.D_MILITARY_DELETE_UNIFORM and
screen.page == screen._type.T_page.Uniforms and
#screen.equip.uniforms > 0 and
not screen.equip.in_name_uniform
end
uniform_delete.title = "Delete uniform"
uniform_delete.message = "Are you sure you want to delete this uniform?"
note_delete = defconf('note-delete')
function note_delete.intercept_key(key)
return key == keys.D_NOTE_DELETE and
ui.main.mode == df.ui_sidebar_mode.NotesPoints and
not ui.waypoints.in_edit_text_mode
end
note_delete.title = "Delete note"
note_delete.message = "Are you sure you want to delete this note?"
route_delete = defconf('route-delete')
function route_delete.intercept_key(key)
return key == keys.D_NOTE_ROUTE_DELETE and
ui.main.mode == df.ui_sidebar_mode.NotesRoutes and
not ui.waypoints.in_edit_name_mode
end
route_delete.title = "Delete route"
route_delete.message = "Are you sure you want to delete this route?"
-- End of confirmation definitions
function check()
local undefined = {}
for id in pairs(get_ids()) do
if not confs[id] then
table.insert(undefined, id)
end
end
if #undefined > 0 then
error('Confirmation definitions missing: ' .. table.concat(undefined, ', '))
end
end
--[[
The C++ plugin invokes methods of individual confirmations through four
functions (corresponding to method names) which receive the relevant screen,
the confirmation ID, and extra arguments in some cases, but these don't have to
do aything unique.
]]
function define_wrapper(name)
_ENV[name] = function(scr, id, ...)
_ENV.screen = scr
if not confs[id] then
error('Bad confirmation ID: ' .. id)
end
return confs[id][name](...)
end
end
define_wrapper('intercept_key')
define_wrapper('get_title')
define_wrapper('get_message')
define_wrapper('get_color')
return _ENV