From a7a5a48c7aab2ec2b2659781de4c10d549fed11b Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 4 May 2021 13:19:49 -0700 Subject: [PATCH 01/17] first stage of blueprint overhaul - make depth and name parameters optional - allow depth to be negative to indicate top-down instead of the previous hard-coded bottom-up - add --cursor for specifying start position (game cursor is not needed if this param is specified) --- plugins/blueprint.cpp | 349 +++++++++++++++++++++----------------- plugins/lua/blueprint.lua | 169 +++++++++++++++++- 2 files changed, 357 insertions(+), 161 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index f460cdb40..fc617af5c 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -8,9 +8,11 @@ #include #include -#include -#include +#include "Console.h" +#include "DataDefs.h" +#include "DataIdentity.h" #include "LuaTools.h" +#include "PluginManager.h" #include "TileTypes.h" #include "modules/Buildings.h" @@ -27,22 +29,63 @@ #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" +#include "df/world.h" using std::string; using std::endl; using std::vector; using std::ofstream; -using std::swap; -using std::find; using std::pair; using namespace DFHack; -using namespace df::enums; 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; -command_result blueprint(color_ostream &out, vector ¶meters); + // 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::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::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "depth", offsetof(blueprint_options, depth), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "name", offsetof(blueprint_options, name), df::identity_traits::get(), 0, 0 }, + { struct_field_info::PRIMITIVE, "auto_phase", offsetof(blueprint_options, auto_phase), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "dig", offsetof(blueprint_options, dig), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::allocator_fn, NULL, "blueprint_options", NULL, blueprint_options_fields); + +command_result blueprint(color_ostream &out, vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { @@ -55,25 +98,6 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) 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 get_building_size(df::building* b) { return pair(b->x2 - b->x1 + 1, b->y2 - b->y1 + 1); @@ -562,7 +586,7 @@ string get_tile_query(df::building* b) 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; out_path << basename << "-" << target << ".csv"; @@ -570,19 +594,15 @@ void init_stream(ofstream &out, std::string basename, std::string target) 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; - std::string basename = "blueprints/" + name; - -#ifdef _WIN32 - // normalize to forward slashes - std::replace(basename.begin(), basename.end(), '\\', '/'); -#endif - + string basename = "blueprints/" + options.name; 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 std::error_code ec; @@ -592,179 +612,200 @@ command_result do_transform(DFCoord start, DFCoord end, string name, uint32_t ph 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"); } - if (phases & PLACE) + if (options.auto_phase || options.place) { - //place = ofstream(name + "-place.csv", ofstream::trunc); 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"); } - if (phases & DIG) + if (options.auto_phase || options.dig) { - //dig = ofstream(name + "-dig.csv", ofstream::trunc); 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 x = start.x; x < end.x; x++) { - df::building* b = DFHack::Buildings::findAtTile(DFCoord(x, y, z)); - if (phases & QUERY) + df::building* b = Buildings::findAtTile(DFCoord(x, y, z)); + if (options.auto_phase || options.query) query << get_tile_query(b) << ','; - if (phases & PLACE) + if (options.auto_phase || options.place) place << get_tile_place(x, y, b) << ','; - if (phases & BUILD) + if (options.auto_phase || options.build) build << get_tile_build(x, y, b) << ','; - if (phases & DIG) + if (options.auto_phase || options.dig) dig << get_tile_dig(x, y, z) << ','; } - if (phases & QUERY) + if (options.auto_phase || options.query) query << "#" << endl; - if (phases & PLACE) + if (options.auto_phase || options.place) place << "#" << endl; - if (phases & BUILD) + if (options.auto_phase || options.build) build << "#" << endl; - if (phases & DIG) + if (options.auto_phase || options.dig) dig << "#" << endl; } - if (z < end.z - 1) + if (z != end.z - z_inc) { - if (phases & QUERY) - query << "#<" << endl; - if (phases & PLACE) - place << "#<" << endl; - if (phases & BUILD) - build << "#<" << endl; - if (phases & DIG) - dig << "#<" << endl; + if (options.auto_phase || options.query) + query << z_key << endl; + if (options.auto_phase || options.place) + place << z_key << endl; + if (options.auto_phase || options.build) + build << z_key << endl; + if (options.auto_phase || options.dig) + dig << z_key << endl; } } - if (phases & QUERY) + if (options.auto_phase || options.query) query.close(); - if (phases & PLACE) + if (options.auto_phase || options.place) place.close(); - if (phases & BUILD) + if (options.auto_phase || options.build) build.close(); - if (phases & DIG) + if (options.auto_phase || options.dig) dig.close(); + return CR_OK; } -bool cmd_option_exists(vector& parameters, const string& option) +static bool get_options(blueprint_options &opts, + const vector ¶meters) +{ + 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")) + { + out.printerr("Failed to load blueprint Lua code"); + return false; + } + + Lua::Push(L, &opts); + + for (const string ¶m : parameters) + Lua::Push(L, param); + + if (!Lua::SafeCall(out, L, parameters.size() + 1, 0)) + return false; + + return true; +} + +static bool do_gui(const vector ¶meters) +{ + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, parameters.size() + 1) || + !Lua::PushModulePublic(out, L, "plugins.blueprint", "do_gui")) + { + out.printerr("Failed to load blueprint Lua code"); + return false; + } + + for (const string ¶m : parameters) + Lua::Push(L, param); + + if (!Lua::SafeCall(out, L, parameters.size(), 0)) + return false; + + return true; +} + +static void print_help() { - return find(parameters.begin(), parameters.end(), option) != parameters.end(); + 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"); + } } command_result blueprint(color_ostream &out, vector ¶meters) { - if (parameters.size() < 4 || parameters.size() > 8) - return help(out); + if (parameters.size() >= 1 and parameters[0] == "gui") + { + return do_gui(parameters) ? CR_OK : CR_FAILURE; + } + + blueprint_options options; + if (!get_options(options, parameters) || options.help) + { + print_help(); + return options.help ? CR_OK : CR_FAILURE; + } + CoreSuspender suspend; if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); return CR_FAILURE; } - int32_t x, y, z; - if (!Gui::getCursorCoords(x, y, z)) - { - out.printerr("Can't get cursor coords! Make sure you have an active cursor in DF.\n"); - 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) + + // start coordinates can come from either the commandline or the map cursor + DFCoord start(options.start); + if (options.start.x == -30000) { - option = DIG | BUILD | PLACE | QUERY; + int32_t x, y, z; + if (!Gui::getCursorCoords(x, y, z)) + { + 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; + } + start.x = x; + start.y = y; + start.z = z; } - else + if (!Maps::isValidTilePos(start)) { - if (cmd_option_exists(parameters, "dig")) - option |= DIG; - if (cmd_option_exists(parameters, "build")) - option |= BUILD; - if (cmd_option_exists(parameters, "place")) - option |= PLACE; - if (cmd_option_exists(parameters, "query")) - option |= QUERY; + out.printerr("Invalid start position: %d,%d,%d\n", + start.x, start.y, start.z); + return CR_FAILURE; } - 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) { - df::coord start, end; - - lua_settop(L, 3); - Lua::CheckDFAssign(L, &start, 1); - if (!start.isValid()) - luaL_argerror(L, 1, "invalid start position"); - Lua::CheckDFAssign(L, &end, 2); - if (!end.isValid()) - luaL_argerror(L, 2, "invalid end position"); - string filename(lua_tostring(L, 3)); + // end coords are one beyond the last processed coordinate. note that + // options.depth can be negative. + DFCoord end(start.x + options.width, start.y + options.height, + start.z + options.depth); + + // crop end coordinate to map bounds. we've already verified that start is + // a valid coordinate, and width, height, and depth are non-zero, so our + // final are is always going to be at least 1x1x1. + df::world::T_map &map = df::global::world->map; + if (end.x > map.x_count) + 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; - DFHack::command_result result = do_transform(start, end, filename, options, err); + command_result result = do_transform(start, end, options, err); if (result != CR_OK) - luaL_error(L, "%s", err.str().c_str()); - lua_pushboolean(L, 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); + out.printerr("%s\n", err.str().c_str()); + return result; } - -DFHACK_PLUGIN_LUA_COMMANDS { - DFHACK_LUA_COMMAND(dig), - DFHACK_LUA_COMMAND(build), - DFHACK_LUA_COMMAND(place), - DFHACK_LUA_COMMAND(query), - DFHACK_LUA_END -}; diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 98f24f487..3944c062b 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -1,14 +1,169 @@ 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) - * build(start, end, name) - * place(start, end, name) - * query(start, end, name) +blueprint +========= ---]] +Records the structure of a portion of your fortress in quickfort blueprints. + +Usage: + + blueprint [] [ []] [] + blueprint gui [ []] [] + +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 is written to the + "bedrooms.csv" file in the "blueprints" directory. + +See the online DFHack documentation for more examples and details. +]=] + +valid_phases = utils.invert{ + 'dig', + 'build', + 'place', + 'query', +} + +function print_help() + print(help_text) +end + +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 ",,", 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 + +-- 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 + +local function parse_positionals(opts, args, start_argidx) + local argidx = start_argidx + + -- 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 valid_phases[phase] then + auto_phase = false + opts[phase] = true + end + 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 + +function parse_gui_commandline(opts, args) + local positionals = process_args(opts, args) + if opts.help then return end + parse_positionals(opts, positionals, 1) +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 and 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 + +function do_gui(command, ...) + local args = {...} + print('launching gui/blueprint') + dfhack.timeout(1, 'frames', + function() dfhack.run_script('gui/blueprint', + table.unpack(args)) end) +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', + width, height, depth, 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 From 265f17a53fec3f7e67f2b3b86c7c294c08827879 Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 4 May 2021 13:21:53 -0700 Subject: [PATCH 02/17] update blueprint docs --- docs/Lua API.rst | 10 ++++--- docs/Plugins.rst | 70 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 94bca09f1..56e20f9aa 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3756,15 +3756,19 @@ the plugin. See existing files in ``plugins/lua`` for examples. blueprint ========= -Native functions provided by the `blueprint` plugin: +Lua functions provided by the `blueprint` plugin to programmatically generate +blueprint files: * ``dig(start, end, name)`` * ``build(start, end, name)`` * ``place(start, end, name)`` * ``query(start, end, name)`` - ``start`` and ``end`` are tables containing positions (see - ``xyz2pos``). ``name`` is used as the basis for the filename. + ``start`` and ``end`` are tables containing positions (see ``xyz2pos``). + ``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: diff --git a/docs/Plugins.rst b/docs/Plugins.rst index 24dd90c43..a60d343ba 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -37,22 +37,70 @@ For more information, see `the full Stonesense README `. 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`. -Usage:: +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: + + blueprint [] [ []] [] + blueprint gui [ []] [] + +Examples: + +blueprint gui + Runs `gui/blueprint`, the interactive blueprint frontend, where all + configuration for a ``blueprint`` command can be set visually and + interactively. - blueprint [dig] [build] [place] [query] +blueprint gui bedrooms dig -\-cursor 108,100,150 + Starts ``gui/blueprint`` with some configuration options preset to custom + values. -Options (If only region and name are given, export all): +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 the + ``bedrooms.csv`` file in the ``blueprints`` directory. -:x,y,z: Size of map area to export -:name: Name of export files -:dig: Export dig commands to "-dig.csv" -:build: Export build commands to "-build.csv" -:place: Export stockpile commands to "-place.csv" -:query: Export query commands to "-query.csv" +Positional Parameters: + +:width: Width of the area (in tiles) to translate. +:height: Height of the area (in tiles) to translate. +:depth: Number of z-levels to translate. Positive numbers go *up* from the + cursor and negative numbers go *down*. Defaults to 1 if not specified, + indicating that the blueprint should only include the current z-level. +: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 ,,: + 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: From aba40b6c44f08175d3623fbb39b0cfdf017d8b6a Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 4 May 2021 13:24:45 -0700 Subject: [PATCH 03/17] update changelog --- docs/changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index b31054119..f871c4527 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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 ## 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 ## Lua From 7c7d96b5c1edbd59aea81262dd8e1abb3c858d21 Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 4 May 2021 14:25:14 -0700 Subject: [PATCH 04/17] output args being passed to gui/blueprint --- plugins/lua/blueprint.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 3944c062b..5b5661d19 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -138,7 +138,7 @@ end function do_gui(command, ...) local args = {...} - print('launching gui/blueprint') + print(('launching gui/blueprint %s'):format(table.concat(args, ' '))) dfhack.timeout(1, 'frames', function() dfhack.run_script('gui/blueprint', table.unpack(args)) end) From 09829551ee911845c60dd22de17618604c8526d0 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 5 May 2021 13:16:04 -0700 Subject: [PATCH 05/17] fix formatting --- plugins/blueprint.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index fc617af5c..3f9e2f301 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -70,17 +70,17 @@ struct blueprint_options { static struct_identity _identity; }; static const struct_field_info blueprint_options_fields[] = { - { struct_field_info::PRIMITIVE, "help", offsetof(blueprint_options, help), &df::identity_traits::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::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "depth", offsetof(blueprint_options, depth), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "name", offsetof(blueprint_options, name), df::identity_traits::get(), 0, 0 }, - { struct_field_info::PRIMITIVE, "auto_phase", offsetof(blueprint_options, auto_phase), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "dig", offsetof(blueprint_options, dig), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "help", offsetof(blueprint_options, help), &df::identity_traits::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::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "depth", offsetof(blueprint_options, depth), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "name", offsetof(blueprint_options, name), df::identity_traits::get(), 0, 0 }, + { struct_field_info::PRIMITIVE, "auto_phase", offsetof(blueprint_options, auto_phase), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "dig", offsetof(blueprint_options, dig), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "build", offsetof(blueprint_options, build), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "place", offsetof(blueprint_options, place), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "query", offsetof(blueprint_options, query), &df::identity_traits::identity, 0, 0 }, { struct_field_info::END } }; struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::allocator_fn, NULL, "blueprint_options", NULL, blueprint_options_fields); From a949065a7e13edb92c1909a184933fed23a9cb35 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 5 May 2021 13:18:43 -0700 Subject: [PATCH 06/17] fix typo in comment --- plugins/blueprint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 3f9e2f301..24d8df26c 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -792,7 +792,7 @@ command_result blueprint(color_ostream &out, vector ¶meters) // crop end coordinate to map bounds. we've already verified that start is // a valid coordinate, and width, height, and depth are non-zero, so our - // final are is always going to be at least 1x1x1. + // final area is always going to be at least 1x1x1. df::world::T_map &map = df::global::world->map; if (end.x > map.x_count) end.x = map.x_count; From cc489db0846b6e16daaba21be0151fd10f43a4ed Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 5 May 2021 13:22:40 -0700 Subject: [PATCH 07/17] add comment to parse_gui_commandline function --- plugins/lua/blueprint.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 5b5661d19..60319b54c 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -107,6 +107,7 @@ local function process_args(opts, args) }) 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 From 816cd5cf27079a45afdccdbb690f81d55a879784 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 7 May 2021 14:07:37 -0700 Subject: [PATCH 08/17] add unit tests --- plugins/lua/blueprint.lua | 37 ++++--- test/plugins/blueprint.lua | 202 +++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 test/plugins/blueprint.lua diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 60319b54c..5dacf40bc 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -5,8 +5,7 @@ local utils = require('utils') -- 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 = -[=[ +local help_text = [=[ blueprint ========= @@ -32,6 +31,8 @@ blueprint 30 40 bedrooms See the online DFHack documentation for more examples and details. ]=] +function print_help() print(help_text) end + valid_phases = utils.invert{ 'dig', 'build', @@ -39,10 +40,6 @@ valid_phases = utils.invert{ 'query', } -function print_help() - print(help_text) -end - local function parse_cursor(opts, arg) local _, _, x, y, z = arg:find('^(%d+),(%d+),(%d+)$') if not x then @@ -57,16 +54,8 @@ local function parse_cursor(opts, arg) opts.start.z = tonumber(z) 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 - local function parse_positionals(opts, args, start_argidx) - local argidx = start_argidx + local argidx = start_argidx or 1 -- set defaults opts.name, opts.auto_phase = 'blueprint', true @@ -111,7 +100,15 @@ end function parse_gui_commandline(opts, args) local positionals = process_args(opts, args) if opts.help then return end - parse_positionals(opts, positionals, 1) + 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, ...) @@ -120,7 +117,7 @@ function parse_commandline(opts, ...) local width, height = tonumber(positionals[1]), tonumber(positionals[2]) if is_bad_dim(width) or is_bad_dim(height) then - qerror(('invalid width and height: "%s" "%s"; width and height must' .. + 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 @@ -160,8 +157,10 @@ local function do_blueprint(start_pos, end_pos, name, phase) local cursor = ('--cursor=%d,%d,%d'):format(x, y, z) - return dfhack.run_command{'blueprint', - width, height, depth, name, phase, cursor} + 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 diff --git a/test/plugins/blueprint.lua b/test/plugins/blueprint.lua new file mode 100644 index 000000000..793fc0949 --- /dev/null +++ b/test/plugins/blueprint.lua @@ -0,0 +1,202 @@ +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 = {} + b.parse_gui_commandline(opts, {'imaname', 'garbagephase'}) + expect.table_eq({auto_phase=true, name='imaname'}, opts) +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_gui_no_arg() + local mock_print, mock_timeout, mock_run_script = + mock.func(), mock.func(), mock.func() + mock.patch( + { + {b, 'print', mock_print}, + {dfhack, 'timeout', mock_timeout}, + {dfhack, 'run_script', mock_run_script}, + }, + function() + b.do_gui('gui') + expect.eq(1, mock_print.call_count) + expect.eq(1, mock_timeout.call_count) + mock_timeout.call_args[1][3]() + expect.eq(1, mock_run_script.call_count) + expect.table_eq({'gui/blueprint'}, mock_run_script.call_args[1]) + end) +end + +function test.do_gui_with_args() + local mock_print, mock_timeout, mock_run_script = + mock.func(), mock.func(), mock.func() + mock.patch( + { + {b, 'print', mock_print}, + {dfhack, 'timeout', mock_timeout}, + {dfhack, 'run_script', mock_run_script}, + }, + function() + b.do_gui('gui', 'arg1', 'arg2', 'arg3') + expect.eq(1, mock_print.call_count) + expect.eq(1, mock_timeout.call_count) + mock_timeout.call_args[1][3]() + expect.eq(1, mock_run_script.call_count) + expect.table_eq({'gui/blueprint', 'arg1', 'arg2', 'arg3'}, + mock_run_script.call_args[1]) + end) +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 From bee0c15ba0fe7f491ea33ad98fc5a0f523e8b2f9 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 8 May 2021 07:34:29 -0700 Subject: [PATCH 09/17] yeah, this isn't lua (though "and" instead of "&&" seems to work in gcc!) --- plugins/blueprint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 24d8df26c..38a03bff7 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -744,7 +744,7 @@ static void print_help() command_result blueprint(color_ostream &out, vector ¶meters) { - if (parameters.size() >= 1 and parameters[0] == "gui") + if (parameters.size() >= 1 && parameters[0] == "gui") { return do_gui(parameters) ? CR_OK : CR_FAILURE; } From b0dba22e72097aa10fbb149d3cb061d1d71a6660 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 14 May 2021 22:51:42 -0700 Subject: [PATCH 10/17] standardize formatting in docs --- docs/Plugins.rst | 52 +++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/docs/Plugins.rst b/docs/Plugins.rst index a60d343ba..01b13eebf 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -46,38 +46,40 @@ 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 [] [ []] [] blueprint gui [ []] [] Examples: -blueprint gui +``blueprint gui`` Runs `gui/blueprint`, the interactive blueprint frontend, where all configuration for a ``blueprint`` command can be set visually and interactively. -blueprint gui bedrooms dig -\-cursor 108,100,150 - Starts ``gui/blueprint`` with some configuration options preset to custom - values. - -blueprint 30 40 bedrooms +``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 the - ``bedrooms.csv`` file in the ``blueprints`` directory. + 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. Positional Parameters: -:width: Width of the area (in tiles) to translate. -:height: Height of the area (in tiles) to translate. -:depth: Number of z-levels to translate. Positive numbers go *up* from the - cursor and negative numbers go *down*. Defaults to 1 if not specified, - indicating that the blueprint should only include the current z-level. -: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. +:``width``: Width of the area (in tiles) to translate. +:``height``: Height of the area (in tiles) to translate. +:``depth``: Number of z-levels to translate. Positive numbers go *up* from the + cursor and negative numbers go *down*. Defaults to 1 if not specified, + indicating that the blueprint should only include the current z-level. +:``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: @@ -85,21 +87,21 @@ 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. +:``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: -:-c, -\-cursor ,,: +:``-c``, ``--cursor ,,``: 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: +:``-h``, ``--help``: Show command help text. .. _remotefortressreader: From 5a149f44e0c1787213968c414b5748e4c1c415e5 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 14 May 2021 23:02:04 -0700 Subject: [PATCH 11/17] use setHotkeyCmd instead of dfhack.run_script --- plugins/blueprint.cpp | 38 +++++++++++++------------------------- plugins/lua/blueprint.lua | 8 -------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 38a03bff7..723e3f1a2 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -691,7 +691,7 @@ static bool get_options(blueprint_options &opts, !Lua::PushModulePublic( out, L, "plugins.blueprint", "parse_commandline")) { - out.printerr("Failed to load blueprint Lua code"); + out.printerr("Failed to load blueprint Lua code\n"); return false; } @@ -706,28 +706,6 @@ static bool get_options(blueprint_options &opts, return true; } -static bool do_gui(const vector ¶meters) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, parameters.size() + 1) || - !Lua::PushModulePublic(out, L, "plugins.blueprint", "do_gui")) - { - out.printerr("Failed to load blueprint Lua code"); - return false; - } - - for (const string ¶m : parameters) - Lua::Push(L, param); - - if (!Lua::SafeCall(out, L, parameters.size(), 0)) - return false; - - return true; -} - static void print_help() { auto L = Lua::Core::State; @@ -738,7 +716,7 @@ static void print_help() !Lua::PushModulePublic(out, L, "plugins.blueprint", "print_help") || !Lua::SafeCall(out, L, 0, 0)) { - out.printerr("Failed to load blueprint Lua code"); + out.printerr("Failed to load blueprint Lua code\n"); } } @@ -746,7 +724,17 @@ command_result blueprint(color_ostream &out, vector ¶meters) { if (parameters.size() >= 1 && parameters[0] == "gui") { - return do_gui(parameters) ? CR_OK : CR_FAILURE; + std::ostringstream command; + command << "gui/blueprint"; + for (const string ¶m : 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; diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 5dacf40bc..7580f17e3 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -134,14 +134,6 @@ function parse_commandline(opts, ...) parse_positionals(opts, positionals, depth and 4 or 3) end -function do_gui(command, ...) - local args = {...} - print(('launching gui/blueprint %s'):format(table.concat(args, ' '))) - dfhack.timeout(1, 'frames', - function() dfhack.run_script('gui/blueprint', - table.unpack(args)) end) -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. From 256ca13668dca7edd7b58e4c538ed78b1796193e Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 14 May 2021 23:07:44 -0700 Subject: [PATCH 12/17] make short help text more generic --- plugins/lua/blueprint.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 7580f17e3..1682b9241 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -25,8 +25,8 @@ blueprint gui 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 the - "bedrooms.csv" file in the "blueprints" directory. + 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. ]=] From 95d97b929ea86a0f0d3cbcc8a8222a73ee793281 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 15 May 2021 05:59:37 -0700 Subject: [PATCH 13/17] remove unit tests for removed function --- test/plugins/blueprint.lua | 39 -------------------------------------- 1 file changed, 39 deletions(-) diff --git a/test/plugins/blueprint.lua b/test/plugins/blueprint.lua index 793fc0949..d2a554847 100644 --- a/test/plugins/blueprint.lua +++ b/test/plugins/blueprint.lua @@ -120,45 +120,6 @@ function test.parse_commandline() 'zero depth') end -function test.do_gui_no_arg() - local mock_print, mock_timeout, mock_run_script = - mock.func(), mock.func(), mock.func() - mock.patch( - { - {b, 'print', mock_print}, - {dfhack, 'timeout', mock_timeout}, - {dfhack, 'run_script', mock_run_script}, - }, - function() - b.do_gui('gui') - expect.eq(1, mock_print.call_count) - expect.eq(1, mock_timeout.call_count) - mock_timeout.call_args[1][3]() - expect.eq(1, mock_run_script.call_count) - expect.table_eq({'gui/blueprint'}, mock_run_script.call_args[1]) - end) -end - -function test.do_gui_with_args() - local mock_print, mock_timeout, mock_run_script = - mock.func(), mock.func(), mock.func() - mock.patch( - { - {b, 'print', mock_print}, - {dfhack, 'timeout', mock_timeout}, - {dfhack, 'run_script', mock_run_script}, - }, - function() - b.do_gui('gui', 'arg1', 'arg2', 'arg3') - expect.eq(1, mock_print.call_count) - expect.eq(1, mock_timeout.call_count) - mock_timeout.call_args[1][3]() - expect.eq(1, mock_run_script.call_count) - expect.table_eq({'gui/blueprint', 'arg1', 'arg2', 'arg3'}, - mock_run_script.call_args[1]) - end) -end - function test.do_blueprint_positive_dims() local mock_run_command = mock.func() mock.patch(dfhack, 'run_command', mock_run_command, From aff5c9bf35448a1a6ba93c8ade2e4d083b4af0d7 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 15 May 2021 12:05:00 -0700 Subject: [PATCH 14/17] add getCursorCoords overload for df::coord and factor out active cursor detection --- library/include/modules/Gui.h | 1 + library/modules/Gui.cpp | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 819ad1558..455032fea 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -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 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 getDesignationCoords (int32_t &x, int32_t &y, int32_t &z); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3ba851438..04225e111 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -648,13 +648,18 @@ bool Gui::item_details_hotkey(df::viewscreen *top) return !!strict_virtual_cast(top); } +static bool has_cursor() +{ + return df::global::cursor && df::global::cursor->x != -30000; +} + bool Gui::cursor_hotkey(df::viewscreen *top) { if (!dwarfmode_hotkey(top)) return false; // Also require the cursor. - if (!df::global::cursor || df::global::cursor->x == -30000) + if (!has_cursor()) return false; return true; @@ -1788,7 +1793,15 @@ bool Gui::getCursorCoords (int32_t &x, int32_t &y, int32_t &z) x = df::global::cursor->x; y = df::global::cursor->y; 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? From 07e29bdc3ac75def23dbc191cd2c6b80578531a6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 15 May 2021 12:05:32 -0700 Subject: [PATCH 15/17] use new getCursorCoord call and move suspender up --- plugins/blueprint.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 723e3f1a2..fc8ac5fa2 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -722,6 +722,8 @@ static void print_help() command_result blueprint(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + if (parameters.size() >= 1 && parameters[0] == "gui") { std::ostringstream command; @@ -744,7 +746,6 @@ command_result blueprint(color_ostream &out, vector ¶meters) return options.help ? CR_OK : CR_FAILURE; } - CoreSuspender suspend; if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); @@ -755,16 +756,12 @@ command_result blueprint(color_ostream &out, vector ¶meters) DFCoord start(options.start); if (options.start.x == -30000) { - int32_t x, y, z; - if (!Gui::getCursorCoords(x, y, z)) + if (!Gui::getCursorCoords(options.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; } - start.x = x; - start.y = y; - start.z = z; } if (!Maps::isValidTilePos(start)) { From 0409b7bca596bd15cfb495eeb197b8bdd9cfc607 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 21 May 2021 06:32:41 -0700 Subject: [PATCH 16/17] modify start, not options.start when getting coord --- plugins/blueprint.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index fc8ac5fa2..2158f4086 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -754,9 +754,9 @@ command_result blueprint(color_ostream &out, vector ¶meters) // start coordinates can come from either the commandline or the map cursor DFCoord start(options.start); - if (options.start.x == -30000) + if (start.x == -30000) { - if (!Gui::getCursorCoords(options.start)) + 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"); From 1aaed3a6ed2add468d6cfcb9cf80acb6bbc748bf Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 21 May 2021 06:33:33 -0700 Subject: [PATCH 17/17] error on invalid phase names --- plugins/lua/blueprint.lua | 12 ++++++++---- test/plugins/blueprint.lua | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 1682b9241..e9b7ec667 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -33,13 +33,15 @@ See the online DFHack documentation for more examples and details. function print_help() print(help_text) end -valid_phases = utils.invert{ +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 @@ -73,10 +75,12 @@ local function parse_positionals(opts, args, start_argidx) local auto_phase = true local phase = args[argidx] while phase do - if valid_phases[phase] then - auto_phase = false - opts[phase] = true + 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 diff --git a/test/plugins/blueprint.lua b/test/plugins/blueprint.lua index d2a554847..e579a668f 100644 --- a/test/plugins/blueprint.lua +++ b/test/plugins/blueprint.lua @@ -49,8 +49,9 @@ function test.parse_gui_commandline() opts) opts = {} - b.parse_gui_commandline(opts, {'imaname', 'garbagephase'}) - expect.table_eq({auto_phase=true, name='imaname'}, opts) + expect.error_match('unknown phase', + function() b.parse_gui_commandline( + opts, {'imaname', 'garbagephase'}) end) end function test.parse_commandline()