diff --git a/data/examples/init/onMapLoad_dreamfort.init b/data/examples/init/onMapLoad_dreamfort.init index fe2b1d54e..8f341cf7e 100644 --- a/data/examples/init/onMapLoad_dreamfort.init +++ b/data/examples/init/onMapLoad_dreamfort.init @@ -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 diff --git a/docs/builtins/enable.rst b/docs/builtins/enable.rst index 7af10a9f3..8525214b3 100644 --- a/docs/builtins/enable.rst +++ b/docs/builtins/enable.rst @@ -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 ----- diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 022d24de6..c20514279 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -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 ` 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 ================ diff --git a/library/Core.cpp b/library/Core.cpp index 836772c04..eed2de30a 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -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; diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua new file mode 100644 index 000000000..008e9443e --- /dev/null +++ b/library/lua/script-manager.lua @@ -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 diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index d63041993..9f2c9029b 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -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)