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 seedwatch all 30
# ensures important tasks get assigned to workers. # ensures important tasks get assigned to workers.
# otherwise these job types can get ignored in busy forts. # otherwise many job types can get ignored in busy forts.
prioritize -aq defaults on-new-fortress prioritize -aq defaults
# autobutcher settings are saved in the savegame, so we only need to set them once. # 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 # 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 commands. Passing plugin names to these commands will enable or disable the
specified plugins. 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 Usage
----- -----

@ -5532,6 +5532,8 @@ Importing scripts
not declare support as described above, although it is preferred to update not declare support as described above, although it is preferred to update
such scripts so that ``reqscript()`` can be used instead. such scripts so that ``reqscript()`` can be used instead.
.. _script-enable-api:
Enabling and disabling scripts 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``: Always ``true`` if the script is being enabled *or* disabled
* ``enable_state``: ``true`` if the script is being enabled, ``false`` otherwise * ``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:: Example usage::
--@ enable = true --@ enable = true
enabled = enabled or false
function isEnabled()
return enabled
end
-- (function definitions...) -- (function definitions...)
if dfhack_flags.enable then if dfhack_flags.enable then
if dfhack_flags.enable_state then if dfhack_flags.enable_state then
start() start()
enabled = true
else else
stop() stop()
enabled = false
end end
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 Save init script
================ ================

@ -826,9 +826,13 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
"%20s\t%-3s%s\n", "%20s\t%-3s%s\n",
(plug->getName()+":").c_str(), (plug->getName()+":").c_str(),
plug->is_enabled() ? "on" : "off", 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") 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"); 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); onStateChange(out, SC_CORE_INITIALIZED);
}
// find the current viewscreen // find the current viewscreen
df::viewscreen *screen = NULL; 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 gui = require('gui')
local json = require('json') local json = require('json')
local scriptmanager = require('script-manager')
local utils = require('utils') local utils = require('utils')
local widgets = require('gui.widgets') local widgets = require('gui.widgets')
@ -250,11 +251,9 @@ local function load_widget(name, widget_class)
end end
end end
local function load_widgets(env_prefix, provider, env_fn) local function load_widgets(env_name, env)
local env_name = env_prefix .. provider local overlay_widgets = env[OVERLAY_WIDGETS_VAR]
local ok, provider_env = pcall(env_fn, env_name) if not overlay_widgets then return end
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 if type(overlay_widgets) ~= 'table' then
dfhack.printerr( dfhack.printerr(
('error loading overlay widgets from "%s": %s map is malformed') ('error loading overlay widgets from "%s": %s map is malformed')
@ -262,7 +261,7 @@ local function load_widgets(env_prefix, provider, env_fn)
return return
end end
for widget_name,widget_class in pairs(overlay_widgets) do 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 if not safecall(load_widget, name, widget_class) then
dfhack.printerr(('error loading overlay widget "%s"'):format(name)) dfhack.printerr(('error loading overlay widget "%s"'):format(name))
end end
@ -274,23 +273,13 @@ function reload()
reset() reset()
for _,plugin in ipairs(dfhack.internal.listPlugins()) do for _,plugin in ipairs(dfhack.internal.listPlugins()) do
load_widgets('plugins.', plugin, require) local env_name = 'plugins.' .. plugin
end local ok, plugin_env = pcall(require, env_name)
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do if ok then
local files = dfhack.filesystem.listdir_recursive( load_widgets(plugin, plugin_env)
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 end
::skip_path::
end end
scriptmanager.foreach_module_script(load_widgets)
for name in pairs(widget_db) do for name in pairs(widget_db) do
table.insert(widget_index, name) table.insert(widget_index, name)