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