update stockpiles command and use new data paths

develop
Myk Taylor 2023-03-15 17:16:42 -07:00
parent 8c0b59c548
commit e21c55d6ff
No known key found for this signature in database
3 changed files with 239 additions and 306 deletions

@ -1,41 +1,68 @@
.. _stocksettings:
stockpiles
==========
.. dfhack-tool::
:summary: Import and export stockpile settings.
:summary: Import, export, or modify stockpile settings and features.
:tags: fort design productivity stockpiles
:no-command:
.. dfhack-command:: savestock
:summary: Exports the configuration of the selected stockpile.
.. dfhack-command:: loadstock
:summary: Imports configuration for the selected stockpile.
Select a stockpile in the UI first to use these commands.
If you are importing or exporting setting and don't want to specify a building
ID, select a stockpile in the UI before running the command.
Usage
-----
``savestock <filename>``
Saves the currently highlighted stockpile's settings to a file in your
Dwarf Fortress folder. This file can be used to copy settings between game
saves or players.
``loadstock <filename>``
Loads a saved stockpile settings file and applies it to the currently
selected stockpile.
::
Filenames with spaces are not supported. Generated materials, divine metals,
etc. are not saved as they are different in every world.
stockpiles [status]
stockpiles list [<search>]
stockpiles export <name> [<options>]
stockpiles import <name> [<options>]
Exported stockpile settings are saved in the ``dfhack-config/stockpiles``
folder, where you can view and delete them, if desired. Names can only
contain numbers, letters, periods, underscores, dashes, and spaces. If
the name has spaces, be sure to surround it with double quotes (:kbd:`"`).
The names of library settings files are all prefixed by the string ``library/``.
You can specify library files explicitly by including the prefix, or you can
just write the short name to use a player-exported file by that name if it
exists, and the library file if it doesn't.
Examples
--------
``savestock food``
Export the stockpile settings for the currently selected stockpile to a
file named ``food.dfstock``.
``loadstock food``
Set the selected stockpile settings to those saved in the ``food.dfstock``
file.
``stockpiles``
Shows the list of all your stockpiles and some relevant statistics.
``stockpiles list``
Shows the list of previously exported stockpile settings files, including
the stockpile configuration library.
``stockpiles list plants``
Shows the list of exported stockpile settings files that include the
substring ``plants``.
``stockpiles import library/plants``
Imports the library ``plants`` settings file into the currently selected
stockpile.
``stockpiles import plants``
Imports a player-exported settings file named ``plants``, or the library
``plants`` settings file if a player-exported file by that name doesn't
exist.
``stockpiles export mysettings``
Export the settings for the currently selected stockpile to a file named
``dfhack-config/stockpiles/mysettings.dfstock``.
Options
-------
``-s``, ``--stockpile <id>``
Specify a specific stockpile ID instead of using the one currently selected
in the UI.
.. _stockpiles-library:
The stockpiles settings library
-------------------------------
DFHack comes with a library of useful stockpile settings files that are ready
for import:
TODO: port alias library here

@ -1,244 +1,135 @@
local _ENV = mkmodule('plugins.stockpiles')
--[[
local argparse = require('argparse')
Native functions:
local STOCKPILES_DIR = "dfhack-config/stockpiles";
local STOCKPILES_LIBRARY_DIR = "hack/data/stockpiles";
* stockpiles_list_settings(dir_path), list files in directory
* stockpiles_load(file), with full path
* stockpiles_save(file), with full path
* isEnabled()
--]]
--
function safe_require(module)
local status, module = pcall(require, module)
return status and module or nil
local function get_sp_name(name, num)
if #name > 0 then return name end
return ('Stockpile %d'):format(num)
end
local gui = require 'gui'
local widgets = require('gui.widgets')
local dlg = require('gui.dialogs')
local script = require 'gui.script'
local persist = safe_require('persist-table')
function ListFilterDialog(args)
args.text = args.prompt or 'Type or select an option'
args.text_pen = COLOR_WHITE
args.with_filter = true
args.icon_width = 2
local choices = {}
if not args.hide_none then
table.insert(choices, {
icon = '?', text = args.none_caption or 'none',
index = -1, name = -1
})
local STATUS_FMT = '%6s %s'
local function print_status()
local sps = df.global.world.buildings.other.STOCKPILE
print(('Current stockpiles: %d'):format(#sps))
if #sps > 0 then
print()
print(STATUS_FMT:format('ID', 'Name'))
print(STATUS_FMT:format('------', '----------'))
end
local filter = args.item_filter
for i,v in ipairs(args.items) do
if not filter or filter(v,-1) then
local name = v
local icon
table.insert(choices, {
icon = icon, search_key = string.lower(name), text = name, index = i
})
for _,sp in ipairs(sps) do
print(STATUS_FMT:format(sp.id, get_sp_name(sp.name, sp.stockpile_number)))
end
end
args.choices = choices
if args.on_select then
local cb = args.on_select
args.on_select = function(idx, obj)
return cb(obj.index, args.items[obj.index])
end
end
return dlg.ListBox(args)
end
function showFilterPrompt(title, list, text,item_filter,hide_none)
ListFilterDialog{
frame_title=title,
items=list,
prompt=text,
item_filter=item_filter,
hide_none=hide_none,
on_select=script.mkresume(true),
on_cancel=script.mkresume(false),
on_close=script.qresume(nil)
}:show()
return script.wait()
end
function init()
if persist == nil then return end
if dfhack.isMapLoaded() then
if persist.GlobalTable.stockpiles == nil then
persist.GlobalTable.stockpiles = {}
persist.GlobalTable.stockpiles['settings_path'] = './stocksettings'
end
end
local function list_dir(path, prefix, filters)
local paths = dfhack.filesystem.listdir_recursive(path, 0, false)
if not paths then
dfhack.printerr(('Cannot find stockpile settings directory: "%s"'):format(path))
return
end
function tablify(iterableObject)
t={}
for k,v in ipairs(iterableObject) do
t[k] = v~=nil and v or 'nil'
local normalized_filters = {}
for _,filter in ipairs(filters or {}) do
table.insert(normalized_filters, filter:lower())
end
return t
for _,v in ipairs(paths) do
local normalized_path = prefix .. v.path:lower()
if v.isdir or not normalized_path:endswith('.dfstock') then goto continue end
normalized_path = normalized_path:sub(1, -9)
if #normalized_filters > 0 then
local matched = false
for _,filter in ipairs(normalized_filters) do
if normalized_path:find(filter, 1, true) then
matched = true
break
end
local filename_invalid_regex = '[^A-Za-z0-9 ._-]'
function valid_filename(filename)
return not filename:match(filename_invalid_regex)
end
function sanitize_filename(filename)
local ret = ''
for i = 1, #filename do
local ch = filename:sub(i, i)
if valid_filename(ch) then
ret = ret .. ch
else
ret = ret .. '-'
if not matched then goto continue end
end
print(('%s%s'):format(prefix, v.path:sub(1, -9)))
::continue::
end
return ret
end
FilenameInputBox = defclass(FilenameInputBox, dlg.InputBox)
function FilenameInputBox:onInput(keys)
if not valid_filename(string.char(keys._STRING or 0)) and not keys.STRING_A000 then
keys._STRING = nil
local function list_settings_files(filters)
list_dir(STOCKPILES_DIR, '', filters)
list_dir(STOCKPILES_LIBRARY_DIR, 'library/', filters)
end
FilenameInputBox.super.onInput(self, keys)
end
function showFilenameInputPrompt(title, text, tcolor, input, min_width)
FilenameInputBox{
frame_title = title,
text = text,
text_pen = tcolor,
input = input,
frame_width = min_width,
on_input = script.mkresume(true),
on_cancel = script.mkresume(false),
on_close = script.qresume(nil)
}:show()
return script.wait()
local function assert_safe_name(name)
if not name or #name == 0 then
qerror('name missing or empty')
end
function load_settings()
init()
local path = get_path()
local ok, list = pcall(stockpiles_list_settings, path)
if not ok then
show_message_box("Stockpile Settings", "The stockpile settings folder doesn't exist.", true)
return
if name:find('[^%a ._-]') then
qerror('name can only contain numbers, letters, periods, underscores, dashes, and spaces')
end
if #list == 0 then
show_message_box("Stockpile Settings", "There are no saved stockpile settings.", true)
return
end
local choice_list = {}
for i,v in ipairs(list) do
choice_list[i] = string.gsub(v, "/", "/ ")
choice_list[i] = string.gsub(choice_list[i], "-", " - ")
choice_list[i] = string.gsub(choice_list[i], "_", " ")
local function get_sp_id(opts)
if opts.id then return opts.id end
local sp = dfhack.gui.getSelectedStockpile()
if sp then return sp.id end
return nil
end
script.start(function()
local ok2,index,name=showFilterPrompt('Stockpile Settings', choice_list, 'Choose a stockpile', function(item) return true end, true)
if ok2 then
local filename = list[index];
stockpiles_load(path..'/'..filename)
end
end)
local function export_stockpile(name, opts)
assert_safe_name(name)
name = STOCKPILES_DIR .. '/' .. name
stockpiles_export(name, get_sp_id(opts))
end
function save_settings(stockpile)
init()
script.start(function()
local suggested = stockpile.name
if #suggested == 0 then
suggested = 'Stock1'
local function import_stockpile(name, opts)
local is_library = false
if name:startswith('library/') then
name = name:sub(9)
is_library = true
end
suggested = sanitize_filename(suggested)
local path = get_path()
local sok,filename = showFilenameInputPrompt('Stockpile Settings', 'Enter filename', COLOR_WHITE, suggested)
if sok then
if filename == nil or filename == '' or not valid_filename(filename) then
script.showMessage('Stockpile Settings', 'Invalid File Name', COLOR_RED)
assert_safe_name(name)
if not is_library and dfhack.filesystem.exists(STOCKPILES_DIR .. '/' .. name .. '.dfstock') then
name = STOCKPILES_DIR .. '/' .. name
else
if not dfhack.filesystem.exists(path) then
dfhack.filesystem.mkdir(path)
end
stockpiles_save(path..'/'..filename)
end
name = STOCKPILES_LIBRARY_DIR .. '/' .. name
end
end)
stockpiles_import(name, get_sp_id(opts))
end
function manage_settings(sp)
init()
if not guard() then return false end
script.start(function()
local list = {'Load', 'Save'}
local tok,i = script.showListPrompt('Stockpile Settings','Load or Save Settings?',COLOR_WHITE,tablify(list))
if tok then
if i == 1 then
load_settings()
else
save_settings(sp)
end
end
end)
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
function show_message_box(title, msg, iserror)
local color = COLOR_WHITE
if iserror then
color = COLOR_RED
end
script.start(function()
script.showMessage(title, msg, color)
end)
return argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() opts.help = true end},
{'s', 'stockpile', has_arg=true,
handler=function(arg) opts.id = argparse.nonnegativeInt(art, 'stockpile') end},
})
end
function guard()
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Stockpile') then
qerror("This script requires a stockpile selected in the 'q' mode")
function parse_commandline(args)
local opts = {}
local positionals = process_args(opts, args)
if opts.help or not positionals then
return false
end
return true
end
function set_path(path)
init()
if persist == nil then
qerror("This version of DFHack doesn't support setting the stockpile settings path. Sorry.")
return
end
persist.GlobalTable.stockpiles['settings_path'] = path
local command = table.remove(positionals, 1)
if not command or command == 'status' then
print_status()
elseif command == 'list' then
list_settings_files(positionals)
elseif command == 'export' then
export_stockpile(positionals[1], opts)
elseif command == 'import' then
import_stockpile(positionals[1], opts)
else
return false
end
function get_path()
init()
if persist == nil then
return "stocksettings"
end
return persist.GlobalTable.stockpiles['settings_path']
return true
end
return _ENV

@ -1,13 +1,19 @@
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
#include "StockpileUtils.h"
#include "StockpileSerializer.h"
#include "modules/Filesystem.h"
#include "modules/Gui.h"
using std::vector;
#include "df/building.h"
#include "df/building_stockpilest.h"
#include <string>
#include <vector>
using std::string;
using std::vector;
using namespace DFHack;
@ -19,112 +25,121 @@ namespace DFHack {
DBG_DECLARE(stockpiles, log, DebugCategory::LINFO);
}
static command_result savestock(color_ostream& out, vector <string>& parameters);
static command_result loadstock(color_ostream& out, vector <string>& parameters);
static command_result do_command(color_ostream &out, vector<string> &parameters);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
DEBUG(log,out).print("initializing %s\n", plugin_name);
commands.push_back(PluginCommand(
"savestock",
"Save the active stockpile's settings to a file.",
savestock,
Gui::any_stockpile_hotkey));
commands.push_back(PluginCommand(
"loadstock",
"Load and apply stockpile settings from a file.",
loadstock,
Gui::any_stockpile_hotkey));
plugin_name,
"Import, export, or modify stockpile settings and features.",
do_command));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream& out) {
return CR_OK;
}
static bool call_stockpiles_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(log).print("calling stockpiles lua function: '%s'\n", fn_name);
// exporting
static command_result savestock(color_ostream& out, vector <string>& parameters) {
df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true);
if (!sp) {
out.printerr("Selected building isn't a stockpile.\n");
return CR_WRONG_USAGE;
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
return Lua::CallLuaModuleFunction(*out, L, "plugins.stockpiles", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
if (parameters.size() > 2) {
out.printerr("Invalid parameters\n");
return CR_WRONG_USAGE;
static command_result do_command(color_ostream &out, vector<string> &parameters) {
CoreSuspender suspend;
bool show_help = false;
if (!call_stockpiles_lua(&out, "parse_commandline", 1, 1,
[&](lua_State *L) {
Lua::PushVector(L, parameters);
},
[&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
})) {
return CR_FAILURE;
}
std::string file;
for (size_t i = 0; i < parameters.size(); ++i) {
const std::string o = parameters.at(i);
if (!o.empty() && o[0] != '-') {
file = o;
return show_help ? CR_WRONG_USAGE : CR_OK;
}
/////////////////////////////////////////////////////
// Lua API
//
static df::building_stockpilest* get_stockpile(int id) {
return virtual_cast<df::building_stockpilest>(df::building::find(id));
}
if (file.empty()) {
out.printerr("You must supply a valid filename.\n");
return CR_WRONG_USAGE;
static bool stockpiles_export(color_ostream& out, string fname, int id) {
df::building_stockpilest* sp = get_stockpile(id);
if (!sp) {
out.printerr("Specified building isn't a stockpile: %d.\n", id);
return false;
}
StockpileSerializer cereal(sp);
if (!is_dfstockfile(fname))
fname += ".dfstock";
if (!is_dfstockfile(file)) file += ".dfstock";
try {
if (!cereal.serialize_to_file(file)) {
out.printerr("could not save to %s\n", file.c_str());
return CR_FAILURE;
StockpileSerializer cereal(sp);
if (!cereal.serialize_to_file(fname)) {
out.printerr("could not save to '%s'\n", fname.c_str());
return false;
}
}
catch (std::exception& e) {
out.printerr("serialization failed: protobuf exception: %s\n", e.what());
return CR_FAILURE;
return false;
}
return CR_OK;
return true;
}
// importing
static command_result loadstock(color_ostream& out, vector <string>& parameters) {
df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true);
static bool stockpiles_import(color_ostream& out, string fname, int id) {
df::building_stockpilest* sp = get_stockpile(id);
if (!sp) {
out.printerr("Selected building isn't a stockpile.\n");
return CR_WRONG_USAGE;
out.printerr("Specified building isn't a stockpile: %d.\n", id);
return false;
}
if (parameters.size() < 1 || parameters.size() > 2) {
out.printerr("Invalid parameters\n");
return CR_WRONG_USAGE;
}
if (!is_dfstockfile(fname))
fname += ".dfstock";
std::string file;
for (size_t i = 0; i < parameters.size(); ++i) {
const std::string o = parameters.at(i);
if (!o.empty() && o[0] != '-') {
file = o;
}
}
if (file.empty()) {
out.printerr("ERROR: missing .dfstock file parameter\n");
return DFHack::CR_WRONG_USAGE;
}
if (!is_dfstockfile(file))
file += ".dfstock";
if (!Filesystem::exists(file)) {
out.printerr("ERROR: the .dfstock file doesn't exist: %s\n", file.c_str());
return CR_WRONG_USAGE;
if (!Filesystem::exists(fname)) {
out.printerr("ERROR: file doesn't exist: '%s'\n", fname.c_str());
return false;
}
StockpileSerializer cereal(sp);
try {
if (!cereal.unserialize_from_file(file)) {
out.printerr("unserialization failed: %s\n", file.c_str());
return CR_FAILURE;
StockpileSerializer cereal(sp);
if (!cereal.unserialize_from_file(fname)) {
out.printerr("deserialization failed: '%s'\n", fname.c_str());
return false;
}
}
catch (std::exception& e) {
out.printerr("unserialization failed: protobuf exception: %s\n", e.what());
return CR_FAILURE;
out.printerr("deserialization failed: protobuf exception: %s\n", e.what());
return false;
}
return CR_OK;
return true;
}
DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(stockpiles_export),
DFHACK_LUA_FUNCTION(stockpiles_import),
DFHACK_LUA_END
};