Merge pull request #3008 from myk002/myk_mod_script_paths

support scripts in mods
develop
Myk 2023-03-25 13:00:15 -07:00 committed by GitHub
commit 0e5afeab59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 322 additions and 139 deletions

@ -235,10 +235,44 @@ root DF folder):
#. :file:`dfhack-config/scripts` #. :file:`dfhack-config/scripts`
#. :file:`save/{world}/scripts` (only if a save is loaded) #. :file:`save/{world}/scripts` (only if a save is loaded)
#. :file:`hack/scripts` #. :file:`hack/scripts`
#. :file:`data/installed_mods/...` (see below)
For example, if ``teleport`` is run, these folders are searched in order for For example, if ``teleport`` is run, these folders are searched in order for
``teleport.lua``, and the first matching file is run. ``teleport.lua``, and the first matching file is run.
Scripts in installed mods
.........................
Script directories in installed mods are automatically added to the script path
according to the following rules:
**If a world is not loaded**, then directories matching the pattern
``data/installed_mods/*/scripts_modinstalled/`` are added to the script path
in alphabetical order.
**If a world is loaded**, then the ``scripts_modactive`` directories of active
mods are also added to the script path according to the active mod load order,
and scripts in active mods take precedence over scripts in
``scripts_modinstalled`` in non-active mods. For example, the search paths for
mods might look like this::
activemod_last_in_load_order/scripts_modactive
activemod_last_in_load_order/scripts_modinstalled
activemod_second_to_last_in_load_order/scripts_modactive
activemod_second_to_last_in_load_order/scripts_modinstalled
...
inactivemod1/scripts_modinstalled
inactivemod2/scripts_modinstalled
...
Not all mods will have script directories, of course, and those mods will not be
added to the script search path. Mods are re-scanned whenever a world is loaded
or unloaded. For more information on scripts and mods, check out the
`modding-guide`.
Custom script paths
...................
Script paths can be added by modifying :file:`dfhack-config/script-paths.txt`. Script paths can be added by modifying :file:`dfhack-config/script-paths.txt`.
Each line should start with one of these characters: Each line should start with one of these characters:

@ -55,6 +55,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `automelt`: now allows metal chests to be melted (workaround for DF bug 2493 is no longer needed) - `automelt`: now allows metal chests to be melted (workaround for DF bug 2493 is no longer needed)
- `orders`: add minimize button to overlay panel so you can get it out of the way to read long statue descriptions when choosing a subject in the details screen - `orders`: add minimize button to overlay panel so you can get it out of the way to read long statue descriptions when choosing a subject in the details screen
- `enable`: can now interpret aliases defined with the `alias` command - `enable`: can now interpret aliases defined with the `alias` command
- scripts in installed mods are now automatically added to the DFHack script path. DFHack recognizes two directories in a mod's folder: ``scripts_modinstalled/`` and ``scripts_modactive/``. ``scripts_modinstalled/`` folders will always be added the script path, regardless of whether the mod is active in a world. ``scripts_modactive/`` folders will only be added to the script path when the mod is active in the current loaded world.
## Documentation ## Documentation
- ``untested`` tag has been renamed to ``unavailable`` to better reflect the status of the remaining unavaialable tools. all of the simply "untested" tools have now been tested and marked as working. the remaining tools are known to need development work before they are available again. - ``untested`` tag has been renamed to ``unavailable`` to better reflect the status of the remaining unavaialable tools. all of the simply "untested" tools have now been tested and marked as working. the remaining tools are known to need development work before they are available again.

@ -8,70 +8,132 @@ DFHack modding guide
What is the difference between a script and a mod? What is the difference between a script and a mod?
-------------------------------------------------- --------------------------------------------------
A script is a single file that can be run as a command in DFHack, like something Well, sometimes there is no difference. A mod is anything you add to the game,
that modifies or displays game data on request. A mod is something you install which can be graphics overrides, content in the raws, DFHack scripts, any, or
to get persistent behavioural changes in the game and/or add new content. Mods all. There are already resources out there for
can contain and use scripts in addition to (or instead of) modifications to the `raws modding <https://dwarffortresswiki.org/index.php/Modding>`__, so this
DF game raws. guide will focus more on scripts, both standalone and as an extension to
raws-based mods.
DFHack scripts are written in Lua. If you don't already know Lua, there's a
great primer at `lua.org <https://www.lua.org/pil/contents.html>`__. A DFHack script is a Lua file that can be run as a command in
DFHack. Scripts can do pretty much anything, from displaying information to
enforcing new game mechanics. If you don't already know Lua, there's a great
primer at `lua.org <https://www.lua.org/pil/contents.html>`__.
Why not just mod the raws? Why not just mod the raws?
-------------------------- --------------------------
It depends on what you want to do. Some mods *are* better to do in just the It depends on what you want to do. Some mods *are* better to do in just the
raws. You don't need DFHack to add a new race or modify attributes, for example. raws. You don't need DFHack to add a new race or modify attributes. However,
However, DFHack scripts can do many things that you just can't do in the raws, DFHack scripts can do many things that you just can't do in the raws, like make
like make a creature that trails smoke. Some things *could* be done in the raws, a creature that trails smoke or launch a unit into the air when they are hit
but writing a script is less hacky, easier to maintain, easier to extend, and is with a certain type of projectile. Some things *could* be done in the raws, but
not prone to side-effects. A great example is adding a syndrome when a reaction a script is better (e.g. easier to maintain, easier to extend, and/or not prone
is performed. If done in the raws, you have to create an exploding boulder to to side-effects). A great example is adding a syndrome when a reaction
apply the syndrome. DFHack scripts can add the syndrome directly and with much is performed. If done in the raws, you have to create an exploding boulder as
more flexibility. In the end, complex mods will likely require a mix of raw an intermediary to apply the syndrome. DFHack scripts can add the syndrome
modding and DFHack scripting. directly and with much more flexibility. In the end, complex mods will likely
require a mix of raw modding and DFHack scripting.
The structure of a mod
----------------------
In the example below, we'll use a mod name of ``example-mod``. I'm sure your
mods will have more creative names! Mods have a basic structure that looks like
this::
info.txt
graphics/...
objects/...
scripts_modactive/example-mod.lua
scripts_modactive/internal/example-mod/...
scripts_modinstalled/...
README.md (optional)
Let's go through that line by line.
- The :file:`info.txt` file contains metadata about your mod that DF will
display in-game. You can read more about this file in the
`Official DF Modding Guide <https://bay12games.com/dwarves/modding_guide.html>`__.
- Modifications to the game raws (potentially with custom raw tokens) go in
the :file:`graphics/` and :file:`objects/` folders. You can read more about
the files that go in these directories on the :wiki:`Modding` wiki page.
- A control script in :file:`scripts_modactive/` directory that handles
system-level event hooks (e.g. reloading state when a world is loaded),
registering `overlays <overlay-dev-guide>`, and
`enabling/disabling <script-enable-api>` your mod. You can put other
scripts in this directory as well if you want them to appear as runnable
DFHack commands when your mod is active for the current world. Lua modules
that your main scripts use, but which don't need to be directly runnable by
the player, should go in a subdirectory under
:file:`scripts_modactive/internal/` so they don't show up in the DFHack
`launcher <gui/launcher>` command autocomplete lists.
- Scripts that you want to be available before a world is loaded (i.e. on the
DF title screen) or that you want to be runnable in any world, regardless
of whether your mod is active, should go in the
:file:`scripts_modinstalled/` folder. You can also have an :file:`internal/`
subfolder in here for private modules if you like.
- Finally, a :file:`README.md` file that has more information about your mod.
If you develop your mod using version control (recommended!), that
:file:`README.md` file can also serve as your git repository documentation.
These files end up in a subdirectory under :file:`data/installed_mods/` when
the mod is selected as "active" for the first time.
What if I just want to distribute a simple script?
--------------------------------------------------
If your mod is just a script with no raws modifications, things get a bit
simpler. All you need is::
info.txt
scripts_modinstalled/yourscript.lua
README.md (optional)
Adding your script to the :file:`scripts_modinstalled/` folder will allow
DFHack to find it and add your mod to the `script-paths`. Your script will be
runnable from the title screen and in any loaded world, regardless of whether
your mod is explicitly "active".
Be sure to remind players to mark your mod as "active" at least once so it gets
installed to the :file:`data/installed_mods/` folder. They may have to create a
new world just so they can mark the mod as "active". This is true both for
players who copied the mod into the :file:`mods/` folder manually and for
players who subscribed via
`Steam Workshop <https://steamcommunity.com/app/975370/workshop/>`__.
A mod-maker's development environment A mod-maker's development environment
------------------------------------- -------------------------------------
While you're writing your mod, you need a place to store your in-development Create a folder for development somewhere outside your Dwarf Fortress
scripts that will: installation directory (e.g. ``/path/to/mymods/``). If you work on multiple
mods, you might want to make a subdirectory for each mod.
- be directly runnable by DFHack If you have changes to the raws, you'll have to copy them into DF's ``data/
- not get lost when you upgrade DFHack installed_mods/`` folder to have them take effect, but you can set things up so
that scripts are run directly from your dev directory. This way, you can edit
your scripts and have the changes available in the game immediately: no
copying, no restarting.
The recommended approach is to create a directory somewhere outside of your DF How does this magic work? Just add a line like this to your
installation (let's call it "/path/to/own-scripts") and do all your script ``dfhack-config/script-paths.txt`` file::
development in there.
Inside your DF installation folder, there is a file named +/path/to/mymods/example-mod/scripts_modinstalled
:file:`dfhack-config/script-paths.txt`. If you add a line like this to that
file::
+/path/to/own-scripts
Then that directory will be searched when you run DFHack commands from inside Then that directory will be searched when you run DFHack commands from inside
the game. The ``+`` at the front of the path means to search that directory the game. The ``+`` at the front of the path means to search that directory
first, before any other script directory (like :file:`hack/scripts` or first, before any other script directory (like :file:`hack/scripts` or other
:file:`raw/scripts`). That way, your latest changes will always be used instead versions of your mod in ``data/installed_mods/``).
of older copies that you may have installed in a DF directory.
For scripts with the same name, the `order of precedence <script-paths>` will
be:
1. ``own-scripts/``
2. ``dfhack-config/scripts/``
3. ``save/*/scripts/``
4. ``hack/scripts/``
The structure of the game The structure of the game
------------------------- -------------------------
"The game" is in the global variable `df <lua-df>`. The game's memory can be "The game" is in the global variable `df <lua-df>`. Most of the information
found in ``df.global``, containing things like the list of all items, whether to relevant to a script is found in ``df.global.world``, which contains things
reindex pathfinding, et cetera. Also relevant to us in ``df`` are the various like the list of all items, whether to reindex pathfinding, et cetera. Also
types found in the game, e.g. ``df.pronoun_type`` which we will be using in this relevant to us are the various data types found in the game, e.g.
guide. We'll explore more of the game structures below. ``df.pronoun_type`` which we will be using in this guide. We'll explore more of
the game structures below.
Your first script Your first script
----------------- -----------------
@ -83,8 +145,8 @@ First line, we get the unit::
local unit = dfhack.gui.getSelectedUnit() local unit = dfhack.gui.getSelectedUnit()
If no unit is selected, an error message will be printed (which can be silenced If no unit is selected, ``unit`` will be ``nil`` and an error message will be
by passing ``true`` to ``getSelectedUnit``) and ``unit`` will be ``nil``. printed (which can be silenced by passing ``true`` to ``getSelectedUnit``).
If ``unit`` is ``nil``, we don't want the script to run anymore:: If ``unit`` is ``nil``, we don't want the script to run anymore::
@ -94,33 +156,32 @@ If ``unit`` is ``nil``, we don't want the script to run anymore::
Now, the field ``sex`` in a unit is an integer, but each integer corresponds to Now, the field ``sex`` in a unit is an integer, but each integer corresponds to
a string value ("it", "she", or "he"). We get this value by indexing the a string value ("it", "she", or "he"). We get this value by indexing the
bidirectional map ``df.pronoun_type``. Indexing the other way, incidentally, bidirectional map ``df.pronoun_type``. Indexing the other way, with one of the
with one of the strings, will yield its corresponding number. So:: strings, will yield its corresponding number. So::
local pronounTypeString = df.pronoun_type[unit.sex] local pronounTypeString = df.pronoun_type[unit.sex]
print(pronounTypeString) print(pronounTypeString)
Simple. Save this as a Lua file in your own scripts directory and run it as Simple. Save this as a Lua file in your own scripts directory and run it from
shown before when a unit is selected in the Dwarf Fortress UI. `gui/launcher` when a unit is selected in the Dwarf Fortress UI.
Exploring DF structures Exploring DF state
----------------------- ------------------
So how could you have known about the field and type we just used? Well, there So how could you have known about the field and type we just used? Well, there
are two main tools for discovering the various fields in the game's data are two main tools for discovering the various fields in the game's data
structures. The first is the ``df-structures`` structures. The first is the ``df-structures``
`repository <https://github.com/DFHack/df-structures>`__ that contains XML files `repository <https://github.com/DFHack/df-structures>`__ that contains XML files
describing the contents of the game's structures. These are complete, but describing the layouts of the game's structures. These are complete, but
difficult to read (for a human). The second option is the `gui/gm-editor` difficult to read (for a human). The second option is the `gui/gm-editor`
script, an interactive data explorer. You can run the script while objects like interface, an interactive data explorer. You can run the script while objects
units are selected to view the data within them. You can also run like units are selected to view the data within them. Press :kbd:`?` while the
``gui/gm-editor scr`` to view the data for the current screen. Press :kbd:`?` script is active to view help.
while the script is active to view help.
Familiarising yourself with the many structs of the game will help with ideas Familiarising yourself with the many structs of the game will help with ideas
immensely, and you can always ask for help in the `right places <support>`. immensely, and you can always ask for help in the `right places <support>`.
Detecting triggers Reacting to events
------------------ ------------------
The common method for injecting new behaviour into the game is to define a The common method for injecting new behaviour into the game is to define a
@ -130,7 +191,7 @@ provides two libraries for this, ``repeat-util`` and `eventful <eventful-api>`.
frames (paused or unpaused), ticks (unpaused), in-game days, months, or years. frames (paused or unpaused), ticks (unpaused), in-game days, months, or years.
If you need to be aware the instant something happens, you'll need to run a If you need to be aware the instant something happens, you'll need to run a
check once a tick. Be careful not to do this gratuitously, though, since check once a tick. Be careful not to do this gratuitously, though, since
running that often can slow down the game! running callbacks too often can slow down the game!
``eventful``, on the other hand, is much more performance-friendly since it will ``eventful``, on the other hand, is much more performance-friendly since it will
only call your callback when a relevant event happens, like a reaction or job only call your callback when a relevant event happens, like a reaction or job
@ -327,7 +388,8 @@ Then, let's make a ``repeat-util`` callback for once a tick::
repeatUtil.scheduleEvery(modId, 1, "ticks", function() repeatUtil.scheduleEvery(modId, 1, "ticks", function()
Let's iterate over every active unit, and for every unit, iterate over their Let's iterate over every active unit, and for every unit, iterate over their
worn items to calculate how much we are going to take from their on-foot movement timers:: worn items to calculate how much we are going to take from their on-foot
movement timers::
for _, unit in ipairs(df.global.world.units.active) do for _, unit in ipairs(df.global.world.units.active) do
local amount = 0 local amount = 0
@ -341,82 +403,78 @@ worn items to calculate how much we are going to take from their on-foot movemen
end end
-- Subtract amount from on-foot movement timers if not on ground -- Subtract amount from on-foot movement timers if not on ground
if not unit.flags1.on_ground then if not unit.flags1.on_ground then
dfhack.units.subtractActionTimers(unit, amount, df.unit_action_type_group.MovementFeet) dfhack.units.subtractActionTimers(unit, amount,
df.unit_action_type_group.MovementFeet)
end end
end end
The structure of a full mod Putting it all together
--------------------------- -----------------------
For reference, `Tachy Guns <https://www.github.com/wolfboyft/tachy-guns>`__ is a
full mod that conforms to this guide.
Create a folder for mod projects somewhere outside your Dwarf Fortress
installation directory (e.g. ``/path/to/mymods/``) and use your mod IDs as the
names for the mod folders within it. In the example below, we'll use a mod ID of
``example-mod``. I'm sure your mods will have more creative names! The
``example-mod`` mod will be developed in the ``/path/to/mymods/example-mod/``
directory and has a basic structure that looks like this::
init.d/example-mod.lua
raw/objects/...
raw/scripts/example-mod.lua
raw/scripts/example-mod/...
README.md
Let's go through that line by line. Ok, you're all set up! Now, let's take a look at an example
``scripts_modinstalled/example-mod.lua`` file::
* A short (one-line) script in ``init.d/`` to initialise your -- main file for example-mod
mod when a save is loaded.
* Modifications to the game raws (potentially with custom raw tokens) go in
``raw/objects/``.
* A control script in ``scripts/`` that handles enabling and disabling your
mod.
* A subfolder for your mod under ``scripts/`` will contain all the internal
scripts and/or modules used by your mod.
It is a good idea to use a version control system to organize changes to your -- these lines indicate that the script supports the "enable"
mod code. You can create a separate Git repository for each of your mods. The -- API so you can start it by running "enable example-mod" and
``README.md`` file will be your mod help text when people browse to your online -- stop it by running "disable example-mod"
repository. --@module = true
--@enable = true
Unless you want to install your ``raw/`` folder into your DF game folder every -- this is the help text that will appear in `help` and
time you make a change to your scripts, you should add your development scripts -- `gui/launcher`. see possible tags here:
directory to your script paths in ``dfhack-config/script-paths.txt``:: -- https://docs.dfhack.org/en/latest/docs/Tags.html
--[====[
example-mod
===========
+/path/to/mymods/example-mod/scripts/ Tags: fort | gameplay
Ok, you're all set up! Now, let's take a look at an example Short one-sentence description ...
``scripts/example-mod.lua`` file::
-- main setup and teardown for example-mod Longer description ...
-- this next line indicates that the script supports the "enable"
-- API so you can start it by running "enable example-mod" and stop
-- it by running "disable example-mod"
--@ enable = true
local usage = [[
Usage Usage
----- -----
enable example-mod enable example-mod
disable example-mod disable example-mod
]] ]====]
local repeatUtil = require('repeat-util') local repeatUtil = require('repeat-util')
local eventful = require('plugins.eventful') local eventful = require('plugins.eventful')
-- you can reference global values or functions declared in any of -- you can reference global values or functions declared in any of
-- your internal scripts -- your internal scripts
local moduleA = reqscript('example-mod/module-a') local moduleA = reqscript('internal/example-mod/module-a')
local moduleB = reqscript('example-mod/module-b') local moduleB = reqscript('internal/example-mod/module-b')
local moduleC = reqscript('example-mod/module-c') local moduleC = reqscript('internal/example-mod/module-c')
local moduleD = reqscript('example-mod/module-d') local moduleD = reqscript('internal/example-mod/module-d')
local GLOBAL_KEY = 'example-mod'
enabled = enabled or false enabled = enabled or false
local modId = 'example-mod'
function isEnabled()
return enabled
end
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc == SC_MAP_UNLOADED then
dfhack.run_command('disable', 'example-mod')
return
end
if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then
return
end
dfhack.run_command('enable', 'example-mod')
end
if not dfhack_flags.enable then if not dfhack_flags.enable then
print(usage) print(dfhack.script_help())
print() print()
print(('Example mod is currently '):format( print(('Example mod is currently '):format(
enabled and 'enabled' or 'disabled')) enabled and 'enabled' or 'disabled'))
@ -472,23 +530,17 @@ Ok, you're all set up! Now, let's take a look at an example
enabled = false enabled = false
end end
You can call ``enable example-mod`` and ``disable example-mod`` yourself while Inside ``scripts_modinstalled/internal/example-mod/module-a.lua`` you could
developing, but for end users you can start your mod automatically from have code like this::
``init.d/example-mod.lua``::
dfhack.run_command('enable example-mod')
Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this::
--@ module = true --@ module = true
-- The above line is required for reqscript to work
function onLoad() -- global variables are exported function onLoad() -- global variables are exported
-- do initialization here -- do initialization here
end end
-- this is an internal function: local functions/variables -- this is a local function: local functions/variables
-- are not exported -- are not accessible to other scripts.
local function usedByOnTick(unit) local function usedByOnTick(unit)
-- ... -- ...
end end
@ -499,6 +551,6 @@ Inside ``raw/scripts/example-mod/module-a.lua`` you could have code like this::
end end
end end
The `reqscript <reqscript>` function reloads scripts that have changed, so you can modify The `reqscript <reqscript>` function reloads scripts that have changed, so you
your scripts while DF is running and just disable/enable your mod to load the can modify your scripts while DF is running and just disable/enable your mod to
changes into your ongoing game! load the changes into your ongoing game!

@ -105,6 +105,7 @@ DBG_DECLARE(core,script,DebugCategory::LINFO);
static const std::string CONFIG_PATH = "dfhack-config/"; static const std::string CONFIG_PATH = "dfhack-config/";
static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/"; static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/";
static const std::string MOD_PATH = "data/installed_mods/";
class MainThread { class MainThread {
public: public:
@ -440,6 +441,12 @@ bool Core::addScriptPath(std::string path, bool search_before)
return true; return true;
} }
bool Core::setModScriptPaths(const std::vector<std::string> &mod_script_paths) {
std::lock_guard<std::mutex> lock(script_path_mutex);
script_paths[2] = mod_script_paths;
return true;
}
bool Core::removeScriptPath(std::string path) bool Core::removeScriptPath(std::string path)
{ {
std::lock_guard<std::mutex> lock(script_path_mutex); std::lock_guard<std::mutex> lock(script_path_mutex);
@ -464,20 +471,21 @@ void Core::getScriptPaths(std::vector<std::string> *dest)
std::lock_guard<std::mutex> lock(script_path_mutex); std::lock_guard<std::mutex> lock(script_path_mutex);
dest->clear(); dest->clear();
std::string df_path = this->p->getPath() + "/"; std::string df_path = this->p->getPath() + "/";
for (auto it = script_paths[0].begin(); it != script_paths[0].end(); ++it) for (auto & path : script_paths[0])
dest->push_back(*it); dest->emplace_back(path);
dest->push_back(df_path + CONFIG_PATH + "scripts"); dest->push_back(df_path + CONFIG_PATH + "scripts");
if (df::global::world && isWorldLoaded()) { if (df::global::world && isWorldLoaded()) {
std::string save = World::ReadWorldFolder(); std::string save = World::ReadWorldFolder();
if (save.size()) if (save.size())
dest->push_back(df_path + "/save/" + save + "/scripts"); dest->emplace_back(df_path + "save/" + save + "/scripts");
} }
dest->push_back(df_path + "/hack/scripts"); dest->emplace_back(df_path + "hack/scripts");
for (auto it = script_paths[1].begin(); it != script_paths[1].end(); ++it) for (auto & path : script_paths[2])
dest->push_back(*it); dest->emplace_back(path);
for (auto & path : script_paths[1])
dest->emplace_back(path);
} }
std::string Core::findScript(std::string name) std::string Core::findScript(std::string name)
{ {
std::vector<std::string> paths; std::vector<std::string> paths;
@ -526,6 +534,62 @@ bool loadScriptPaths(color_ostream &out, bool silent = false)
return true; return true;
} }
bool loadModScriptPaths(color_ostream &out) {
std::map<std::string, bool> files;
Filesystem::listdir_recursive(MOD_PATH, files, 0);
DEBUG(script,out).print("found %zd installed mods\n", files.size());
if (!files.size())
return true;
for (auto & entry : files) {
DEBUG(script,out).print(" %s\n", entry.first.c_str());
}
std::vector<std::string> mod_paths;
if (Core::getInstance().isWorldLoaded()) {
DEBUG(script,out).print("active load order:\n");
for (auto & path : df::global::world->object_loader.object_load_order_src_dir) {
DEBUG(script,out).print(" %s\n", path->c_str());
if (0 == path->find(MOD_PATH))
mod_paths.emplace_back(*path);
}
}
std::vector<std::string> mod_script_paths;
for (auto pathit = mod_paths.rbegin(); pathit != mod_paths.rend(); ++pathit) {
std::string active_path = *pathit + "scripts_modactive";
std::string installed_path = *pathit + "scripts_modinstalled";
DEBUG(script,out).print("checking active path: %s\n", pathit->c_str());
if (Filesystem::isdir(active_path))
mod_script_paths.emplace_back(active_path);
if (Filesystem::isdir(installed_path))
mod_script_paths.emplace_back(installed_path);
std::string slashless = *pathit;
slashless.resize(slashless.size()-1);
if (0 == files.erase(slashless)) {
WARN(script,out).print("script path not found: '%s'\n", pathit->c_str());
}
}
for (auto & entry : files) {
if (!entry.second)
continue;
DEBUG(script,out).print("checking inactive path: %s\n", entry.first.c_str());
std::string installed_path = entry.first + "/scripts_modinstalled";
if (Filesystem::isdir(installed_path))
mod_script_paths.emplace_back(installed_path);
}
DEBUG(script,out).print("final mod script paths:\n");
for (auto & path : mod_script_paths)
DEBUG(script,out).print(" %s\n", path.c_str());
Core::getInstance().setModScriptPaths(mod_script_paths);
return true;
}
static std::map<std::string, state_change_event> state_change_event_map; static std::map<std::string, state_change_event> state_change_event_map;
static void sc_event_map_init() { static void sc_event_map_init() {
if (!state_change_event_map.size()) if (!state_change_event_map.size())
@ -2113,14 +2177,22 @@ void Core::onStateChange(color_ostream &out, state_change_event event)
switch (event) switch (event)
{ {
case SC_CORE_INITIALIZED: case SC_CORE_INITIALIZED:
{ {
auto L = Lua::Core::State; loadModScriptPaths(out);
Lua::StackUnwinder top(L); auto L = Lua::Core::State;
Lua::CallLuaModuleFunction(con, L, "helpdb", "refresh"); Lua::StackUnwinder top(L);
Lua::CallLuaModuleFunction(con, L, "script-manager", "reload"); Lua::CallLuaModuleFunction(con, L, "helpdb", "refresh");
} Lua::CallLuaModuleFunction(con, L, "script-manager", "reload");
break; break;
}
case SC_WORLD_LOADED: case SC_WORLD_LOADED:
{
loadModScriptPaths(out);
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
Lua::CallLuaModuleFunction(con, L, "script-manager", "reload");
// fallthrough
}
case SC_WORLD_UNLOADED: case SC_WORLD_UNLOADED:
case SC_MAP_LOADED: case SC_MAP_LOADED:
case SC_MAP_UNLOADED: case SC_MAP_UNLOADED:
@ -2185,6 +2257,10 @@ void Core::onStateChange(color_ostream &out, state_change_event event)
if (event == SC_WORLD_UNLOADED) if (event == SC_WORLD_UNLOADED)
{ {
Persistence::Internal::clear(); Persistence::Internal::clear();
loadModScriptPaths(out);
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
Lua::CallLuaModuleFunction(con, L, "script-manager", "reload");
} }
} }

@ -158,6 +158,7 @@ namespace DFHack
bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false); bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false);
bool addScriptPath(std::string path, bool search_before = false); bool addScriptPath(std::string path, bool search_before = false);
bool setModScriptPaths(const std::vector<std::string> &mod_script_paths);
bool removeScriptPath(std::string path); bool removeScriptPath(std::string path);
std::string findScript(std::string name); std::string findScript(std::string name);
void getScriptPaths(std::vector<std::string> *dest); void getScriptPaths(std::vector<std::string> *dest);
@ -239,7 +240,7 @@ namespace DFHack
std::vector<std::unique_ptr<Module>> allModules; std::vector<std::unique_ptr<Module>> allModules;
DFHack::PluginManager * plug_mgr; DFHack::PluginManager * plug_mgr;
std::vector<std::string> script_paths[2]; std::vector<std::string> script_paths[3];
std::mutex script_path_mutex; std::mutex script_path_mutex;
// hotkey-related stuff // hotkey-related stuff

@ -12,6 +12,8 @@ local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt'
local SCRIPT_DOC_BEGIN = '[====[' local SCRIPT_DOC_BEGIN = '[====['
local SCRIPT_DOC_END = ']====]' local SCRIPT_DOC_END = ']====]'
local GLOBAL_KEY = 'HELPDB'
-- enums -- enums
local ENTRY_TYPES = { local ENTRY_TYPES = {
BUILTIN='builtin', BUILTIN='builtin',
@ -423,6 +425,14 @@ function refresh()
ensure_db() ensure_db()
end end
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc ~= SC_WORLD_LOADED then
return
end
-- pick up widgets from active mods
refresh()
end
local function parse_blocks(text) local function parse_blocks(text)
local blocks = {} local blocks = {}
for line in text:gmatch('[^\n]*') do for line in text:gmatch('[^\n]*') do

@ -8,6 +8,7 @@ local widgets = require('gui.widgets')
local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json' local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json'
local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS' local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS'
local GLOBAL_KEY = 'OVERLAY'
local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2 local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2
@ -311,6 +312,14 @@ function reload()
reposition_widgets() reposition_widgets()
end end
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc ~= SC_WORLD_LOADED then
return
end
-- pick up widgets from active mods
reload()
end
local function dump_widget_config(name, widget) local function dump_widget_config(name, widget)
local pos = overlay_config[name].pos local pos = overlay_config[name].pos
print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y)) print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y))