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 stockpiles
========== ==========
.. dfhack-tool:: .. dfhack-tool::
:summary: Import and export stockpile settings. :summary: Import, export, or modify stockpile settings and features.
:tags: fort design productivity stockpiles :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 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, stockpiles [status]
etc. are not saved as they are different in every world. 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 Examples
-------- --------
``savestock food`` ``stockpiles``
Export the stockpile settings for the currently selected stockpile to a Shows the list of all your stockpiles and some relevant statistics.
file named ``food.dfstock``. ``stockpiles list``
``loadstock food`` Shows the list of previously exported stockpile settings files, including
Set the selected stockpile settings to those saved in the ``food.dfstock`` the stockpile configuration library.
file. ``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 _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 local function get_sp_name(name, num)
* stockpiles_load(file), with full path if #name > 0 then return name end
* stockpiles_save(file), with full path return ('Stockpile %d'):format(num)
* isEnabled()
--]]
--
function safe_require(module)
local status, module = pcall(require, module)
return status and module or nil
end end
local STATUS_FMT = '%6s %s'
local gui = require 'gui' local function print_status()
local widgets = require('gui.widgets') local sps = df.global.world.buildings.other.STOCKPILE
local dlg = require('gui.dialogs') print(('Current stockpiles: %d'):format(#sps))
local script = require 'gui.script' if #sps > 0 then
local persist = safe_require('persist-table') print()
print(STATUS_FMT:format('ID', 'Name'))
print(STATUS_FMT:format('------', '----------'))
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
})
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
})
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
for _,sp in ipairs(sps) do
print(STATUS_FMT:format(sp.id, get_sp_name(sp.name, sp.stockpile_number)))
end end
return dlg.ListBox(args)
end end
function showFilterPrompt(title, list, text,item_filter,hide_none) local function list_dir(path, prefix, filters)
ListFilterDialog{ local paths = dfhack.filesystem.listdir_recursive(path, 0, false)
frame_title=title, if not paths then
items=list, dfhack.printerr(('Cannot find stockpile settings directory: "%s"'):format(path))
prompt=text, return
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 normalized_filters = {}
for _,filter in ipairs(filters or {}) do
table.insert(normalized_filters, filter:lower())
end end
end for _,v in ipairs(paths) do
local normalized_path = prefix .. v.path:lower()
function tablify(iterableObject) if v.isdir or not normalized_path:endswith('.dfstock') then goto continue end
t={} normalized_path = normalized_path:sub(1, -9)
for k,v in ipairs(iterableObject) do if #normalized_filters > 0 then
t[k] = v~=nil and v or 'nil' local matched = false
for _,filter in ipairs(normalized_filters) do
if normalized_path:find(filter, 1, true) then
matched = true
break
end end
return t
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 .. '-'
end end
if not matched then goto continue end
end end
return ret print(('%s%s'):format(prefix, v.path:sub(1, -9)))
end ::continue::
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
end end
FilenameInputBox.super.onInput(self, keys)
end end
function showFilenameInputPrompt(title, text, tcolor, input, min_width) local function list_settings_files(filters)
FilenameInputBox{ list_dir(STOCKPILES_DIR, '', filters)
frame_title = title, list_dir(STOCKPILES_LIBRARY_DIR, 'library/', filters)
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()
end end
function load_settings() local function assert_safe_name(name)
init() if not name or #name == 0 then
local path = get_path() qerror('name missing or empty')
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
end end
if #list == 0 then if name:find('[^%a ._-]') then
show_message_box("Stockpile Settings", "There are no saved stockpile settings.", true) qerror('name can only contain numbers, letters, periods, underscores, dashes, and spaces')
return
end end
end
local choice_list = {} local function get_sp_id(opts)
for i,v in ipairs(list) do if opts.id then return opts.id end
choice_list[i] = string.gsub(v, "/", "/ ") local sp = dfhack.gui.getSelectedStockpile()
choice_list[i] = string.gsub(choice_list[i], "-", " - ") if sp then return sp.id end
choice_list[i] = string.gsub(choice_list[i], "_", " ") return nil
end end
script.start(function() local function export_stockpile(name, opts)
local ok2,index,name=showFilterPrompt('Stockpile Settings', choice_list, 'Choose a stockpile', function(item) return true end, true) assert_safe_name(name)
if ok2 then name = STOCKPILES_DIR .. '/' .. name
local filename = list[index]; stockpiles_export(name, get_sp_id(opts))
stockpiles_load(path..'/'..filename)
end
end)
end end
function save_settings(stockpile) local function import_stockpile(name, opts)
init() local is_library = false
script.start(function() if name:startswith('library/') then
local suggested = stockpile.name name = name:sub(9)
if #suggested == 0 then is_library = true
suggested = 'Stock1'
end end
suggested = sanitize_filename(suggested) assert_safe_name(name)
local path = get_path() if not is_library and dfhack.filesystem.exists(STOCKPILES_DIR .. '/' .. name .. '.dfstock') then
local sok,filename = showFilenameInputPrompt('Stockpile Settings', 'Enter filename', COLOR_WHITE, suggested) name = STOCKPILES_DIR .. '/' .. name
if sok then
if filename == nil or filename == '' or not valid_filename(filename) then
script.showMessage('Stockpile Settings', 'Invalid File Name', COLOR_RED)
else else
if not dfhack.filesystem.exists(path) then name = STOCKPILES_LIBRARY_DIR .. '/' .. name
dfhack.filesystem.mkdir(path)
end
stockpiles_save(path..'/'..filename)
end end
end stockpiles_import(name, get_sp_id(opts))
end)
end end
function manage_settings(sp) local function process_args(opts, args)
init() if args[1] == 'help' then
if not guard() then return false end opts.help = true
script.start(function() return
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
end)
end
function show_message_box(title, msg, iserror) return argparse.processArgsGetopt(args, {
local color = COLOR_WHITE {'h', 'help', handler=function() opts.help = true end},
if iserror then {'s', 'stockpile', has_arg=true,
color = COLOR_RED handler=function(arg) opts.id = argparse.nonnegativeInt(art, 'stockpile') end},
end })
script.start(function()
script.showMessage(title, msg, color)
end)
end end
function guard() function parse_commandline(args)
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Stockpile') then local opts = {}
qerror("This script requires a stockpile selected in the 'q' mode") local positionals = process_args(opts, args)
if opts.help or not positionals then
return false return false
end end
return true
end
function set_path(path) local command = table.remove(positionals, 1)
init() if not command or command == 'status' then
if persist == nil then print_status()
qerror("This version of DFHack doesn't support setting the stockpile settings path. Sorry.") elseif command == 'list' then
return 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 end
persist.GlobalTable.stockpiles['settings_path'] = path
end
function get_path() return true
init()
if persist == nil then
return "stocksettings"
end
return persist.GlobalTable.stockpiles['settings_path']
end end
return _ENV return _ENV

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