diff --git a/docs/plugins/burrow.rst b/docs/plugins/burrow.rst index b1c966ea2..24f1b72d4 100644 --- a/docs/plugins/burrow.rst +++ b/docs/plugins/burrow.rst @@ -1,46 +1,36 @@ -burrows -======= +burrow +====== .. dfhack-tool:: - :summary: Auto-expand burrows as you dig. - :tags: unavailable - :no-command: + :summary: Quickly adjust burrow tiles and units. + :tags: fort auto design productivity units -.. dfhack-command:: burrow - :summary: Quickly add units/tiles to burrows. +This tool has two modes. When enabled, it monitors burrows with names that end +in ``+``. If a wall at the edge of such a burrow is dug out, the burrow will be +automatically extended to include the newly-revealed adjacent walls. -When a wall inside a burrow with a name ending in ``+`` is dug out, the burrow -will be extended to newly-revealed adjacent walls. +When run as a command, it can quickly adjust which tiles and/or units are +associated with the burrow. Usage ----- -``burrow enable auto-grow`` - When a wall inside a burrow with a name ending in '+' is dug out, the burrow - will be extended to newly-revealed adjacent walls. This final '+' may be - omitted in burrow name args of other ``burrow`` commands. Note that digging - 1-wide corridors with the miner inside the burrow is SLOW. -``burrow disable auto-grow`` - Disables auto-grow processing. -``burrow clear-unit [ ...]`` - Remove all units from the named burrows. -``burrow clear-tiles [ ...]`` - Remove all tiles from the named burrows. -``burrow set-units target-burrow [ ...]`` - Clear all units from the target burrow, then add units from the named source - burrows. -``burrow add-units target-burrow [ ...]`` - Add units from the source burrows to the target. -``burrow remove-units target-burrow [ ...]`` - Remove units in source burrows from the target. -``burrow set-tiles target-burrow [ ...]`` - Clear target burrow tiles and add tiles from the names source burrows. -``burrow add-tiles target-burrow [ ...]`` - Add tiles from the source burrows to the target. -``burrow remove-tiles target-burrow [ ...]`` - Remove tiles in source burrows from the target. - -In place of a source burrow, you can use one of the following keywords: +:: + + enable burrow + burrow tiles|units clear [ ...] [] + burrow tiles|units set|add|remove [...] [] + burrow tiles box-add|box-remove [] [] [] + burrow tiles flood-add|flood-remove [] + +The burrows can be referenced by name or by the internal numeric burrow ID. If +referenced by name, the first burrow that matches the name (case sensitive) +will be targeted. If a burrow name ends in ``+`` (to indicate that it should be +auto-expanded), the final ``+`` does not need to be specified on the +commandline. + +For ``set``, ``add``, or ``remove`` commands, instead of a burrow, you can +specify one of the following all-caps keywords: - ``ABOVE_GROUND`` - ``SUBTERRANEAN`` @@ -51,4 +41,75 @@ In place of a source burrow, you can use one of the following keywords: - ``HIDDEN`` - ``REVEALED`` -to add tiles with the given properties. +to add or remove tiles with the corresponding properties. + +Flood fill selects tiles spreading out from a starting tile if they: + +- match the inside/outside and hidden/revealed properties of the starting tile +- match the walkability group of the starting tile OR (if the starting tile is + walkable) is adjacent to a tile with the same walkability group as the + starting tile + +When flood adding, the flood fill will also stop at any tiles that have already +been added to the burrow. Similarly for flood removing, the flood will also +stop at tiles that are not in the burrow. + +Examples +-------- + +``enable burrow`` + Start monitoring burrows that have names ending in '+' and automatically + expand them when walls that border the burrows are dug out. +``burrow tiles clear Safety`` + Remove all tiles from the burrow named ``Safety`` (in preparation for + adding new tiles elsewhere, presumably). +``burrow units clear Farmhouse Workshops`` + Remove all units from the burrows named ``Farmhouse`` and ``Workshops``. +``multicmd burrow tiles set Inside INSIDE; burrow tiles remove Inside HIDDEN`` + Reset the burrow named ``Inside`` to include all the currently revealed, + interior tiles. +``burrow units set "Core Fort" Peasants Skilled`` + Clear all units from the burrow named ``Core Fort``, then add units + currently assigned to the ``Peasants`` and ``Skilled`` burrows. +``burrow tiles box-add Safety 0,0,0`` + Add all tiles to the burrow named ``Safety`` that are within the volume of + the box starting at coordinate 0, 0, 0 (the upper left corner of the bottom + level) and ending at the current location of the keyboard cursor. +``burrow tiles flood-add Safety --cur-zlevel`` + Flood-add the tiles on the current z-level with the same properties as the + tile under the keyboard cursor to the burrow named ``Safety``. + +Options +------- + +``-c``, ``--cursor `` + Indicate the starting position of the box or flood fill. If not specified, + the position of the keyboard cursor is used. +``-d``, ``--dry-run`` + Report what would be done, but don't actually change anything. +``-z``, ``--cur-zlevel`` + Restricts the operation to the currently visible z-level. + +Note +---- + +If you are auto-expanding a burrow (whose name ends in a ``+``) and the miner +who is digging to expand the burrow is assigned to that burrow, then 1-wide +corridors that expand the burrow will have very slow progress. This is because +the burrow is expanded to include the next dig job only after the miner has +chosen a next tile to dig, which may be far away. 2-wide cooridors are much +more efficient when expanding a burrow since the "next" tile to dig will still +be nearby. + +Overlay +------- + +When painting burrows in the vanilla UI, a few extra mouse operations are +supported. If you box select across multiple z-levels, you will be able to +select the entire volume instead of just the selected area on the z-level that +you are currently looking at. + +In addition, double-clicking will start a flood fill from the target tile. + +The box and flood fill actions respect the UI setting for whether the burrow is +being added to or erased. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 114386ad3..efeae25f9 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -90,7 +90,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) dfhack_plugin(autoslab autoslab.cpp) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) - #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) + dfhack_plugin(burrow burrow.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) add_subdirectory(buildingplan) dfhack_plugin(changeitem changeitem.cpp) diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index c39253488..59da9b929 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -1,3 +1,496 @@ +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "TileTypes.h" + +#include "modules/Burrows.h" +#include "modules/Persistence.h" +#include "modules/World.h" + +#include "df/burrow.h" +#include "df/tile_designation.h" +#include "df/world.h" + +using std::vector; +using std::string; +using namespace DFHack; + +DFHACK_PLUGIN("burrow"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(window_z); +REQUIRE_GLOBAL(world); + +// logging levels can be dynamically controlled with the `debugfilter` command. +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(burrow, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(burrow, cycle, DebugCategory::LINFO); +} + +static const auto CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; +enum ConfigValues { + CONFIG_IS_ENABLED = 0, +}; +static int get_config_val(int index) { + if (!config.isValid()) + return -1; + return config.ival(index); +} +static bool get_config_bool(int index) { + return get_config_val(index) == 1; +} +static void set_config_val(int index, int value) { + if (config.isValid()) + config.ival(index) = value; +} +static void set_config_bool(int index, bool value) { + set_config_val(index, value ? 1 : 0); +} + +static const int32_t CYCLE_TICKS = 100; +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static command_result do_command(color_ostream &out, vector ¶meters); +static void do_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(status, out).print("initializing %s\n", plugin_name); + commands.push_back( + PluginCommand("burrow", + "Quickly adjust burrow tiles and units.", + do_command)); + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status, out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + if (enable) + do_cycle(out); + } + else { + DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) { + DEBUG(status, out).print("shutting down %s\n", plugin_name); + return CR_OK; +} + +DFhackCExport command_result plugin_load_data(color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(status, out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + } + + // we have to copy our enabled flag into the global plugin variable, but + // all the other state we can directly read/modify from the persistent + // data structure. + is_enabled = get_config_bool(CONFIG_IS_ENABLED); + DEBUG(status, out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(status, out).print("world unloaded; disabling %s\n", plugin_name); + is_enabled = false; + } + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + CoreSuspender suspend; + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + do_cycle(out); + return CR_OK; +} + +static bool call_burrow_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(status).print("calling %s lua function: '%s'\n", plugin_name, 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.burrow", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!call_burrow_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic +// + +static void do_cycle(color_ostream &out) +{ + // mark that we have recently run + cycle_timestamp = world->frame_counter; + + // TODO +} + +///////////////////////////////////////////////////// +// Lua API +// + +static void get_bool_field(lua_State *L, int idx, const char *name, bool *dest) { + lua_getfield(L, idx, name); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return; + } + *dest = lua_toboolean(L, -1); + lua_pop(L, 1); +} + +static void get_opts(lua_State *L, int idx, bool &dry_run, bool &zlevel) { + if (lua_gettop(L) < idx) + return; + get_bool_field(L, idx, "dry_run", &dry_run); + get_bool_field(L, idx, "zlevel", &zlevel); +} + +static bool get_int_field(lua_State *L, int idx, const char *name, int16_t *dest) { + lua_getfield(L, idx, name); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return false; + } + *dest = lua_tointeger(L, -1); + lua_pop(L, 1); + return true; +} + +static bool get_bounds(lua_State *L, int idx, df::coord &pos1, df::coord &pos2) { + return get_int_field(L, idx, "x1", &pos1.x) && + get_int_field(L, idx, "y1", &pos1.y) && + get_int_field(L, idx, "z1", &pos1.z) && + get_int_field(L, idx, "x2", &pos2.x) && + get_int_field(L, idx, "y2", &pos2.y) && + get_int_field(L, idx, "z2", &pos2.z); +} + +static df::burrow* get_burrow(lua_State *L, int idx) { + df::burrow *burrow = NULL; + if (lua_isuserdata(L, idx)) + burrow = Lua::GetDFObject(L, idx); + else if (lua_isstring(L, idx)) + burrow = Burrows::findByName(luaL_checkstring(L, idx)); + else if (lua_isinteger(L, idx)) + burrow = df::burrow::find(luaL_checkinteger(L, idx)); + return burrow; +} + +static int burrow_tiles_clear(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_clear\n"); + + int32_t count = 0; + lua_pushnil(L); // first key + while (lua_next(L, 1)) { + df::burrow * burrow = get_burrow(L, -1); + if (burrow) { + count += burrow->block_x.size(); + Burrows::clearTiles(burrow); + } + lua_pop(L, 1); // remove value, leave key + } + + Lua::Push(L, count); + return 1; +} + +static int burrow_tiles_set(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_set\n"); + // TODO + return 0; +} + +static int burrow_tiles_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_add\n"); + // TODO + return 0; +} + +static int burrow_tiles_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_remove\n"); + // TODO + return 0; +} + +static int box_fill(lua_State *L, bool enable) { + df::coord pos_start, pos_end; + bool dry_run = false, zlevel = false; + + df::burrow *burrow = get_burrow(L, 1); + if (!burrow) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return 0; + } + + if (!get_bounds(L, 2, pos_start, pos_end)) { + luaL_argerror(L, 2, "invalid box bounds"); + return 0; + } + get_opts(L, 3, dry_run, zlevel); + + if (zlevel) { + pos_start.z = *window_z; + pos_end.z = *window_z; + } + + int32_t count = 0; + for (int32_t z = pos_start.z; z <= pos_end.z; ++z) { + for (int32_t y = pos_start.y; y <= pos_end.y; ++y) { + for (int32_t x = pos_start.x; x <= pos_end.x; ++x) { + df::coord pos(x, y, z); + if (enable != Burrows::isAssignedTile(burrow, pos)) + ++count; + if (!dry_run) + Burrows::setAssignedTile(burrow, pos, enable); + } + } + } + + Lua::Push(L, count); + return 1; +} + +static int burrow_tiles_box_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_box_add\n"); + return box_fill(L, true); +} + +static int burrow_tiles_box_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_box_remove\n"); + return box_fill(L, false); +} + +static int flood_fill(lua_State *L, bool enable) { + df::coord start_pos; + bool dry_run = false, zlevel = false; + + df::burrow *burrow = get_burrow(L, 1); + if (!burrow) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return 0; + } + + Lua::CheckDFAssign(L, &start_pos, 2); + get_opts(L, 3, dry_run, zlevel); + + // record properties to match + df::tile_designation *start_des = Maps::getTileDesignation(start_pos); + if (!start_des) { + luaL_argerror(L, 2, "invalid starting coordinates"); + return 0; + } + uint16_t start_walk = Maps::getWalkableGroup(start_pos); + + int32_t count = 0; + + std::stack flood; + flood.emplace(start_pos); + + while(!flood.empty()) { + const df::coord pos = flood.top(); + flood.pop(); + + df::tile_designation *des = Maps::getTileDesignation(pos); + if(!des || + des->bits.outside != start_des->bits.outside || + des->bits.hidden != start_des->bits.hidden) + { + continue; + } + + if (!start_walk && Maps::getWalkableGroup(pos)) + continue; + + if (pos != start_pos && enable == Burrows::isAssignedTile(burrow, pos)) + continue; + + ++count; + if (!dry_run) + Burrows::setAssignedTile(burrow, pos, enable); + + // only go one tile outside of a walkability group + if (start_walk && start_walk != Maps::getWalkableGroup(pos)) + continue; + + flood.emplace(pos.x-1, pos.y-1, pos.z); + flood.emplace(pos.x, pos.y-1, pos.z); + flood.emplace(pos.x+1, pos.y-1, pos.z); + flood.emplace(pos.x-1, pos.y, pos.z); + flood.emplace(pos.x+1, pos.y, pos.z); + flood.emplace(pos.x-1, pos.y+1, pos.z); + flood.emplace(pos.x, pos.y+1, pos.z); + flood.emplace(pos.x+1, pos.y+1, pos.z); + + if (!zlevel) { + df::coord pos_above(pos); + ++pos_above.z; + df::tiletype *tt = Maps::getTileType(pos); + df::tiletype *tt_above = Maps::getTileType(pos_above); + if (tt_above && LowPassable(*tt_above)) + flood.emplace(pos_above); + if (tt && LowPassable(*tt)) + flood.emplace(pos.x, pos.y, pos.z-1); + } + } + + Lua::Push(L, count); + return 1; +} + +static int burrow_tiles_flood_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_flood_add\n"); + return flood_fill(L, true); +} + +static int burrow_tiles_flood_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_tiles_flood_remove\n"); + return flood_fill(L, false); +} + +static int burrow_units_clear(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_clear\n"); + // TODO + return 0; +} + +static int burrow_units_set(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_set\n"); + // TODO + return 0; +} + +static int burrow_units_add(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_add\n"); + // TODO + return 0; +} + +static int burrow_units_remove(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(status,*out).print("entering burrow_units_remove\n"); + // TODO + return 0; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(burrow_tiles_clear), + DFHACK_LUA_COMMAND(burrow_tiles_set), + DFHACK_LUA_COMMAND(burrow_tiles_add), + DFHACK_LUA_COMMAND(burrow_tiles_remove), + DFHACK_LUA_COMMAND(burrow_tiles_box_add), + DFHACK_LUA_COMMAND(burrow_tiles_box_remove), + DFHACK_LUA_COMMAND(burrow_tiles_flood_add), + DFHACK_LUA_COMMAND(burrow_tiles_flood_remove), + DFHACK_LUA_COMMAND(burrow_units_clear), + DFHACK_LUA_COMMAND(burrow_units_set), + DFHACK_LUA_COMMAND(burrow_units_add), + DFHACK_LUA_COMMAND(burrow_units_remove), + DFHACK_LUA_END +}; + + + + + + + + + + +/* + + #include "Core.h" #include "Console.h" #include "Export.h" @@ -13,7 +506,6 @@ #include "modules/MapCache.h" #include "modules/World.h" #include "modules/Units.h" -#include "modules/Burrows.h" #include "TileTypes.h" #include "DataDefs.h" @@ -37,26 +529,16 @@ using namespace DFHack; using namespace df::enums; using namespace dfproto; -DFHACK_PLUGIN("burrows"); +DFHACK_PLUGIN("burrow"); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(gamemode); -/* - * Initialization. - */ - -static command_result burrow(color_ostream &out, vector & parameters); - static void init_map(color_ostream &out); static void deinit_map(color_ostream &out); DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { - commands.push_back( - PluginCommand("burrow", - "Quick commands for burrow control.", - burrow)); if (Core::getInstance().isMapLoaded()) init_map(out); @@ -90,10 +572,6 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan return CR_OK; } -/* - * State change tracking. - */ - static int name_burrow_id = -1; static void handle_burrow_rename(color_ostream &out, df::burrow *burrow); @@ -214,10 +692,6 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) return CR_OK; } -/* - * Config and processing - */ - static std::map name_lookup; static void parse_names() @@ -680,3 +1154,4 @@ static command_result burrow(color_ostream &out, vector ¶meters) return CR_OK; } +*/ diff --git a/plugins/lua/burrow.lua b/plugins/lua/burrow.lua index 7c8753bf7..9b7d8c63d 100644 --- a/plugins/lua/burrow.lua +++ b/plugins/lua/burrow.lua @@ -1,23 +1,241 @@ -local _ENV = mkmodule('plugins.burrows') +local _ENV = mkmodule('plugins.burrow') --[[ - Native events: + Provided events: * onBurrowRename(burrow) * onDigComplete(job_type,pos,old_tiletype,new_tiletype) - Native functions: +--]] - * findByName(name) -> burrow - * copyUnits(dest,src,enable) - * copyTiles(dest,src,enable) - * setTilesByKeyword(dest,kwd,enable) -> success +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') - 'enable' selects between add and remove modes +--------------------------------- +-- BurrowDesignationOverlay +-- ---]] +local selection_rect = df.global.selection_rect +local if_burrow = df.global.game.main_interface.burrow + +local function is_choosing_area(pos) + return if_burrow.doing_rectangle and + selection_rect.start_x >= 0 and + (pos or dfhack.gui.getMousePos()) +end + +local function reset_selection_rect() + selection_rect.start_x = -30000 + selection_rect.start_y = -30000 + selection_rect.start_z = -30000 +end + +local function get_bounds(pos1, pos2) + pos1 = pos1 or dfhack.gui.getMousePos() + pos2 = pos2 or xyz2pos(selection_rect.start_x, selection_rect.start_y, selection_rect.start_z) + local bounds = { + x1=math.min(pos1.x, pos2.x), + x2=math.max(pos1.x, pos2.x), + y1=math.min(pos1.y, pos2.y), + y2=math.max(pos1.y, pos2.y), + z1=math.min(pos1.z, pos2.z), + z2=math.max(pos1.z, pos2.z), + } + + -- clamp to map edges + bounds = { + x1=math.max(0, bounds.x1), + x2=math.min(df.global.world.map.x_count-1, bounds.x2), + y1=math.max(0, bounds.y1), + y2=math.min(df.global.world.map.y_count-1, bounds.y2), + z1=math.max(0, bounds.z1), + z2=math.min(df.global.world.map.z_count-1, bounds.z2), + } + + return bounds +end + +local function get_cur_area_dims() + local bounds = get_bounds() + return bounds.x2 - bounds.x1 + 1, + bounds.y2 - bounds.y1 + 1, + bounds.z2 - bounds.z1 + 1 +end + +BurrowDesignationOverlay = defclass(BurrowDesignationOverlay, overlay.OverlayWidget) +BurrowDesignationOverlay.ATTRS{ + default_pos={x=6,y=9}, + viewscreens='dwarfmode/Burrow/Paint', + default_enabled=true, + frame={w=54, h=1}, +} + +function BurrowDesignationOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={t=0, l=0}, + subviews={ + widgets.Label{ + frame={t=0, l=1}, + text='Double-click to fill. Shift double-click to 3D fill.', + auto_width=true, + visible=function() return not is_choosing_area() end, + }, + widgets.Label{ + frame={t=0, l=1}, + text_pen=COLOR_DARKGREY, + text={ + '3D box select enabled: ', + {text=function() return ('%dx%dx%d'):format(get_cur_area_dims()) end}, + }, + visible=is_choosing_area, + }, + }, + }, + } +end + +local function flood_fill(pos, erasing, do_3d, painting_burrow) + local opts = {zlevel=not do_3d} + if erasing then + burrow_tiles_flood_remove(painting_burrow, pos, opts) + else + burrow_tiles_flood_add(painting_burrow, pos, opts) + end + reset_selection_rect() +end + +local function box_fill(bounds, erasing, painting_burrow) + if bounds.z1 == bounds.z2 then return end + if erasing then + burrow_tiles_box_remove(painting_burrow, bounds) + else + burrow_tiles_box_add(painting_burrow, bounds) + end +end + +function BurrowDesignationOverlay:onInput(keys) + if self:inputToSubviews(keys) then + return true + -- don't perform burrow modifications immediately -- painting_burrow may not yet + -- have been initialized. instead, allow clicks to go through so that vanilla + -- behavior is triggered before we modify the burrow further + elseif keys._MOUSE_L then + local pos = dfhack.gui.getMousePos() + if pos then + local now_ms = dfhack.getTickCount() + if not same_xyz(pos, self.saved_pos) then + self.last_click_ms = now_ms + self.saved_pos = pos + else + if now_ms - self.last_click_ms <= widgets.DOUBLE_CLICK_MS then + self.last_click_ms = 0 + self.pending_fn = curry(flood_fill, pos, if_burrow.erasing, dfhack.internal.getModifiers().shift) + return + else + self.last_click_ms = now_ms + end + end + if is_choosing_area(pos) then + self.pending_fn = curry(box_fill, get_bounds(pos), if_burrow.erasing) + return + end + end + end +end + +function BurrowDesignationOverlay:onRenderBody(dc) + BurrowDesignationOverlay.super.onRenderBody(self, dc) + local pending_fn = self.pending_fn + self.pending_fn = nil + if pending_fn and if_burrow.painting_burrow then + pending_fn(if_burrow.painting_burrow) + end +end + +OVERLAY_WIDGETS = { + designation=BurrowDesignationOverlay, +} rawset_default(_ENV, dfhack.burrows) +--------------------------------- +-- commandline handling +-- + +local function set_add_remove(mode, which, params, opts) + local target_burrow = table.remove(params, 1) + return _ENV[('burrow_%s_%s'):format(mode, which)](target_burrow, params, opts) +end + +local function tiles_box_add_remove(which, params, opts) + local target_burrow = table.remove(params, 1) + local pos1 = argparse.coords(params[1] or 'here', 'pos') + local pos2 = opts.cursor or argparse.coords(params[2] or 'here', 'pos') + local bounds = get_bounds(pos1, pos2) + return _ENV['burrow_tiles_box_'..which](target_burrow, bounds, opts) +end + +local function tiles_flood_add_remove(which, params, opts) + local target_burrow = table.remove(params, 1) + local pos = opts.cursor or argparse.coords('here', 'pos') + return _ENV['burrow_tiles_flood_'..which](target_burrow, pos, opts) +end + +local function run_command(mode, command, params, opts) + if mode == 'tiles' then + if command == 'clear' then + return burrow_tiles_clear(params, opts) + elseif command == 'set' or command == 'add' or command == 'remove' then + return set_add_remove('tiles', command, params, opts) + elseif command == 'box-add' or command == 'box-remove' then + return tiles_box_add_remove(command:sub(5), params, opts) + elseif command == 'flood-add' or command == 'flood-remove' then + return tiles_flood_add_remove(command:sub(7), params, opts) + else + return false + end + elseif mode == 'units' then + if command == 'clear' then + return burrow_units_clear(params) + elseif command == 'set' or command == 'add' or command == 'remove' then + return set_add_remove('units', command, params, opts) + else + return false + end + else + return false + end +end + +function parse_commandline(...) + local args, opts = {...}, {} + + if args[1] == 'help' then + return false + end + + local positionals = argparse.processArgsGetopt(args, { + {'c', 'cursor', hasArg=true, + handler=function(optarg) opts.cursor = argparse.coords(optarg, 'cursor') end}, + {'d', 'dry-run', handler=function() opts.dry_run = true end}, + {'h', 'help', handler=function() opts.help = true end}, + {'z', 'cur-zlevel', handler=function() opts.zlevel = true end}, + }) + + if opts.help then + return false + end + + local mode = table.remove(positionals, 1) + local command = table.remove(positionals, 1) + local ret = run_command(mode, command, positionals, opts) + + if not ret then return false end + + print(('%d %s affected'):format(ret, mode)) + return true +end + return _ENV