implement CLI interface

develop
myk002 2022-11-02 12:33:06 -07:00
parent a76c04c9ec
commit 6e6e174c31
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
2 changed files with 245 additions and 71 deletions

@ -1,5 +1,6 @@
local _ENV = mkmodule('plugins.overlay') local _ENV = mkmodule('plugins.overlay')
local gui = require('gui')
local json = require('json') local json = require('json')
local utils = require('utils') local utils = require('utils')
local widgets = require('gui.widgets') local widgets = require('gui.widgets')
@ -8,10 +9,179 @@ local WIDGETS_ENABLED_FILE = 'dfhack-config/overlay/widgets.json'
local WIDGETS_STATE_DIR = 'dfhack-config/overlay/widgets/' local WIDGETS_STATE_DIR = 'dfhack-config/overlay/widgets/'
local widget_db = {} -- map of widget name to state local widget_db = {} -- map of widget name to state
local widget_index = {} -- list of widget names
local active_hotspot_widgets = {} -- map of widget names to the db entry local active_hotspot_widgets = {} -- map of widget names to the db entry
local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db
local active_triggered_screen = nil local active_triggered_screen = nil
local function load_config(path)
local ok, config = safecall(json.decode_file, path)
return ok and config or {}
end
local function save_config(data, path)
if not safecall(json.encode_file, data, path) then
dfhack.printerr(('failed to save overlay config file: "%s"')
:format(path))
end
end
local function normalize_list(element_or_list)
if type(element_or_list) == 'table' then return element_or_list end
return {element_or_list}
end
-- allow "short form" to be specified, but use "long form"
local function normalize_viewscreen_name(vs_name)
if vs_name:match('viewscreen_.*st') then return vs_name end
return 'viewscreen_' .. vs_name .. 'st'
end
local function is_empty(tbl)
for _ in pairs(tbl) do
return false
end
return true
end
local function get_name(name_or_number)
local num = tonumber(name_or_number)
if num and widget_index[num] then
return widget_index[num]
end
return tostring(name_or_number)
end
local function do_by_names_or_numbers(args, fn)
for _,name_or_number in ipairs(normalize_list(args)) do
local name = get_name(name_or_number)
local db_entry = widget_db[name]
if not db_entry then
dfhack.printerr(('widget not found: "%s"'):format(name))
else
fn(name, db_entry)
end
end
end
local function save_enabled()
local enabled_map = {}
for name,db_entry in pairs(widget_db) do
enabled_map[name] = db_entry.enabled
end
save_config(enabled_map, WIDGETS_ENABLED_FILE)
end
local function do_enable(args)
do_by_names_or_numbers(args, function(name, db_entry)
db_entry.enabled = true
if db_entry.config.hotspot then
active_hotspot_widgets[name] = db_entry
end
for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do
vs_name = normalize_viewscreen_name(vs_name)
ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry
end
end)
save_enabled()
end
local function do_disable(args)
do_by_names_or_numbers(args, function(name, db_entry)
db_entry.enabled = false
if db_entry.config.hotspot then
active_hotspot_widgets[name] = nil
end
for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do
vs_name = normalize_viewscreen_name(vs_name)
ensure_key(active_viewscreen_widgets, vs_name)[name] = nil
if is_empty(active_viewscreen_widgets[vs_name]) then
active_viewscreen_widgets[vs_name] = nil
end
end
end)
save_enabled()
end
local function do_list(args)
local filter = args and #args > 0
for i,name in ipairs(widget_index) do
if filter then
local passes = false
for _,str in ipairs(args) do
if name:find(str) then
passes = true
break
end
end
if not passes then goto continue end
end
local enabled = widget_db[name].enabled
dfhack.color(enabled and COLOR_YELLOW or COLOR_LIGHTGREEN)
dfhack.print(enabled and '[enabled] ' or '[disabled]')
dfhack.color()
print((' %d) %s%s'):format(i, name,
widget_db[name].widget.overlay_trigger and ' (can trigger)' or ''))
::continue::
end
end
local function make_frame(config, old_frame)
local old_frame, frame = old_frame or {}, {}
frame.w, frame.h = old_frame.w, old_frame.h
local pos = utils.assign({x=-1, y=20}, config.pos or {})
-- if someone accidentally uses 1-based instead of 0-based indexing, fix it
if pos.x == 0 then pos.x = 1 end
if pos.y == 0 then pos.y = 1 end
if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end
if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end
return frame
end
local function get_screen_rect()
local w, h = dfhack.screen.getWindowSize()
return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)}
end
local function do_reposition(args)
local name_or_number, x, y = table.unpack(args)
local name = get_name(name_or_number)
local db_entry = widget_db[name]
local config = db_entry.config
config.pos.x, config.pos.y = tonumber(x), tonumber(y)
db_entry.widget.frame = make_frame(config, db_entry.widget.frame)
db_entry.widget:updateLayout(get_screen_rect())
save_config(config, WIDGETS_STATE_DIR .. name .. '.json')
end
local function do_trigger(args)
local target = args[1]
do_by_names_or_numbers(target, function(name, db_entry)
if db_entry.widget.overlay_trigger then
db_entry.widget:overlay_trigger()
end
end)
end
local command_fns = {
enable=do_enable,
disable=do_disable,
list=do_list,
reload=function() reload() end,
reposition=do_reposition,
trigger=do_trigger,
}
local HELP_ARGS = utils.invert{'help', '--help', '-h'}
function overlay_command(args)
local command = table.remove(args, 1) or 'help'
if HELP_ARGS[command] or not command_fns[command] then return false end
command_fns[command](args)
return true
end
local function instantiate_widget(name, config) local function instantiate_widget(name, config)
local provider = config.provider local provider = config.provider
local ok, provider_env = pcall(require, provider) local ok, provider_env = pcall(require, provider)
@ -37,50 +207,25 @@ local function instantiate_widget(name, config)
return nil return nil
end end
local frame = {} return provider_env[classname]{frame=make_frame(config)}
local pos = utils.assign({x=-1, y=20}, config.pos or {})
if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end
if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end
return provider_env[classname]{frame=frame}
end
local function normalize_list(element_or_list)
if type(element_or_list) == 'table' then return element_or_list end
return {element_or_list}
end
-- allow "short form" to be specified, but use "long form"
local function normalize_viewscreen_name(vs_name)
if vs_name:match('viewscreen_.*st') then return vs_name end
return 'viewscreen_' .. vs_name .. 'st'
end end
local function load_widget(name, config, enabled) local function load_widget(name, config, enabled)
local widget = instantiate_widget(name, config) local widget = instantiate_widget(name, config)
if not widget then return end if not widget then return end
local db_entry = { local db_entry = {
enabled=enabled,
config=config,
widget=widget, widget=widget,
next_update_ms=widget.overlay_onupdate and 0 or math.huge, next_update_ms=widget.overlay_onupdate and 0 or math.huge,
} }
widget_db[name] = db_entry widget_db[name] = db_entry
if not enabled then return end if enabled then do_enable(name) end
if config.hotspot then
active_hotspot_widgets[name] = db_entry
end
for vs_name in ipairs(normalize_list(config.viewscreens)) do
vs_name = normalize_viewscreen_name(vs_name)
ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry
end
end
local function load_config(fname)
local ok, config = pcall(json.decode_file, fname)
return ok and config or {}
end end
function reload() function reload()
widget_db = {} widget_db = {}
widget_index = {}
active_hotspot_widgets = {} active_hotspot_widgets = {}
active_viewscreen_widgets = {} active_viewscreen_widgets = {}
active_triggered_screen = nil active_triggered_screen = nil
@ -100,6 +245,23 @@ function reload()
load_widget(name, widget_config, not not enabled_map[name]) load_widget(name, widget_config, not not enabled_map[name])
::continue:: ::continue::
end end
for name in pairs(widget_db) do
table.insert(widget_index, name)
end
table.sort(widget_index)
reposition_widgets()
end
local function detect_frame_change(widget, fn)
local frame = widget.frame
local w, h = frame.w, frame.h
local ret = fn()
if w ~= frame.w or h ~= frame.h then
widget:updateLayout()
end
return ret
end end
-- reduces the next call by a small random amount to introduce jitter into the -- reduces the next call by a small random amount to introduce jitter into the
@ -108,9 +270,10 @@ local function do_update(db_entry, now_ms, vs)
if db_entry.next_update_ms > now_ms then return end if db_entry.next_update_ms > now_ms then return end
local w = db_entry.widget local w = db_entry.widget
local freq_ms = w.overlay_onupdate_max_freq_seconds * 1000 local freq_ms = w.overlay_onupdate_max_freq_seconds * 1000
local jitter = math.rand(0, freq_ms // 8) -- up to ~12% jitter local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter
db_entry.next_update_ms = now_ms + freq_ms - jitter db_entry.next_update_ms = now_ms + freq_ms - jitter
if w:overlay_onupdate(vs) then if detect_frame_change(w,
function() return w:overlay_onupdate(vs) end) then
active_triggered_screen = w:overlay_trigger() active_triggered_screen = w:overlay_trigger()
if active_triggered_screen then return true end if active_triggered_screen then return true end
end end
@ -140,7 +303,11 @@ function feed_viewscreen_widgets(vs_name, keys)
local vs_widgets = active_viewscreen_widgets[vs_name] local vs_widgets = active_viewscreen_widgets[vs_name]
if not vs_widgets then return false end if not vs_widgets then return false end
for _,db_entry in pairs(vs_widgets) do for _,db_entry in pairs(vs_widgets) do
if db_entry.widget:onInput(keys) then return true end local widget = db_entry.widget
if detect_frame_change(widget,
function() return widget:onInput(keys) end) then
return true
end
end end
return false return false
end end
@ -148,18 +315,18 @@ end
function render_viewscreen_widgets(vs_name) function render_viewscreen_widgets(vs_name)
local vs_widgets = active_viewscreen_widgets[vs_name] local vs_widgets = active_viewscreen_widgets[vs_name]
if not vs_widgets then return false end if not vs_widgets then return false end
local dc = Painter.new() local dc = gui.Painter.new()
for _,db_entry in pairs(vs_widgets) do for _,db_entry in pairs(vs_widgets) do
db_entry.widget:render(dc) local widget = db_entry.widget
detect_frame_change(widget, function() widget:render(dc) end)
end end
end end
-- called when the DF window is resized -- called when the DF window is resized
function reposition_widgets() function reposition_widgets()
local w, h = dscreen.getWindowSize() local sr = get_screen_rect()
local vr = ViewRect{rect=mkdims_wh(0, 0, w, h)}
for _,db_entry in pairs(widget_db) do for _,db_entry in pairs(widget_db) do
db_entry.widget:updateLayout(vr) db_entry.widget:updateLayout(sr)
end end
end end
@ -168,4 +335,10 @@ OverlayWidget.ATTRS{
overlay_onupdate_max_freq_seconds=5, overlay_onupdate_max_freq_seconds=5,
} }
function OverlayWidget:preinit(info)
info.frame = info.frame or {}
info.frame.w = info.frame.w or 5
info.frame.h = info.frame.h or 1
end
return _ENV return _ENV

@ -103,9 +103,8 @@ namespace DFHack {
static df::coord2d screenSize; static df::coord2d screenSize;
template<typename FA, typename FR> template<typename FA, typename FR>
static void call_overlay_lua(const char *fn_name, int nargs, int nres, static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs,
FA && args_lambda, int nres, FA && args_lambda, FR && res_lambda) {
FR && res_lambda) {
DEBUG(event).print("calling overlay lua function: '%s'\n", fn_name); DEBUG(event).print("calling overlay lua function: '%s'\n", fn_name);
CoreSuspender guard; CoreSuspender guard;
@ -113,32 +112,33 @@ static void call_overlay_lua(const char *fn_name, int nargs, int nres,
auto L = Lua::Core::State; auto L = Lua::Core::State;
Lua::StackUnwinder top(L); Lua::StackUnwinder top(L);
color_ostream &out = Core::getInstance().getConsole(); if (!out)
out = &Core::getInstance().getConsole();
if (!lua_checkstack(L, 1 + nargs) || if (!lua_checkstack(L, 1 + nargs) ||
!Lua::PushModulePublic( !Lua::PushModulePublic(
out, L, "plugins.overlay", fn_name)) { *out, L, "plugins.overlay", fn_name)) {
out.printerr("Failed to load overlay Lua code\n"); out->printerr("Failed to load overlay Lua code\n");
return; return;
} }
std::forward<FA&&>(args_lambda)(L); std::forward<FA&&>(args_lambda)(L);
if (!Lua::SafeCall(out, L, nargs, nres)) if (!Lua::SafeCall(*out, L, nargs, nres))
out.printerr("Failed Lua call to '%s'\n", fn_name); out->printerr("Failed Lua call to '%s'\n", fn_name);
std::forward<FR&&>(res_lambda)(L); std::forward<FR&&>(res_lambda)(L);
} }
static auto DEFAULT_LAMBDA = [](lua_State *){}; static auto DEFAULT_LAMBDA = [](lua_State *){};
template<typename FA> template<typename FA>
static void call_overlay_lua(const char *fn_name, int nargs, int nres, static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs,
FA && args_lambda) { int nres, FA && args_lambda) {
call_overlay_lua(fn_name, nargs, nres, args_lambda, DEFAULT_LAMBDA); call_overlay_lua(out, fn_name, nargs, nres, args_lambda, DEFAULT_LAMBDA);
} }
static void call_overlay_lua(const char *fn_name) { static void call_overlay_lua(color_ostream *out, const char *fn_name) {
call_overlay_lua(fn_name, 0, 0, DEFAULT_LAMBDA, DEFAULT_LAMBDA); call_overlay_lua(out, fn_name, 0, 0, DEFAULT_LAMBDA, DEFAULT_LAMBDA);
} }
template<class T> template<class T>
@ -147,14 +147,15 @@ struct viewscreen_overlay : T {
DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) {
INTERPOSE_NEXT(logic)(); INTERPOSE_NEXT(logic)();
call_overlay_lua("update_viewscreen_widgets", 2, 0, [&](lua_State *L) { call_overlay_lua(NULL, "update_viewscreen_widgets", 2, 0,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName()); Lua::Push(L, T::_identity.getName());
Lua::Push(L, this); Lua::Push(L, this);
}); });
} }
DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set<df::interface_key> *input)) { DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set<df::interface_key> *input)) {
bool input_is_handled = false; bool input_is_handled = false;
call_overlay_lua("feed_viewscreen_widgets", 2, 1, call_overlay_lua(NULL, "feed_viewscreen_widgets", 2, 1,
[&](lua_State *L) { [&](lua_State *L) {
Lua::Push(L, T::_identity.getName()); Lua::Push(L, T::_identity.getName());
Lua::PushInterfaceKeys(L, *input); Lua::PushInterfaceKeys(L, *input);
@ -166,7 +167,8 @@ struct viewscreen_overlay : T {
} }
DEFINE_VMETHOD_INTERPOSE(void, render, ()) { DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
INTERPOSE_NEXT(render)(); INTERPOSE_NEXT(render)();
call_overlay_lua("render_viewscreen_widgets", 2, 0, [&](lua_State *L) { call_overlay_lua(NULL, "render_viewscreen_widgets", 2, 0,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName()); Lua::Push(L, T::_identity.getName());
Lua::Push(L, this); Lua::Push(L, this);
}); });
@ -362,15 +364,14 @@ DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
#undef INTERPOSE_HOOKS_FAILED #undef INTERPOSE_HOOKS_FAILED
static command_result overlay_cmd(color_ostream &out, std::vector <std::string> & parameters) { static command_result overlay_cmd(color_ostream &out, std::vector <std::string> & parameters) {
if (DBG_NAME(control).isEnabled(DebugCategory::LDEBUG)) { bool show_help = false;
DEBUG(control).print("interpreting command with %zu parameters:\n", call_overlay_lua(&out, "overlay_command", 1, 1, [&](lua_State *L) {
parameters.size()); Lua::PushVector(L, parameters);
for (auto &param : parameters) { }, [&](lua_State *L) {
DEBUG(control).print(" %s\n", param.c_str()); show_help = !lua_toboolean(L, -1);
} });
}
return CR_OK; return show_help ? CR_WRONG_USAGE : CR_OK;
} }
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) { DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
@ -381,7 +382,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
overlay_cmd)); overlay_cmd));
screenSize = Screen::getWindowSize(); screenSize = Screen::getWindowSize();
call_overlay_lua("reload"); call_overlay_lua(&out, "reload");
return plugin_enable(out, true); return plugin_enable(out, true);
} }
@ -393,9 +394,9 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) {
DFhackCExport command_result plugin_onupdate (color_ostream &out) { DFhackCExport command_result plugin_onupdate (color_ostream &out) {
df::coord2d newScreenSize = Screen::getWindowSize(); df::coord2d newScreenSize = Screen::getWindowSize();
if (newScreenSize != screenSize) { if (newScreenSize != screenSize) {
call_overlay_lua("reposition_widgets"); call_overlay_lua(&out, "reposition_widgets");
screenSize = newScreenSize; screenSize = newScreenSize;
} }
call_overlay_lua("update_hotspot_widgets"); call_overlay_lua(&out, "update_hotspot_widgets");
return CR_OK; return CR_OK;
} }