From cf1e4e69ca94af0d410a96114d883d54704121a3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Oct 2023 19:26:15 -0700 Subject: [PATCH] basic commandline processing and skeleton lua interface --- docs/plugins/burrow.rst | 6 +- plugins/burrow.cpp | 380 +++++++++++++++++++++++++++++++++++++--- plugins/lua/burrow.lua | 141 ++++++++++++--- 3 files changed, 476 insertions(+), 51 deletions(-) diff --git a/docs/plugins/burrow.rst b/docs/plugins/burrow.rst index c366b2787..b14be928c 100644 --- a/docs/plugins/burrow.rst +++ b/docs/plugins/burrow.rst @@ -20,12 +20,12 @@ Usage enable burrow burrow tiles|units clear [ ...] [] burrow tiles|units set|add|remove [...] [] - burrow tiles box-add|box-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, all burrows that match the name (case sensitive) will be -included. If a burrow name ends in ``+`` (to indicate that it should be +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. diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index c39253488..4a6d8ee8b 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -1,3 +1,361 @@ +#include "Core.h" +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Burrows.h" +#include "modules/Persistence.h" +#include "modules/World.h" + +#include "df/burrow.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_opts(lua_State *L, int idx, bool &dry_run, bool &cur_zlevel) { + // TODO +} + +static void get_bounds(lua_State *L, int idx, df::coord &pos1, df::coord &pos2) { + // TODO +} + +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"); + return 0; +} + +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"); + 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"); + 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"); + return 0; +} + +static int box_fill(lua_State *L, bool enable) { + df::coord pos_start, pos_end; + bool dry_run = false, cur_zlevel = false; + + df::burrow *burrow = NULL; + if (lua_isuserdata(L, 1)) + burrow = Lua::GetDFObject(L, 1); + else if (lua_isstring(L, 1)) + burrow = Burrows::findByName(luaL_checkstring(L, 1)); + else if (lua_isinteger(L, 1)) + burrow = df::burrow::find(luaL_checkinteger(L, 1)); + + if (!burrow) { + luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); + return 0; + } + + get_bounds(L, 2, pos_start, pos_end); + get_opts(L, 3, dry_run, cur_zlevel); + + if (cur_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 (!Burrows::isAssignedTile(burrow, pos)) + ++count; + if (!dry_run) + Burrows::setAssignedTile(burrow, pos, true); + } + } + } + + 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 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 0; +} + +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 0; +} + +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"); + 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"); + 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"); + 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"); + 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 +371,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 +394,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 +437,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 +557,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 +1019,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 e3c2d0bfa..536372fae 100644 --- a/plugins/lua/burrow.lua +++ b/plugins/lua/burrow.lua @@ -21,6 +21,10 @@ local _ENV = mkmodule('plugins.burrow') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') +--------------------------------- +-- BurrowDesignationOverlay +-- + local selection_rect = df.global.selection_rect local if_burrow = df.global.game.main_interface.burrow @@ -36,15 +40,16 @@ local function reset_selection_rect() selection_rect.start_z = -30000 end -local function get_bounds(pos) - pos = pos or dfhack.gui.getMousePos() +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(selection_rect.start_x, pos.x), - x2=math.max(selection_rect.start_x, pos.x), - y1=math.min(selection_rect.start_y, pos.y), - y2=math.max(selection_rect.start_y, pos.y), - z1=math.min(selection_rect.start_z, pos.z), - z2=math.max(selection_rect.start_z, pos.z), + 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 @@ -69,10 +74,10 @@ end BurrowDesignationOverlay = defclass(BurrowDesignationOverlay, overlay.OverlayWidget) BurrowDesignationOverlay.ATTRS{ - default_pos={x=6,y=8}, + default_pos={x=6,y=9}, viewscreens='dwarfmode/Burrow/Paint', default_enabled=true, - frame={w=30, h=2}, + frame={w=54, h=1}, } function BurrowDesignationOverlay:init() @@ -82,39 +87,41 @@ function BurrowDesignationOverlay:init() subviews={ widgets.Label{ frame={t=0, l=1}, - text='Double-click to flood fill.', + text='Double-click to fill.', + auto_width=true, }, - }, - }, - widgets.BannerPanel{ - frame={t=1, l=0}, - subviews={ widgets.Label{ - frame={t=0, l=1}, + frame={t=0, l=25}, text_pen=COLOR_DARKGREY, text={ 'Selected area: ', {text=function() return ('%dx%dx%d'):format(get_cur_area_dims()) end}, }, + visible=is_choosing_area, }, }, - visible=is_choosing_area, }, } end -local function flood_fill(pos, painting_burrow) - print('flood fill, erasing:', if_burrow.erasing) - print('origin pos:') - printall(pos) +local function flood_fill(pos, erasing, painting_burrow) + if erasing then + burrow_tiles_flood_remove(painting_burrow, pos) + else + burrow_tiles_flood_add(painting_burrow, pos) + end reset_selection_rect() end -local function box_fill(bounds, painting_burrow) - print('box fill, erasing:', if_burrow.erasing) - print('bounds:') - printall(bounds) +local function box_fill(bounds, erasing, painting_burrow) if bounds.z1 == bounds.z2 then return end + local pos1 = xyz2pos(bounds.x1, bounds.y1, bounds.z1) + local pos2 = xyz2pos(bounds.x2, bounds.y2, bounds.z2) + if erasing then + burrow_tiles_box_remove(painting_burrow, pos1, pos2) + else + burrow_tiles_box_add(painting_burrow, pos1, pos2) + end end function BurrowDesignationOverlay:onInput(keys) @@ -133,14 +140,14 @@ function BurrowDesignationOverlay:onInput(keys) 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) + self.pending_fn = curry(flood_fill, pos, if_burrow.erasing) 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)) + self.pending_fn = curry(box_fill, get_bounds(pos), if_burrow.erasing) return end end @@ -162,4 +169,82 @@ OVERLAY_WIDGETS = { 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