From 1ebf58ff8352c45429fd2d64d04734c01946e77b Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 4 Nov 2022 12:57:05 -0700 Subject: [PATCH] rewrite conf management according to discord convo move non-user-editable state into the widget config remove registry json files all user-editable state is now in a single overlay.json file --- dfhack-config/overlay/widgets.json | 2 - plugins/lua/overlay.lua | 332 +++++++++++++++++------------ 2 files changed, 199 insertions(+), 135 deletions(-) delete mode 100644 dfhack-config/overlay/widgets.json diff --git a/dfhack-config/overlay/widgets.json b/dfhack-config/overlay/widgets.json deleted file mode 100644 index 2c63c0851..000000000 --- a/dfhack-config/overlay/widgets.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index cb499eaa9..e23caa3e7 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -5,27 +5,56 @@ local json = require('json') local utils = require('utils') local widgets = require('gui.widgets') -local WIDGETS_ENABLED_FILE = 'dfhack-config/overlay/widgets.json' -local WIDGETS_STATE_DIR = 'dfhack-config/overlay/widgets/' +local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json' +local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS' -local widget_db = {} -- map of widget name to state -local widget_index = {} -- list of widget names +local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2 + +-- ---------------- -- +-- state and config -- +-- ---------------- -- + +local active_triggered_screen = nil -- if non-nil, hotspots will not get updates +local widget_db = {} -- map of widget name to ephermeral state +local widget_index = {} -- ordered list of widget names +local overlay_config = {} -- map of widget name to persisted state 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_triggered_screen = nil -local function load_config(path) - local ok, config = safecall(json.decode_file, path) - return ok and config or {} +local function reset() + if active_triggered_screen then + active_triggered_screen:dismiss() + end + active_triggered_screen = nil + + widget_db = {} + widget_index = {} + + local ok, config = pcall(json.decode_file, OVERLAY_CONFIG_FILE) + overlay_config = ok and config or {} + + active_hotspot_widgets = {} + active_viewscreen_widgets = {} end -local function save_config(data, path) - if not safecall(json.encode_file, data, path) then +local function save_config() + if not safecall(json.encode_file, overlay_config, OVERLAY_CONFIG_FILE) then dfhack.printerr(('failed to save overlay config file: "%s"') :format(path)) end end +local function triggered_screen_has_lock() + if not active_triggered_screen then return false end + if active_triggered_screen:isActive() then return true end + active_triggered_screen = nil + return false +end + +-- ----------- -- +-- utility fns -- +-- ----------- -- + local function normalize_list(element_or_list) if type(element_or_list) == 'table' then return element_or_list end return {element_or_list} @@ -44,6 +73,32 @@ local function is_empty(tbl) return true end +local function sanitize_pos(pos) + local x = math.floor(tonumber(pos.x) or DEFAULT_X_POS) + local y = math.floor(tonumber(pos.y) or DEFAULT_Y_POS) + -- if someone accidentally uses 1-based instead of 0-based indexing, fix it + if x == 0 then x = 1 end + if y == 0 then y = 1 end + return {x=x, y=y} +end + +local function make_frame(pos, old_frame) + old_frame = old_frame or {} + local frame = {w=old_frame.w, h=old_frame.h} + 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 + +-- ------------- -- +-- CLI functions -- +-- ------------- -- + local function get_name(name_or_number) local num = tonumber(name_or_number) if num and widget_index[num] then @@ -64,47 +119,46 @@ local function do_by_names_or_numbers(args, fn) 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) +local function do_enable(args, quiet, skip_save) do_by_names_or_numbers(args, function(name, db_entry) - db_entry.enabled = true - if db_entry.config.hotspot then + overlay_config[name].enabled = true + if db_entry.widget.hotspot then active_hotspot_widgets[name] = db_entry end - for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do + for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry end + if not quiet then + print(('enabled widget %s'):format(name)) + end end) - save_enabled() + if not skip_save then + save_config() + end 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 + overlay_config[name].enabled = false + if db_entry.widget.hotspot then active_hotspot_widgets[name] = nil end - for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do + for _,vs_name in ipairs(normalize_list(db_entry.widget.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 + print(('disabled widget %s'):format(name)) end) - save_enabled() + save_config() end local function do_list(args) local filter = args and #args > 0 + local num_filtered = 0 for i,name in ipairs(widget_index) do if filter then local passes = false @@ -114,51 +168,125 @@ local function do_list(args) break end end - if not passes then goto continue end + if not passes then + num_filtered = num_filtered + 1 + goto continue + end end - local enabled = widget_db[name].enabled + local db_entry = widget_db[name] + local enabled = overlay_config[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 '')) + db_entry.widget.overlay_trigger and ' (can trigger)' or '')) ::continue:: end + if num_filtered > 0 then + print(('(%d widgets filtered out)'):format(num_filtered)) + 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 +local function load_widget(name, widget_class) + local widget = widget_class{name=name} + widget_db[name] = { + widget=widget, + next_update_ms=widget.overlay_onupdate and 0 or math.huge, + } + if not overlay_config[name] then overlay_config[name] = {} end + local config = overlay_config[name] + if not config.pos then + config.pos = sanitize_pos(widget.default_pos) + end + widget.frame = make_frame(config.pos, widget.frame) + if config.enabled then + do_enable(name, true, true) + else + config.enabled = false + end end -local function get_screen_rect() - local w, h = dfhack.screen.getWindowSize() - return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)} +local function load_widgets(env_prefix, provider, env_fn) + local env_name = env_prefix .. provider + local ok, provider_env = pcall(env_fn, env_name) + if not ok or not provider_env[OVERLAY_WIDGETS_VAR] then return end + local overlay_widgets = provider_env[OVERLAY_WIDGETS_VAR] + if type(overlay_widgets) ~= 'table' then + dfhack.printerr( + ('error loading overlay widgets from "%s": %s map is malformed') + :format(env_name, OVERLAY_WIDGETS_VAR)) + return + end + for widget_name,widget_class in pairs(overlay_widgets) do + local name = provider .. '.' .. widget_name + if not safecall(load_widget, name, widget_class) then + dfhack.printerr(('error loading overlay widget "%s"'):format(name)) + end + end +end + +-- also called directly from cpp on init +function reload() + reset() + + for _,plugin in ipairs(dfhack.internal.listPlugins()) do + load_widgets('plugins.', plugin, require) + end + for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do + local files = dfhack.filesystem.listdir_recursive( + script_path, nil, false) + if not files then goto skip_path end + for _,f in ipairs(files) do + if not f.isdir and + f.path:endswith('.lua') and + not f.path:startswith('test/') and + not f.path:startswith('internal/') then + local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua' + load_widgets('', script_name, reqscript) + end + end + ::skip_path:: + end + + for name in pairs(widget_db) do + table.insert(widget_index, name) + end + table.sort(widget_index) + + reposition_widgets() +end + +local function do_reload() + reload() + print('reloaded overlay configuration') 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') + -- TODO: check existence of widget, validate numbers, warn if offscreen + local pos = sanitize_pos{x=tonumber(x), y=tonumber(y)} + overlay_config[name].pos = pos + local widget = widget_db[name].widget + widget.frame = make_frame(pos, widget.frame) + widget:updateLayout(get_screen_rect()) + save_config() + print(('repositioned widget %s to x=%d, y=%d'):format(name, pos.x, pos.y)) end +-- note that the widget does not have to be enabled to be triggered local function do_trigger(args) + if triggered_screen_has_lock() then + dfhack.printerr( + 'cannot trigger widget; another widget is already active') + return + end 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() + local widget = db_entry.widget + if widget.overlay_trigger then + widget:overlay_trigger() + print(('triggered widget %s'):format(name)) end end) end @@ -167,7 +295,7 @@ local command_fns = { enable=do_enable, disable=do_disable, list=do_list, - reload=function() reload() end, + reload=do_reload, reposition=do_reposition, trigger=do_trigger, } @@ -177,82 +305,13 @@ 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 provider = config.provider - local ok, provider_env = pcall(require, provider) - if not ok then - ok, provider_env = pcall(require, 'plugins.'..provider) - end - if not ok then - ok, provider_env = pcall(reqscript, provider) - end - if not ok then - dfhack.printerr( - ('error loading overlay widget "%s": could not find provider' .. - ' environment "%s"') - :format(name, provider)) - return nil - end - - local classname = config.class - if not provider_env[classname] then - dfhack.printerr( - ('error loading overlay widget "%s": could not find class "%s"') - :format(name, classname)) - return nil - end - - return provider_env[classname]{frame=make_frame(config)} -end - -local function load_widget(name, config, enabled) - local widget = instantiate_widget(name, config) - if not widget then return end - local db_entry = { - enabled=enabled, - config=config, - widget=widget, - next_update_ms=widget.overlay_onupdate and 0 or math.huge, - } - widget_db[name] = db_entry - if enabled then do_enable(name) end -end - -function reload() - widget_db = {} - widget_index = {} - active_hotspot_widgets = {} - active_viewscreen_widgets = {} - active_triggered_screen = nil - - local enabled_map = load_config(WIDGETS_ENABLED_FILE) - for _,fname in ipairs(dfhack.filesystem.listdir(WIDGETS_STATE_DIR)) do - local _,_,name = fname:find('^(.*)%.json$') - if not name then goto continue end - local widget_config = load_config(WIDGETS_STATE_DIR..fname) - if not widget_config.provider or not widget_config.class then - dfhack.printerr( - ('error loading overlay widget "%s": "provider" and' .. - ' "class" must be specified in %s%s') - :format(name, WIDGETS_STATE_DIR, fname)) - goto continue - end - load_widget(name, widget_config, not not enabled_map[name]) - ::continue:: - end - - for name in pairs(widget_db) do - table.insert(widget_index, name) - end - table.sort(widget_index) - - reposition_widgets() -end +-- ---------------- -- +-- event management -- +-- ---------------- -- local function detect_frame_change(widget, fn) local frame = widget.frame @@ -280,10 +339,7 @@ local function do_update(db_entry, now_ms, vs) end function update_hotspot_widgets() - if active_triggered_screen then - if active_triggered_screen:isActive() then return end - active_triggered_screen = nil - end + if triggered_screen_has_lock() then return end local now_ms = dfhack.getTickCount() for _,db_entry in pairs(active_hotspot_widgets) do if do_update(db_entry, now_ms) then return end @@ -330,15 +386,25 @@ function reposition_widgets() end end +-- ------------------------------------------------- -- +-- OverlayWidget (base class of all overlay widgets) -- +-- ------------------------------------------------- -- + OverlayWidget = defclass(OverlayWidget, widgets.Widget) OverlayWidget.ATTRS{ - overlay_onupdate_max_freq_seconds=5, + name=DEFAULT_NIL, -- this is set by the framework to the widget name + default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- initial widget screen pos, 1-based + hotspot=false, -- whether to call overlay_onupdate for all screens + viewscreens={}, -- override with list of viewscrens to interpose + overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate } -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 +-- set defaults for frame. the widget is expected to keep these up to date as +-- display contents change. +function OverlayWidget:init() + self.frame = self.frame or {} + self.frame.w = self.frame.w or 5 + self.frame.h = self.frame.h or 1 end return _ENV