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
local normalized_filters = {}
for _,filter in ipairs(filters or {}) do
table.insert(normalized_filters, filter:lower())
end
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
if not matched then goto continue end
end end
print(('%s%s'):format(prefix, v.path:sub(1, -9)))
::continue::
end end
return dlg.ListBox(args)
end end
function showFilterPrompt(title, list, text,item_filter,hide_none) local function list_settings_files(filters)
ListFilterDialog{ list_dir(STOCKPILES_DIR, '', filters)
frame_title=title, list_dir(STOCKPILES_LIBRARY_DIR, 'library/', filters)
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 end
function init() local function assert_safe_name(name)
if persist == nil then return end if not name or #name == 0 then
if dfhack.isMapLoaded() then qerror('name missing or empty')
if persist.GlobalTable.stockpiles == nil then
persist.GlobalTable.stockpiles = {}
persist.GlobalTable.stockpiles['settings_path'] = './stocksettings'
end
end end
end if name:find('[^%a ._-]') then
qerror('name can only contain numbers, letters, periods, underscores, dashes, and spaces')
function tablify(iterableObject)
t={}
for k,v in ipairs(iterableObject) do
t[k] = v~=nil and v or 'nil'
end end
return t
end end
local filename_invalid_regex = '[^A-Za-z0-9 ._-]' local function get_sp_id(opts)
if opts.id then return opts.id end
function valid_filename(filename) local sp = dfhack.gui.getSelectedStockpile()
return not filename:match(filename_invalid_regex) if sp then return sp.id end
return nil
end end
function sanitize_filename(filename) local function export_stockpile(name, opts)
local ret = '' assert_safe_name(name)
for i = 1, #filename do name = STOCKPILES_DIR .. '/' .. name
local ch = filename:sub(i, i) stockpiles_export(name, get_sp_id(opts))
if valid_filename(ch) then
ret = ret .. ch
else
ret = ret .. '-'
end
end
return ret
end end
FilenameInputBox = defclass(FilenameInputBox, dlg.InputBox) local function import_stockpile(name, opts)
function FilenameInputBox:onInput(keys) local is_library = false
if not valid_filename(string.char(keys._STRING or 0)) and not keys.STRING_A000 then if name:startswith('library/') then
keys._STRING = nil name = name:sub(9)
is_library = true
end end
FilenameInputBox.super.onInput(self, keys) assert_safe_name(name)
end if not is_library and dfhack.filesystem.exists(STOCKPILES_DIR .. '/' .. name .. '.dfstock') then
name = STOCKPILES_DIR .. '/' .. name
function showFilenameInputPrompt(title, text, tcolor, input, min_width) else
FilenameInputBox{ name = STOCKPILES_LIBRARY_DIR .. '/' .. name
frame_title = title, end
text = text, stockpiles_import(name, get_sp_id(opts))
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 process_args(opts, args)
init() if args[1] == 'help' then
local path = get_path() opts.help = true
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
if #list == 0 then
show_message_box("Stockpile Settings", "There are no saved stockpile settings.", true)
return return
end end
local choice_list = {} return argparse.processArgsGetopt(args, {
for i,v in ipairs(list) do {'h', 'help', handler=function() opts.help = true end},
choice_list[i] = string.gsub(v, "/", "/ ") {'s', 'stockpile', has_arg=true,
choice_list[i] = string.gsub(choice_list[i], "-", " - ") handler=function(arg) opts.id = argparse.nonnegativeInt(art, 'stockpile') end},
choice_list[i] = string.gsub(choice_list[i], "_", " ") })
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)
end
function save_settings(stockpile)
init()
script.start(function()
local suggested = stockpile.name
if #suggested == 0 then
suggested = 'Stock1'
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)
else
if not dfhack.filesystem.exists(path) then
dfhack.filesystem.mkdir(path)
end
stockpiles_save(path..'/'..filename)
end
end
end)
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)
end end
function show_message_box(title, msg, iserror) function parse_commandline(args)
local color = COLOR_WHITE local opts = {}
if iserror then local positionals = process_args(opts, args)
color = COLOR_RED
end
script.start(function()
script.showMessage(title, msg, color)
end)
end
function guard() if opts.help or not positionals then
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Stockpile') then
qerror("This script requires a stockpile selected in the 'q' mode")
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
};