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.
develop
Myk Taylor 2020-10-29 11:00:49 -07:00
parent 9344e41e0a
commit 22ac163d55
6 changed files with 278 additions and 60 deletions

@ -3784,8 +3784,9 @@ buildingplan
Native functions provided by the `buildingplan` plugin: 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 * ``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 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 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. * ``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.

@ -90,7 +90,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_lib_static) dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_lib_static)
dfhack_plugin(autohauler autohauler.cpp) dfhack_plugin(autohauler autohauler.cpp)
dfhack_plugin(autolabor autolabor.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(automelt automelt.cpp)
dfhack_plugin(autotrade autotrade.cpp) dfhack_plugin(autotrade autotrade.cpp)
dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua)

@ -5,6 +5,7 @@
#include <vector> #include <vector>
#include "Core.h" #include "Core.h"
#include "LuaTools.h"
#include <Console.h> #include <Console.h>
#include <Export.h> #include <Export.h>
#include <PluginManager.h> #include <PluginManager.h>
@ -504,7 +505,7 @@ static bool find_anchor_in_spiral(const df::coord &start)
return found; 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(); valid_building_sites.clear();
open_air_sites.clear(); open_air_sites.clear();
@ -519,7 +520,8 @@ static bool find_valid_building_sites(bool in_future_placement_mode)
continue; continue;
building_site site(df::coord(xB, yB, box_second.z), false); 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); valid_building_sites.push_back(site);
else if (site.in_open_air) else if (site.in_open_air)
{ {
@ -531,6 +533,8 @@ static bool find_valid_building_sites(bool in_future_placement_mode)
} }
} }
if (!use_buildingplan)
{
size_t last_open_air_count = 0; size_t last_open_air_count = 0;
while (valid_building_sites.size() > 0 && open_air_sites.size() != last_open_air_count) while (valid_building_sites.size() > 0 && open_air_sites.size() != last_open_air_count)
{ {
@ -546,10 +550,98 @@ static bool find_valid_building_sites(bool in_future_placement_mode)
} }
} }
}
return valid_building_sites.size() > 0; 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) static bool designate_new_construction(df::coord &pos, df::construction_type &type, df::item *item)
{ {
auto newinst = Buildings::allocInstance(pos, building_type::Construction, type); 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()) 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; 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; revert_to_last_used_type = !revert_to_last_used_type;
} }
@ -739,7 +833,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
return; 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; allow_future_placement = !allow_future_placement;
} }
@ -816,6 +910,15 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
return; 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 //END UI Methods
@ -876,16 +979,17 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
static bool saved_revert_setting = false; static bool saved_revert_setting = false;
static bool auto_select_applied = false; static bool auto_select_applied = false;
bool use_buildingplan = is_buildingplan_managed();
box_select_mode = SELECT_MATERIALS; box_select_mode = SELECT_MATERIALS;
if (new_start) if (new_start)
{ {
bool ok_to_continue = false; bool ok_to_continue = false;
bool in_future_placement_mode = 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) 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 else
@ -893,7 +997,8 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest
ok_to_continue = true; 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); 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(); building_site site = valid_building_sites.front();
valid_building_sites.pop_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) if (box_select_materials.size() > 0)
{ {
df::construction_type type = (df::construction_type) ui_build_selector->building_subtype; 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 // Allocation done, reset
move_cursor(box_second); 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; 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); 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) else if (in_placement_stage() && ui_build_selector->building_subtype < 7)
{ {
OutputString(COLOR_BROWN, x, y, "DFHack Options", 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, "Auto Mat-select", "a", auto_choose_materials, true, left_margin);
AMOutputToggleString(x, y, "Reselect Type", "t", revert_to_last_used_type, true, left_margin); AMOutputToggleString(x, y, "Reselect Type", "t", revert_to_last_used_type, true, left_margin);
}
++y; ++y;
AMOutputToggleString(x, y, "Box Select", "b", box_select_enabled, true, left_margin); 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); 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); OutputHotkeyString(x, y, (hollow_selection) ? "Make Solid" : "Make Hollow", "h", true, left_margin);
if (use_buildingplan)
++y;
else
AMOutputToggleString(x, y, "Open Placement", "o", allow_future_placement, true, left_margin); 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) if (box_select_enabled)
{ {
Screen::Pen pen(' ',COLOR_BLACK); Screen::Pen pen(' ',COLOR_BLACK);

@ -309,7 +309,6 @@ static std::string get_item_label(const BuildingTypeKey &key, int item_idx)
if (!s) if (!s)
return "No string"; return "No string";
lua_pop(L, 1);
return s; return s;
} }
@ -323,22 +322,27 @@ static bool construct_planned_building()
if (!(lua_checkstack(L, 1) && if (!(lua_checkstack(L, 1) &&
Lua::PushModulePublic(out, L, "plugins.buildingplan", Lua::PushModulePublic(out, L, "plugins.buildingplan",
"construct_building_from_ui_state") && "construct_buildings_from_ui_state") &&
Lua::SafeCall(out, L, 0, 1))) Lua::SafeCall(out, L, 0, 1)))
{ {
return false; return false;
} }
// register all returned buildings with planner
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
auto bld = Lua::GetDFObject<df::building>(L, -1); auto bld = Lua::GetDFObject<df::building>(L, -1);
lua_pop(L, 1);
if (!bld) if (!bld)
{ {
out.printerr("buildingplan: construct_building_from_ui_state() failed\n"); out.printerr(
"buildingplan: construct_buildings_from_ui_state() failed\n");
return false; return false;
} }
planner.addPlannedBuilding(bld); planner.addPlannedBuilding(bld);
lua_pop(L, 1);
}
return true; 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 struct buildingplan_query_hook : public df::viewscreen_dwarfmodest
{ {
typedef df::viewscreen_dwarfmodest interpose_base; typedef df::viewscreen_dwarfmodest interpose_base;
@ -583,7 +610,11 @@ struct buildingplan_place_hook : public df::viewscreen_dwarfmodest
if (!is_planmode_enabled(key)) if (!is_planmode_enabled(key))
return false; 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()) 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; int y = 23;
if (ui_build_selector->building_type == df::building_type::Construction if (is_automaterial_managed(ui_build_selector->building_type,
&& ui_build_selector->building_subtype < ui_build_selector->building_subtype))
df::construction_type::TrackN)
{ {
// try not to conflict with the automaterial plugin UI // avoid conflict with the automaterial plugin UI
y = 34; y = 36;
} }
if (show_help) if (show_help)
@ -920,6 +950,12 @@ DFhackCExport command_result plugin_shutdown(color_ostream &)
// Lua API section // 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, static bool isPlannableBuilding(df::building_type type,
int16_t subtype, int16_t subtype,
int32_t custom) { int32_t custom) {
@ -950,6 +986,7 @@ static void setSetting(std::string name, bool value) {
} }
DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(isPlanModeEnabled),
DFHACK_LUA_FUNCTION(isPlannableBuilding), DFHACK_LUA_FUNCTION(isPlannableBuilding),
DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(addPlannedBuilding),
DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(doCycle),

@ -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

@ -5,6 +5,7 @@ local _ENV = mkmodule('plugins.buildingplan')
Native functions: Native functions:
* void setSetting(string name, boolean value) * 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) * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)
* void addPlannedBuilding(df::building *bld) * void addPlannedBuilding(df::building *bld)
* void doCycle() * void doCycle()
@ -64,7 +65,9 @@ function get_item_label(btype, subtype, custom, reverse_idx)
end end
-- needs the core suspended -- 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 uibs = df.global.ui_build_selector
local world = df.global.world local world = df.global.world
local direction = world.selected_direction local direction = world.selected_direction
@ -76,13 +79,29 @@ function construct_building_from_ui_state()
local pos = guidm.getCursorPos() local pos = guidm.getCursorPos()
pos.x = pos.x - math.floor(width/2) pos.x = pos.x - math.floor(width/2)
pos.y = pos.y - math.floor(height/2) pos.y = pos.y - math.floor(height/2)
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
local blds = {}
for y=min_y,max_y do for x=min_x,max_x do
local bld, err = dfhack.buildings.constructBuilding{ local bld, err = dfhack.buildings.constructBuilding{
type=uibs.building_type, subtype=uibs.building_subtype, type=uibs.building_type, subtype=uibs.building_subtype,
custom=uibs.custom_type, pos=pos, width=width, height=height, custom=uibs.custom_type, pos=xyz2pos(x, y, pos.z),
direction=direction} width=width, height=height, direction=direction}
if err then error(err) end if err then
-- assign fields for the types that need them. we can't pass them all in to for _,b in ipairs(blds) do
-- the call to constructBuilding since attempting to assign unrelated 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. -- fields to building types that don't support them causes errors.
for k,v in pairs(bld) do for k,v in pairs(bld) do
if k == 'friction' then bld.friction = uibs.friction end if k == 'friction' then bld.friction = uibs.friction end
@ -91,7 +110,9 @@ function construct_building_from_ui_state()
if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_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 if k == 'speed' then bld.speed = uibs.speed end
end end
return bld table.insert(blds, bld)
end end
return blds
end end
-- --