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