Merge branch 'develop' into bplan_itemselection

develop
Myk 2023-04-07 01:06:38 -07:00 committed by GitHub
commit cd209d2f54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 293 additions and 97 deletions

@ -0,0 +1,4 @@
# DO NOT EDIT THIS FILE
# Please use gui/control-panel to edit this file
enable faststart

@ -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

@ -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 ]

@ -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

@ -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

@ -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 <https://steamcommunity.com/app/975370/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 <https://steamcommunity.com/app/975370/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

@ -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

@ -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<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);
}
}
static void loadModScriptPaths(color_ostream &out) {
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
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);
}
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<std::string, state_change_event> 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:

@ -2729,13 +2729,9 @@ static int filesystem_listdir_recursive(lua_State *L)
include_prefix = lua_toboolean(L, 3);
std::map<std::string, bool> 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;
}

@ -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

@ -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)

@ -0,0 +1,69 @@
// Fast Startup tweak
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <MiscUtils.h>
#include <VTableInterpose.h>
#include "df/viewscreen_initial_prepst.h"
#include <vector>
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 <PluginCommand> &commands)
{
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
INTERPOSE_HOOK(prep_hook, logic).remove();
return CR_OK;
}

@ -1 +1 @@
Subproject commit 53f0aedf9f7df33a4f79246ba46de02794619d09
Subproject commit a4e5d4514ec33462ee0c0bd25e02d4a9c3e2ce01