From e21c55d6ffc1d5ac3a0fb608f3eadeb7ab1dd944 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Mar 2023 17:16:42 -0700 Subject: [PATCH] update stockpiles command and use new data paths --- docs/plugins/stockpiles.rst | 79 +++++--- plugins/lua/stockpiles.lua | 299 ++++++++++-------------------- plugins/stockpiles/stockpiles.cpp | 167 +++++++++-------- 3 files changed, 239 insertions(+), 306 deletions(-) diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 01cd3159b..7ca0c5039 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -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 `` - 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 `` - 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 [] + stockpiles export [] + stockpiles import [] + +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 `` + 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 diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index ca8c28cd4..723ff97f6 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -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 - }) - end + 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]) +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 + 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 + print(('%s%s'):format(prefix, v.path:sub(1, -9))) + ::continue:: 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() +local function list_settings_files(filters) + list_dir(STOCKPILES_DIR, '', filters) + list_dir(STOCKPILES_LIBRARY_DIR, 'library/', filters) 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 +local function assert_safe_name(name) + if not name or #name == 0 then + qerror('name missing or empty') end -end - -function tablify(iterableObject) - t={} - for k,v in ipairs(iterableObject) do - t[k] = v~=nil and v or 'nil' + if name:find('[^%a ._-]') then + qerror('name can only contain numbers, letters, periods, underscores, dashes, and spaces') end - return t end -local filename_invalid_regex = '[^A-Za-z0-9 ._-]' - -function valid_filename(filename) - return not filename:match(filename_invalid_regex) +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 -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 - return ret +local function export_stockpile(name, opts) + assert_safe_name(name) + name = STOCKPILES_DIR .. '/' .. name + stockpiles_export(name, get_sp_id(opts)) 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 import_stockpile(name, opts) + local is_library = false + if name:startswith('library/') then + name = name:sub(9) + is_library = true 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() + assert_safe_name(name) + if not is_library and dfhack.filesystem.exists(STOCKPILES_DIR .. '/' .. name .. '.dfstock') then + name = STOCKPILES_DIR .. '/' .. name + else + name = STOCKPILES_LIBRARY_DIR .. '/' .. name + end + stockpiles_import(name, get_sp_id(opts)) 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 - end - if #list == 0 then - show_message_box("Stockpile Settings", "There are no saved stockpile settings.", true) +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = 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], "_", " ") - 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) + 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 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) -end +function parse_commandline(args) + local opts = {} + local positionals = process_args(opts, args) -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") + 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 + 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 - persist.GlobalTable.stockpiles['settings_path'] = path -end -function get_path() - init() - if persist == nil then - return "stocksettings" - end - return persist.GlobalTable.stockpiles['settings_path'] + return true end return _ENV diff --git a/plugins/stockpiles/stockpiles.cpp b/plugins/stockpiles/stockpiles.cpp index 98c94ace0..e7b3525ef 100644 --- a/plugins/stockpiles/stockpiles.cpp +++ b/plugins/stockpiles/stockpiles.cpp @@ -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 +#include + 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 & parameters); -static command_result loadstock(color_ostream& out, vector & parameters); +static command_result do_command(color_ostream &out, vector ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); -DFhackCExport command_result plugin_init(color_ostream& out, std::vector & commands) { - 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); + + 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(args_lambda), + std::forward(res_lambda)); } -// exporting -static command_result savestock(color_ostream& out, vector & parameters) { - df::building_stockpilest* sp = Gui::getSelectedStockpile(out, true); - if (!sp) { - out.printerr("Selected building isn't a stockpile.\n"); - return CR_WRONG_USAGE; +static command_result do_command(color_ostream &out, vector ¶meters) { + 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; } - if (parameters.size() > 2) { - out.printerr("Invalid parameters\n"); - return CR_WRONG_USAGE; - } + return show_help ? CR_WRONG_USAGE : CR_OK; +} - 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("You must supply a valid filename.\n"); - return CR_WRONG_USAGE; +///////////////////////////////////////////////////// +// Lua API +// + +static df::building_stockpilest* get_stockpile(int id) { + return virtual_cast(df::building::find(id)); +} + +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 & 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 +};