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:
* ``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.

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

@ -5,6 +5,7 @@
#include <vector>
#include "Core.h"
#include "LuaTools.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
@ -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<building_site> current_open_air_list = open_air_sites;
open_air_sites.clear();
for (deque<building_site>::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<building_site> current_open_air_list = open_air_sites;
open_air_sites.clear();
for (deque<building_site>::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);

@ -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<df::building>(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<df::building>(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),

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