Merge pull request #2473 from myk002/myk_script_empowerment

Allow scripts to self-enable and automatically restore state
develop
Myk 2022-12-10 22:40:35 -08:00 committed by GitHub
commit c9bef96ec4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 25 deletions

@ -48,8 +48,8 @@ enable seedwatch
seedwatch all 30
# ensures important tasks get assigned to workers.
# otherwise these job types can get ignored in busy forts.
prioritize -aq defaults
# otherwise many job types can get ignored in busy forts.
on-new-fortress prioritize -aq defaults
# autobutcher settings are saved in the savegame, so we only need to set them once.
# this way, any custom settings you set during gameplay are not overwritten

@ -16,6 +16,9 @@ and disabled plugins, and shows whether that can be changed through the same
commands. Passing plugin names to these commands will enable or disable the
specified plugins.
If you are a script developer, see `script-enable-api` for how to expose whether
your script is currently enabled or disabled.
Usage
-----

@ -5532,6 +5532,8 @@ Importing scripts
not declare support as described above, although it is preferred to update
such scripts so that ``reqscript()`` can be used instead.
.. _script-enable-api:
Enabling and disabling scripts
==============================
@ -5546,18 +5548,64 @@ table passed to the script will have the following fields set:
* ``enable``: Always ``true`` if the script is being enabled *or* disabled
* ``enable_state``: ``true`` if the script is being enabled, ``false`` otherwise
If you declare a global function named ``isEnabled`` that returns a boolean
indicating whether your script is enabled, then your script will be listed among
the other enableable scripts and plugins when the player runs the `enable`
command.
Example usage::
--@ enable = true
enabled = enabled or false
function isEnabled()
return enabled
end
-- (function definitions...)
if dfhack_flags.enable then
if dfhack_flags.enable_state then
start()
enabled = true
else
stop()
enabled = false
end
end
If the state of your script can be tied to an active savegame, then your script
should hook the appropriate events to load persisted state when a savegame is
loaded. For example::
local json = require('json')
local persist = require('persist-table')
local GLOBAL_KEY = 'my-script-name'
g_state = g_state or {}
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then
return
end
local state = json.decode(persist.GlobalTable[GLOBAL_KEY] or '')
g_state = state or {}
end
The attachment to ``dfhack.onStateChange`` should appear in your script code
outside of any function. DFHack will load your script as a module just before
the ``SC_DFHACK_INITIALIZED`` state change event is sent, giving your code an
opportunity to run and attach hooks before the game is loaded.
If an enableable script is added to a DFHack `script path <script-paths>` while
DF is running, then it will miss the initial sweep that loads all the module
scripts and any ``onStateChange`` handlers the script may want to register will
not be registered until the script is loaded via some means, either by running
it or loading it as a module. If you just added new scripts that you want to
load so they can attach their ``onStateChange`` handlers, run ``enable`` without
parameters or call ``:lua require('script-manager').reload()`` to scan and load
all script modules.
Save init script
================

@ -826,9 +826,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
"%20s\t%-3s%s\n",
(plug->getName()+":").c_str(),
plug->is_enabled() ? "on" : "off",
plug->can_set_enabled() ? "" : " (controlled elsewhere)"
plug->can_set_enabled() ? "" : " (controlled internally)"
);
}
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
Lua::CallLuaModuleFunction(con, L, "script-manager", "list");
}
}
else if (first == "ls" || first == "dir")
@ -1833,8 +1837,12 @@ void Core::doUpdate(color_ostream &out, bool first_update)
{
Lua::Core::Reset(out, "DF code execution");
if (first_update)
if (first_update) {
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
Lua::CallLuaModuleFunction(out, L, "script-manager", "reload");
onStateChange(out, SC_CORE_INITIALIZED);
}
// find the current viewscreen
df::viewscreen *screen = NULL;

@ -0,0 +1,56 @@
local _ENV = mkmodule('script-manager')
local utils = require('utils')
-- for each script that can be loaded as a module, calls cb(script_name, env)
function foreach_module_script(cb)
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'
local ok, script_env = pcall(reqscript, script_name)
if ok then
cb(script_name, script_env)
end
end
end
::skip_path::
end
end
local enabled_map = {}
local function process_script(env_name, env)
local global_name = 'isEnabled'
local fn = env[global_name]
if not fn then return end
if type(fn) ~= 'function' then
dfhack.printerr(
('error registering %s() from "%s": global' ..
' value is not a function'):format(global_name, env_name))
return
end
enabled_map[env_name] = fn
end
function reload()
enabled_map = utils.OrderedTable()
foreach_module_script(process_script)
end
function list()
-- call reload every time we list to make sure we get scripts that have
-- just been added
reload()
for name,fn in pairs(enabled_map) do
print(('%20s\t%-3s'):format(name..':', fn() and 'on' or 'off'))
end
end
return _ENV

@ -2,6 +2,7 @@ local _ENV = mkmodule('plugins.overlay')
local gui = require('gui')
local json = require('json')
local scriptmanager = require('script-manager')
local utils = require('utils')
local widgets = require('gui.widgets')
@ -250,11 +251,9 @@ local function load_widget(name, widget_class)
end
end
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]
local function load_widgets(env_name, env)
local overlay_widgets = env[OVERLAY_WIDGETS_VAR]
if not overlay_widgets then return end
if type(overlay_widgets) ~= 'table' then
dfhack.printerr(
('error loading overlay widgets from "%s": %s map is malformed')
@ -262,7 +261,7 @@ local function load_widgets(env_prefix, provider, env_fn)
return
end
for widget_name,widget_class in pairs(overlay_widgets) do
local name = provider .. '.' .. widget_name
local name = env_name .. '.' .. widget_name
if not safecall(load_widget, name, widget_class) then
dfhack.printerr(('error loading overlay widget "%s"'):format(name))
end
@ -274,23 +273,13 @@ 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
local env_name = 'plugins.' .. plugin
local ok, plugin_env = pcall(require, env_name)
if ok then
load_widgets(plugin, plugin_env)
end
::skip_path::
end
scriptmanager.foreach_module_script(load_widgets)
for name in pairs(widget_db) do
table.insert(widget_index, name)