From 22ac163d55a0b83c9b360a8e916cc1833bf59050 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Oct 2020 11:00:49 -0700 Subject: [PATCH 1/2] improve UX between automaterial and buildingplan solves the confusing behavior when both automaterial and buildingplan are enabled for constructions. the two plugins now communicate with each other over the Lua layer to negotiate consistent behavior. if neither plugin is enabled, the standard DF UI acts as normal if automaterial is enabled but buildingplan is not, then automaterial behavior is unchanged. if buildingplan is enabled and automaterial is not then behavior is the same as other buildings with buildingplan (no material selection screen, screen stays on building placement screen after placement). this commit fixes a bug, though, where buildingplan would only lay down a single tile of contruction instead of a solid block when a block is requested. if both plugins are enabled but buildingplan is not enabled for the building type then automaterial is unchanged from previous behavior, execpt for an additional header showing the separation between automaterial hotkeys and buildingplan hotkeys. finally, if both plugins are enabled and buildingplan is enabled for the building type then buildingplan behavior prevails, but the box select and hollow designations features of automaterial are still usable and useful. the 'Auto Mat-select', 'Reselect Type', and "Open Placement" automaterial hotkeys are hidden in the UI and ignored in the feed. This is because buildingplan takes over material selection, so 'Auto Mat-select' doesn't make sense. Buildingplan also already stays on the placement screen after placement, so 'Reselect Type' is not necessary. And all buildingplan-placed buildings have relaxed placement restrictions (e.g. they can be built in mid-air) so 'Open Placement' is also not necessary. The missing options are replaced with blank lines so the vertical alignment of all other options stays constant. we also remove a few extra lua_pop() calls that are made superfluous by the StackUnwinder. --- docs/Lua API.rst | 5 +- plugins/CMakeLists.txt | 2 +- plugins/automaterial.cpp | 186 ++++++++++++++++++++++++++++++----- plugins/buildingplan.cpp | 69 ++++++++++--- plugins/lua/automaterial.lua | 23 +++++ plugins/lua/buildingplan.lua | 53 +++++++--- 6 files changed, 278 insertions(+), 60 deletions(-) create mode 100644 plugins/lua/automaterial.lua diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 4c6f0d6fa..e69b13500 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3784,8 +3784,9 @@ buildingplan Native functions provided by the `buildingplan` plugin: -* ``bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)`` returns whether the building type is handled by buildingplan -* ``void addPlannedBuilding(df::building *bld)`` suspends the building jobs and adds the building to the monitor list +* ``bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)`` returns whether the building type is handled by buildingplan. +* ``bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom)`` returns whether the buildingplan UI is enabled for the specified building type. +* ``void addPlannedBuilding(df::building *bld)`` suspends the building jobs and adds the building to the monitor list. * ``void doCycle()`` runs a check for whether buildlings in the monitor list can be assigned items and unsuspended. This method runs automatically twice a game day, so you only need to call it directly if you want buildingplan to do a check right now. * ``void scheduleCycle()`` schedules a cycle to be run during the next non-paused game frame. Can be called multiple times while the game is paused and only one cycle will be scheduled. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 46a7d4097..9f5b2c679 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -90,7 +90,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_lib_static) dfhack_plugin(autohauler autohauler.cpp) dfhack_plugin(autolabor autolabor.cpp) - dfhack_plugin(automaterial automaterial.cpp) + dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua) dfhack_plugin(automelt automelt.cpp) dfhack_plugin(autotrade autotrade.cpp) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) diff --git a/plugins/automaterial.cpp b/plugins/automaterial.cpp index 40dc2bb48..a238216c8 100644 --- a/plugins/automaterial.cpp +++ b/plugins/automaterial.cpp @@ -5,6 +5,7 @@ #include #include "Core.h" +#include "LuaTools.h" #include #include #include @@ -504,7 +505,7 @@ static bool find_anchor_in_spiral(const df::coord &start) return found; } -static bool find_valid_building_sites(bool in_future_placement_mode) +static bool find_valid_building_sites(bool in_future_placement_mode, bool use_buildingplan) { valid_building_sites.clear(); open_air_sites.clear(); @@ -519,7 +520,8 @@ static bool find_valid_building_sites(bool in_future_placement_mode) continue; building_site site(df::coord(xB, yB, box_second.z), false); - if (is_valid_building_site(site, false, true, in_future_placement_mode)) + // if we're using buildingplan, it will take care of filtering out bad tiles + if (use_buildingplan || is_valid_building_site(site, false, true, in_future_placement_mode)) valid_building_sites.push_back(site); else if (site.in_open_air) { @@ -531,25 +533,115 @@ static bool find_valid_building_sites(bool in_future_placement_mode) } } - size_t last_open_air_count = 0; - while (valid_building_sites.size() > 0 && open_air_sites.size() != last_open_air_count) + if (!use_buildingplan) { - last_open_air_count = open_air_sites.size(); - deque current_open_air_list = open_air_sites; - open_air_sites.clear(); - for (deque::iterator it = current_open_air_list.begin(); it != current_open_air_list.end(); it++) + size_t last_open_air_count = 0; + while (valid_building_sites.size() > 0 && open_air_sites.size() != last_open_air_count) { - if (is_orthogonal_to_pending_construction(*it)) - valid_building_sites.push_back(*it); - else - open_air_sites.push_back(*it); - } + last_open_air_count = open_air_sites.size(); + deque current_open_air_list = open_air_sites; + open_air_sites.clear(); + for (deque::iterator it = current_open_air_list.begin(); it != current_open_air_list.end(); it++) + { + if (is_orthogonal_to_pending_construction(*it)) + valid_building_sites.push_back(*it); + else + open_air_sites.push_back(*it); + } + } } return valid_building_sites.size() > 0; } +static bool is_buildingplan_enabled() +{ + 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.buildingplan", "isEnabled") && + Lua::SafeCall(out, L, 0, 1))) + { + return false; + } + + return lua_toboolean(L, -1); +} + +static bool is_buildingplan_planmode_enabled( + df::building_type type, int16_t subtype, int32_t custom) +{ + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, 4) || + !Lua::PushModulePublic( + out, L, "plugins.buildingplan", "isPlanModeEnabled")) + return false; + + Lua::Push(L, type); + Lua::Push(L, subtype); + Lua::Push(L, custom); + + if (!Lua::SafeCall(out, L, 3, 1)) + return false; + + return lua_toboolean(L, -1); +} + +static bool is_buildingplan_managed() +{ + return is_buildingplan_enabled() && + is_buildingplan_planmode_enabled(ui_build_selector->building_type, + ui_build_selector->building_subtype, + ui_build_selector->custom_type); +} + +static bool build_with_buildingplan_box_select(const df::coord &pos) +{ + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); + + CoreSuspendClaimer suspend; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, 5) || + !Lua::PushModulePublic( + out, L, "plugins.automaterial", + "build_with_buildingplan_box_select")) + { + return false; + } + + Lua::Push(L, ui_build_selector->building_subtype); + Lua::Push(L, pos.x); + Lua::Push(L, pos.y); + Lua::Push(L, pos.z); + + if (!Lua::SafeCall(out, L, 4, 1)) + return false; + + return lua_toboolean(L, -1); +} + +static bool build_with_buildingplan_ui() +{ + auto L = Lua::Core::State; + color_ostream_proxy out(Core::getInstance().getConsole()); + + CoreSuspendClaimer suspend; + Lua::StackUnwinder top(L); + + return lua_checkstack(L, 1) && + Lua::PushModulePublic(out, L, "plugins.automaterial", + "build_with_buildingplan_ui") && + Lua::SafeCall(out, L, 0, 1); +} + static bool designate_new_construction(df::coord &pos, df::construction_type &type, df::item *item) { auto newinst = Buildings::allocInstance(pos, building_type::Construction, type); @@ -722,11 +814,13 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest } else if (in_placement_stage()) { - if (input->count(interface_key::CUSTOM_A)) + bool use_buildingplan = is_buildingplan_managed(); + + if (!use_buildingplan && input->count(interface_key::CUSTOM_A)) { auto_choose_materials = !auto_choose_materials; } - else if (input->count(interface_key::CUSTOM_T)) + else if (!use_buildingplan && input->count(interface_key::CUSTOM_T)) { revert_to_last_used_type = !revert_to_last_used_type; } @@ -739,7 +833,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest return; } - else if (input->count(interface_key::CUSTOM_O)) + else if (!use_buildingplan && input->count(interface_key::CUSTOM_O)) { allow_future_placement = !allow_future_placement; } @@ -816,6 +910,15 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest return; } } + else if (use_buildingplan + && ui_build_selector->errors.size() == 0 + && input->count(interface_key::SELECT)) + { + build_with_buildingplan_ui(); + Gui::refreshSidebar(); + input->clear(); + return; + } } } //END UI Methods @@ -876,16 +979,17 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest static bool saved_revert_setting = false; static bool auto_select_applied = false; + bool use_buildingplan = is_buildingplan_managed(); box_select_mode = SELECT_MATERIALS; if (new_start) { bool ok_to_continue = false; bool in_future_placement_mode = false; - if (!find_valid_building_sites(false)) + if (!find_valid_building_sites(false, use_buildingplan)) { if (allow_future_placement) { - in_future_placement_mode = find_valid_building_sites(true); + in_future_placement_mode = find_valid_building_sites(true, use_buildingplan); } } else @@ -893,7 +997,8 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest ok_to_continue = true; } - if (in_future_placement_mode) + // if using buildingplan, we don't need an anchor + if (!use_buildingplan && in_future_placement_mode) { ok_to_continue = find_anchor_in_spiral(valid_building_sites[0].pos); } @@ -924,6 +1029,15 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest { building_site site = valid_building_sites.front(); valid_building_sites.pop_front(); + + if (use_buildingplan) + { + // we don't actually care if this fails. buildingplan will return + // false when it filters out bad tiles, and that's ok. + build_with_buildingplan_box_select(site.pos); + continue; + } + if (box_select_materials.size() > 0) { df::construction_type type = (df::construction_type) ui_build_selector->building_subtype; @@ -986,8 +1100,10 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest // Allocation done, reset move_cursor(box_second); + // if we're using buildingplan, we nevef actually leave the placement + // screen, so there's no need to re-enter the screen revert_to_last_used_type = saved_revert_setting; - if (!revert_to_last_used_type) + if (!use_buildingplan && !revert_to_last_used_type) { send_key(df::interface_key::LEAVESCREEN); } @@ -1091,9 +1207,18 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest } else if (in_placement_stage() && ui_build_selector->building_subtype < 7) { - OutputString(COLOR_BROWN, x, y, "DFHack Options", true, left_margin); - AMOutputToggleString(x, y, "Auto Mat-select", "a", auto_choose_materials, true, left_margin); - AMOutputToggleString(x, y, "Reselect Type", "t", revert_to_last_used_type, true, left_margin); + bool use_buildingplan = is_buildingplan_managed(); + + OutputString(COLOR_BROWN, x, y, "DFHack Automaterial Options", true, left_margin); + if (use_buildingplan) + { + y += 2; + } + else + { + AMOutputToggleString(x, y, "Auto Mat-select", "a", auto_choose_materials, true, left_margin); + AMOutputToggleString(x, y, "Reselect Type", "t", revert_to_last_used_type, true, left_margin); + } ++y; AMOutputToggleString(x, y, "Box Select", "b", box_select_enabled, true, left_margin); @@ -1101,9 +1226,20 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest { AMOutputToggleString(x, y, "Show Box Mask", "x", show_box_selection, true, left_margin); OutputHotkeyString(x, y, (hollow_selection) ? "Make Solid" : "Make Hollow", "h", true, left_margin); - AMOutputToggleString(x, y, "Open Placement", "o", allow_future_placement, true, left_margin); + + if (use_buildingplan) + ++y; + else + AMOutputToggleString(x, y, "Open Placement", "o", allow_future_placement, true, left_margin); } - ++y; + else + { + y += 3; + } + y += 2; + if (is_buildingplan_enabled()) + OutputString(COLOR_BROWN, x, y, "DFHack Buildingplan Options", true, left_margin); + if (box_select_enabled) { Screen::Pen pen(' ',COLOR_BLACK); diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index ad0bdf358..ecd07a9e9 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -309,7 +309,6 @@ static std::string get_item_label(const BuildingTypeKey &key, int item_idx) if (!s) return "No string"; - lua_pop(L, 1); return s; } @@ -323,22 +322,27 @@ static bool construct_planned_building() if (!(lua_checkstack(L, 1) && Lua::PushModulePublic(out, L, "plugins.buildingplan", - "construct_building_from_ui_state") && + "construct_buildings_from_ui_state") && Lua::SafeCall(out, L, 0, 1))) { return false; } - auto bld = Lua::GetDFObject(L, -1); - lua_pop(L, 1); - - if (!bld) + // register all returned buildings with planner + lua_pushnil(L); + while (lua_next(L, -2) != 0) { - out.printerr("buildingplan: construct_building_from_ui_state() failed\n"); - return false; - } + auto bld = Lua::GetDFObject(L, -1); + if (!bld) + { + out.printerr( + "buildingplan: construct_buildings_from_ui_state() failed\n"); + return false; + } - planner.addPlannedBuilding(bld); + planner.addPlannedBuilding(bld); + lua_pop(L, 1); + } return true; } @@ -373,6 +377,29 @@ static void show_global_settings_dialog() } } +static bool is_automaterial_enabled() +{ + 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.automaterial", "isEnabled") && + Lua::SafeCall(out, L, 0, 1))) + { + return false; + } + + return lua_toboolean(L, -1); +} + +static bool is_automaterial_managed(df::building_type type, int16_t subtype) +{ + return is_automaterial_enabled() + && type == df::building_type::Construction + && subtype < df::construction_type::TrackN; +} + struct buildingplan_query_hook : public df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; @@ -583,7 +610,11 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest if (!is_planmode_enabled(key)) return false; - if (input->count(interface_key::SELECT)) + // if automaterial is enabled, let it handle building allocation and + // registration with planner + if (input->count(interface_key::SELECT) && + !is_automaterial_managed(ui_build_selector->building_type, + ui_build_selector->building_subtype)) { if (ui_build_selector->errors.size() == 0 && construct_planned_building()) { @@ -679,12 +710,11 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest int y = 23; - if (ui_build_selector->building_type == df::building_type::Construction - && ui_build_selector->building_subtype < - df::construction_type::TrackN) + if (is_automaterial_managed(ui_build_selector->building_type, + ui_build_selector->building_subtype)) { - // try not to conflict with the automaterial plugin UI - y = 34; + // avoid conflict with the automaterial plugin UI + y = 36; } if (show_help) @@ -920,6 +950,12 @@ DFhackCExport command_result plugin_shutdown(color_ostream &) // Lua API section +static bool isPlanModeEnabled(df::building_type type, + int16_t subtype, + int32_t custom) { + return planmode_enabled[toBuildingTypeKey(type, subtype, custom)]; +} + static bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) { @@ -950,6 +986,7 @@ static void setSetting(std::string name, bool value) { } DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(isPlanModeEnabled), DFHACK_LUA_FUNCTION(isPlannableBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), diff --git a/plugins/lua/automaterial.lua b/plugins/lua/automaterial.lua new file mode 100644 index 000000000..1cd7e9faf --- /dev/null +++ b/plugins/lua/automaterial.lua @@ -0,0 +1,23 @@ +local _ENV = mkmodule('plugins.automaterial') + +local buildingplan = require('plugins.buildingplan') + +-- construct the building and register it with buildingplan for item selection +function build_with_buildingplan_box_select(subtype, x, y, z) + local pos = xyz2pos(x, y, z) + local bld, err = dfhack.buildings.constructBuilding{ + type=df.building_type.Construction, subtype=subtype, pos=pos} + -- it's not a user error if we can't place a building here; just indicate + -- that no building was placed by returning false. + if err then return false end + buildingplan.addPlannedBuilding(bld) + return true +end + +function build_with_buildingplan_ui() + for _,bld in ipairs(buildingplan.construct_buildings_from_ui_state()) do + buildingplan.addPlannedBuilding(bld) + end +end + +return _ENV diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 73fe160fb..04f6debff 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -5,6 +5,7 @@ local _ENV = mkmodule('plugins.buildingplan') Native functions: * void setSetting(string name, boolean value) + * bool isPlanModeEnabled(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) * void addPlannedBuilding(df::building *bld) * void doCycle() @@ -64,7 +65,9 @@ function get_item_label(btype, subtype, custom, reverse_idx) end -- needs the core suspended -function construct_building_from_ui_state() +-- returns a vector of constructed buildings (usually of size 1, but potentially +-- more for constructions) +function construct_buildings_from_ui_state() local uibs = df.global.ui_build_selector local world = df.global.world local direction = world.selected_direction @@ -76,22 +79,40 @@ function construct_building_from_ui_state() local pos = guidm.getCursorPos() pos.x = pos.x - math.floor(width/2) pos.y = pos.y - math.floor(height/2) - local bld, err = dfhack.buildings.constructBuilding{ - type=uibs.building_type, subtype=uibs.building_subtype, - custom=uibs.custom_type, pos=pos, width=width, height=height, - direction=direction} - if err then error(err) end - -- assign fields for the types that need them. we can't pass them all in to - -- the call to constructBuilding since attempting to assign unrelated - -- fields to building types that don't support them causes errors. - for k,v in pairs(bld) do - if k == 'friction' then bld.friction = uibs.friction end - if k == 'use_dump' then bld.use_dump = uibs.use_dump end - if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end - if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end - if k == 'speed' then bld.speed = uibs.speed end + local min_x, max_x = pos.x, pos.x + local min_y, max_y = pos.y, pos.y + if width == 1 and height == 1 and + (world.building_width > 1 or world.building_height > 1) then + min_x = math.ceil(pos.x - world.building_width/2) + max_x = math.floor(pos.x + world.building_width/2) + min_y = math.ceil(pos.y - world.building_height/2) + max_y = math.floor(pos.y + world.building_height/2) end - return bld + local blds = {} + for y=min_y,max_y do for x=min_x,max_x do + local bld, err = dfhack.buildings.constructBuilding{ + type=uibs.building_type, subtype=uibs.building_subtype, + custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z), + width=width, height=height, direction=direction} + if err then + for _,b in ipairs(blds) do + dfhack.buildings.deconstruct(b) + end + error(err) + end + -- assign fields for the types that need them. we can't pass them all in + -- to the call to constructBuilding since attempting to assign unrelated + -- fields to building types that don't support them causes errors. + for k,v in pairs(bld) do + if k == 'friction' then bld.friction = uibs.friction end + if k == 'use_dump' then bld.use_dump = uibs.use_dump end + if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end + if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end + if k == 'speed' then bld.speed = uibs.speed end + end + table.insert(blds, bld) + end end + return blds end -- From 65114d904c97424c90637a1571c9d224ffc7762c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Oct 2020 11:53:31 -0700 Subject: [PATCH 2/2] fix typo in comment --- plugins/automaterial.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/automaterial.cpp b/plugins/automaterial.cpp index a238216c8..7940fbdb9 100644 --- a/plugins/automaterial.cpp +++ b/plugins/automaterial.cpp @@ -1100,7 +1100,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest // Allocation done, reset move_cursor(box_second); - // if we're using buildingplan, we nevef actually leave the placement + // if we're using buildingplan, we never actually leave the placement // screen, so there's no need to re-enter the screen revert_to_last_used_type = saved_revert_setting; if (!use_buildingplan && !revert_to_last_used_type)