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 end
for _,sp in ipairs(sps) do
local filter = args.item_filter print(STATUS_FMT:format(sp.id, get_sp_name(sp.name, sp.stockpile_number)))
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
end end
args.choices = choices local function list_dir(path, prefix, filters)
local paths = dfhack.filesystem.listdir_recursive(path, 0, false)
if args.on_select then if not paths then
local cb = args.on_select dfhack.printerr(('Cannot find stockpile settings directory: "%s"'):format(path))
args.on_select = function(idx, obj) return
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
end end
local normalized_filters = {}
function tablify(iterableObject) for _,filter in ipairs(filters or {}) do
t={} table.insert(normalized_filters, filter:lower())
for k,v in ipairs(iterableObject) do
t[k] = v~=nil and v or 'nil'
end 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 end
local filename_invalid_regex = '[^A-Za-z0-9 ._-]'
function valid_filename(filename)
return not filename:match(filename_invalid_regex)
end end
if not matched then goto continue 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
print(('%s%s'):format(prefix, v.path:sub(1, -9)))
::continue::
end end
return ret
end end
FilenameInputBox = defclass(FilenameInputBox, dlg.InputBox) local function list_settings_files(filters)
function FilenameInputBox:onInput(keys) list_dir(STOCKPILES_DIR, '', filters)
if not valid_filename(string.char(keys._STRING or 0)) and not keys.STRING_A000 then list_dir(STOCKPILES_LIBRARY_DIR, 'library/', filters)
keys._STRING = nil
end 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 end
if name:find('[^%a ._-]') then
function load_settings() qerror('name can only contain numbers, letters, periods, underscores, dashes, and spaces')
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
end end
if #list == 0 then
show_message_box("Stockpile Settings", "There are no saved stockpile settings.", true)
return
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
end) stockpiles_import(name, get_sp_id(opts))
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)
end elseif command == 'export' then
persist.GlobalTable.stockpiles['settings_path'] = path export_stockpile(positionals[1], opts)
elseif command == 'import' then
import_stockpile(positionals[1], opts)
else
return false
end 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) { 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( commands.push_back(PluginCommand(
"savestock", plugin_name,
"Save the active stockpile's settings to a file.", "Import, export, or modify stockpile settings and features.",
savestock, do_command));
Gui::any_stockpile_hotkey));
commands.push_back(PluginCommand(
"loadstock",
"Load and apply stockpile settings from a file.",
loadstock,
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);
// exporting CoreSuspender guard;
static command_result savestock(color_ostream& out, vector <string>& parameters) {
df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true); auto L = Lua::Core::State;
if (!sp) { Lua::StackUnwinder top(L);
out.printerr("Selected building isn't a stockpile.\n");
return CR_WRONG_USAGE; 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) { static command_result do_command(color_ostream &out, vector<string> &parameters) {
out.printerr("Invalid parameters\n"); CoreSuspender suspend;
return CR_WRONG_USAGE;
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; return show_help ? CR_WRONG_USAGE : CR_OK;
for (size_t i = 0; i < parameters.size(); ++i) {
const std::string o = parameters.at(i);
if (!o.empty() && o[0] != '-') {
file = o;
} }
/////////////////////////////////////////////////////
// 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"); 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
};