diff --git a/docs/Core.rst b/docs/Core.rst
index c06a413da..5decd668d 100644
--- a/docs/Core.rst
+++ b/docs/Core.rst
@@ -243,18 +243,31 @@ For example, if ``teleport`` is run, these folders are searched in order for
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::
+Scripts in mods are automatically added to the script path. The following
+directories are searched for mods::
+
+ ../../workshop/content/975370/ (the DF Steam workshop directory)
+ mods/
+ data/installed_mods/
+
+Each mod can have two directories that contain scripts:
+
+- ``scripts_modactive/`` is added to the script path if and only if the mod is
+ active in the loaded world.
+- ``scripts_modinstalled/`` is added to the script path as long as the mod is
+ installed in one of the searched mod directories.
+
+Multiple versions of a mod may be installed at the same time. If a mod is
+active in a loaded world, then the scripts for the version of the mod that is
+active will be added to the script path. Otherwise, the latest version of each
+mod is added to the script path.
+
+Scripts for active mods take precedence according to their load order when you
+generated the current world.
+
+Scripts for non-active mods are ordered by their containing mod's ID.
+
+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
diff --git a/docs/changelog.txt b/docs/changelog.txt
index a8375f4b5..f96f29dbd 100644
--- a/docs/changelog.txt
+++ b/docs/changelog.txt
@@ -44,6 +44,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## Misc Improvements
- `buildingplan`: items in the item selection dialog should now use the same item quality symbols as the base game
-@ `buildingplan`: rearranged elements of ``planneroverlay`` interface
+- Mods: scripts in mods that are only in the steam workshop directory are now accessible. this means that a script-only mod that you never mark as "active" when generating a world will still receive automatic updates and be usable from in-game
+- Mods: scripts from only the most recent version of an installed mod are added to the script path
## Documentation
diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst
index b386b48c3..0c9513fef 100644
--- a/docs/guides/modding-guide.rst
+++ b/docs/guides/modding-guide.rst
@@ -77,8 +77,11 @@ Let's go through that line by line.
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.
+These files end up in a subdirectory under :file:`mods/` when players copy them
+in or install them from the
+`Steam Workshop `__, and in
+: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?
--------------------------------------------------
@@ -95,13 +98,6 @@ 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 `__.
-
A mod-maker's development environment
-------------------------------------
@@ -109,11 +105,11 @@ Create a folder for development somewhere outside your Dwarf Fortress
installation directory (e.g. ``/path/to/mymods/``). If you work on multiple
mods, you might want to make a subdirectory for each mod.
-If you have changes to the raws, you'll have to copy them into DF's ``data/
-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.
+If you have changes to the raws, you'll have to copy them into DF's
+``data/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.
How does this magic work? Just add a line like this to your
``dfhack-config/script-paths.txt`` file::
@@ -123,7 +119,7 @@ How does this magic work? Just add a line like this to your
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
first, before any other script directory (like :file:`hack/scripts` or other
-versions of your mod in ``data/installed_mods/``).
+versions of your mod in the DF mod folders).
The structure of the game
-------------------------
diff --git a/library/Core.cpp b/library/Core.cpp
index b5337b827..d13c82fa5 100644
--- a/library/Core.cpp
+++ b/library/Core.cpp
@@ -105,7 +105,6 @@ DBG_DECLARE(core,script,DebugCategory::LINFO);
static const std::string CONFIG_PATH = "dfhack-config/";
static const std::string CONFIG_DEFAULTS_PATH = "hack/data/dfhack-config-defaults/";
-static const std::string MOD_PATH = "data/installed_mods/";
class MainThread {
public:
@@ -534,60 +533,19 @@ bool loadScriptPaths(color_ostream &out, bool silent = false)
return true;
}
-bool loadModScriptPaths(color_ostream &out) {
- std::map 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 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);
- }
- }
-
+static void loadModScriptPaths(color_ostream &out) {
+ auto L = Lua::Core::State;
+ Lua::StackUnwinder top(L);
std::vector 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);
- }
-
+ Lua::CallLuaModuleFunction(out, L, "script-manager", "get_mod_script_paths", 0, 1,
+ Lua::DEFAULT_LUA_LAMBDA,
+ [&](lua_State *L) {
+ Lua::GetVector(L, mod_script_paths);
+ });
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 state_change_event_map;
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 5573d3f20..d30dc7f77 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -2729,13 +2729,9 @@ static int filesystem_listdir_recursive(lua_State *L)
include_prefix = lua_toboolean(L, 3);
std::map files;
int err = DFHack::Filesystem::listdir_recursive(dir, files, depth, include_prefix);
- if (err)
- {
+ if (err != -1) {
lua_pushnil(L);
- if (err == -1)
- lua_pushfstring(L, "max depth exceeded: %d", depth);
- else
- lua_pushstring(L, strerror(err));
+ lua_pushstring(L, strerror(err));
lua_pushinteger(L, err);
return 3;
}
diff --git a/library/lua/script-manager.lua b/library/lua/script-manager.lua
index cc5dd9fe3..1a7161a10 100644
--- a/library/lua/script-manager.lua
+++ b/library/lua/script-manager.lua
@@ -2,6 +2,9 @@ local _ENV = mkmodule('script-manager')
local utils = require('utils')
+---------------------
+-- enabled API
+
-- 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
@@ -57,4 +60,107 @@ function list()
end
end
+---------------------
+-- mod script paths
+
+-- this perhaps could/should be queried from the Steam API
+-- are there any installation configurations where this will be wrong, though?
+local WORKSHOP_MODS_PATH = '../../workshop/content/975370/'
+local MODS_PATH = 'mods/'
+local INSTALLED_MODS_PATH = 'data/installed_mods/'
+
+-- last instance of the same version of the same mod wins, so read them in this
+-- order (in increasing order of liklihood that players may have made custom
+-- changes to the files)
+local MOD_PATH_ROOTS = {WORKSHOP_MODS_PATH, MODS_PATH, INSTALLED_MODS_PATH}
+
+local function get_mod_id_and_version(path)
+ local idfile = path .. '/info.txt'
+ local ok, lines = pcall(io.lines, idfile)
+ if not ok then return end
+ local id, version
+ for line in lines do
+ if not id then
+ _,_,id = line:find('^%[ID:([^%]]+)%]')
+ end
+ if not version then
+ -- note this doesn't include the closing brace since some people put
+ -- non-number characters in here, and DF only reads the digits as the
+ -- numeric version
+ _,_,version = line:find('^%[NUMERIC_VERSION:(%d+)')
+ end
+ -- note that we do *not* want to break out of this loop early since
+ -- lines has to hit EOF to close the file
+ end
+ return id, version
+end
+
+local function add_script_path(mod_script_paths, path)
+ if dfhack.filesystem.isdir(path) then
+ print('indexing scripts from mod script path: ' .. path)
+ table.insert(mod_script_paths, path)
+ end
+end
+
+local function add_script_paths(mod_script_paths, base_path, include_modactive)
+ if not base_path:endswith('/') then
+ base_path = base_path .. '/'
+ end
+ if include_modactive then
+ add_script_path(mod_script_paths, base_path..'scripts_modactive')
+ end
+ add_script_path(mod_script_paths, base_path..'scripts_modinstalled')
+end
+
+function get_mod_script_paths()
+ -- ordered map of mod id -> {handled=bool, versions=map of version -> path}
+ local mods = utils.OrderedTable()
+ local mod_script_paths = {}
+
+ -- if a world is loaded, process active mods first, and lock to active version
+ if dfhack.isWorldLoaded() then
+ for _,path in ipairs(df.global.world.object_loader.object_load_order_src_dir) do
+ path = tostring(path)
+ if not path:startswith(INSTALLED_MODS_PATH) then goto continue end
+ local id = get_mod_id_and_version(path)
+ if not id then goto continue end
+ mods[id] = {handled=true}
+ add_script_paths(mod_script_paths, path, true)
+ ::continue::
+ end
+ end
+
+ -- assemble version -> path maps for all (non-handled) mod source dirs
+ for _,mod_path_root in ipairs(MOD_PATH_ROOTS) do
+ local files = dfhack.filesystem.listdir_recursive(mod_path_root, 0)
+ if not files then goto skip_path_root end
+ for _,f in ipairs(files) do
+ if not f.isdir then goto continue end
+ local id, version = get_mod_id_and_version(f.path)
+ if not id or not version then goto continue end
+ local mod = ensure_key(mods, id)
+ if mod.handled then goto continue end
+ ensure_key(mod, 'versions')[version] = f.path
+ ::continue::
+ end
+ ::skip_path_root::
+ end
+
+ -- add script paths from most recent version of all not-yet-handled mods
+ for _,v in pairs(mods) do
+ if v.handled then goto continue end
+ local max_version, path
+ for version,mod_path in pairs(v.versions) do
+ if not max_version or max_version < version then
+ path = mod_path
+ max_version = version
+ end
+ end
+ add_script_paths(mod_script_paths, path)
+ ::continue::
+ end
+
+ return mod_script_paths
+end
+
return _ENV
diff --git a/scripts b/scripts
index 53f0aedf9..e6216cc28 160000
--- a/scripts
+++ b/scripts
@@ -1 +1 @@
-Subproject commit 53f0aedf9f7df33a4f79246ba46de02794619d09
+Subproject commit e6216cc28e4315df5fb128411d0ca57fe78ccb2b