diff --git a/data/dfhack-config/init/dfhack.control-panel-system.init b/data/dfhack-config/init/dfhack.control-panel-system.init
new file mode 100644
index 000000000..8b1431373
--- /dev/null
+++ b/data/dfhack-config/init/dfhack.control-panel-system.init
@@ -0,0 +1,4 @@
+# DO NOT EDIT THIS FILE
+# Please use gui/control-panel to edit this file
+
+enable faststart
diff --git a/data/dfhack-config/init/onMapLoad.control-panel-new-fort.init b/data/dfhack-config/init/onMapLoad.control-panel-new-fort.init
new file mode 100644
index 000000000..66a07d14f
--- /dev/null
+++ b/data/dfhack-config/init/onMapLoad.control-panel-new-fort.init
@@ -0,0 +1,4 @@
+# DO NOT EDIT THIS FILE
+# Please use gui/control-panel to edit this file
+
+on-new-fortress enable fix/protect-nicks
diff --git a/data/dfhack-config/init/onMapLoad.control-panel-repeats.init b/data/dfhack-config/init/onMapLoad.control-panel-repeats.init
new file mode 100644
index 000000000..cbed00b67
--- /dev/null
+++ b/data/dfhack-config/init/onMapLoad.control-panel-repeats.init
@@ -0,0 +1,5 @@
+# DO NOT EDIT THIS FILE
+# Please use gui/control-panel to edit this file
+
+repeat --name general-strike --time 1 --timeUnits days --command [ fix/general-strike -q ]
+repeat --name warn-starving --time 10 --timeUnits days --command [ warn-starving ]
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 1e152f30e..5e11cc18b 100644
--- a/docs/changelog.txt
+++ b/docs/changelog.txt
@@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
# Future
## New Plugins
+- `faststart`: speeds up the "Loading..." screen so the Main Menu appears sooner
## Fixes
- `hotkeys`: hotkey hints on menu popup will no longer get their last character cut off by the scrollbar
@@ -44,6 +45,10 @@ 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 ``itemselection`` 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
+- Mods: give active mods a chance to reattach their load hooks when a world is reloaded
+- `gui/control-panel`: bugfix services are now enabled by default
## Documentation
diff --git a/docs/guides/modding-guide.rst b/docs/guides/modding-guide.rst
index b386b48c3..38117503c 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
-------------------------
@@ -465,6 +461,11 @@ Ok, you're all set up! Now, let's take a look at an example
dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc == SC_MAP_UNLOADED then
dfhack.run_command('disable', 'example-mod')
+
+ -- ensure our mod doesn't try to enable itself when a different
+ -- world is loaded where we are *not* active
+ dfhack.onStateChange[GLOBAL_KEY] = nil
+
return
end
diff --git a/docs/plugins/faststart.rst b/docs/plugins/faststart.rst
new file mode 100644
index 000000000..b39269e01
--- /dev/null
+++ b/docs/plugins/faststart.rst
@@ -0,0 +1,18 @@
+faststart
+=========
+
+.. dfhack-tool::
+ :summary: Makes the main menu appear sooner.
+ :tags: dfhack interface
+ :no-command:
+
+This plugin accelerates the initial "Loading..." screen that appears when the
+game first starts, so you don't have to wait as long before the Main Menu
+appears and you can start playing.
+
+Usage
+-----
+
+::
+
+ enable faststart
diff --git a/library/Core.cpp b/library/Core.cpp
index b5337b827..97c69946e 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;
@@ -2190,7 +2148,10 @@ void Core::onStateChange(color_ostream &out, state_change_event event)
loadModScriptPaths(out);
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
- Lua::CallLuaModuleFunction(con, L, "script-manager", "reload");
+ Lua::CallLuaModuleFunction(con, L, "script-manager", "reload", 1, 0,
+ [](lua_State* L) {
+ Lua::Push(L, true);
+ });
// fallthrough
}
case SC_WORLD_UNLOADED:
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 5573d3f20..08903c7bd 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 != 0 && 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..450012357 100644
--- a/library/lua/script-manager.lua
+++ b/library/lua/script-manager.lua
@@ -2,23 +2,31 @@ 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)
+function foreach_module_script(cb, preprocess_script_file_fn)
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
+ if f.isdir or not f.path:endswith('.lua') or
+ f.path:startswith('.git') or
+ f.path:startswith('test/') or
+ f.path:startswith('internal/') then
+ goto continue
+ end
+ if preprocess_script_file_fn then
+ preprocess_script_file_fn(script_path, f.path)
+ end
+ 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
+ ::continue::
end
::skip_path::
end
@@ -39,9 +47,17 @@ local function process_script(env_name, env)
enabled_map[env_name] = fn
end
-function reload()
+function reload(refresh_active_mod_scripts)
enabled_map = utils.OrderedTable()
- foreach_module_script(process_script)
+ local force_refresh_fn = refresh_active_mod_scripts and function(script_path, script_name)
+ if script_path:find('scripts_modactive') then
+ internal_script = dfhack.internal.scripts[script_path..'/'..script_name]
+ if internal_script then
+ internal_script.env = nil
+ end
+ end
+ end or nil
+ foreach_module_script(process_script, force_refresh_fn)
end
local function ensure_loaded()
@@ -57,4 +73,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 mod scripts: ' .. 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.value)
+ 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/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index dc7fa9276..53d0296c7 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -112,6 +112,7 @@ dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua)
#dfhack_plugin(embark-tools embark-tools.cpp)
dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua)
dfhack_plugin(fastdwarf fastdwarf.cpp)
+dfhack_plugin(faststart faststart.cpp)
dfhack_plugin(filltraffic filltraffic.cpp)
#dfhack_plugin(fix-unit-occupancy fix-unit-occupancy.cpp)
#dfhack_plugin(fixveins fixveins.cpp)
diff --git a/plugins/faststart.cpp b/plugins/faststart.cpp
new file mode 100644
index 000000000..de014801c
--- /dev/null
+++ b/plugins/faststart.cpp
@@ -0,0 +1,69 @@
+// Fast Startup tweak
+
+#include "Core.h"
+#include
+#include
+#include
+#include
+#include
+
+#include "df/viewscreen_initial_prepst.h"
+#include
+
+using namespace DFHack;
+using namespace df::enums;
+using std::vector;
+
+// Uncomment this to make the Loading screen as fast as possible
+// This has the side effect of removing the dwarf face animation
+// (and briefly making the game become unresponsive)
+
+//#define REALLY_FAST
+
+DFHACK_PLUGIN("faststart");
+DFHACK_PLUGIN_IS_ENABLED(is_enabled);
+
+struct prep_hook : df::viewscreen_initial_prepst
+{
+ typedef df::viewscreen_initial_prepst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(void, logic, ())
+ {
+#ifdef REALLY_FAST
+ while (breakdown_level != interface_breakdown_types::STOPSCREEN)
+ {
+ render_count++;
+ INTERPOSE_NEXT(logic)();
+ }
+#else
+ render_count = 4;
+ INTERPOSE_NEXT(logic)();
+#endif
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(prep_hook, logic);
+
+DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
+{
+ if (enable != is_enabled)
+ {
+ if (!INTERPOSE_HOOK(prep_hook, logic).apply(enable))
+ return CR_FAILURE;
+
+ is_enabled = enable;
+ }
+
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands)
+{
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ INTERPOSE_HOOK(prep_hook, logic).remove();
+ return CR_OK;
+}
diff --git a/scripts b/scripts
index 53f0aedf9..a4e5d4514 160000
--- a/scripts
+++ b/scripts
@@ -1 +1 @@
-Subproject commit 53f0aedf9f7df33a4f79246ba46de02794619d09
+Subproject commit a4e5d4514ec33462ee0c0bd25e02d4a9c3e2ce01