Merge remote-tracking branch 'myk002/myk_blueprint' into develop

develop
lethosor 2021-05-24 23:48:21 -04:00
commit 4f976a5909
No known key found for this signature in database
GPG Key ID: 76A269552F4F58C1
8 changed files with 588 additions and 176 deletions

@ -3756,15 +3756,19 @@ the plugin. See existing files in ``plugins/lua`` for examples.
blueprint blueprint
========= =========
Native functions provided by the `blueprint` plugin: Lua functions provided by the `blueprint` plugin to programmatically generate
blueprint files:
* ``dig(start, end, name)`` * ``dig(start, end, name)``
* ``build(start, end, name)`` * ``build(start, end, name)``
* ``place(start, end, name)`` * ``place(start, end, name)``
* ``query(start, end, name)`` * ``query(start, end, name)``
``start`` and ``end`` are tables containing positions (see ``start`` and ``end`` are tables containing positions (see ``xyz2pos``).
``xyz2pos``). ``name`` is used as the basis for the filename. ``name`` is used as the basis for the generated filenames.
The names of the functions are also available as the keys of the
``valid_phases`` table.
.. _building-hacks: .. _building-hacks:

@ -37,22 +37,72 @@ For more information, see `the full Stonesense README <stonesense>`.
blueprint blueprint
========= =========
Exports a portion of your fortress into QuickFort style blueprint files. The ``blueprint`` command exports the structure of a portion of your fortress in
a blueprint file that you (or anyone else) can later play back with `quickfort`.
Blueprints are ``.csv`` or ``.xlsx`` files created in the ``blueprints``
subdirectory of your DF folder. The map area to turn into a blueprint is either
selected interactively with the ``blueprint gui`` command or, if the GUI is not
used, starts at the active cursor location and extends right and down for the
requested width and height.
Usage:: Usage::
blueprint <x> <y> <z> <name> [dig] [build] [place] [query] blueprint <width> <height> [<depth>] [<name> [<phases>]] [<options>]
blueprint gui [<name> [<phases>]] [<options>]
Examples:
``blueprint gui``
Runs `gui/blueprint`, the interactive blueprint frontend, where all
configuration for a ``blueprint`` command can be set visually and
interactively.
``blueprint 30 40 bedrooms``
Generates blueprints for an area 30 tiles wide by 40 tiles tall, starting
from the active cursor on the current z-level. Output is written to files
with names matching the pattern ``bedrooms-PHASE.csv`` in the ``blueprints``
directory.
``blueprint 30 40 bedrooms dig --cursor 108,100,150``
Generates only the ``bedrooms-dig.csv`` file from the previous example, and
the blueprint start coordinate is set to a specific value instead of using
the in-game cursor position.
Options (If only region and name are given, export all): Positional Parameters:
:x,y,z: Size of map area to export :``width``: Width of the area (in tiles) to translate.
:name: Name of export files :``height``: Height of the area (in tiles) to translate.
:dig: Export dig commands to "<name>-dig.csv" :``depth``: Number of z-levels to translate. Positive numbers go *up* from the
:build: Export build commands to "<name>-build.csv" cursor and negative numbers go *down*. Defaults to 1 if not specified,
:place: Export stockpile commands to "<name>-place.csv" indicating that the blueprint should only include the current z-level.
:query: Export query commands to "<name>-query.csv" :``name``: Base name for blueprint files created in the ``blueprints``
directory. If no name is specified, "blueprint" is used by default. The
string must contain some characters other than numbers so the name won't be
confused with the optional ``depth`` parameter.
Phases:
If you want to generate blueprints only for specific phases, add their names to
the commandline, anywhere after the blueprint base name. You can list multiple
phases; just separate them with a space.
:``dig``: Generate quickfort ``#dig`` blueprints.
:``build``: Generate quickfort ``#build`` blueprints for constructions and
buildings.
:``place``: Generate quickfort ``#place`` blueprints for placing stockpiles.
:``query``: Generate quickfort ``#query`` blueprints for configuring rooms.
If no phases are specified, all blueprints are created.
Options:
Goes very well with `quickfort`, for re-importing. :``-c``, ``--cursor <x>,<y>,<z>``:
Use the specified map coordinates instead of the current cursor position for
the upper left corner of the blueprint range. If this option is specified,
then an active game map cursor is not necessary.
:``-h``, ``--help``:
Show command help text.
.. _remotefortressreader: .. _remotefortressreader:

@ -41,6 +41,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- ``quickfortress.csv`` blueprint: fixed refuse stockpile config and prevented stockpiles from covering stairways - ``quickfortress.csv`` blueprint: fixed refuse stockpile config and prevented stockpiles from covering stairways
## Misc Improvements ## Misc Improvements
- `blueprint`: make ``depth`` and ``name`` parameters optional. ``depth`` now defaults to ``1`` (current level only) and ``name`` defaults to "blueprint"
- `blueprint`: allow ``depth`` to be negative, which will result in the blueprints being written from the highest z-level to the lowest. before, blueprints were always written from the lowest z-level to the highest.
- `blueprint`: add the ``--cursor`` option to set the starting coordinate for the generated blueprints. a game cursor is no longer necessary if this option is used.
- `tweak` hide-priority: changed so that priorities stay hidden (or visible) when exiting and re-entering the designations menu - `tweak` hide-priority: changed so that priorities stay hidden (or visible) when exiting and re-entering the designations menu
## Lua ## Lua

@ -156,6 +156,7 @@ namespace DFHack
DFHACK_EXPORT bool setViewCoords (const int32_t x, const int32_t y, const int32_t z); DFHACK_EXPORT bool setViewCoords (const int32_t x, const int32_t y, const int32_t z);
DFHACK_EXPORT bool getCursorCoords (int32_t &x, int32_t &y, int32_t &z); DFHACK_EXPORT bool getCursorCoords (int32_t &x, int32_t &y, int32_t &z);
DFHACK_EXPORT bool getCursorCoords (df::coord &pos);
DFHACK_EXPORT bool setCursorCoords (const int32_t x, const int32_t y, const int32_t z); DFHACK_EXPORT bool setCursorCoords (const int32_t x, const int32_t y, const int32_t z);
DFHACK_EXPORT bool getDesignationCoords (int32_t &x, int32_t &y, int32_t &z); DFHACK_EXPORT bool getDesignationCoords (int32_t &x, int32_t &y, int32_t &z);

@ -648,13 +648,18 @@ bool Gui::item_details_hotkey(df::viewscreen *top)
return !!strict_virtual_cast<df::viewscreen_itemst>(top); return !!strict_virtual_cast<df::viewscreen_itemst>(top);
} }
static bool has_cursor()
{
return df::global::cursor && df::global::cursor->x != -30000;
}
bool Gui::cursor_hotkey(df::viewscreen *top) bool Gui::cursor_hotkey(df::viewscreen *top)
{ {
if (!dwarfmode_hotkey(top)) if (!dwarfmode_hotkey(top))
return false; return false;
// Also require the cursor. // Also require the cursor.
if (!df::global::cursor || df::global::cursor->x == -30000) if (!has_cursor())
return false; return false;
return true; return true;
@ -1788,7 +1793,15 @@ bool Gui::getCursorCoords (int32_t &x, int32_t &y, int32_t &z)
x = df::global::cursor->x; x = df::global::cursor->x;
y = df::global::cursor->y; y = df::global::cursor->y;
z = df::global::cursor->z; z = df::global::cursor->z;
return (x == -30000) ? false : true; return has_cursor();
}
bool Gui::getCursorCoords (df::coord &pos)
{
pos.x = df::global::cursor->x;
pos.y = df::global::cursor->y;
pos.z = df::global::cursor->z;
return has_cursor();
} }
//FIXME: confine writing of coords to map bounds? //FIXME: confine writing of coords to map bounds?

@ -8,9 +8,11 @@
#include <algorithm> #include <algorithm>
#include <sstream> #include <sstream>
#include <Console.h> #include "Console.h"
#include <PluginManager.h> #include "DataDefs.h"
#include "DataIdentity.h"
#include "LuaTools.h" #include "LuaTools.h"
#include "PluginManager.h"
#include "TileTypes.h" #include "TileTypes.h"
#include "modules/Buildings.h" #include "modules/Buildings.h"
@ -27,20 +29,61 @@
#include "df/building_trapst.h" #include "df/building_trapst.h"
#include "df/building_water_wheelst.h" #include "df/building_water_wheelst.h"
#include "df/building_workshopst.h" #include "df/building_workshopst.h"
#include "df/world.h"
using std::string; using std::string;
using std::endl; using std::endl;
using std::vector; using std::vector;
using std::ofstream; using std::ofstream;
using std::swap;
using std::find;
using std::pair; using std::pair;
using namespace DFHack; using namespace DFHack;
using namespace df::enums;
DFHACK_PLUGIN("blueprint"); DFHACK_PLUGIN("blueprint");
REQUIRE_GLOBAL(world);
enum phase {DIG=1, BUILD=2, PLACE=4, QUERY=8}; struct blueprint_options {
// whether to display help
bool help = false;
// starting tile coordinate of the translation area (if not set then all
// coordinates are set to -30000)
df::coord start;
// dimensions of translation area. width and height are guaranteed to be
// greater than 0. depth can be positive or negative, but not zero.
int32_t width = 0;
int32_t height = 0;
int32_t depth = 0;
// base name to use for generated files
string name;
// whether to autodetect which phases to output
bool auto_phase = false;
// if not autodetecting, which phases to output
bool dig = false;
bool build = false;
bool place = false;
bool query = false;
static struct_identity _identity;
};
static const struct_field_info blueprint_options_fields[] = {
{ struct_field_info::PRIMITIVE, "help", offsetof(blueprint_options, help), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::SUBSTRUCT, "start", offsetof(blueprint_options, start), &df::coord::_identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "width", offsetof(blueprint_options, width), &df::identity_traits<int32_t>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits<int32_t>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "depth", offsetof(blueprint_options, depth), &df::identity_traits<int32_t>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "name", offsetof(blueprint_options, name), df::identity_traits<string>::get(), 0, 0 },
{ struct_field_info::PRIMITIVE, "auto_phase", offsetof(blueprint_options, auto_phase), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "dig", offsetof(blueprint_options, dig), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits<bool>::identity, 0, 0 },
{ struct_field_info::END }
};
struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::allocator_fn<blueprint_options>, NULL, "blueprint_options", NULL, blueprint_options_fields);
command_result blueprint(color_ostream &out, vector<string> &parameters); command_result blueprint(color_ostream &out, vector<string> &parameters);
@ -55,25 +98,6 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out)
return CR_OK; return CR_OK;
} }
command_result help(color_ostream &out)
{
out << "blueprint width height depth name [dig] [build] [place] [query]" << endl
<< " width, height, depth: area to translate in tiles" << endl
<< " name: base name for blueprint files" << endl
<< " dig: generate blueprints for digging" << endl
<< " build: generate blueprints for building" << endl
<< " place: generate blueprints for stockpiles" << endl
<< " query: generate blueprints for querying (room designations)" << endl
<< " defaults to generating all blueprints" << endl
<< endl
<< "blueprint translates a portion of your fortress into blueprints suitable for" << endl
<< " digfort/fortplan/quickfort. Blueprints are created in the \"blueprints\"" << endl
<< " subdirectory of the DF folder with names following a \"name-phase.csv\" pattern." << endl
<< " Translation starts at the current cursor location and includes all tiles in the" << endl
<< " range specified." << endl;
return CR_OK;
}
pair<uint32_t, uint32_t> get_building_size(df::building* b) pair<uint32_t, uint32_t> get_building_size(df::building* b)
{ {
return pair<uint32_t, uint32_t>(b->x2 - b->x1 + 1, b->y2 - b->y1 + 1); return pair<uint32_t, uint32_t>(b->x2 - b->x1 + 1, b->y2 - b->y1 + 1);
@ -562,7 +586,7 @@ string get_tile_query(df::building* b)
return " "; return " ";
} }
void init_stream(ofstream &out, std::string basename, std::string target) void init_stream(ofstream &out, string basename, string target)
{ {
std::ostringstream out_path; std::ostringstream out_path;
out_path << basename << "-" << target << ".csv"; out_path << basename << "-" << target << ".csv";
@ -570,19 +594,15 @@ void init_stream(ofstream &out, std::string basename, std::string target)
out << "#" << target << endl; out << "#" << target << endl;
} }
command_result do_transform(DFCoord start, DFCoord end, string name, uint32_t phases, std::ostringstream &err) command_result do_transform(const DFCoord &start, const DFCoord &end,
const blueprint_options &options,
std::ostringstream &err)
{ {
ofstream dig, build, place, query; ofstream dig, build, place, query;
std::string basename = "blueprints/" + name; string basename = "blueprints/" + options.name;
#ifdef _WIN32
// normalize to forward slashes
std::replace(basename.begin(), basename.end(), '\\', '/');
#endif
size_t last_slash = basename.find_last_of("/"); size_t last_slash = basename.find_last_of("/");
std::string parent_path = basename.substr(0, last_slash); string parent_path = basename.substr(0, last_slash);
// create output directory if it doesn't already exist // create output directory if it doesn't already exist
std::error_code ec; std::error_code ec;
@ -592,179 +612,185 @@ command_result do_transform(DFCoord start, DFCoord end, string name, uint32_t ph
return CR_FAILURE; return CR_FAILURE;
} }
if (phases & QUERY) if (options.auto_phase || options.query)
{ {
//query = ofstream((name + "-query.csv").c_str(), ofstream::trunc);
init_stream(query, basename, "query"); init_stream(query, basename, "query");
} }
if (phases & PLACE) if (options.auto_phase || options.place)
{ {
//place = ofstream(name + "-place.csv", ofstream::trunc);
init_stream(place, basename, "place"); init_stream(place, basename, "place");
} }
if (phases & BUILD) if (options.auto_phase || options.build)
{ {
//build = ofstream(name + "-build.csv", ofstream::trunc);
init_stream(build, basename, "build"); init_stream(build, basename, "build");
} }
if (phases & DIG) if (options.auto_phase || options.dig)
{ {
//dig = ofstream(name + "-dig.csv", ofstream::trunc);
init_stream(dig, basename, "dig"); init_stream(dig, basename, "dig");
} }
if (start.x > end.x)
{
swap(start.x, end.x);
start.x++;
end.x++;
}
if (start.y > end.y)
{
swap(start.y, end.y);
start.y++;
end.y++;
}
if (start.z > end.z)
{
swap(start.z, end.z);
start.z++;
end.z++;
}
for (int32_t z = start.z; z < end.z; z++) const int32_t z_inc = start.z < end.z ? 1 : -1;
const string z_key = start.z < end.z ? "#<" : "#>";
for (int32_t z = start.z; z != end.z; z += z_inc)
{ {
for (int32_t y = start.y; y < end.y; y++) for (int32_t y = start.y; y < end.y; y++)
{ {
for (int32_t x = start.x; x < end.x; x++) for (int32_t x = start.x; x < end.x; x++)
{ {
df::building* b = DFHack::Buildings::findAtTile(DFCoord(x, y, z)); df::building* b = Buildings::findAtTile(DFCoord(x, y, z));
if (phases & QUERY) if (options.auto_phase || options.query)
query << get_tile_query(b) << ','; query << get_tile_query(b) << ',';
if (phases & PLACE) if (options.auto_phase || options.place)
place << get_tile_place(x, y, b) << ','; place << get_tile_place(x, y, b) << ',';
if (phases & BUILD) if (options.auto_phase || options.build)
build << get_tile_build(x, y, b) << ','; build << get_tile_build(x, y, b) << ',';
if (phases & DIG) if (options.auto_phase || options.dig)
dig << get_tile_dig(x, y, z) << ','; dig << get_tile_dig(x, y, z) << ',';
} }
if (phases & QUERY) if (options.auto_phase || options.query)
query << "#" << endl; query << "#" << endl;
if (phases & PLACE) if (options.auto_phase || options.place)
place << "#" << endl; place << "#" << endl;
if (phases & BUILD) if (options.auto_phase || options.build)
build << "#" << endl; build << "#" << endl;
if (phases & DIG) if (options.auto_phase || options.dig)
dig << "#" << endl; dig << "#" << endl;
} }
if (z < end.z - 1) if (z != end.z - z_inc)
{ {
if (phases & QUERY) if (options.auto_phase || options.query)
query << "#<" << endl; query << z_key << endl;
if (phases & PLACE) if (options.auto_phase || options.place)
place << "#<" << endl; place << z_key << endl;
if (phases & BUILD) if (options.auto_phase || options.build)
build << "#<" << endl; build << z_key << endl;
if (phases & DIG) if (options.auto_phase || options.dig)
dig << "#<" << endl; dig << z_key << endl;
} }
} }
if (phases & QUERY) if (options.auto_phase || options.query)
query.close(); query.close();
if (phases & PLACE) if (options.auto_phase || options.place)
place.close(); place.close();
if (phases & BUILD) if (options.auto_phase || options.build)
build.close(); build.close();
if (phases & DIG) if (options.auto_phase || options.dig)
dig.close(); dig.close();
return CR_OK; return CR_OK;
} }
bool cmd_option_exists(vector<string>& parameters, const string& option) static bool get_options(blueprint_options &opts,
const vector<string> &parameters)
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, parameters.size() + 2) ||
!Lua::PushModulePublic(
out, L, "plugins.blueprint", "parse_commandline"))
{ {
return find(parameters.begin(), parameters.end(), option) != parameters.end(); out.printerr("Failed to load blueprint Lua code\n");
return false;
}
Lua::Push(L, &opts);
for (const string &param : parameters)
Lua::Push(L, param);
if (!Lua::SafeCall(out, L, parameters.size() + 1, 0))
return false;
return true;
}
static void print_help()
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 1) ||
!Lua::PushModulePublic(out, L, "plugins.blueprint", "print_help") ||
!Lua::SafeCall(out, L, 0, 0))
{
out.printerr("Failed to load blueprint Lua code\n");
}
} }
command_result blueprint(color_ostream &out, vector<string> &parameters) command_result blueprint(color_ostream &out, vector<string> &parameters)
{ {
if (parameters.size() < 4 || parameters.size() > 8)
return help(out);
CoreSuspender suspend; CoreSuspender suspend;
if (parameters.size() >= 1 && parameters[0] == "gui")
{
std::ostringstream command;
command << "gui/blueprint";
for (const string &param : parameters)
{
command << " " << param;
}
string command_str = command.str();
out.print("launching %s\n", command_str.c_str());
Core::getInstance().setHotkeyCmd(command_str);
return CR_OK;
}
blueprint_options options;
if (!get_options(options, parameters) || options.help)
{
print_help();
return options.help ? CR_OK : CR_FAILURE;
}
if (!Maps::IsValid()) if (!Maps::IsValid())
{ {
out.printerr("Map is not available!\n"); out.printerr("Map is not available!\n");
return CR_FAILURE; return CR_FAILURE;
} }
int32_t x, y, z;
if (!Gui::getCursorCoords(x, y, z)) // start coordinates can come from either the commandline or the map cursor
DFCoord start(options.start);
if (start.x == -30000)
{ {
out.printerr("Can't get cursor coords! Make sure you have an active cursor in DF.\n"); if (!Gui::getCursorCoords(start))
{
out.printerr("Can't get cursor coords! Make sure you specify the"
" --cursor parameter or have an active cursor in DF.\n");
return CR_FAILURE; return CR_FAILURE;
} }
DFCoord start (x, y, z);
DFCoord end (x + stoi(parameters[0]), y + stoi(parameters[1]), z + stoi(parameters[2]));
uint32_t option = 0;
if (parameters.size() == 4)
{
option = DIG | BUILD | PLACE | QUERY;
} }
else if (!Maps::isValidTilePos(start))
{ {
if (cmd_option_exists(parameters, "dig")) out.printerr("Invalid start position: %d,%d,%d\n",
option |= DIG; start.x, start.y, start.z);
if (cmd_option_exists(parameters, "build")) return CR_FAILURE;
option |= BUILD;
if (cmd_option_exists(parameters, "place"))
option |= PLACE;
if (cmd_option_exists(parameters, "query"))
option |= QUERY;
}
std::ostringstream err;
DFHack::command_result result = do_transform(start, end, parameters[3], option, err);
if (result != CR_OK)
out.printerr("%s\n", err.str().c_str());
return result;
} }
static int create(lua_State *L, uint32_t options) { // end coords are one beyond the last processed coordinate. note that
df::coord start, end; // options.depth can be negative.
DFCoord end(start.x + options.width, start.y + options.height,
lua_settop(L, 3); start.z + options.depth);
Lua::CheckDFAssign(L, &start, 1);
if (!start.isValid()) // crop end coordinate to map bounds. we've already verified that start is
luaL_argerror(L, 1, "invalid start position"); // a valid coordinate, and width, height, and depth are non-zero, so our
Lua::CheckDFAssign(L, &end, 2); // final area is always going to be at least 1x1x1.
if (!end.isValid()) df::world::T_map &map = df::global::world->map;
luaL_argerror(L, 2, "invalid end position"); if (end.x > map.x_count)
string filename(lua_tostring(L, 3)); end.x = map.x_count;
if (end.y > map.y_count)
end.y = map.y_count;
if (end.z > map.z_count)
end.z = map.z_count;
if (end.z < -1)
end.z = -1;
std::ostringstream err; std::ostringstream err;
DFHack::command_result result = do_transform(start, end, filename, options, err); command_result result = do_transform(start, end, options, err);
if (result != CR_OK) if (result != CR_OK)
luaL_error(L, "%s", err.str().c_str()); out.printerr("%s\n", err.str().c_str());
lua_pushboolean(L, result); return result;
return 1;
}
static int dig(lua_State *L) {
return create(L, DIG);
}
static int build(lua_State *L) {
return create(L, BUILD);
}
static int place(lua_State *L) {
return create(L, PLACE);
}
static int query(lua_State *L) {
return create(L, QUERY);
} }
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(dig),
DFHACK_LUA_COMMAND(build),
DFHACK_LUA_COMMAND(place),
DFHACK_LUA_COMMAND(query),
DFHACK_LUA_END
};

@ -1,14 +1,165 @@
local _ENV = mkmodule('plugins.blueprint') local _ENV = mkmodule('plugins.blueprint')
--[[ local utils = require('utils')
Native functions: -- the info here is very basic and minimal, so hopefully we won't need to change
-- it when features are added and the full blueprint docs in Plugins.rst are
-- updated.
local help_text = [=[
* dig(start, end, name) blueprint
* build(start, end, name) =========
* place(start, end, name)
* query(start, end, name)
--]] Records the structure of a portion of your fortress in quickfort blueprints.
Usage:
blueprint <width> <height> [<depth>] [<name> [<phases>]] [<options>]
blueprint gui [<name> [<phases>]] [<options>]
Examples:
blueprint gui
Runs gui/blueprint, the interactive blueprint frontend, where all
configuration can be set visually and interactively.
blueprint 30 40 bedrooms
Generates blueprints for an area 30 tiles wide by 40 tiles tall, starting
from the active cursor on the current z-level. Output files are written to
the "blueprints" directory.
See the online DFHack documentation for more examples and details.
]=]
function print_help() print(help_text) end
local valid_phase_list = {
'dig',
'build',
'place',
'query',
}
valid_phases = utils.invert(valid_phase_list)
local function parse_cursor(opts, arg)
local _, _, x, y, z = arg:find('^(%d+),(%d+),(%d+)$')
if not x then
qerror(('invalid argument for --cursor option: "%s"; expected format' ..
' is "<x>,<y>,<z>", for example: "30,60,150"'):format(arg))
end
-- be careful not to replace struct members when called from C++, but also
-- create the table as needed when called from lua
if not opts.start then opts.start = {} end
opts.start.x = tonumber(x)
opts.start.y = tonumber(y)
opts.start.z = tonumber(z)
end
local function parse_positionals(opts, args, start_argidx)
local argidx = start_argidx or 1
-- set defaults
opts.name, opts.auto_phase = 'blueprint', true
local name = args[argidx]
if not name then return end
if name == '' then
qerror(('invalid basename: "%s"; must be a valid, non-empty pathname')
:format(args[argidx]))
end
argidx = argidx + 1
-- normalize paths to forward slashes
opts.name = name:gsub(package.config:sub(1,1), "/")
local auto_phase = true
local phase = args[argidx]
while phase do
if not valid_phases[phase] then
qerror(('unknown phase: "%s"; expected one of: %s'):
format(phase, table.concat(valid_phase_list, ', ')))
end
auto_phase = false
opts[phase] = true
argidx = argidx + 1
phase = args[argidx]
end
opts.auto_phase = auto_phase
end
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
return utils.processArgsGetopt(args, {
{'c', 'cursor', hasArg=true,
handler=function(optarg) parse_cursor(opts, optarg) end},
{'h', 'help', handler=function() opts.help = true end},
})
end
-- used by the gui/blueprint script
function parse_gui_commandline(opts, args)
local positionals = process_args(opts, args)
if opts.help then return end
parse_positionals(opts, positionals)
end
-- dimension must be a non-nil integer that is >= 1 (or at least non-zero if
-- negative_ok is true)
local function is_bad_dim(dim, negative_ok)
return not dim or
(not negative_ok and dim < 1 or dim == 0) or
dim ~= math.floor(dim)
end
function parse_commandline(opts, ...)
local positionals = process_args(opts, {...})
if opts.help then return end
local width, height = tonumber(positionals[1]), tonumber(positionals[2])
if is_bad_dim(width) or is_bad_dim(height) then
qerror(('invalid width or height: "%s" "%s"; width and height must' ..
' be positive integers'):format(positionals[1], positionals[2]))
end
opts.width, opts.height, opts.depth = width, height, 1
local depth = tonumber(positionals[3])
if depth then
if is_bad_dim(depth, true) then
qerror(('invalid depth: "%s"; must be a non-zero integer')
:format(positionals[3]))
end
opts.depth = depth
end
parse_positionals(opts, positionals, depth and 4 or 3)
end
-- compatibility with old exported API. we route the request back through
-- run_command so we have a unified path for parameter processing and invariant
-- checking.
local function do_blueprint(start_pos, end_pos, name, phase)
local width = math.abs(start_pos.x - end_pos.x) + 1
local height = math.abs(start_pos.y - end_pos.y) + 1
local depth = math.abs(start_pos.z - end_pos.z) + 1
if start_pos.z > end_pos.z then depth = -depth end
local x = math.min(start_pos.x, end_pos.x)
local y = math.min(start_pos.y, end_pos.y)
local z = start_pos.z
local cursor = ('--cursor=%d,%d,%d'):format(x, y, z)
return dfhack.run_command('blueprint',
tostring(width), tostring(height),
tostring(depth), tostring(name),
phase, cursor)
end
for phase in pairs(valid_phases) do
_ENV[phase] = function(s, e, n) do_blueprint(s, e, n, phase) end
end
return _ENV return _ENV

@ -0,0 +1,164 @@
local b = require('plugins.blueprint')
-- also covers code shared between parse_gui_commandline and parse_commandline
function test.parse_gui_commandline()
local opts = {}
b.parse_gui_commandline(opts, {})
expect.table_eq({auto_phase=true, name='blueprint'}, opts)
opts = {}
b.parse_gui_commandline(opts, {'help'})
expect.table_eq({help=true}, opts)
opts = {}
b.parse_gui_commandline(opts, {'--help'})
expect.table_eq({help=true}, opts)
opts = {}
b.parse_gui_commandline(opts, {'-h'})
expect.table_eq({help=true}, opts)
opts = {}
b.parse_gui_commandline(opts, {'--cursor=1,2,3'})
expect.table_eq({auto_phase=true, name='blueprint', start={x=1,y=2,z=3}},
opts)
opts = {}
expect.error_match('invalid argument',
function() b.parse_gui_commandline(
opts, {'--cursor=-1,2,3'}) end,
'negative coordinate')
opts = {}
expect.error_match('invalid argument',
function() b.parse_gui_commandline(
opts, {'--cursor=1,b,3'}) end,
'non-numeric coordinate')
opts = {}
b.parse_gui_commandline(opts, {'imaname'})
expect.table_eq({auto_phase=true, name='imaname'}, opts)
opts = {}
expect.error_match('invalid basename',
function() b.parse_gui_commandline(opts, {''}) end)
opts = {}
b.parse_gui_commandline(opts, {'imaname', 'dig', 'query'})
expect.table_eq({auto_phase=false, name='imaname', dig=true, query=true},
opts)
opts = {}
expect.error_match('unknown phase',
function() b.parse_gui_commandline(
opts, {'imaname', 'garbagephase'}) end)
end
function test.parse_commandline()
local opts = {}
b.parse_commandline(opts, '1', '2')
expect.table_eq({auto_phase=true,name='blueprint',width=1,height=2,depth=1},
opts)
opts = {}
b.parse_commandline(opts, '1', '2', '3')
expect.table_eq({auto_phase=true,name='blueprint',width=1,height=2,depth=3},
opts)
opts = {}
b.parse_commandline(opts, '1', '2', '-3')
expect.table_eq({auto_phase=true,name='blueprint',width=1,height=2,depth=-3},
opts)
opts = {}
b.parse_commandline(opts, '1', '2', 'imaname')
expect.table_eq({auto_phase=true,name='imaname',width=1,height=2,depth=1},
opts)
opts = {}
b.parse_commandline(opts, '1', '2', '10imaname')
expect.table_eq({auto_phase=true,name='10imaname',width=1,height=2,depth=1},
opts, 'invalid depth is considered a basename')
opts = {}
b.parse_commandline(opts, '1', '2', '-10imaname')
expect.table_eq({auto_phase=true,name='-10imaname',width=1,height=2,depth=1},
opts, 'invalid negative depth is considered a basename')
opts = {}
b.parse_commandline(opts, '1', '2', '3', 'imaname')
expect.table_eq({auto_phase=true,name='imaname',width=1,height=2,depth=3},
opts)
opts = {}
expect.error_match('invalid width or height',
function() b.parse_commandline(opts) end,
'missing width')
opts = {}
expect.error_match('invalid width or height',
function() b.parse_commandline(opts, '10') end,
'missing height')
opts = {}
expect.error_match('invalid width or height',
function() b.parse_commandline(opts, '0') end,
'zero height')
opts = {}
expect.error_match('invalid width or height',
function() b.parse_commandline(opts, 'hi') end,
'invalid width')
opts = {}
expect.error_match('invalid width or height',
function() b.parse_commandline(opts, '10', 'hi') end,
'invalid height')
opts = {}
expect.error_match('invalid depth',
function() b.parse_commandline(opts, '1', '2', '0') end,
'zero depth')
end
function test.do_blueprint_positive_dims()
local mock_run_command = mock.func()
mock.patch(dfhack, 'run_command', mock_run_command,
function()
local spos = {x=10, y=20, z=30}
local epos = {x=11, y=21, z=31}
b.query(spos, epos, 'imaname')
expect.eq(1, mock_run_command.call_count)
expect.table_eq({'blueprint', '2', '2', '2', 'imaname', 'query',
'--cursor=10,20,30'},
mock_run_command.call_args[1])
end)
end
function test.do_blueprint_negative_dims()
local mock_run_command = mock.func()
mock.patch(dfhack, 'run_command', mock_run_command,
function()
local spos = {x=11, y=21, z=31}
local epos = {x=10, y=20, z=30}
b.query(spos, epos, 'imaname')
expect.eq(1, mock_run_command.call_count)
expect.table_eq({'blueprint', '2', '2', '-2', 'imaname', 'query',
'--cursor=10,20,31'},
mock_run_command.call_args[1])
end)
end
function test.do_blueprint_ensure_cursor_is_at_upper_left()
local mock_run_command = mock.func()
mock.patch(dfhack, 'run_command', mock_run_command,
function()
local spos = {x=11, y=20, z=30}
local epos = {x=10, y=21, z=31}
b.query(spos, epos, 'imaname')
expect.eq(1, mock_run_command.call_count)
expect.table_eq({'blueprint', '2', '2', '2', 'imaname', 'query',
'--cursor=10,20,30'},
mock_run_command.call_args[1])
end)
end