From b443f81ecdf0710fbf231c9bb9e4bcbf8975395a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 8 Feb 2023 18:47:10 -0800 Subject: [PATCH 01/47] print out more status info for buildingplan --- docs/plugins/buildingplan.rst | 49 +++++++++---------------------- plugins/buildingplan.cpp | 54 +++++++++++++++++++++++++++-------- plugins/lua/buildingplan.lua | 11 +++++-- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index 1eb18b1d5..c51f79721 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -11,11 +11,14 @@ available, and they will be created in a suspended state. Buildingplan will periodically scan for appropriate items, and the jobs will be unsuspended when the items are available. -This is very useful when combined with manager work orders or `workflow` -- you -can set a constraint to always have one or two doors/beds/tables/chairs/etc. -available, and place as many as you like. Materials are used to build the -planned buildings as they are produced, with minimal space dedicated to -stockpiles. +This is very powerful when used with tools like `quickfort`, which allow you to +set a building plan according to a blueprint, and the buildings will simply be +built when you can build them. + +You can use manager work orders or `workflow` to ensure you always have one or +two doors/beds/tables/chairs/etc. available, and place as many as you like. +Materials are used to build the planned buildings as they are produced, with +minimal space dedicated to stockpiles. Usage ----- @@ -23,37 +26,27 @@ Usage :: enable buildingplan - buildingplan set + buildingplan [status] buildingplan set true|false -Running ``buildingplan set`` without parameters displays the current settings. - .. _buildingplan-settings: Global settings --------------- -The buildingplan plugin has global settings that can be set from the UI -(:kbd:`G` from any building placement screen, for example: -:kbd:`b`:kbd:`a`:kbd:`G`). These settings can also be set via the -``buildingplan set`` command. The available settings are: +The buildingplan plugin has several global settings that affect what materials +can be chosen when attaching items to planned buildings: -``all_enabled`` (default: false) - Enable planning mode for all building types. ``blocks``, ``boulders``, ``logs``, ``bars`` (defaults: true, true, true, false) Allow blocks, boulders, logs, or bars to be matched for generic "building material" items. -``quickfort_mode`` (default: false) - Enable compatibility mode for the legacy Python Quickfort (this setting is - not required for DFHack `quickfort`) -The settings for ``blocks``, ``boulders``, ``logs``, and ``bars`` are saved with -your fort, so you only have to set them once and they will be persisted in your -save. +These settings are saved with your fort, so you only have to set them once and +they will be persisted in your save. If you normally embark with some blocks on hand for early workshops, you might want to add this line to your ``dfhack-config/init/onMapLoad.init`` file to -always configure buildingplan to just use blocks for buildings and +always configure `buildingplan` to just use blocks for buildings and constructions:: on-new-fortress buildingplan set boulders false; buildingplan set logs false @@ -76,17 +69,3 @@ keep the filter values that were set when the building was placed. For example, you can be sure that all your constructed walls are the same color by setting a filter to accept only certain types of stone. - -Quickfort mode --------------- - -If you use the external Python Quickfort to apply building blueprints instead of -the native DFHack `quickfort` script, you must enable Quickfort mode. This -temporarily enables buildingplan for all building types and adds an extra blank -screen after every building placement. This "dummy" screen is needed for Python -Quickfort to interact successfully with Dwarf Fortress. - -Note that Quickfort mode is only for compatibility with the legacy Python -Quickfort. The DFHack `quickfort` script does not need this Quickfort mode to be -enabled. The `quickfort` script will successfully integrate with buildingplan as -long as the buildingplan plugin itself is enabled. diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index 039c83b0f..81f026cbc 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -15,14 +15,14 @@ #include "df/job_item.h" #include "df/world.h" -#include +#include #include #include #include using std::map; using std::pair; -using std::queue; +using std::deque; using std::string; using std::unordered_map; using std::vector; @@ -34,11 +34,8 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); -// logging levels can be dynamically controlled with the `debugfilter` command. namespace DFHack { - // for configuration-related logging DBG_DECLARE(buildingplan, status, DebugCategory::LINFO); - // for logging during the periodic scan DBG_DECLARE(buildingplan, cycle, DebugCategory::LINFO); } @@ -108,7 +105,7 @@ static PersistentDataItem config; // building id -> PlannedBuilding unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) -map>>> tasks; +map>>> tasks; // note that this just removes the PlannedBuilding. the tasks will get dropped // as we discover them in the tasks queues and they fail to be found in planned_buildings. @@ -359,7 +356,7 @@ static void finalizeBuilding(color_ostream &out, df::building * bld) { Job::checkBuildingsNow(); } -static df::building * popInvalidTasks(color_ostream &out, queue> & task_queue) { +static df::building * popInvalidTasks(color_ostream &out, deque> & task_queue) { while (!task_queue.empty()) { auto & task = task_queue.front(); auto id = task.first; @@ -369,13 +366,13 @@ static df::building * popInvalidTasks(color_ostream &out, queue>> & buckets) { + map>> & buckets) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); auto item_vector = df::global::world->items.other[other_id]; DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", @@ -423,7 +420,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, // items so if buildingplan is turned off, the building will // be completed with the correct number of items. --job->job_items[filter_idx]->quantity; - task_queue.pop(); + task_queue.pop_front(); if (isJobReady(out, job)) { finalizeBuilding(out, bld); planned_buildings.at(id).remove(out); @@ -586,7 +583,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { // as invalid for (auto vector_id : vector_ids) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); + tasks[vector_id][bucket].push_back(std::make_pair(id, job_item_idx)); DEBUG(status,out).print("added task: %s/%s/%d,%d; " "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), @@ -609,13 +606,46 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { static void printStatus(color_ostream &out) { DEBUG(status,out).print("entering buildingplan_printStatus\n"); out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); - out.print(" finding materials for %zd buildings\n", planned_buildings.size()); out.print("Current settings:\n"); out.print(" use blocks: %s\n", get_config_bool(config, CONFIG_BLOCKS) ? "yes" : "no"); out.print(" use boulders: %s\n", get_config_bool(config, CONFIG_BOULDERS) ? "yes" : "no"); out.print(" use logs: %s\n", get_config_bool(config, CONFIG_LOGS) ? "yes" : "no"); out.print(" use bars: %s\n", get_config_bool(config, CONFIG_BARS) ? "yes" : "no"); out.print("\n"); + + map counts; + int32_t total = 0; + for (auto &buckets : tasks) { + for (auto &bucket_queue : buckets.second) { + deque> &tqueue = bucket_queue.second; + for (auto it = tqueue.begin(); it != tqueue.end();) { + auto & task = *it; + auto id = task.first; + df::building *bld = NULL; + if (!planned_buildings.count(id) || + !(bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out))) { + DEBUG(status,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", + id, task.second); + it = tqueue.erase(it); + continue; + } + auto *jitem = bld->jobs[0]->job_items[task.second]; + int32_t quantity = jitem->quantity; + if (quantity) { + string desc = toLower(ENUM_KEY_STR(item_type, jitem->item_type)); + counts[desc] += quantity; + total += quantity; + } + ++it; + } + } + } + + out.print("Waiting for %d item(s) to be produced or %zd building(s):\n", + total, planned_buildings.size()); + for (auto &count : counts) + out.print(" %3d %s\n", count.second, count.first.c_str()); + out.print("\n"); } static bool setSetting(color_ostream &out, string name, bool value) { diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9b953dd7c..4420f8534 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -4,8 +4,6 @@ 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) * bool isPlannedBuilding(df::building *bld) * void addPlannedBuilding(df::building *bld) @@ -36,6 +34,15 @@ function parse_commandline(...) return false end + local command = table.remove(positionals, 1) + if not command or command == 'status' then + printStatus() + elseif command == 'set' then + setSetting(positionals[1], positionals[2] == 'true') + else + return false + end + return true end From 0cb1c09549574daefe92035e16dba656def022bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 8 Feb 2023 19:26:39 -0800 Subject: [PATCH 02/47] implement skeletons for buildingplan overlays --- plugins/lua/buildingplan.lua | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 4420f8534..86f8699eb 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -53,6 +53,84 @@ function get_num_filters(btype, subtype, custom) return 0 end +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) +PlannerOverlay.ATTRS{ + default_pos={x=46,y=18}, + default_enabled=true, + viewscreens='dwarfmode/Building/Placement', + frame={w=30, h=4}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function PlannerOverlay:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + frame={t=0, l=0}, + label='build when materials are available', + key='CUSTOM_CTRL_B', + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='configure materials', + key='CUSTOM_CTRL_E', + on_activate=do_export, + }, + } +end + +InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) +InspectorOverlay.ATTRS{ + default_pos={x=-41,y=14}, + default_enabled=true, + viewscreens='dwarfmode/ViewSheets/BUILDING', + frame={w=30, h=5}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function InspectorOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Waiting for items:', + }, + widgets.Label{ + frame={t=1, l=0}, + text='items', + }, + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label='make top priority', + key='CUSTOM_CTRL_T', + }, + } +end + +function InspectorOverlay:onInput(keys) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return false + end + return InspectorOverlay.super.onInput(self, keys) +end + +function InspectorOverlay:render(dc) + if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then + return + end + InspectorOverlay.super.render(self, dc) +end + +OVERLAY_WIDGETS = { + planner=PlannerOverlay, + inspector=InspectorOverlay, +} + + local dialogs = require('gui.dialogs') local guidm = require('gui.dwarfmode') From 1c3a5fa1700c1fb9e45f24942689fd1f529ee74e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 9 Feb 2023 00:13:53 -0800 Subject: [PATCH 03/47] initial building placement code --- plugins/lua/buildingplan.lua | 371 ++++++++++++++++++++++++----------- 1 file changed, 260 insertions(+), 111 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 86f8699eb..4167ebb41 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -13,6 +13,11 @@ local _ENV = mkmodule('plugins.buildingplan') --]] local argparse = require('argparse') +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require('gui.widgets') require('dfhack.buildings') local function process_args(opts, args) @@ -47,42 +52,278 @@ function parse_commandline(...) end function get_num_filters(btype, subtype, custom) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if filters then return #filters end - return 0 + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + return filters and #filters or 0 end -local gui = require('gui') -local overlay = require('plugins.overlay') -local widgets = require('gui.widgets') +-------------------------------- +-- Planner Overlay +-- + +local uibs = df.global.buildreq + +local function cur_building_has_no_area() + if uibs.building_type == df.building_type.Construction then return false end + local filters = dfhack.buildings.getFiltersByType({}, + uibs.building_type, uibs.building_subtype, uibs.custom_type) + -- this works because all variable-size buildings have either no item + -- filters or a quantity of -1 for their first (and only) item + return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 +end + +local function get_cur_area_dims() + if not is_choosing_area() then return 1, 1 end + return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, + math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 +end + +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_plannable() + return get_cur_filters() and + not (uibs.building_type == df.building_type.Construction + and uibs.building_subtype == df.construction_type.TrackNSEW) +end + +local direction_panel_frame = {t=4, h=13, w=46, r=28} + +local direction_panel_types = utils.invert{ + df.building_type.Bridge, + df.building_type.ScrewPump, + df.building_type.WaterWheel, + df.building_type.AxleHorizontal, + df.building_type.Rollers, +} + +local function has_direction_panel() + return direction_panel_types[uibs.building_type] + or (uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.TrackStop) +end + +local function is_over_direction_panel() + if not has_direction_panel() then return false end + local v = widgets.Widget{frame=direction_panel_frame} + local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) + v:updateLayout(gui.ViewRect{rect=rect}) + return v:getMousePos() +end + +local function to_title_case(str) + str = str:gsub('(%a)([%w_]*)', + function (first, rest) return first:upper()..rest:lower() end) + str = str:gsub('_', ' ') + return str +end + +-- returns a reasonable label for the item based on the qualities of the filter +function get_item_label(idx) + local filter = get_cur_filters()[idx] + local desc = 'Unknown' + if filter.has_tool_use then + desc = to_title_case(df.tool_uses[filter.has_tool_use]) + end + if filter.item_type then + desc = to_title_case(df.item_type[filter.item_type]) + end + if filter.flags2 and filter.flags2.building_material then + desc = "Generic building material"; + if filter.flags2.fire_safe then + desc = "Fire-safe building material"; + end + if filter.flags2.magma_safe then + desc = "Magma-safe building material"; + end + elseif filter.vector_id then + desc = to_title_case(df.job_item_vector_id[filter.vector_id]) + end + + local quantity = filter.quantity or 1 + local dimx, dimy = get_cur_area_dims() + if quantity < 1 then + quantity = ((dimx * dimy) // 4) + 1 + else + quantity = quantity * dimx * dimy + end + return ('%s (need: %d)'):format(desc, quantity) +end + +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 1 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=function() return get_item_label(self.idx) end}} + }, + } +end PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ - default_pos={x=46,y=18}, + default_pos={x=6,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=30, h=4}, - frame_style=gui.MEDIUM_FRAME, + frame={w=54, h=9}, + frame_style=gui.PANEL_FRAME, frame_background=gui.CLEAR_PEN, } function PlannerOverlay:init() self:addviews{ - widgets.ToggleHotkeyLabel{ - frame={t=0, l=0}, - label='build when materials are available', - key='CUSTOM_CTRL_B', + widgets.Label{ + frame={}, + auto_width=true, + text='No items required.', + visible=function() return #get_cur_filters() == 0 end, }, - widgets.HotkeyLabel{ - frame={t=1, l=0}, - label='configure materials', - key='CUSTOM_CTRL_E', - on_activate=do_export, + ItemLine{frame={t=0, l=0}, idx=1}, + ItemLine{frame={t=2, l=0}, idx=2}, + ItemLine{frame={t=4, l=0}, idx=3}, + ItemLine{frame={t=6, l=0}, idx=4}, + widgets.Label{ + frame={b=0, l=17}, + text={ + 'Selected area: ', + {text=function() + return ('%d x %d'):format(get_cur_area_dims()) + end + }, + }, + visible=is_choosing_area, }, } end +function PlannerOverlay:do_config() + dfhack.run_script('gui/buildingplan') +end + +function PlannerOverlay:onInput(keys) + if not is_plannable() then return false end + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + return false + end + if PlannerOverlay.super.onInput(self, keys) then + return true + end + if keys._MOUSE_L_DOWN then + if is_over_direction_panel() then return false end + if self:getMouseFramePos() then return true end + if #uibs.errors > 0 then return true end + local pos = dfhack.gui.getMousePos() + if pos then + if is_choosing_area() or cur_building_has_no_area() then + if #get_cur_filters() == 0 then + return false -- we don't add value; let the game place it + end + self:place_building() + uibs.selection_pos:clear() + return true + elseif not is_choosing_area() then + return false + end + end + end + return keys._MOUSE_L +end + +function PlannerOverlay:render(dc) + if not is_plannable() then return end + PlannerOverlay.super.render(self, dc) +end + +local to_pen = dfhack.pen.parse +local GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, + tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} +local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, + tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} + +function PlannerOverlay:onRenderFrame(dc, rect) + PlannerOverlay.super.onRenderFrame(self, dc, rect) + + if not is_choosing_area() then return end + + local bounds = { + x1 = math.min(uibs.selection_pos.x, uibs.pos.x), + x2 = math.max(uibs.selection_pos.x, uibs.pos.x), + y1 = math.min(uibs.selection_pos.y, uibs.pos.y), + y2 = math.max(uibs.selection_pos.y, uibs.pos.y), + } + + local pen = #uibs.errors > 0 and BAD_PEN or GOOD_PEN + + local function get_overlay_pen(pos) + return pen + end + + guidm.renderMapOverlay(get_overlay_pen, bounds) +end + +function PlannerOverlay:place_building() + local direction = uibs.direction + local has_selection = is_choosing_area() + local width = has_selection and math.abs(uibs.selection_pos.x - uibs.pos.x) + 1 or 1 + local height = has_selection and math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 or 1 + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area + local pos = xyz2pos( + has_selection and math.min(uibs.selection_pos.x, uibs.pos.x) or uibs.pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, uibs.pos.y) or uibs.pos.y - adjusted_height//2, + uibs.pos.z + ) + local min_x, max_x = pos.x, pos.x + local min_y, max_y = pos.y, pos.y + if adjusted_width == 1 and adjusted_height == 1 and (width > 1 or height > 1) then + min_x = math.ceil(pos.x - width/2) + max_x = min_x + width - 1 + min_y = math.ceil(pos.y - height/2) + max_y = min_y + height - 1 + end + 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=adjusted_width, height=adjusted_height, direction=direction} + if err then + for _,b in ipairs(blds) do + dfhack.buildings.deconstruct(b) + end + dfhack.printerr(err) + return + 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 + for _,bld in ipairs(blds) do + addPlannedBuilding(bld) + end +end + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, @@ -134,48 +375,6 @@ OVERLAY_WIDGETS = { local dialogs = require('gui.dialogs') local guidm = require('gui.dwarfmode') -local function to_title_case(str) - str = str:gsub('(%a)([%w_]*)', - function (first, rest) return first:upper()..rest:lower() end) - str = str:gsub('_', ' ') - return str -end - -local function get_filter(btype, subtype, custom, reverse_idx) - local filters = dfhack.buildings.getFiltersByType( - {}, btype, subtype, custom) - if not filters or reverse_idx < 0 or reverse_idx >= #filters then - error(string.format('invalid index: %d', reverse_idx)) - end - return filters[#filters-reverse_idx] -end - --- returns a reasonable label for the item based on the qualities of the filter --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function get_item_label(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.has_tool_use then - return to_title_case(df.tool_uses[filter.has_tool_use]) - end - if filter.item_type then - return to_title_case(df.item_type[filter.item_type]) - end - if filter.flags2 and filter.flags2.building_material then - if filter.flags2.fire_safe then - return "Fire-safe building material"; - end - if filter.flags2.magma_safe then - return "Magma-safe building material"; - end - return "Generic building material"; - end - if filter.vector_id then - return to_title_case(df.job_item_vector_id[filter.vector_id]) - end - return "Unknown"; -end - -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. -- does not need the core suspended @@ -191,56 +390,6 @@ function item_can_be_improved(btype, subtype, custom, reverse_idx) filter.item_type ~= df.item_type.BOULDER end --- needs the core suspended --- 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.buildreq - local world = df.global.world - local direction = world.selected_direction - local _, width, height = dfhack.buildings.getCorrectSize( - world.building_width, world.building_height, uibs.building_type, - uibs.building_subtype, uibs.custom_type, direction) - -- the cursor is at the center of the building; we need the upper-left - -- corner of the building - local pos = guidm.getCursorPos() - pos.x = pos.x - math.floor(width/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 = min_x + world.building_width - 1 - min_y = math.ceil(pos.y - world.building_height/2) - max_y = min_y + world.building_height - 1 - end - 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 -- -- GlobalSettings dialog From dd6f71c665f568c04751fcff0d864ada1620bc0c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Feb 2023 02:10:07 -0800 Subject: [PATCH 04/47] handle stairs and 3 dimensions --- plugins/lua/buildingplan.lua | 133 ++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 27 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 4167ebb41..c06ddbc15 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -76,9 +76,10 @@ local function is_choosing_area() end local function get_cur_area_dims() - if not is_choosing_area() then return 1, 1 end + if not is_choosing_area() then return 1, 1, 1 end return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, - math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 + math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, + math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 end local function get_cur_filters() @@ -92,6 +93,11 @@ local function is_plannable() and uibs.building_subtype == df.construction_type.TrackNSEW) end +local function is_stairs() + return uibs.building_type == df.building_type.Construction + and uibs.building_subtype == df.construction_type.UpDownStair +end + local direction_panel_frame = {t=4, h=13, w=46, r=28} local direction_panel_types = utils.invert{ @@ -108,9 +114,23 @@ local function has_direction_panel() and uibs.building_subtype == df.trap_type.TrackStop) end -local function is_over_direction_panel() - if not has_direction_panel() then return false end - local v = widgets.Widget{frame=direction_panel_frame} +local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28} + +local function has_pressure_plate_panel() + return uibs.building_type == df.building_type.Trap + and uibs.building_subtype == df.trap_type.PressurePlate +end + +local function is_over_options_panel() + local frame = nil + if has_direction_panel() then + frame = direction_panel_frame + elseif has_pressure_plate_panel() then + frame = pressure_plate_panel_frame + else + return false + end + local v = widgets.Widget{frame=frame} local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) v:updateLayout(gui.ViewRect{rect=rect}) return v:getMousePos() @@ -146,11 +166,11 @@ function get_item_label(idx) end local quantity = filter.quantity or 1 - local dimx, dimy = get_cur_area_dims() + local dimx, dimy, dimz = get_cur_area_dims() if quantity < 1 then - quantity = ((dimx * dimy) // 4) + 1 + quantity = (((dimx * dimy) // 4) + 1) * dimz else - quantity = quantity * dimx * dimy + quantity = quantity * dimx * dimy * dimz end return ('%s (need: %d)'):format(desc, quantity) end @@ -193,12 +213,36 @@ function PlannerOverlay:init() ItemLine{frame={t=2, l=0}, idx=2}, ItemLine{frame={t=4, l=0}, idx=3}, ItemLine{frame={t=6, l=0}, idx=4}, + widgets.CycleHotkeyLabel{ + view_id="stairs_top_subtype", + frame={t=2, l=0}, + key="CUSTOM_R", + label="Top Stair Type: ", + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Down', value=df.construction_type.DownStair}, + }, + }, + widgets.CycleHotkeyLabel { + view_id="stairs_bottom_subtype", + frame={t=3, l=0}, + key="CUSTOM_B", + label="Bottom Stair Type: ", + visible=is_stairs, + options={ + {label='Auto', value='auto'}, + {label='UpDown', value=df.construction_type.UpDownStair}, + {label='Up', value=df.construction_type.UpStair}, + }, + }, widgets.Label{ frame={b=0, l=17}, text={ 'Selected area: ', {text=function() - return ('%d x %d'):format(get_cur_area_dims()) + return ('%d x %d x %d'):format(get_cur_area_dims()) end }, }, @@ -220,7 +264,7 @@ function PlannerOverlay:onInput(keys) return true end if keys._MOUSE_L_DOWN then - if is_over_direction_panel() then return false end + if is_over_options_panel() then return false end if self:getMouseFramePos() then return true end if #uibs.errors > 0 then return true end local pos = dfhack.gui.getMousePos() @@ -274,37 +318,72 @@ end function PlannerOverlay:place_building() local direction = uibs.direction - local has_selection = is_choosing_area() - local width = has_selection and math.abs(uibs.selection_pos.x - uibs.pos.x) + 1 or 1 - local height = has_selection and math.abs(uibs.selection_pos.y - uibs.pos.y) + 1 or 1 + local width, height, depth = get_cur_area_dims() local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) - -- get the upper-left corner of the building/area - local pos = xyz2pos( + -- get the upper-left corner of the building/area at min z-level + local has_selection = is_choosing_area() + local start_pos = xyz2pos( has_selection and math.min(uibs.selection_pos.x, uibs.pos.x) or uibs.pos.x - adjusted_width//2, has_selection and math.min(uibs.selection_pos.y, uibs.pos.y) or uibs.pos.y - adjusted_height//2, - uibs.pos.z + has_selection and math.min(uibs.selection_pos.z, uibs.pos.z) or uibs.pos.z ) - local min_x, max_x = pos.x, pos.x - local min_y, max_y = pos.y, pos.y - if adjusted_width == 1 and adjusted_height == 1 and (width > 1 or height > 1) then - min_x = math.ceil(pos.x - width/2) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 - min_y = math.ceil(pos.y - height/2) max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end 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), + local subtype = uibs.building_subtype + for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do + local pos = xyz2pos(x, y, z) + if is_stairs() then + if z == min_z then + subtype = self.subviews.stairs_bottom_subtype:getOptionValue() + if subtype == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape == df.tiletype_shape.STAIR_DOWN then + subtype = uibs.building_subtype + else + subtype = df.construction_type.UpStair + end + end + elseif z == max_z then + subtype = self.subviews.stairs_top_subtype:getOptionValue() + if subtype == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape == df.tiletype_shape.STAIR_UP then + subtype = uibs.building_subtype + else + subtype = df.construction_type.DownStair + end + end + else + subtype = uibs.building_subtype + end + end + local bld, err = dfhack.buildings.constructBuilding{pos=pos, + type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=adjusted_width, height=adjusted_height, direction=direction} if err then for _,b in ipairs(blds) do dfhack.buildings.deconstruct(b) end - dfhack.printerr(err) + dfhack.printerr(err .. (' (%d, %d, %d)'):format(pos.x, pos.y, pos.z)) return end -- assign fields for the types that need them. we can't pass them all in @@ -318,7 +397,7 @@ function PlannerOverlay:place_building() if k == 'speed' then bld.speed = uibs.speed end end table.insert(blds, bld) - end end + end end end for _,bld in ipairs(blds) do addPlannedBuilding(bld) end From 584e891154239ae26bdeaa03fd9a2f1eadcc19b3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Feb 2023 02:21:19 -0800 Subject: [PATCH 05/47] more skeleton for inspector --- plugins/lua/buildingplan.lua | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index c06ddbc15..d9bcdb0d3 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -197,7 +197,7 @@ PlannerOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=54, h=9}, - frame_style=gui.PANEL_FRAME, + frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -408,7 +408,7 @@ InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=5}, + frame={w=30, h=9}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -421,10 +421,27 @@ function InspectorOverlay:init() }, widgets.Label{ frame={t=1, l=0}, - text='items', + text='item1', }, - widgets.HotkeyLabel{ + widgets.Label{ frame={t=2, l=0}, + text='item2', + }, + widgets.Label{ + frame={t=3, l=0}, + text='item3', + }, + widgets.Label{ + frame={t=4, l=0}, + text='item4', + }, + widgets.HotkeyLabel{ + frame={t=5, l=0}, + label='adjust filters', + key='CUSTOM_CTRL_F', + }, + widgets.HotkeyLabel{ + frame={t=6, l=0}, label='make top priority', key='CUSTOM_CTRL_T', }, From c490be0271eb1a2ba5362bef55d73773aac16cee Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Feb 2023 17:53:22 -0800 Subject: [PATCH 06/47] mark as tested to facilitate testing the commandline --- docs/plugins/buildingplan.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index c51f79721..a331e9eb8 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -3,7 +3,7 @@ buildingplan .. dfhack-tool:: :summary: Plan building construction before you have materials. - :tags: untested fort design buildings + :tags: fort design buildings This plugin adds a planning mode for building placement. You can then place furniture, constructions, and other buildings before the required materials are From a9d9e0e50c24d41dc27774b90142ac4e56b2c9ef Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Feb 2023 16:24:10 -0800 Subject: [PATCH 07/47] skeleton for quantity scanning --- plugins/buildingplan.cpp | 6 ++++++ plugins/lua/buildingplan.lua | 38 ++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index 81f026cbc..a76e81d4a 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -707,6 +707,11 @@ static void scheduleCycle(color_ostream &out) { cycle_requested = true; } +static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering countAvailableItems\n"); + return 10; +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(printStatus), DFHACK_LUA_FUNCTION(setSetting), @@ -715,5 +720,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(addPlannedBuilding), DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), + DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index d9bcdb0d3..5da03e38f 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -143,8 +143,7 @@ local function to_title_case(str) return str end --- returns a reasonable label for the item based on the qualities of the filter -function get_item_label(idx) +function get_item_line_text(idx) local filter = get_cur_filters()[idx] local desc = 'Unknown' if filter.has_tool_use then @@ -154,17 +153,24 @@ function get_item_label(idx) desc = to_title_case(df.item_type[filter.item_type]) end if filter.flags2 and filter.flags2.building_material then - desc = "Generic building material"; + desc = "Generic material"; if filter.flags2.fire_safe then - desc = "Fire-safe building material"; + desc = "Fire-safe material"; end if filter.flags2.magma_safe then - desc = "Magma-safe building material"; + desc = "Magma-safe material"; end elseif filter.vector_id then desc = to_title_case(df.job_item_vector_id[filter.vector_id]) end + if desc:endswith('s') then + desc = desc:sub(1,-2) + end + if desc == 'Trappart' then + desc = 'Mechanism' + end + local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims() if quantity < 1 then @@ -172,7 +178,14 @@ function get_item_label(idx) else quantity = quantity * dimx * dimy * dimz end - return ('%s (need: %d)'):format(desc, quantity) + desc = ('%d %s%s'):format(quantity, desc, quantity == 1 and '' or 's') + + local available = countAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, idx - 1) + local note = available >= quantity and + 'Can build now' or 'Will wait for item' + + return ('%-21s%s%s'):format(desc:sub(1,21), (' '):rep(13), note) end ItemLine = defclass(ItemLine, widgets.Panel) @@ -186,7 +199,11 @@ function ItemLine:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={{text=function() return get_item_label(self.idx) end}} + text={{text=function() return get_item_line_text(self.idx) end}}, + }, + widgets.Label{ + frame={t=0, l=22}, + text='[filter][x]', }, } end @@ -215,7 +232,7 @@ function PlannerOverlay:init() ItemLine{frame={t=6, l=0}, idx=4}, widgets.CycleHotkeyLabel{ view_id="stairs_top_subtype", - frame={t=2, l=0}, + frame={t=3, l=0}, key="CUSTOM_R", label="Top Stair Type: ", visible=is_stairs, @@ -227,7 +244,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id="stairs_bottom_subtype", - frame={t=3, l=0}, + frame={t=4, l=0}, key="CUSTOM_B", label="Bottom Stair Type: ", visible=is_stairs, @@ -242,7 +259,7 @@ function PlannerOverlay:init() text={ 'Selected area: ', {text=function() - return ('%d x %d x %d'):format(get_cur_area_dims()) + return ('%dx%dx%d'):format(get_cur_area_dims()) end }, }, @@ -401,6 +418,7 @@ function PlannerOverlay:place_building() for _,bld in ipairs(blds) do addPlannedBuilding(bld) end + scheduleCycle() end InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) From 4b7bc937a41c9eb889e59b19f93837629e249d06 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Feb 2023 17:43:03 -0800 Subject: [PATCH 08/47] remove old buildingplan files --- plugins/buildingplan/buildingplan-planner.cpp | 1074 --------------- plugins/buildingplan/buildingplan-planner.h | 140 -- plugins/buildingplan/buildingplan-rooms.cpp | 226 ---- plugins/buildingplan/buildingplan-rooms.h | 51 - plugins/buildingplan/buildingplan.cpp | 1168 ----------------- plugins/buildingplan/buildingplan.h | 8 - 6 files changed, 2667 deletions(-) delete mode 100644 plugins/buildingplan/buildingplan-planner.cpp delete mode 100644 plugins/buildingplan/buildingplan-planner.h delete mode 100644 plugins/buildingplan/buildingplan-rooms.cpp delete mode 100644 plugins/buildingplan/buildingplan-rooms.h delete mode 100644 plugins/buildingplan/buildingplan.cpp delete mode 100644 plugins/buildingplan/buildingplan.h diff --git a/plugins/buildingplan/buildingplan-planner.cpp b/plugins/buildingplan/buildingplan-planner.cpp deleted file mode 100644 index 07f23150a..000000000 --- a/plugins/buildingplan/buildingplan-planner.cpp +++ /dev/null @@ -1,1074 +0,0 @@ -#include -#include // for CHAR_BIT - -#include "df/building_design.h" -#include "df/building_doorst.h" -#include "df/building_type.h" -#include "df/general_ref_building_holderst.h" -#include "df/job_item.h" -#include "df/buildreq.h" - -#include "modules/Buildings.h" -#include "modules/Gui.h" -#include "modules/Job.h" - -#include "LuaTools.h" -#include "../uicommon.h" - -#include "buildingplan.h" - -static const std::string planned_building_persistence_key_v1 = "buildingplan/constraints"; -static const std::string planned_building_persistence_key_v2 = "buildingplan/constraints2"; -static const std::string global_settings_persistence_key = "buildingplan/global"; - -/* - * ItemFilter - */ - -ItemFilter::ItemFilter() -{ - clear(); -} - -void ItemFilter::clear() -{ - min_quality = df::item_quality::Ordinary; - max_quality = df::item_quality::Masterful; - decorated_only = false; - clearMaterialMask(); - materials.clear(); -} - -bool ItemFilter::deserialize(std::string ser) -{ - clear(); - - std::vector tokens; - split_string(&tokens, ser, "/"); - if (tokens.size() != 5) - { - debug("invalid ItemFilter serialization: '%s'", ser.c_str()); - return false; - } - - if (!deserializeMaterialMask(tokens[0]) || !deserializeMaterials(tokens[1])) - return false; - - setMinQuality(atoi(tokens[2].c_str())); - setMaxQuality(atoi(tokens[3].c_str())); - decorated_only = static_cast(atoi(tokens[4].c_str())); - return true; -} - -bool ItemFilter::deserializeMaterialMask(std::string ser) -{ - if (ser.empty()) - return true; - - if (!parseJobMaterialCategory(&mat_mask, ser)) - { - debug("invalid job material category serialization: '%s'", ser.c_str()); - return false; - } - return true; -} - -bool ItemFilter::deserializeMaterials(std::string ser) -{ - if (ser.empty()) - return true; - - std::vector mat_names; - split_string(&mat_names, ser, ","); - for (auto m = mat_names.begin(); m != mat_names.end(); m++) - { - DFHack::MaterialInfo material; - if (!material.find(*m) || !material.isValid()) - { - debug("invalid material name serialization: '%s'", ser.c_str()); - return false; - } - materials.push_back(material); - } - return true; -} - -// format: mat,mask,elements/materials,list/minq/maxq/decorated -std::string ItemFilter::serialize() const -{ - std::ostringstream ser; - ser << bitfield_to_string(mat_mask, ",") << "/"; - if (!materials.empty()) - { - ser << materials[0].getToken(); - for (size_t i = 1; i < materials.size(); ++i) - ser << "," << materials[i].getToken(); - } - ser << "/" << static_cast(min_quality); - ser << "/" << static_cast(max_quality); - ser << "/" << static_cast(decorated_only); - return ser.str(); -} - -void ItemFilter::clearMaterialMask() -{ - mat_mask.whole = 0; -} - -void ItemFilter::addMaterialMask(uint32_t mask) -{ - mat_mask.whole |= mask; -} - -void ItemFilter::setMaterials(std::vector materials) -{ - this->materials = materials; -} - -static void clampItemQuality(df::item_quality *quality) -{ - if (*quality > item_quality::Artifact) - { - debug("clamping quality to Artifact"); - *quality = item_quality::Artifact; - } - if (*quality < item_quality::Ordinary) - { - debug("clamping quality to Ordinary"); - *quality = item_quality::Ordinary; - } -} - -void ItemFilter::setMinQuality(int quality) -{ - min_quality = static_cast(quality); - clampItemQuality(&min_quality); - if (max_quality < min_quality) - max_quality = min_quality; -} - -void ItemFilter::setMaxQuality(int quality) -{ - max_quality = static_cast(quality); - clampItemQuality(&max_quality); - if (max_quality < min_quality) - min_quality = max_quality; -} - -void ItemFilter::incMinQuality() { setMinQuality(min_quality + 1); } -void ItemFilter::decMinQuality() { setMinQuality(min_quality - 1); } -void ItemFilter::incMaxQuality() { setMaxQuality(max_quality + 1); } -void ItemFilter::decMaxQuality() { setMaxQuality(max_quality - 1); } - -void ItemFilter::toggleDecoratedOnly() { decorated_only = !decorated_only; } - -static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } - -uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; } - -std::vector ItemFilter::getMaterials() const -{ - std::vector descriptions; - transform_(materials, descriptions, material_to_string_fn); - - if (descriptions.size() == 0) - bitfield_to_string(&descriptions, mat_mask); - - if (descriptions.size() == 0) - descriptions.push_back("any"); - - return descriptions; -} - -std::string ItemFilter::getMinQuality() const -{ - return ENUM_KEY_STR(item_quality, min_quality); -} - -std::string ItemFilter::getMaxQuality() const -{ - return ENUM_KEY_STR(item_quality, max_quality); -} - -bool ItemFilter::getDecoratedOnly() const -{ - return decorated_only; -} - -bool ItemFilter::matchesMask(DFHack::MaterialInfo &mat) const -{ - return mat_mask.whole ? mat.matches(mat_mask) : true; -} - -bool ItemFilter::matches(df::dfhack_material_category mask) const -{ - return mask.whole & mat_mask.whole; -} - -bool ItemFilter::matches(DFHack::MaterialInfo &material) const -{ - for (auto it = materials.begin(); it != materials.end(); ++it) - if (material.matches(*it)) - return true; - return false; -} - -bool ItemFilter::matches(df::item *item) const -{ - if (item->getQuality() < min_quality || item->getQuality() > max_quality) - return false; - - if (decorated_only && !item->hasImprovements()) - return false; - - auto imattype = item->getActualMaterial(); - auto imatindex = item->getActualMaterialIndex(); - auto item_mat = DFHack::MaterialInfo(imattype, imatindex); - - return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat); -} - - -/* - * PlannedBuilding - */ - -// format: itemfilterser|itemfilterser|... -static std::string serializeFilters(const std::vector &filters) -{ - std::ostringstream ser; - if (!filters.empty()) - { - ser << filters[0].serialize(); - for (size_t i = 1; i < filters.size(); ++i) - ser << "|" << filters[i].serialize(); - } - return ser.str(); -} - -static std::vector deserializeFilters(std::string ser) -{ - std::vector isers; - split_string(&isers, ser, "|"); - std::vector ret; - for (auto & iser : isers) - { - ItemFilter filter; - if (filter.deserialize(iser)) - ret.push_back(filter); - } - return ret; -} - -static size_t getNumFilters(BuildingTypeKey key) -{ - 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", "get_num_filters")) - { - debug("failed to push the lua method on the stack"); - return 0; - } - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - - if (!Lua::SafeCall(out, L, 3, 1)) - { - debug("lua call failed"); - return 0; - } - - int num_filters = lua_tonumber(L, -1); - lua_pop(L, 1); - return num_filters; -} - -PlannedBuilding::PlannedBuilding(df::building *building, const std::vector &filters) - : building(building), - building_id(building->id), - filters(filters) -{ - config = DFHack::World::AddPersistentData(planned_building_persistence_key_v2); - config.ival(0) = building_id; - config.val() = serializeFilters(filters); -} - -PlannedBuilding::PlannedBuilding(PersistentDataItem &config) - : config(config), - building(df::building::find(config.ival(0))), - building_id(config.ival(0)), - filters(deserializeFilters(config.val())) -{ - if (building) - { - if (filters.size() != - getNumFilters(toBuildingTypeKey(building))) - { - debug("invalid ItemFilter vector serialization: '%s'", - config.val().c_str()); - building = NULL; - } - } -} - -// Ensure the building still exists and is in a valid state. It can disappear -// for lots of reasons, such as running the game with the buildingplan plugin -// disabled, manually removing the building, modifying it via the API, etc. -bool PlannedBuilding::isValid() const -{ - return building && df::building::find(building_id) - && building->getBuildStage() == 0; -} - -void PlannedBuilding::remove() -{ - DFHack::World::DeletePersistentData(config); - building = NULL; -} - -df::building * PlannedBuilding::getBuilding() -{ - return building; -} - -const std::vector & PlannedBuilding::getFilters() const -{ - // if we want to be able to dynamically change the filters, we'll need to - // re-bucket the tasks in Planner. - return filters; -} - - -/* - * BuildingTypeKey - */ - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom) -{ - return std::make_tuple(btype, subtype, custom); -} - -BuildingTypeKey toBuildingTypeKey(df::building *bld) -{ - return std::make_tuple( - bld->getType(), bld->getSubtype(), bld->getCustomType()); -} - -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs) -{ - return std::make_tuple( - uibs->building_type, uibs->building_subtype, uibs->custom_type); -} - -// rotates a size_t value left by count bits -// assumes count is not 0 or >= size_t_bits -// replace this with std::rotl when we move to C++20 -static std::size_t rotl_size_t(size_t val, uint32_t count) -{ - static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); - return val << count | val >> (size_t_bits - count); -} - -std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const -{ - // cast first param to appease gcc-4.8, which is missing the enum - // specializations for std::hash - std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); - std::size_t h2 = std::hash()(std::get<1>(key)); - std::size_t h3 = std::hash()(std::get<2>(key)); - - return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); -} - - -/* - * Planner - */ - -// convert v1 persistent data into v2 format -// we can remove this conversion code once v2 has been live for a while -void migrateV1ToV2() -{ - std::vector configs; - DFHack::World::GetPersistentData(&configs, planned_building_persistence_key_v1); - if (configs.empty()) - return; - - debug("migrating %zu persisted configs to new format", configs.size()); - for (auto config : configs) - { - df::building *bld = df::building::find(config.ival(1)); - if (!bld) - { - debug("buliding no longer exists; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - if (bld->getBuildStage() != 0 || bld->jobs.size() != 1 - || bld->jobs[0]->job_items.size() != 1) - { - debug("building in invalid state; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - // fix up the building so we can set the material properties later - bld->mat_type = -1; - bld->mat_index = -1; - - // the v1 filters are not initialized correctly and will match any item. - // we need to fix them up a bit. - auto filter = bld->jobs[0]->job_items[0]; - df::item_type type; - switch (bld->getType()) - { - case df::building_type::Armorstand: type = df::item_type::ARMORSTAND; break; - case df::building_type::Bed: type = df::item_type::BED; break; - case df::building_type::Chair: type = df::item_type::CHAIR; break; - case df::building_type::Coffin: type = df::item_type::COFFIN; break; - case df::building_type::Door: type = df::item_type::DOOR; break; - case df::building_type::Floodgate: type = df::item_type::FLOODGATE; break; - case df::building_type::Hatch: type = df::item_type::HATCH_COVER; break; - case df::building_type::GrateWall: type = df::item_type::GRATE; break; - case df::building_type::GrateFloor: type = df::item_type::GRATE; break; - case df::building_type::BarsVertical: type = df::item_type::BAR; break; - case df::building_type::BarsFloor: type = df::item_type::BAR; break; - case df::building_type::Cabinet: type = df::item_type::CABINET; break; - case df::building_type::Box: type = df::item_type::BOX; break; - case df::building_type::Weaponrack: type = df::item_type::WEAPONRACK; break; - case df::building_type::Statue: type = df::item_type::STATUE; break; - case df::building_type::Slab: type = df::item_type::SLAB; break; - case df::building_type::Table: type = df::item_type::TABLE; break; - case df::building_type::WindowGlass: type = df::item_type::WINDOW; break; - case df::building_type::AnimalTrap: type = df::item_type::ANIMALTRAP; break; - case df::building_type::Chain: type = df::item_type::CHAIN; break; - case df::building_type::Cage: type = df::item_type::CAGE; break; - case df::building_type::TractionBench: type = df::item_type::TRACTION_BENCH; break; - default: - debug("building has unhandled type; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - filter->item_type = type; - filter->item_subtype = -1; - filter->mat_type = -1; - filter->mat_index = -1; - filter->flags1.whole = 0; - filter->flags2.whole = 0; - filter->flags2.bits.allow_artifact = true; - filter->flags3.whole = 0; - filter->flags4 = 0; - filter->flags5 = 0; - filter->metal_ore = -1; - filter->min_dimension = -1; - filter->has_tool_use = df::tool_uses::NONE; - filter->quantity = 1; - - std::vector tokens; - split_string(&tokens, config.val(), "/"); - if (tokens.size() != 2) - { - debug("invalid v1 format; removing config"); - DFHack::World::DeletePersistentData(config); - continue; - } - - ItemFilter item_filter; - item_filter.deserializeMaterialMask(tokens[0]); - item_filter.deserializeMaterials(tokens[1]); - item_filter.setMinQuality(config.ival(2) - 1); - item_filter.setMaxQuality(config.ival(4) - 1); - if (config.ival(3) - 1) - item_filter.toggleDecoratedOnly(); - - // create the v2 record - std::vector item_filters; - item_filters.push_back(item_filter); - PlannedBuilding pb(bld, item_filters); - - // remove the v1 record - DFHack::World::DeletePersistentData(config); - debug("v1 %s(%d) record successfully migrated", - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - bld->id); - } -} - -// assumes no setting has '=' or '|' characters -static std::string serialize_settings(std::map & settings) -{ - std::ostringstream ser; - for (auto & entry : settings) - { - ser << entry.first << "=" << (entry.second ? "1" : "0") << "|"; - } - return ser.str(); -} - -static void deserialize_settings(std::map & settings, - std::string ser) -{ - std::vector tokens; - split_string(&tokens, ser, "|"); - for (auto token : tokens) - { - if (token.empty()) - continue; - - std::vector parts; - split_string(&parts, token, "="); - if (parts.size() != 2) - { - debug("invalid serialized setting format: '%s'", token.c_str()); - continue; - } - std::string key = parts[0]; - if (settings.count(key) == 0) - { - debug("unknown serialized setting: '%s", key.c_str()); - continue; - } - settings[key] = static_cast(atoi(parts[1].c_str())); - debug("deserialized setting: %s = %d", key.c_str(), settings[key]); - } -} - -static DFHack::PersistentDataItem init_global_settings( - std::map & settings) -{ - settings.clear(); - settings["blocks"] = true; - settings["boulders"] = true; - settings["logs"] = true; - settings["bars"] = false; - - // load persistent global settings if they exist; otherwise create them - std::vector items; - DFHack::World::GetPersistentData(&items, global_settings_persistence_key); - if (items.size() == 1) - { - DFHack::PersistentDataItem & config = items[0]; - deserialize_settings(settings, config.val()); - return config; - } - - debug("initializing persistent global settings"); - DFHack::PersistentDataItem config = - DFHack::World::AddPersistentData(global_settings_persistence_key); - config.val() = serialize_settings(settings); - return config; -} - -const std::map & Planner::getGlobalSettings() const -{ - return global_settings; -} - -bool Planner::setGlobalSetting(std::string name, bool value) -{ - if (global_settings.count(name) == 0) - { - debug("attempted to set invalid setting: '%s'", name.c_str()); - return false; - } - debug("global setting '%s' %d -> %d", - name.c_str(), global_settings[name], value); - global_settings[name] = value; - if (config.isValid()) - config.val() = serialize_settings(global_settings); - return true; -} - -void Planner::reset() -{ - debug("resetting Planner state"); - default_item_filters.clear(); - planned_buildings.clear(); - tasks.clear(); - - config = init_global_settings(global_settings); - - migrateV1ToV2(); - - std::vector items; - DFHack::World::GetPersistentData(&items, planned_building_persistence_key_v2); - debug("found data for %zu planned building(s)", items.size()); - - for (auto i = items.begin(); i != items.end(); i++) - { - PlannedBuilding pb(*i); - if (!pb.isValid()) - { - debug("discarding invalid planned building"); - pb.remove(); - continue; - } - - if (registerTasks(pb)) - planned_buildings.insert(std::make_pair(pb.getBuilding()->id, pb)); - } -} - -void Planner::addPlannedBuilding(df::building *bld) -{ - auto item_filters = getItemFilters(toBuildingTypeKey(bld)).get(); - // not a supported type - if (item_filters.empty()) - { - debug("failed to add building: unsupported type"); - return; - } - - // protect against multiple registrations - if (planned_buildings.count(bld->id) != 0) - { - debug("failed to add building: already registered"); - return; - } - - PlannedBuilding pb(bld, item_filters); - if (pb.isValid() && registerTasks(pb)) - { - for (auto job : bld->jobs) - job->flags.bits.suspend = true; - - planned_buildings.insert(std::make_pair(bld->id, pb)); - } - else - { - pb.remove(); - } -} - -static std::string getBucket(const df::job_item & ji, - const std::vector & item_filters) -{ - std::ostringstream ser; - - // pull out and serialize only known relevant fields. if we miss a few, then - // the filter bucket will be slighly less specific than it could be, but - // that's probably ok. we'll just end up bucketing slightly different items - // together. this is only a problem if the different filter at the front of - // the queue doesn't match any available items and blocks filters behind it - // that could be matched. - ser << ji.item_type << ':' << ji.item_subtype << ':' << ji.mat_type << ':' - << ji.mat_index << ':' << ji.flags1.whole << ':' << ji.flags2.whole - << ':' << ji.flags3.whole << ':' << ji.flags4 << ':' << ji.flags5 << ':' - << ji.metal_ore << ':' << ji.has_tool_use; - - for (auto & item_filter : item_filters) - { - ser << ':' << item_filter.serialize(); - } - - return ser.str(); -} - -// get a list of item vectors that we should search for matches -static std::vector getVectorIds(df::job_item *job_item, - const std::map & global_settings) -{ - std::vector ret; - - // if the filter already has the vector_id set to something specific, use it - if (job_item->vector_id > df::job_item_vector_id::IN_PLAY) - { - debug("using vector_id from job_item: %s", - ENUM_KEY_STR(job_item_vector_id, job_item->vector_id).c_str()); - ret.push_back(job_item->vector_id); - return ret; - } - - // if the filer is for building material, refer to our global settings for - // which vectors to search - if (job_item->flags2.bits.building_material) - { - if (global_settings.at("blocks")) - ret.push_back(df::job_item_vector_id::BLOCKS); - if (global_settings.at("boulders")) - ret.push_back(df::job_item_vector_id::BOULDER); - if (global_settings.at("logs")) - ret.push_back(df::job_item_vector_id::WOOD); - if (global_settings.at("bars")) - ret.push_back(df::job_item_vector_id::BAR); - } - - // fall back to IN_PLAY if no other vector was appropriate - if (ret.empty()) - ret.push_back(df::job_item_vector_id::IN_PLAY); - return ret; -} - -bool Planner::registerTasks(PlannedBuilding & pb) -{ - df::building * bld = pb.getBuilding(); - if (bld->jobs.size() != 1) - { - debug("unexpected number of jobs: want 1, got %zu", bld->jobs.size()); - return false; - } - auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) - { - debug("unexpected number of job items: want >0, got %d", num_job_items); - return false; - } - int32_t id = bld->id; - for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) - { - auto job_item = job_items[job_item_idx]; - auto bucket = getBucket(*job_item, pb.getFilters()); - auto vector_ids = getVectorIds(job_item, global_settings); - - // if there are multiple vector_ids, schedule duplicate tasks. after - // the correct number of items are matched, the extras will get popped - // as invalid - for (auto vector_id : vector_ids) - { - for (int item_num = 0; item_num < job_item->quantity; ++item_num) - { - tasks[vector_id][bucket].push(std::make_pair(id, job_item_idx)); - debug("added task: %s/%s/%d,%d; " - "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket.c_str(), id, job_item_idx, tasks.size(), - tasks[vector_id].size(), tasks[vector_id][bucket].size()); - } - } - } - return true; -} - -PlannedBuilding * Planner::getPlannedBuilding(df::building *bld) -{ - if (!bld || planned_buildings.count(bld->id) == 0) - return NULL; - return &planned_buildings.at(bld->id); -} - -bool Planner::isPlannableBuilding(BuildingTypeKey key) -{ - return getNumFilters(key) >= 1; -} - -Planner::ItemFiltersWrapper Planner::getItemFilters(BuildingTypeKey key) -{ - static std::vector empty_vector; - static const ItemFiltersWrapper empty_ret(empty_vector); - - size_t nfilters = getNumFilters(key); - if (nfilters < 1) - return empty_ret; - while (default_item_filters[key].size() < nfilters) - default_item_filters[key].push_back(ItemFilter()); - return ItemFiltersWrapper(default_item_filters[key]); -} - -// precompute a bitmask with bad item flags -struct BadFlags -{ - uint32_t whole; - - BadFlags() - { - df::item_flags flags; - #define F(x) flags.bits.x = true; - F(dump); F(forbid); F(garbage_collect); - F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(construction); F(in_job); - F(owned); F(in_chest); F(removed); F(encased); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) -{ - static BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile() - // TODO: make this configurable - && !(item->getType() == df::item_type::BOX && item->isBag()); -} - -static bool matchesFilters(df::item * item, - df::job_item * job_item, - const ItemFilter & item_filter) -{ - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()) - && item_filter.matches(item); -} - -// note that this just removes the PlannedBuilding. the tasks will get dropped -// as we discover them in the tasks queues and they fail their isValid() check. -// this "lazy" task cleaning algorithm works because there is no way to -// re-register a building once it has been removed -- if it fails isValid() -// then it has either been built or desroyed. therefore there is no chance of -// duplicate tasks getting added to the tasks queues. -void Planner::unregisterBuilding(int32_t id) -{ - if (planned_buildings.count(id) > 0) - { - planned_buildings.at(id).remove(); - planned_buildings.erase(id); - } -} - -static bool isJobReady(df::job * job) -{ - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) - { - debug("building needs %d more item(s)", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) -{ - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(df::building * bld) -{ - debug("finalizing building %d", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item, though if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) - { - df::item *item = attached_item->item; - rough = rough || item->getType() == item_type::BOULDER; - if (bld->mat_type == -1) - { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) - { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) - { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -void Planner::popInvalidTasks(std::queue> & task_queue) -{ - while (!task_queue.empty()) - { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) - { - PlannedBuilding & pb = planned_buildings.at(id); - if (pb.isValid() && - pb.getBuilding()->jobs[0]->job_items[task.second]->quantity) - { - break; - } - } - debug("discarding invalid task: bld=%d, job_item_idx=%d", - id, task.second); - task_queue.pop(); - unregisterBuilding(id); - } -} - -void Planner::doVector(df::job_item_vector_id vector_id, - std::map>> & buckets) -{ - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - debug("matching %zu item(s) in vector %s against %zu filter bucket(s)", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) - { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end();) - { - auto & task_queue = bucket_it->second; - popInvalidTasks(task_queue); - if (task_queue.empty()) - { - debug("removing empty bucket: %s/%s; %zu bucket(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto & pb = planned_buildings.at(id); - auto building = pb.getBuilding(); - auto job = building->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], - pb.getFilters()[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - debug("attached %s %s to filter %d for %s(%d): %s/%s", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, building->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop(); - if (isJobReady(job)) - { - finalizeBuilding(building); - unregisterBuilding(id); - } - if (task_queue.empty()) - { - debug( - "removing empty item bucket: %s/%s; %zu left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast -{ - std::vector vectors; - VectorsToScanLast() - { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -void Planner::doCycle() -{ - debug("running cycle for %zu registered building(s)", - planned_buildings.size()); - static const VectorsToScanLast vectors_to_scan_last; - for (auto it = tasks.begin(); it != tasks.end();) - { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) - { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) - { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(vector_id, buckets); - if (buckets.empty()) - { - debug("removing empty vector: %s; %zu vector(s) left", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - debug("cycle done; %zu registered building(s) left", - planned_buildings.size()); -} - -Planner planner; diff --git a/plugins/buildingplan/buildingplan-planner.h b/plugins/buildingplan/buildingplan-planner.h deleted file mode 100644 index 7b1615704..000000000 --- a/plugins/buildingplan/buildingplan-planner.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include -#include - -#include "df/building.h" -#include "df/dfhack_material_category.h" -#include "df/item_quality.h" -#include "df/job_item.h" - -#include "modules/Materials.h" -#include "modules/Persistence.h" - -class ItemFilter -{ -public: - ItemFilter(); - - void clear(); - bool deserialize(std::string ser); - std::string serialize() const; - - void addMaterialMask(uint32_t mask); - void clearMaterialMask(); - void setMaterials(std::vector materials); - - void incMinQuality(); - void decMinQuality(); - void incMaxQuality(); - void decMaxQuality(); - void toggleDecoratedOnly(); - - uint32_t getMaterialMask() const; - std::vector getMaterials() const; - std::string getMinQuality() const; - std::string getMaxQuality() const; - bool getDecoratedOnly() const; - - bool matches(df::dfhack_material_category mask) const; - bool matches(DFHack::MaterialInfo &material) const; - bool matches(df::item *item) const; - -private: - // remove friend declaration when we no longer need v1 deserialization - friend void migrateV1ToV2(); - - df::dfhack_material_category mat_mask; - std::vector materials; - df::item_quality min_quality; - df::item_quality max_quality; - bool decorated_only; - - bool deserializeMaterialMask(std::string ser); - bool deserializeMaterials(std::string ser); - void setMinQuality(int quality); - void setMaxQuality(int quality); - bool matchesMask(DFHack::MaterialInfo &mat) const; -}; - -class PlannedBuilding -{ -public: - PlannedBuilding(df::building *building, const std::vector &filters); - PlannedBuilding(DFHack::PersistentDataItem &config); - - bool isValid() const; - void remove(); - - df::building * getBuilding(); - const std::vector & getFilters() const; - -private: - DFHack::PersistentDataItem config; - df::building *building; - const df::building::key_field_type building_id; - const std::vector filters; -}; - -// building type, subtype, custom -typedef std::tuple BuildingTypeKey; - -BuildingTypeKey toBuildingTypeKey( - df::building_type btype, int16_t subtype, int32_t custom); -BuildingTypeKey toBuildingTypeKey(df::building *bld); -BuildingTypeKey toBuildingTypeKey(df::ui_build_selector *uibs); - -struct BuildingTypeKeyHash -{ - std::size_t operator() (const BuildingTypeKey & key) const; -}; - -class Planner -{ -public: - class ItemFiltersWrapper - { - public: - ItemFiltersWrapper(std::vector & item_filters) - : item_filters(item_filters) { } - std::vector::reverse_iterator rbegin() const { return item_filters.rbegin(); } - std::vector::reverse_iterator rend() const { return item_filters.rend(); } - const std::vector & get() const { return item_filters; } - private: - std::vector &item_filters; - }; - - const std::map & getGlobalSettings() const; - bool setGlobalSetting(std::string name, bool value); - - void reset(); - - void addPlannedBuilding(df::building *bld); - PlannedBuilding *getPlannedBuilding(df::building *bld); - - bool isPlannableBuilding(BuildingTypeKey key); - - // returns an empty vector if the type is not supported - ItemFiltersWrapper getItemFilters(BuildingTypeKey key); - - void doCycle(); - -private: - DFHack::PersistentDataItem config; - std::map global_settings; - std::unordered_map, - BuildingTypeKeyHash> default_item_filters; - // building id -> PlannedBuilding - std::unordered_map planned_buildings; - // vector id -> filter bucket -> queue of (building id, job_item index) - std::map>>> tasks; - - bool registerTasks(PlannedBuilding &plannedBuilding); - void unregisterBuilding(int32_t id); - void popInvalidTasks(std::queue> &task_queue); - void doVector(df::job_item_vector_id vector_id, - std::map>> & buckets); -}; - -extern Planner planner; diff --git a/plugins/buildingplan/buildingplan-rooms.cpp b/plugins/buildingplan/buildingplan-rooms.cpp deleted file mode 100644 index a08c85804..000000000 --- a/plugins/buildingplan/buildingplan-rooms.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "buildingplan.h" - -#include -#include -#include - -#include -#include -#include - -using namespace DFHack; - -bool canReserveRoom(df::building *building) -{ - if (!building) - return false; - - if (building->jobs.size() > 0 && building->jobs[0]->job_type == df::job_type::DestroyBuilding) - return false; - - return building->is_room; -} - -std::vector getUniqueNoblePositions(df::unit *unit) -{ - std::vector np; - Units::getNoblePositions(&np, unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == "MILITIA_CAPTAIN") - { - np.erase(iter); - break; - } - } - - return np; -} - -/* - * ReservedRoom - */ - -ReservedRoom::ReservedRoom(df::building *building, std::string noble_code) -{ - this->building = building; - config = DFHack::World::AddPersistentData("buildingplan/reservedroom"); - config.val() = noble_code; - config.ival(1) = building->id; - pos = df::coord(building->centerx, building->centery, building->z); -} - -ReservedRoom::ReservedRoom(PersistentDataItem &config, color_ostream &) -{ - this->config = config; - - building = df::building::find(config.ival(1)); - if (!building) - return; - pos = df::coord(building->centerx, building->centery, building->z); -} - -bool ReservedRoom::checkRoomAssignment() -{ - if (!isValid()) - return false; - - auto np = getOwnersNobleCode(); - bool correctOwner = false; - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - correctOwner = true; - break; - } - } - - if (correctOwner) - return true; - - for (auto iter = df::global::world->units.active.begin(); iter != df::global::world->units.active.end(); iter++) - { - df::unit* unit = *iter; - if (!Units::isCitizen(unit)) - continue; - - if (!Units::isActive(unit)) - continue; - - np = getUniqueNoblePositions(unit); - for (auto iter = np.begin(); iter != np.end(); iter++) - { - if (iter->position->code == getCode()) - { - Buildings::setOwner(building, unit); - break; - } - } - } - - return true; -} - -void ReservedRoom::remove() { DFHack::World::DeletePersistentData(config); } - -bool ReservedRoom::isValid() -{ - if (!building) - return false; - - if (Buildings::findAtTile(pos) != building) - return false; - - return canReserveRoom(building); -} - -int32_t ReservedRoom::getId() -{ - if (!isValid()) - return 0; - - return building->id; -} - -std::string ReservedRoom::getCode() { return config.val(); } - -void ReservedRoom::setCode(const std::string &noble_code) { config.val() = noble_code; } - -std::vector ReservedRoom::getOwnersNobleCode() -{ - if (!building->owner) - return std::vector (); - - return getUniqueNoblePositions(building->owner); -} - -/* - * RoomMonitor - */ - -std::string RoomMonitor::getReservedNobleCode(int32_t buildingId) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId == iter->getId()) - return iter->getCode(); - } - - return ""; -} - -void RoomMonitor::toggleRoomForPosition(int32_t buildingId, std::string noble_code) -{ - bool found = false; - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (buildingId != iter->getId()) - { - continue; - } - else - { - if (noble_code == iter->getCode()) - { - iter->remove(); - reservedRooms.erase(iter); - } - else - { - iter->setCode(noble_code); - } - found = true; - break; - } - } - - if (!found) - { - ReservedRoom room(df::building::find(buildingId), noble_code); - reservedRooms.push_back(room); - } -} - -void RoomMonitor::doCycle() -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end();) - { - if (iter->checkRoomAssignment()) - { - ++iter; - } - else - { - iter->remove(); - iter = reservedRooms.erase(iter); - } - } -} - -void RoomMonitor::reset(color_ostream &out) -{ - reservedRooms.clear(); - std::vector items; - DFHack::World::GetPersistentData(&items, "buildingplan/reservedroom"); - - for (auto i = items.begin(); i != items.end(); i++) - { - ReservedRoom rr(*i, out); - if (rr.isValid()) - addRoom(rr); - } -} - -void RoomMonitor::addRoom(ReservedRoom &rr) -{ - for (auto iter = reservedRooms.begin(); iter != reservedRooms.end(); iter++) - { - if (iter->getId() == rr.getId()) - return; - } - - reservedRooms.push_back(rr); -} - -RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan-rooms.h b/plugins/buildingplan/buildingplan-rooms.h deleted file mode 100644 index 3880dbe06..000000000 --- a/plugins/buildingplan/buildingplan-rooms.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include "modules/Persistence.h" -#include "modules/Units.h" - -class ReservedRoom -{ -public: - ReservedRoom(df::building *building, std::string noble_code); - - ReservedRoom(DFHack::PersistentDataItem &config, DFHack::color_ostream &out); - - bool checkRoomAssignment(); - void remove(); - bool isValid(); - - int32_t getId(); - std::string getCode(); - void setCode(const std::string &noble_code); - -private: - df::building *building; - DFHack::PersistentDataItem config; - df::coord pos; - - std::vector getOwnersNobleCode(); -}; - -class RoomMonitor -{ -public: - RoomMonitor() { } - - std::string getReservedNobleCode(int32_t buildingId); - - void toggleRoomForPosition(int32_t buildingId, std::string noble_code); - - void doCycle(); - - void reset(DFHack::color_ostream &out); - -private: - std::vector reservedRooms; - - void addRoom(ReservedRoom &rr); -}; - -bool canReserveRoom(df::building *building); -std::vector getUniqueNoblePositions(df::unit *unit); - -extern RoomMonitor roomMonitor; diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp deleted file mode 100644 index cd4e84a6e..000000000 --- a/plugins/buildingplan/buildingplan.cpp +++ /dev/null @@ -1,1168 +0,0 @@ -#include "df/construction_type.h" -#include "df/entity_position.h" -#include "df/interface_key.h" -#include "df/buildreq.h" -#include "df/viewscreen_dwarfmodest.h" - -#include "modules/Gui.h" -#include "modules/Maps.h" -#include "modules/World.h" - -#include "Core.h" -#include "LuaTools.h" -#include "PluginManager.h" - -#include "../uicommon.h" -#include "../listcolumn.h" -#include "buildingplan.h" - -DFHACK_PLUGIN("buildingplan"); -#define PLUGIN_VERSION "2.0" -REQUIRE_GLOBAL(plotinfo); -REQUIRE_GLOBAL(ui_build_selector); -REQUIRE_GLOBAL(world); // used in buildingplan library - -#define MAX_MASK 10 -#define MAX_MATERIAL 21 - -bool show_help = false; -bool quickfort_mode = false; -bool all_enabled = false; -bool in_dummy_screen = false; -std::unordered_map planmode_enabled; - -bool show_debugging = false; - -void debug(const char *fmt, ...) -{ - if (!show_debugging) - return; - - color_ostream_proxy out(Core::getInstance().getConsole()); - out.print("DEBUG(buildingplan): "); - va_list args; - va_start(args, fmt); - out.vprint(fmt, args); - va_end(args); - out.print("\n"); -} - -class ViewscreenChooseMaterial : public dfhack_viewscreen -{ -public: - ViewscreenChooseMaterial(ItemFilter &filter); - - void feed(set *input); - - void render(); - - std::string getFocusString() { return "buildingplan_choosemat"; } - -private: - ListColumn masks_column; - ListColumn materials_column; - int selected_column; - ItemFilter &filter; - - void addMaskEntry(df::dfhack_material_category &mask, const std::string &text) - { - auto entry = ListEntry(pad_string(text, MAX_MASK, false), mask); - if (filter.matches(mask)) - entry.selected = true; - - masks_column.add(entry); - } - - void populateMasks() - { - masks_column.clear(); - df::dfhack_material_category mask; - - mask.whole = 0; - mask.bits.stone = true; - addMaskEntry(mask, "Stone"); - - mask.whole = 0; - mask.bits.wood = true; - addMaskEntry(mask, "Wood"); - - mask.whole = 0; - mask.bits.metal = true; - addMaskEntry(mask, "Metal"); - - mask.whole = 0; - mask.bits.soap = true; - addMaskEntry(mask, "Soap"); - - masks_column.filterDisplay(); - } - - void populateMaterials() - { - materials_column.clear(); - df::dfhack_material_category selected_category; - std::vector selected_masks = masks_column.getSelectedElems(); - if (selected_masks.size() == 1) - selected_category = selected_masks[0]; - else if (selected_masks.size() > 1) - return; - - df::world_raws &raws = world->raws; - for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++) - { - auto obj = raws.mat_table.builtin[i]; - if (obj) - { - MaterialInfo material; - material.decode(i, -1); - addMaterialEntry(selected_category, material, material.toString()); - } - } - - for (size_t i = 0; i < raws.inorganics.size(); i++) - { - MaterialInfo material; - material.decode(0, i); - addMaterialEntry(selected_category, material, material.toString()); - } - - decltype(selected_category) wood_flag; - wood_flag.bits.wood = true; - if (!selected_category.whole || selected_category.bits.wood) - { - for (size_t i = 0; i < raws.plants.all.size(); i++) - { - df::plant_raw *p = raws.plants.all[i]; - for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++) - { - if (p->material[j]->id != "WOOD") - continue; - - MaterialInfo material; - material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i); - auto name = material.toString(); - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; - - materials_column.add(entry); - } - } - } - materials_column.sort(); - } - - void addMaterialEntry(df::dfhack_material_category &selected_category, - MaterialInfo &material, std::string name) - { - if (!selected_category.whole || material.matches(selected_category)) - { - ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); - if (filter.matches(material)) - entry.selected = true; - - materials_column.add(entry); - } - } - - void validateColumn() - { - set_to_limit(selected_column, 1); - } - - void resize(int32_t x, int32_t y) - { - dfhack_viewscreen::resize(x, y); - masks_column.resize(); - materials_column.resize(); - } -}; - -const DFHack::MaterialInfo &material_info_identity_fn(const DFHack::MaterialInfo &m) { return m; } - -ViewscreenChooseMaterial::ViewscreenChooseMaterial(ItemFilter &filter) - : filter(filter) -{ - selected_column = 0; - masks_column.setTitle("Type"); - masks_column.multiselect = true; - masks_column.allow_search = false; - masks_column.left_margin = 2; - materials_column.left_margin = MAX_MASK + 3; - materials_column.setTitle("Material"); - materials_column.multiselect = true; - - masks_column.changeHighlight(0); - - populateMasks(); - populateMaterials(); - - masks_column.selectDefaultEntry(); - materials_column.selectDefaultEntry(); - materials_column.changeHighlight(0); -} - -void ViewscreenChooseMaterial::feed(set *input) -{ - bool key_processed = false; - switch (selected_column) - { - case 0: - key_processed = masks_column.feed(input); - if (input->count(interface_key::SELECT)) - populateMaterials(); // Redo materials lists based on category selection - break; - case 1: - key_processed = materials_column.feed(input); - break; - } - - if (key_processed) - return; - - if (input->count(interface_key::LEAVESCREEN)) - { - input->clear(); - Screen::dismiss(this); - return; - } - if (input->count(interface_key::CUSTOM_SHIFT_C)) - { - filter.clear(); - masks_column.clearSelection(); - materials_column.clearSelection(); - populateMaterials(); - } - else if (input->count(interface_key::SEC_SELECT)) - { - // Convert list selections to material filters - filter.clearMaterialMask(); - - // Category masks - auto masks = masks_column.getSelectedElems(); - for (auto it = masks.begin(); it != masks.end(); ++it) - filter.addMaterialMask(it->whole); - - // Specific materials - auto materials = materials_column.getSelectedElems(); - std::vector materialInfos; - transform_(materials, materialInfos, material_info_identity_fn); - filter.setMaterials(materialInfos); - - Screen::dismiss(this); - } - else if (input->count(interface_key::STANDARDSCROLL_LEFT)) - { - --selected_column; - validateColumn(); - } - else if (input->count(interface_key::STANDARDSCROLL_RIGHT)) - { - selected_column++; - validateColumn(); - } - else if (enabler->tracking_on && enabler->mouse_lbut) - { - if (masks_column.setHighlightByMouse()) - selected_column = 0; - else if (materials_column.setHighlightByMouse()) - selected_column = 1; - - enabler->mouse_lbut = enabler->mouse_rbut = 0; - } -} - -void ViewscreenChooseMaterial::render() -{ - if (Screen::isDismissed(this)) - return; - - dfhack_viewscreen::render(); - - Screen::clear(); - Screen::drawBorder(" Building Material "); - - masks_column.display(selected_column == 0); - materials_column.display(selected_column == 1); - - int32_t y = gps->dimy - 3; - int32_t x = 2; - OutputHotkeyString(x, y, "Toggle", interface_key::SELECT); - x += 3; - OutputHotkeyString(x, y, "Save", interface_key::SEC_SELECT); - x += 3; - OutputHotkeyString(x, y, "Clear", interface_key::CUSTOM_SHIFT_C); - x += 3; - OutputHotkeyString(x, y, "Cancel", interface_key::LEAVESCREEN); -} - -//START Viewscreen Hook -static bool is_planmode_enabled(BuildingTypeKey key) -{ - return planmode_enabled[key] || quickfort_mode || all_enabled; -} - -static std::string get_item_label(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "get_item_label")) - return "Failed push"; - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); - - if (!Lua::SafeCall(out, L, 4, 1)) - return "Failed call"; - - const char *s = lua_tostring(L, -1); - if (!s) - return "No string"; - - return s; -} - -static bool item_can_be_improved(const BuildingTypeKey &key, int item_idx) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 5) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "item_can_be_improved")) - return false; - - Lua::Push(L, std::get<0>(key)); - Lua::Push(L, std::get<1>(key)); - Lua::Push(L, std::get<2>(key)); - Lua::Push(L, item_idx); - - if (!Lua::SafeCall(out, L, 4, 1)) - return false; - - return lua_toboolean(L, -1); -} - -static bool construct_planned_building() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - - CoreSuspendClaimer suspend; - Lua::StackUnwinder top(L); - - if (!(lua_checkstack(L, 1) && - Lua::PushModulePublic(out, L, "plugins.buildingplan", - "construct_buildings_from_ui_state") && - Lua::SafeCall(out, L, 0, 1))) - { - return false; - } - - // register all returned buildings with planner - lua_pushnil(L); - while (lua_next(L, -2) != 0) - { - auto bld = Lua::GetDFObject(L, -1); - if (!bld) - { - out.printerr( - "buildingplan: construct_buildings_from_ui_state() failed\n"); - return false; - } - - planner.addPlannedBuilding(bld); - lua_pop(L, 1); - } - - return true; -} - -static void show_global_settings_dialog() -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "show_global_settings_dialog")) - { - debug("Failed to push the module"); - return; - } - - lua_newtable(L); - int ctable = lua_gettop(L); - Lua::SetField(L, quickfort_mode, ctable, "quickfort_mode"); - Lua::SetField(L, all_enabled, ctable, "all_enabled"); - - for (auto & setting : planner.getGlobalSettings()) - { - Lua::SetField(L, setting.second, ctable, setting.first.c_str()); - } - - if (!Lua::SafeCall(out, L, 1, 0)) - { - debug("Failed call to show_global_settings_dialog"); - return; - } -} - -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; - - // no non-static fields allowed (according to VTableInterpose.h) - static df::building *bld; - static PlannedBuilding *pb; - static int filter_count; - static int filter_idx; - - // logic is reversed since we're starting at the last filter - bool hasNextFilter() const { return filter_idx > 0; } - bool hasPrevFilter() const { return filter_idx + 1 < filter_count; } - - bool isInPlannedBuildingQueryMode() - { - return (plotinfo->main.mode == df::ui_sidebar_mode::QueryBuilding || - plotinfo->main.mode == df::ui_sidebar_mode::BuildingItems) && - planner.getPlannedBuilding(world->selected_building); - } - - // reinit static fields when selected building changes - void initStatics() - { - df::building *cur_bld = world->selected_building; - if (bld != cur_bld) - { - bld = cur_bld; - pb = planner.getPlannedBuilding(bld); - filter_count = pb->getFilters().size(); - filter_idx = filter_count - 1; - } - } - - static void invalidateStatics() - { - bld = NULL; - } - - bool handleInput(set *input) - { - if (!isInPlannedBuildingQueryMode() || Gui::inRenameBuilding()) - return false; - - initStatics(); - - if (input->count(interface_key::SUSPENDBUILDING)) - return true; // Don't unsuspend planned buildings - if (input->count(interface_key::DESTROYBUILDING)) - { - // remove persistent data - pb->remove(); - // still allow the building to be removed - return false; - } - - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - --filter_idx; - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - ++filter_idx; - else - return false; - return true; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - static bool is_filter_satisfied(df::building *bld, int filter_idx) - { - if (!bld - || bld->jobs.size() < 1 - || int(bld->jobs[0]->job_items.size()) <= filter_idx) - return false; - - // if all items for this filter are attached, the quantity will be 0 - return bld->jobs[0]->job_items[filter_idx]->quantity == 0; - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInPlannedBuildingQueryMode()) - return; - - initStatics(); - - // Hide suspend toggle option - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 20; - Screen::Pen pen(' ', COLOR_BLACK); - Screen::fillRect(pen, x, y, dims.menu_x2, y); - - bool attached = is_filter_satisfied(pb->getBuilding(), filter_idx); - - auto & filter = pb->getFilters()[filter_idx]; - y = 24; - std::string item_label = - stl_sprintf("Item %d of %d (%s)", filter_count - filter_idx, filter_count, attached ? "attached" : "pending"); - OutputString(COLOR_WHITE, x, y, "Planned Building Filter", true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, item_label.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(toBuildingTypeKey(bld), filter_idx).c_str(), true, left_margin); - ++y; - if (item_can_be_improved(toBuildingTypeKey(bld), filter_idx)) - { - OutputString(COLOR_BROWN, x, y, "Min Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMinQuality(), true, left_margin); - OutputString(COLOR_BROWN, x, y, "Max Quality: ", false, left_margin); - OutputString(COLOR_BLUE, x, y, filter.getMaxQuality(), true, left_margin); - if (filter.getDecoratedOnly()) - OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin); - } - - OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin); - auto filters = filter.getMaterials(); - for (auto it = filters.begin(); it != filters.end(); ++it) - OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin); - - ++y; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } -}; - -df::building * buildingplan_query_hook::bld; -PlannedBuilding * buildingplan_query_hook::pb; -int buildingplan_query_hook::filter_count; -int buildingplan_query_hook::filter_idx; - -struct buildingplan_place_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - // no non-static fields allowed (according to VTableInterpose.h) - static BuildingTypeKey key; - static std::vector::reverse_iterator filter_rbegin; - static std::vector::reverse_iterator filter_rend; - static std::vector::reverse_iterator filter; - static int filter_count; - static int filter_idx; - - bool hasNextFilter() const { return filter + 1 != filter_rend; } - bool hasPrevFilter() const { return filter != filter_rbegin; } - - bool isInPlannedBuildingPlacementMode() - { - return plotinfo->main.mode == ui_sidebar_mode::Build && - df::global::ui_build_selector && - df::global::ui_build_selector->stage < 2 && - planner.isPlannableBuilding(toBuildingTypeKey(ui_build_selector)); - } - - // reinit static fields when selected building type changes - void initStatics() - { - BuildingTypeKey cur_key = toBuildingTypeKey(ui_build_selector); - if (key != cur_key) - { - key = cur_key; - auto wrapper = planner.getItemFilters(key); - filter_rbegin = wrapper.rbegin(); - filter_rend = wrapper.rend(); - filter = filter_rbegin; - filter_count = wrapper.get().size(); - filter_idx = filter_count - 1; - } - } - - static void invalidateStatics() - { - key = BuildingTypeKey(); - } - - bool handleInput(set *input) - { - if (!isInPlannedBuildingPlacementMode()) - { - show_help = false; - return false; - } - - initStatics(); - - if (in_dummy_screen) - { - if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT) - || input->count(interface_key::LEAVESCREEN)) - { - in_dummy_screen = false; - // pass LEAVESCREEN up to parent view - input->clear(); - input->insert(interface_key::LEAVESCREEN); - return false; - } - return true; - } - - if (input->count(interface_key::CUSTOM_P) || - input->count(interface_key::CUSTOM_G) || - input->count(interface_key::CUSTOM_D) || - input->count(interface_key::CUSTOM_Q) || - input->count(interface_key::CUSTOM_W) || - input->count(interface_key::CUSTOM_A) || - input->count(interface_key::CUSTOM_S) || - input->count(interface_key::CUSTOM_M)) - { - show_help = true; - } - - if (!quickfort_mode && !all_enabled - && input->count(interface_key::CUSTOM_SHIFT_P)) - { - planmode_enabled[key] = !planmode_enabled[key]; - if (!is_planmode_enabled(key)) - Gui::refreshSidebar(); - return true; - } - if (input->count(interface_key::CUSTOM_SHIFT_G)) - { - show_global_settings_dialog(); - return true; - } - - if (!is_planmode_enabled(key)) - return false; - - // 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()) - { - Gui::refreshSidebar(); - if (quickfort_mode) - in_dummy_screen = true; - } - return true; - } - - - - if (input->count(interface_key::CUSTOM_SHIFT_M)) - Screen::show(dts::make_unique(*filter), plugin_self); - - if (item_can_be_improved(key, filter_idx)) - { - if (input->count(interface_key::CUSTOM_SHIFT_Q)) - filter->decMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_W)) - filter->incMinQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_A)) - filter->decMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_S)) - filter->incMaxQuality(); - else if (input->count(interface_key::CUSTOM_SHIFT_D)) - filter->toggleDecoratedOnly(); - } - - // ctrl+Right - if (input->count(interface_key::A_MOVE_E_DOWN) && hasNextFilter()) - { - ++filter; - --filter_idx; - } - // ctrl+Left - else if (input->count(interface_key::A_MOVE_W_DOWN) && hasPrevFilter()) - { - --filter; - ++filter_idx; - } - else - return false; - return true; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - initStatics(); - - bool plannable = isInPlannedBuildingPlacementMode(); - if (plannable && is_planmode_enabled(key)) - { - if (ui_build_selector->stage < 1) - // No materials but turn on cursor - ui_build_selector->stage = 1; - - for (auto iter = ui_build_selector->errors.begin(); - iter != ui_build_selector->errors.end();) - { - // FIXME Hide bags - if (((*iter)->find("Needs") != string::npos - && **iter != "Needs adjacent wall") - || (*iter)->find("No access") != string::npos) - iter = ui_build_selector->errors.erase(iter); - else - ++iter; - } - } - - INTERPOSE_NEXT(render)(); - - if (!plannable) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - - if (in_dummy_screen) - { - Screen::Pen pen(' ',COLOR_BLACK); - int y = dims.y1 + 1; - Screen::fillRect(pen, x, y, dims.menu_x2, y + 20); - - ++y; - - OutputString(COLOR_BROWN, x, y, - "Placeholder for legacy Quickfort. This screen is not required for DFHack native quickfort.", - true, left_margin); - OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin); - return; - } - - int y = 23; - - if (is_automaterial_managed(ui_build_selector->building_type, - ui_build_selector->building_subtype)) - { - // avoid conflict with the automaterial plugin UI - y = 36; - } - - if (show_help) - { - OutputString(COLOR_BROWN, x, y, "Note: "); - OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin); - } - - OutputHotkeyString(x, y, "Planning Mode", interface_key::CUSTOM_SHIFT_P); - OutputString(COLOR_WHITE, x, y, ": "); - if (quickfort_mode) - OutputString(COLOR_YELLOW, x, y, "Quickfort", true, left_margin); - else if (all_enabled) - OutputString(COLOR_YELLOW, x, y, "All", true, left_margin); - else if (planmode_enabled[key]) - OutputString(COLOR_GREEN, x, y, "On", true, left_margin); - else - OutputString(COLOR_GREY, x, y, "Off", true, left_margin); - OutputHotkeyString(x, y, "Global Settings", interface_key::CUSTOM_SHIFT_G, - true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - - if (!is_planmode_enabled(key)) - return; - - y += 2; - std::string title = - stl_sprintf("Filter for Item %d of %d:", - filter_count - filter_idx, filter_count); - OutputString(COLOR_WHITE, x, y, title.c_str(), true, left_margin + 1); - OutputString(COLOR_WHITE, x, y, get_item_label(key, filter_idx).c_str(), true, left_margin); - - if (item_can_be_improved(key, filter_idx)) - { - OutputHotkeyString(x, y, "Min Quality: ", "QW", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin); - - OutputHotkeyString(x, y, "Max Quality: ", "AS", false, 0, COLOR_WHITE, COLOR_LIGHTRED); - OutputString(COLOR_BROWN, x, y, filter->getMaxQuality(), true, left_margin); - - OutputToggleString(x, y, "Decorated Only", interface_key::CUSTOM_SHIFT_D, - filter->getDecoratedOnly(), true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } - - OutputHotkeyString(x, y, "Material Filter:", interface_key::CUSTOM_SHIFT_M, true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - auto filter_descriptions = filter->getMaterials(); - for (auto it = filter_descriptions.begin(); - it != filter_descriptions.end(); ++it) - OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); - - y += 2; - if (hasPrevFilter()) - OutputHotkeyString(x, y, "Prev Item", "Ctrl+Left", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - if (hasNextFilter()) - OutputHotkeyString(x, y, "Next Item", "Ctrl+Right", true, - left_margin, COLOR_WHITE, COLOR_LIGHTRED); - } -}; - -BuildingTypeKey buildingplan_place_hook::key; -std::vector::reverse_iterator buildingplan_place_hook::filter_rbegin; -std::vector::reverse_iterator buildingplan_place_hook::filter_rend; -std::vector::reverse_iterator buildingplan_place_hook::filter; -int buildingplan_place_hook::filter_count; -int buildingplan_place_hook::filter_idx; - -struct buildingplan_room_hook : public df::viewscreen_dwarfmodest -{ - typedef df::viewscreen_dwarfmodest interpose_base; - - std::vector getNoblePositionOfSelectedBuildingOwner() - { - std::vector np; - if (plotinfo->main.mode != df::ui_sidebar_mode::QueryBuilding || - !world->selected_building || - !world->selected_building->owner) - { - return np; - } - - switch (world->selected_building->getType()) - { - case building_type::Bed: - case building_type::Chair: - case building_type::Table: - break; - default: - return np; - } - - return getUniqueNoblePositions(world->selected_building->owner); - } - - bool isInNobleRoomQueryMode() - { - if (getNoblePositionOfSelectedBuildingOwner().size() > 0) - return canReserveRoom(world->selected_building); - else - return false; - } - - bool handleInput(set *input) - { - if (!isInNobleRoomQueryMode()) - return false; - - if (Gui::inRenameBuilding()) - return false; - auto np = getNoblePositionOfSelectedBuildingOwner(); - df::interface_key last_token = get_string_key(input); - if (last_token >= Screen::charToKey('1') - && last_token <= Screen::charToKey('9')) - { - size_t index = last_token - Screen::charToKey('1'); - if (index >= np.size()) - return false; - roomMonitor.toggleRoomForPosition(world->selected_building->id, np.at(index).position->code); - return true; - } - - return false; - } - - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) - { - if (!handleInput(input)) - INTERPOSE_NEXT(feed)(input); - } - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (!isInNobleRoomQueryMode()) - return; - - auto np = getNoblePositionOfSelectedBuildingOwner(); - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = dims.menu_x1 + 1; - int x = left_margin; - int y = 24; - OutputString(COLOR_BROWN, x, y, "DFHack", true, left_margin); - OutputString(COLOR_WHITE, x, y, "Auto-allocate to:", true, left_margin); - for (size_t i = 0; i < np.size() && i < 9; i++) - { - bool enabled = - roomMonitor.getReservedNobleCode(world->selected_building->id) - == np[i].position->code; - OutputToggleString(x, y, np[i].position->name[0].c_str(), - int_to_string(i+1).c_str(), enabled, true, left_margin); - } - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, feed); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_query_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_place_hook, render); -IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_room_hook, render); - -DFHACK_PLUGIN_IS_ENABLED(is_enabled); - -static bool setSetting(std::string name, bool value); - -static bool isTrue(std::string val) -{ - val = toLower(val); - return val == "on" || val == "true" || val == "y" || val == "yes" - || val == "1"; -} - -static command_result buildingplan_cmd(color_ostream &out, vector & parameters) -{ - if (parameters.empty()) - return CR_OK; - - std::string cmd = toLower(parameters[0]); - - if (cmd.size() >= 1 && cmd[0] == 'v') - { - out.print("buildingplan version: %s\n", PLUGIN_VERSION); - } - else if (parameters.size() >= 2 && cmd == "debug") - { - show_debugging = isTrue(parameters[1]); - out.print("buildingplan debugging: %s\n", - show_debugging ? "enabled" : "disabled"); - } - else if (cmd == "set") - { - if (!is_enabled) - { - out.printerr( - "ERROR: buildingplan must be enabled before you can" - " read or set buildingplan global settings."); - return CR_FAILURE; - } - - if (!DFHack::Core::getInstance().isMapLoaded()) - { - out.printerr( - "ERROR: A map must be loaded before you can read or set" - "buildingplan global settings. Try adding your" - "'buildingplan set' commands to the onMapLoad.init file.\n"); - return CR_FAILURE; - } - - if (parameters.size() == 1) - { - // display current settings - out.print("active settings:\n"); - - out.print(" all_enabled = %s\n", all_enabled ? "true" : "false"); - for (auto & setting : planner.getGlobalSettings()) - { - out.print(" %s = %s\n", setting.first.c_str(), - setting.second ? "true" : "false"); - } - - out.print(" quickfort_mode = %s\n", - quickfort_mode ? "true" : "false"); - } - else if (parameters.size() == 3) - { - // set a setting - std::string setting = toLower(parameters[1]); - bool val = isTrue(parameters[2]); - if (!setSetting(setting, val)) - { - out.printerr("ERROR: invalid parameter: '%s'\n", - parameters[1].c_str()); - } - } - else - { - out.printerr("ERROR: invalid syntax\n"); - } - } - - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (!gps) - return CR_FAILURE; - - if (enable != is_enabled) - { - if (DFHack::Core::getInstance().isMapLoaded()) - planner.reset(); - - if (!INTERPOSE_HOOK(buildingplan_query_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, feed).apply(enable) || - !INTERPOSE_HOOK(buildingplan_query_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_place_hook, render).apply(enable) || - !INTERPOSE_HOOK(buildingplan_room_hook, render).apply(enable)) - return CR_FAILURE; - - is_enabled = enable; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back( - PluginCommand("buildingplan", - "Plan building construction before you have materials.", - buildingplan_cmd)); - - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - buildingplan_place_hook::invalidateStatics(); - buildingplan_query_hook::invalidateStatics(); - planner.reset(); - roomMonitor.reset(out); - break; - default: - break; - } - - return CR_OK; -} - -static bool is_paused() -{ - return World::ReadPauseState() || - plotinfo->main.mode > df::ui_sidebar_mode::Squads || - !strict_virtual_cast(Gui::getCurViewscreen(true)); -} - -static bool cycle_requested = false; - -#define DAY_TICKS 1200 -DFhackCExport command_result plugin_onupdate(color_ostream &) -{ - if (Maps::IsValid() && !is_paused() - && (cycle_requested || world->frame_counter % (DAY_TICKS/2) == 0)) - { - planner.doCycle(); - roomMonitor.doCycle(); - cycle_requested = false; - } - - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown(color_ostream &) -{ - return CR_OK; -} - -// Lua API section - -static bool isPlanModeEnabled(df::building_type type, - int16_t subtype, - int32_t custom) { - return is_planmode_enabled(toBuildingTypeKey(type, subtype, custom)); -} - -static bool isPlannableBuilding(df::building_type type, - int16_t subtype, - int32_t custom) { - return planner.isPlannableBuilding( - toBuildingTypeKey(type, subtype, custom)); -} - -static bool isPlannedBuilding(df::building *bld) { - return !!planner.getPlannedBuilding(bld); -} - -static void addPlannedBuilding(df::building *bld) { - planner.addPlannedBuilding(bld); -} - -static void doCycle() { - planner.doCycle(); -} - -static void scheduleCycle() { - cycle_requested = true; -} - -static bool setSetting(std::string name, bool value) { - if (name == "quickfort_mode") - { - debug("setting quickfort_mode %d -> %d", quickfort_mode, value); - quickfort_mode = value; - return true; - } - if (name == "all_enabled") - { - debug("setting all_enabled %d -> %d", all_enabled, value); - all_enabled = value; - return true; - } - return planner.setGlobalSetting(name, value); -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(isPlanModeEnabled), - DFHACK_LUA_FUNCTION(isPlannableBuilding), - DFHACK_LUA_FUNCTION(isPlannedBuilding), - DFHACK_LUA_FUNCTION(addPlannedBuilding), - DFHACK_LUA_FUNCTION(doCycle), - DFHACK_LUA_FUNCTION(scheduleCycle), - DFHACK_LUA_FUNCTION(setSetting), - DFHACK_LUA_END -}; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h deleted file mode 100644 index e906ef1a7..000000000 --- a/plugins/buildingplan/buildingplan.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "buildingplan-planner.h" -#include "buildingplan-rooms.h" - -void debug(const char *fmt, ...) Wformat(printf,1,2); - -extern bool show_debugging; From 0faa160eaac6ff244ed5e5058f9657e01e028784 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Feb 2023 18:45:26 -0800 Subject: [PATCH 09/47] split buildingplan into a project --- plugins/CMakeLists.txt | 2 +- plugins/buildingplan/CMakeLists.txt | 10 +- plugins/{ => buildingplan}/buildingplan.cpp | 343 ++------------------ plugins/buildingplan/buildingplan.h | 28 ++ plugins/buildingplan/buildingplan_cycle.cpp | 275 ++++++++++++++++ plugins/buildingplan/itemfilter.cpp | 0 plugins/buildingplan/itemfilter.h | 1 + plugins/buildingplan/plannedbuilding.cpp | 36 ++ plugins/buildingplan/plannedbuilding.h | 25 ++ 9 files changed, 395 insertions(+), 325 deletions(-) rename plugins/{ => buildingplan}/buildingplan.cpp (55%) create mode 100644 plugins/buildingplan/buildingplan.h create mode 100644 plugins/buildingplan/buildingplan_cycle.cpp create mode 100644 plugins/buildingplan/itemfilter.cpp create mode 100644 plugins/buildingplan/itemfilter.h create mode 100644 plugins/buildingplan/plannedbuilding.cpp create mode 100644 plugins/buildingplan/plannedbuilding.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7940c3994..82722f16a 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -89,7 +89,7 @@ dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) #dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) #dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua) -dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) +add_subdirectory(buildingplan) #dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) diff --git a/plugins/buildingplan/CMakeLists.txt b/plugins/buildingplan/CMakeLists.txt index 1d34b169a..85475edaa 100644 --- a/plugins/buildingplan/CMakeLists.txt +++ b/plugins/buildingplan/CMakeLists.txt @@ -2,10 +2,12 @@ project(buildingplan) set(COMMON_HDRS buildingplan.h - buildingplan-planner.h - buildingplan-rooms.h + itemfilter.h + plannedbuilding.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) -dfhack_plugin(buildingplan buildingplan.cpp buildingplan-planner.cpp - buildingplan-rooms.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) +dfhack_plugin(buildingplan + buildingplan.cpp buildingplan_cycle.cpp itemfilter.cpp plannedbuilding.cpp + ${COMMON_HDRS} + LINK_LIBRARIES lua) diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp similarity index 55% rename from plugins/buildingplan.cpp rename to plugins/buildingplan/buildingplan.cpp index a76e81d4a..3d26a7c24 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -1,28 +1,17 @@ -#include "Core.h" +#include "plannedbuilding.h" +#include "buildingplan.h" + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" -#include "modules/Items.h" -#include "modules/Job.h" -#include "modules/Materials.h" -#include "modules/Persistence.h" #include "modules/World.h" -#include "df/building.h" -#include "df/building_design.h" #include "df/item.h" #include "df/job_item.h" #include "df/world.h" -#include -#include -#include -#include - using std::map; -using std::pair; -using std::deque; using std::string; using std::unordered_map; using std::vector; @@ -40,72 +29,29 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; -static const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; - -enum ConfigValues { - CONFIG_BLOCKS = 1, - CONFIG_BOULDERS = 2, - CONFIG_LOGS = 3, - CONFIG_BARS = 4, -}; - -enum BuildingConfigValues { - BLD_CONFIG_ID = 0, -}; +const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; -static int get_config_val(PersistentDataItem &c, int index) { +int get_config_val(PersistentDataItem &c, int index) { if (!c.isValid()) return -1; return c.ival(index); } -static bool get_config_bool(PersistentDataItem &c, int index) { +bool get_config_bool(PersistentDataItem &c, int index) { return get_config_val(c, index) == 1; } -static void set_config_val(PersistentDataItem &c, int index, int value) { +void set_config_val(PersistentDataItem &c, int index, int value) { if (c.isValid()) c.ival(index) = value; } -static void set_config_bool(PersistentDataItem &c, int index, bool value) { +void set_config_bool(PersistentDataItem &c, int index, bool value) { set_config_val(c, index, value ? 1 : 0); } -class PlannedBuilding { -public: - const df::building::key_field_type id; - - PlannedBuilding(color_ostream &out, df::building *building) : id(building->id) { - DEBUG(status,out).print("creating persistent data for building %d\n", id); - bld_config = DFHack::World::AddPersistentData(BLD_CONFIG_KEY); - set_config_val(bld_config, BLD_CONFIG_ID, id); - } - - PlannedBuilding(DFHack::PersistentDataItem &bld_config) - : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } - - void remove(color_ostream &out); - - // Ensure the building still exists and is in a valid state. It can disappear - // for lots of reasons, such as running the game with the buildingplan plugin - // disabled, manually removing the building, modifying it via the API, etc. - df::building * getBuildingIfValidOrRemoveIfNot(color_ostream &out) { - auto bld = df::building::find(id); - bool valid = bld && bld->getBuildStage() == 0; - if (!valid) { - remove(out); - return NULL; - } - return bld; - } - -private: - DFHack::PersistentDataItem bld_config; -}; - static PersistentDataItem config; // building id -> PlannedBuilding unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) -map>>> tasks; +Tasks tasks; // note that this just removes the PlannedBuilding. the tasks will get dropped // as we discover them in the tasks queues and they fail to be found in planned_buildings. @@ -115,7 +61,7 @@ map>>> tasks; // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { DEBUG(status,out).print("removing persistent data for building %d\n", id); - DFHack::World::DeletePersistentData(config); + World::DeletePersistentData(config); if (planned_buildings.count(id) > 0) planned_buildings.erase(id); } @@ -124,7 +70,9 @@ static const int32_t CYCLE_TICKS = 600; // twice per game day 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); +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings); + static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -186,7 +134,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (event == DFHack::SC_WORLD_UNLOADED) { + if (event == SC_WORLD_UNLOADED) { DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); planned_buildings.clear(); tasks.clear(); @@ -196,6 +144,14 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan static bool cycle_requested = false; +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; + + buildingplan_cycle(out, tasks, planned_buildings); +} + DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (!Core::getInstance().isWorldLoaded()) return CR_OK; @@ -249,259 +205,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) return show_help ? CR_WRONG_USAGE : CR_OK; } -///////////////////////////////////////////////////// -// cycle logic -// - -struct BadFlags { - uint32_t whole; - - BadFlags() { - df::item_flags flags; - #define F(x) flags.bits.x = true; - F(dump); F(forbid); F(garbage_collect); - F(hostile); F(on_fire); F(rotten); F(trader); - F(in_building); F(construction); F(in_job); - F(owned); F(in_chest); F(removed); F(encased); - F(spider_web); - #undef F - whole = flags.whole; - } -}; - -static bool itemPassesScreen(df::item * item) { - static const BadFlags bad_flags; - return !(item->flags.whole & bad_flags.whole) - && !item->isAssignedToStockpile(); -} - -static bool matchesFilters(df::item * item, df::job_item * job_item) { - // check the properties that are not checked by Job::isSuitableItem() - if (job_item->item_type > -1 && job_item->item_type != item->getType()) - return false; - - if (job_item->item_subtype > -1 && - job_item->item_subtype != item->getSubtype()) - return false; - - if (job_item->flags2.bits.building_material && !item->isBuildMat()) - return false; - - if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) - return false; - - if (job_item->has_tool_use > df::tool_uses::NONE - && !item->hasToolUse(job_item->has_tool_use)) - return false; - - return DFHack::Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) - && DFHack::Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), - item->getType()); -} - -static bool isJobReady(color_ostream &out, df::job * job) { - int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } - if (needed_items) { - DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); - return false; - } - return true; -} - -static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { - // we want the items in the opposite order of the filters - return a->job_item_idx > b->job_item_idx; -} - -// this function does not remove the job_items since their quantity fields are -// now all at 0, so there is no risk of having extra items attached. we don't -// remove them to keep the "finalize with buildingplan active" path as similar -// as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(color_ostream &out, df::building * bld) { - DEBUG(cycle,out).print("finalizing building %d\n", bld->id); - auto job = bld->jobs[0]; - - // sort the items so they get added to the structure in the correct order - std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); - - // derive the material properties of the building and job from the first - // applicable item. if any boulders are involved, it makes the whole - // structure "rough". - bool rough = false; - for (auto attached_item : job->items) { - df::item *item = attached_item->item; - rough = rough || item->getType() == df::item_type::BOULDER; - if (bld->mat_type == -1) { - bld->mat_type = item->getMaterial(); - job->mat_type = bld->mat_type; - } - if (bld->mat_index == -1) { - bld->mat_index = item->getMaterialIndex(); - job->mat_index = bld->mat_index; - } - } - - if (bld->needsDesign()) { - auto act = (df::building_actual *)bld; - if (!act->design) - act->design = new df::building_design(); - act->design->flags.bits.rough = rough; - } - - // we're good to go! - job->flags.bits.suspend = false; - Job::checkBuildingsNow(); -} - -static df::building * popInvalidTasks(color_ostream &out, deque> & task_queue) { - while (!task_queue.empty()) { - auto & task = task_queue.front(); - auto id = task.first; - if (planned_buildings.count(id) > 0) { - auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); - if (bld && bld->jobs[0]->job_items[task.second]->quantity) - return bld; - } - DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); - task_queue.pop_front(); - } - return NULL; -} - -static void doVector(color_ostream &out, df::job_item_vector_id vector_id, - map>> & buckets) { - auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); - auto item_vector = df::global::world->items.other[other_id]; - DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", - item_vector.size(), - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - buckets.size()); - for (auto item_it = item_vector.rbegin(); - item_it != item_vector.rend(); - ++item_it) { - auto item = *item_it; - if (!itemPassesScreen(item)) - continue; - for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { - auto & task_queue = bucket_it->second; - auto bld = popInvalidTasks(out, task_queue); - if (!bld) { - DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - bucket_it = buckets.erase(bucket_it); - continue; - } - auto & task = task_queue.front(); - auto id = task.first; - auto job = bld->jobs[0]; - auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx]) - && DFHack::Job::attachJobItem(job, item, - df::job_item_ref::Hauled, filter_idx)) - { - MaterialInfo material; - material.decode(item); - ItemTypeInfo item_type; - item_type.decode(item); - DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", - material.toString().c_str(), - item_type.toString().c_str(), - filter_idx, - ENUM_KEY_STR(building_type, bld->getType()).c_str(), - id, - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str()); - // keep quantity aligned with the actual number of remaining - // items so if buildingplan is turned off, the building will - // be completed with the correct number of items. - --job->job_items[filter_idx]->quantity; - task_queue.pop_front(); - if (isJobReady(out, job)) { - finalizeBuilding(out, bld); - planned_buildings.at(id).remove(out); - } - if (task_queue.empty()) { - DEBUG(cycle,out).print( - "removing empty item bucket: %s/%s; %zu left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - bucket_it->first.c_str(), - buckets.size() - 1); - buckets.erase(bucket_it); - } - // we found a home for this item; no need to look further - break; - } - ++bucket_it; - } - if (buckets.empty()) - break; - } -} - -struct VectorsToScanLast { - std::vector vectors; - VectorsToScanLast() { - // order is important here. we want to match boulders before wood and - // everything before bars. blocks are not listed here since we'll have - // already scanned them when we did the first pass through the buckets. - vectors.push_back(df::job_item_vector_id::BOULDER); - vectors.push_back(df::job_item_vector_id::WOOD); - vectors.push_back(df::job_item_vector_id::BAR); - } -}; - -static void do_cycle(color_ostream &out) { - static const VectorsToScanLast vectors_to_scan_last; - - // mark that we have recently run - cycle_timestamp = world->frame_counter; - cycle_requested = false; - - DEBUG(cycle,out).print("running %s cycle for %zu registered buildings\n", - plugin_name, planned_buildings.size()); - - for (auto it = tasks.begin(); it != tasks.end(); ) { - auto vector_id = it->first; - // we could make this a set, but it's only three elements - if (std::find(vectors_to_scan_last.vectors.begin(), - vectors_to_scan_last.vectors.end(), - vector_id) != vectors_to_scan_last.vectors.end()) { - ++it; - continue; - } - - auto & buckets = it->second; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - it = tasks.erase(it); - } - else - ++it; - } - for (auto vector_id : vectors_to_scan_last.vectors) { - if (tasks.count(vector_id) == 0) - continue; - auto & buckets = tasks[vector_id]; - doVector(out, vector_id, buckets); - if (buckets.empty()) { - DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", - ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), - tasks.size() - 1); - tasks.erase(vector_id); - } - } - DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", - planned_buildings.size()); -} - ///////////////////////////////////////////////////// // Lua API // core will already be suspended when coming in through here @@ -617,7 +320,7 @@ static void printStatus(color_ostream &out) { int32_t total = 0; for (auto &buckets : tasks) { for (auto &bucket_queue : buckets.second) { - deque> &tqueue = bucket_queue.second; + Bucket &tqueue = bucket_queue.second; for (auto it = tqueue.begin(); it != tqueue.end();) { auto & task = *it; auto id = task.first; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h new file mode 100644 index 000000000..ac6d3a9a6 --- /dev/null +++ b/plugins/buildingplan/buildingplan.h @@ -0,0 +1,28 @@ +#pragma once + +#include "modules/Persistence.h" + +#include "df/job_item_vector_id.h" + +#include + +typedef std::deque> Bucket; +typedef std::map> Tasks; + +extern const std::string BLD_CONFIG_KEY; + +enum ConfigValues { + CONFIG_BLOCKS = 1, + CONFIG_BOULDERS = 2, + CONFIG_LOGS = 3, + CONFIG_BARS = 4, +}; + +enum BuildingConfigValues { + BLD_CONFIG_ID = 0, +}; + +int get_config_val(DFHack::PersistentDataItem &c, int index); +bool get_config_bool(DFHack::PersistentDataItem &c, int index); +void set_config_val(DFHack::PersistentDataItem &c, int index, int value); +void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp new file mode 100644 index 000000000..875cd432f --- /dev/null +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -0,0 +1,275 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" + +#include "modules/Items.h" +#include "modules/Job.h" +#include "modules/Materials.h" + +#include "df/building_design.h" +#include "df/item.h" +#include "df/job.h" +#include "df/job_item.h" +#include "df/world.h" + +#include + +using std::map; +using std::string; +using std::unordered_map; + +namespace DFHack { + DBG_EXTERN(buildingplan, cycle); +} + +using namespace DFHack; + +struct BadFlags { + uint32_t whole; + + BadFlags() { + df::item_flags flags; + #define F(x) flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(in_job); + F(owned); F(in_chest); F(removed); F(encased); + F(spider_web); + #undef F + whole = flags.whole; + } +}; + +static bool itemPassesScreen(df::item * item) { + static const BadFlags bad_flags; + return !(item->flags.whole & bad_flags.whole) + && !item->isAssignedToStockpile(); +} + +static bool matchesFilters(df::item * item, df::job_item * job_item) { + // check the properties that are not checked by Job::isSuitableItem() + if (job_item->item_type > -1 && job_item->item_type != item->getType()) + return false; + + if (job_item->item_subtype > -1 && + job_item->item_subtype != item->getSubtype()) + return false; + + if (job_item->flags2.bits.building_material && !item->isBuildMat()) + return false; + + if (job_item->metal_ore > -1 && !item->isMetalOre(job_item->metal_ore)) + return false; + + if (job_item->has_tool_use > df::tool_uses::NONE + && !item->hasToolUse(job_item->has_tool_use)) + return false; + + return Job::isSuitableItem( + job_item, item->getType(), item->getSubtype()) + && Job::isSuitableMaterial( + job_item, item->getMaterial(), item->getMaterialIndex(), + item->getType()); +} + +static bool isJobReady(color_ostream &out, df::job * job) { + int needed_items = 0; + for (auto job_item : job->job_items) { needed_items += job_item->quantity; } + if (needed_items) { + DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); + return false; + } + return true; +} + +static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { + // we want the items in the opposite order of the filters + return a->job_item_idx > b->job_item_idx; +} + +// this function does not remove the job_items since their quantity fields are +// now all at 0, so there is no risk of having extra items attached. we don't +// remove them to keep the "finalize with buildingplan active" path as similar +// as possible to the "finalize with buildingplan disabled" path. +static void finalizeBuilding(color_ostream &out, df::building * bld) { + DEBUG(cycle,out).print("finalizing building %d\n", bld->id); + auto job = bld->jobs[0]; + + // sort the items so they get added to the structure in the correct order + std::sort(job->items.begin(), job->items.end(), job_item_idx_lt); + + // derive the material properties of the building and job from the first + // applicable item. if any boulders are involved, it makes the whole + // structure "rough". + bool rough = false; + for (auto attached_item : job->items) { + df::item *item = attached_item->item; + rough = rough || item->getType() == df::item_type::BOULDER; + if (bld->mat_type == -1) { + bld->mat_type = item->getMaterial(); + job->mat_type = bld->mat_type; + } + if (bld->mat_index == -1) { + bld->mat_index = item->getMaterialIndex(); + job->mat_index = bld->mat_index; + } + } + + if (bld->needsDesign()) { + auto act = (df::building_actual *)bld; + if (!act->design) + act->design = new df::building_design(); + act->design->flags.bits.rough = rough; + } + + // we're good to go! + job->flags.bits.suspend = false; + Job::checkBuildingsNow(); +} + +static df::building * popInvalidTasks(color_ostream &out, Bucket &task_queue, + unordered_map &planned_buildings) { + while (!task_queue.empty()) { + auto & task = task_queue.front(); + auto id = task.first; + if (planned_buildings.count(id) > 0) { + auto bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out); + if (bld && bld->jobs[0]->job_items[task.second]->quantity) + return bld; + } + DEBUG(cycle,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", id, task.second); + task_queue.pop_front(); + } + return NULL; +} + +static void doVector(color_ostream &out, df::job_item_vector_id vector_id, + map &buckets, + unordered_map &planned_buildings) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + auto item_vector = df::global::world->items.other[other_id]; + DEBUG(cycle,out).print("matching %zu item(s) in vector %s against %zu filter bucket(s)\n", + item_vector.size(), + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + buckets.size()); + for (auto item_it = item_vector.rbegin(); + item_it != item_vector.rend(); + ++item_it) { + auto item = *item_it; + if (!itemPassesScreen(item)) + continue; + for (auto bucket_it = buckets.begin(); bucket_it != buckets.end(); ) { + auto & task_queue = bucket_it->second; + auto bld = popInvalidTasks(out, task_queue, planned_buildings); + if (!bld) { + DEBUG(cycle,out).print("removing empty bucket: %s/%s; %zu bucket(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + bucket_it = buckets.erase(bucket_it); + continue; + } + auto & task = task_queue.front(); + auto id = task.first; + auto job = bld->jobs[0]; + auto filter_idx = task.second; + if (matchesFilters(item, job->job_items[filter_idx]) + && Job::attachJobItem(job, item, + df::job_item_ref::Hauled, filter_idx)) + { + MaterialInfo material; + material.decode(item); + ItemTypeInfo item_type; + item_type.decode(item); + DEBUG(cycle,out).print("attached %s %s to filter %d for %s(%d): %s/%s\n", + material.toString().c_str(), + item_type.toString().c_str(), + filter_idx, + ENUM_KEY_STR(building_type, bld->getType()).c_str(), + id, + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str()); + // keep quantity aligned with the actual number of remaining + // items so if buildingplan is turned off, the building will + // be completed with the correct number of items. + --job->job_items[filter_idx]->quantity; + task_queue.pop_front(); + if (isJobReady(out, job)) { + finalizeBuilding(out, bld); + planned_buildings.at(id).remove(out); + } + if (task_queue.empty()) { + DEBUG(cycle,out).print( + "removing empty item bucket: %s/%s; %zu left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + bucket_it->first.c_str(), + buckets.size() - 1); + buckets.erase(bucket_it); + } + // we found a home for this item; no need to look further + break; + } + ++bucket_it; + } + if (buckets.empty()) + break; + } +} + +struct VectorsToScanLast { + std::vector vectors; + VectorsToScanLast() { + // order is important here. we want to match boulders before wood and + // everything before bars. blocks are not listed here since we'll have + // already scanned them when we did the first pass through the buckets. + vectors.push_back(df::job_item_vector_id::BOULDER); + vectors.push_back(df::job_item_vector_id::WOOD); + vectors.push_back(df::job_item_vector_id::BAR); + } +}; + +void buildingplan_cycle(color_ostream &out, Tasks &tasks, + unordered_map &planned_buildings) { + static const VectorsToScanLast vectors_to_scan_last; + + DEBUG(cycle,out).print( + "running buildingplan cycle for %zu registered buildings\n", + planned_buildings.size()); + + for (auto it = tasks.begin(); it != tasks.end(); ) { + auto vector_id = it->first; + // we could make this a set, but it's only three elements + if (std::find(vectors_to_scan_last.vectors.begin(), + vectors_to_scan_last.vectors.end(), + vector_id) != vectors_to_scan_last.vectors.end()) { + ++it; + continue; + } + + auto & buckets = it->second; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + it = tasks.erase(it); + } + else + ++it; + } + for (auto vector_id : vectors_to_scan_last.vectors) { + if (tasks.count(vector_id) == 0) + continue; + auto & buckets = tasks[vector_id]; + doVector(out, vector_id, buckets, planned_buildings); + if (buckets.empty()) { + DEBUG(cycle,out).print("removing empty vector: %s; %zu vector(s) left\n", + ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), + tasks.size() - 1); + tasks.erase(vector_id); + } + } + DEBUG(cycle,out).print("cycle done; %zu registered building(s) left\n", + planned_buildings.size()); +} diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h new file mode 100644 index 000000000..6f70f09be --- /dev/null +++ b/plugins/buildingplan/itemfilter.h @@ -0,0 +1 @@ +#pragma once diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp new file mode 100644 index 000000000..c03f56161 --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -0,0 +1,36 @@ +#include "plannedbuilding.h" +#include "buildingplan.h" + +#include "Debug.h" + +#include "modules/World.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); + DBG_EXTERN(buildingplan, cycle); +} + +using namespace DFHack; + +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) + : id(building->id) { + DEBUG(status,out).print("creating persistent data for building %d\n", id); + bld_config = World::AddPersistentData(BLD_CONFIG_KEY); + set_config_val(bld_config, BLD_CONFIG_ID, id); +} + +PlannedBuilding::PlannedBuilding(PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } + +// Ensure the building still exists and is in a valid state. It can disappear +// for lots of reasons, such as running the game with the buildingplan plugin +// disabled, manually removing the building, modifying it via the API, etc. +df::building * PlannedBuilding::getBuildingIfValidOrRemoveIfNot(color_ostream &out) { + auto bld = df::building::find(id); + bool valid = bld && bld->getBuildStage() == 0; + if (!valid) { + remove(out); + return NULL; + } + return bld; +} diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h new file mode 100644 index 000000000..9f0273b82 --- /dev/null +++ b/plugins/buildingplan/plannedbuilding.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Core.h" + +#include "modules/Persistence.h" + +#include "df/building.h" + +class PlannedBuilding { +public: + const df::building::key_field_type id; + + PlannedBuilding(DFHack::color_ostream &out, df::building *building); + PlannedBuilding(DFHack::PersistentDataItem &bld_config); + + void remove(DFHack::color_ostream &out); + + // Ensure the building still exists and is in a valid state. It can disappear + // for lots of reasons, such as running the game with the buildingplan plugin + // disabled, manually removing the building, modifying it via the API, etc. + df::building * getBuildingIfValidOrRemoveIfNot(DFHack::color_ostream &out); + +private: + DFHack::PersistentDataItem bld_config; +}; From e5c3a2b519bda50f76c1d99f27f8bfed55af79c0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Feb 2023 16:54:38 -0800 Subject: [PATCH 10/47] dynamically count available materials when placing --- plugins/buildingplan/buildingplan.cpp | 157 ++++++++++++++++---- plugins/buildingplan/buildingplan.h | 4 + plugins/buildingplan/buildingplan_cycle.cpp | 5 +- plugins/lua/buildingplan.lua | 112 ++++++++++---- 4 files changed, 214 insertions(+), 64 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 3d26a7c24..2c5d96b94 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -47,11 +47,37 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) { set_config_val(c, index, value ? 1 : 0); } +// building type, subtype, custom +typedef std::tuple BuildingTypeKey; + +// rotates a size_t value left by count bits +// assumes count is not 0 or >= size_t_bits +// replace this with std::rotl when we move to C++20 +static std::size_t rotl_size_t(size_t val, uint32_t count) +{ + static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); + return val << count | val >> (size_t_bits - count); +} + +struct BuildingTypeKeyHash { + std::size_t operator() (const BuildingTypeKey & key) const { + // cast first param to appease gcc-4.8, which is missing the enum + // specializations for std::hash + std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); + std::size_t h2 = std::hash()(std::get<1>(key)); + std::size_t h3 = std::hash()(std::get<2>(key)); + + return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); + } +}; + static PersistentDataItem config; +// for use in counting available materials for the UI +static unordered_map, BuildingTypeKeyHash> job_item_repo; // building id -> PlannedBuilding -unordered_map planned_buildings; +static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) -Tasks tasks; +static Tasks tasks; // note that this just removes the PlannedBuilding. the tasks will get dropped // as we discover them in the tasks queues and they fail to be found in planned_buildings. @@ -61,7 +87,7 @@ Tasks tasks; // no chance of duplicate tasks getting added to the tasks queues. void PlannedBuilding::remove(color_ostream &out) { DEBUG(status,out).print("removing persistent data for building %d\n", id); - World::DeletePersistentData(config); + World::DeletePersistentData(bld_config); if (planned_buildings.count(id) > 0) planned_buildings.erase(id); } @@ -106,6 +132,31 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { return CR_OK; } +static void validate_config(color_ostream &out, bool verbose = false) { + if (get_config_bool(config, CONFIG_BLOCKS) + || get_config_bool(config, CONFIG_BOULDERS) + || get_config_bool(config, CONFIG_LOGS) + || get_config_bool(config, CONFIG_BARS)) + return; + + if (verbose) + out.printerr("all contruction materials disabled; resetting config\n"); + + set_config_bool(config, CONFIG_BLOCKS, true); + set_config_bool(config, CONFIG_BOULDERS, true); + set_config_bool(config, CONFIG_LOGS, true); + set_config_bool(config, CONFIG_BARS, false); +} + +static void clear_job_item_repo() { + for (auto &entry : job_item_repo) { + for (auto &jitem : entry.second) { + delete jitem; + } + } + job_item_repo.clear(); +} + DFhackCExport command_result plugin_load_data (color_ostream &out) { cycle_timestamp = 0; config = World::GetPersistentData(CONFIG_KEY); @@ -113,15 +164,13 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { if (!config.isValid()) { DEBUG(status,out).print("no config found in this save; initializing\n"); config = World::AddPersistentData(CONFIG_KEY); - set_config_bool(config, CONFIG_BLOCKS, true); - set_config_bool(config, CONFIG_BOULDERS, true); - set_config_bool(config, CONFIG_LOGS, true); - set_config_bool(config, CONFIG_BARS, false); } + validate_config(out); DEBUG(status,out).print("loading persisted state\n"); planned_buildings.clear(); tasks.clear(); + clear_job_item_repo(); vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); @@ -138,30 +187,11 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); planned_buildings.clear(); tasks.clear(); + clear_job_item_repo(); } return CR_OK; } -static bool cycle_requested = false; - -static void do_cycle(color_ostream &out) { - // mark that we have recently run - cycle_timestamp = world->frame_counter; - cycle_requested = false; - - buildingplan_cycle(out, tasks, planned_buildings); -} - -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (!Core::getInstance().isWorldLoaded()) - return CR_OK; - - if (is_enabled && - (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) - do_cycle(out); - return CR_OK; -} - static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, int nargs = 0, int nres = 0, Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, @@ -182,6 +212,27 @@ static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, std::forward(res_lambda)); } +static bool cycle_requested = false; + +static void do_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; + cycle_requested = false; + + buildingplan_cycle(out, tasks, planned_buildings); + call_buildingplan_lua(&out, "reset_counts"); +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (!Core::getInstance().isWorldLoaded()) + return CR_OK; + + if (is_enabled && + (cycle_requested || world->frame_counter - cycle_timestamp >= CYCLE_TICKS)) + do_cycle(out); + return CR_OK; +} + static command_result do_command(color_ostream &out, vector ¶meters) { CoreSuspender suspend; @@ -228,8 +279,7 @@ static string getBucket(const df::job_item & ji) { } // get a list of item vectors that we should search for matches -static vector getVectorIds(color_ostream &out, df::job_item *job_item) -{ +static vector getVectorIds(color_ostream &out, df::job_item *job_item) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -344,7 +394,7 @@ static void printStatus(color_ostream &out) { } } - out.print("Waiting for %d item(s) to be produced or %zd building(s):\n", + out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", total, planned_buildings.size()); for (auto &count : counts) out.print(" %3d %s\n", count.second, count.first.c_str()); @@ -365,6 +415,9 @@ static bool setSetting(color_ostream &out, string name, bool value) { out.printerr("unrecognized setting: '%s'\n", name.c_str()); return false; } + + validate_config(out, true); + call_buildingplan_lua(&out, "reset_counts"); return true; } @@ -412,7 +465,49 @@ static void scheduleCycle(color_ostream &out) { static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(status,out).print("entering countAvailableItems\n"); - return 10; + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + BuildingTypeKey key(type, subtype, custom); + auto &job_items = job_item_repo[key]; + if (index >= job_items.size()) { + for (int i = job_items.size(); i <= index; ++i) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, type); + Lua::Push(L, subtype); + Lua::Push(L, custom); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for index=%d: %p\n", + index, jitem); + if (!jitem) + failed = true; + else + job_items.emplace_back(jitem); + }) || failed) { + return 0; + } + } + } + + auto &jitem = job_items[index]; + auto vector_ids = getVectorIds(out, jitem); + + int count = 0; + for (auto vector_id : vector_ids) { + auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); + for (auto &item : df::global::world->items.other[other_id]) { + if (itemPassesScreen(item) && matchesFilters(item, jitem)) + ++count; + } + } + + DEBUG(status,out).print("found matches %d\n", count); + return count; } DFHACK_PLUGIN_LUA_FUNCTIONS { diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index ac6d3a9a6..0e7e288ac 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -2,6 +2,7 @@ #include "modules/Persistence.h" +#include "df/job_item.h" #include "df/job_item_vector_id.h" #include @@ -26,3 +27,6 @@ int get_config_val(DFHack::PersistentDataItem &c, int index); bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); + +bool itemPassesScreen(df::item * item); +bool matchesFilters(df::item * item, df::job_item * job_item); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 875cd432f..6d5e4a405 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -10,7 +10,6 @@ #include "df/building_design.h" #include "df/item.h" #include "df/job.h" -#include "df/job_item.h" #include "df/world.h" #include @@ -41,13 +40,13 @@ struct BadFlags { } }; -static bool itemPassesScreen(df::item * item) { +bool itemPassesScreen(df::item * item) { static const BadFlags bad_flags; return !(item->flags.whole & bad_flags.whole) && !item->isAssignedToStockpile(); } -static bool matchesFilters(df::item * item, df::job_item * job_item) { +bool matchesFilters(df::item * item, df::job_item * job_item) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 5da03e38f..24ef90903 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -56,6 +56,19 @@ function get_num_filters(btype, subtype, custom) return filters and #filters or 0 end +function get_job_item(btype, subtype, custom, index) + local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom) + if not filters or not filters[index] then return nil end + local obj = df.job_item:new() + obj:assign(filters[index]) + return obj +end + +local reset_counts_flag = false +function reset_counts() + reset_counts_flag = true +end + -------------------------------- -- Planner Overlay -- @@ -143,8 +156,32 @@ local function to_title_case(str) return str end -function get_item_line_text(idx) - local filter = get_cur_filters()[idx] +ItemLine = defclass(ItemLine, widgets.Panel) +ItemLine.ATTRS{ + idx=DEFAULT_NIL, +} + +function ItemLine:init() + self.frame.h = 1 + self.visible = function() return #get_cur_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=function() return self:get_item_line_text() end}}, + }, + widgets.Label{ + frame={t=0, l=22}, + text='[filter][x]', + }, + } +end + +function ItemLine:reset() + self.desc = nil + self.available = nil +end + +local function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use then desc = to_title_case(df.tool_uses[filter.has_tool_use]) @@ -170,7 +207,10 @@ function get_item_line_text(idx) if desc == 'Trappart' then desc = 'Mechanism' end + return desc +end +local function get_quantity(filter) local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims() if quantity < 1 then @@ -178,34 +218,29 @@ function get_item_line_text(idx) else quantity = quantity * dimx * dimy * dimz end - desc = ('%d %s%s'):format(quantity, desc, quantity == 1 and '' or 's') + return quantity +end - local available = countAvailableItems(uibs.building_type, +function ItemLine:get_item_line_text() + local idx = self.idx + local filter = get_cur_filters()[idx] + local quantity = get_quantity(filter) + + self.desc = self.desc or get_desc(filter) + local line = ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') + + self.available = self.available or countAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - local note = available >= quantity and + local note = self.available >= quantity and 'Can build now' or 'Will wait for item' - return ('%-21s%s%s'):format(desc:sub(1,21), (' '):rep(13), note) + return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(13), note) end -ItemLine = defclass(ItemLine, widgets.Panel) -ItemLine.ATTRS{ - idx=DEFAULT_NIL, -} - -function ItemLine:init() - self.frame.h = 1 - self.visible = function() return #get_cur_filters() >= self.idx end - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text={{text=function() return get_item_line_text(self.idx) end}}, - }, - widgets.Label{ - frame={t=0, l=22}, - text='[filter][x]', - }, - } +function ItemLine:reduce_quantity() + if not self.available then return end + local filter = get_cur_filters()[self.idx] + self.available = math.max(0, self.available - get_quantity(filter)) end PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) @@ -226,10 +261,10 @@ function PlannerOverlay:init() text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{frame={t=0, l=0}, idx=1}, - ItemLine{frame={t=2, l=0}, idx=2}, - ItemLine{frame={t=4, l=0}, idx=3}, - ItemLine{frame={t=6, l=0}, idx=4}, + ItemLine{view_id='item1', frame={t=0, l=0}, idx=1}, + ItemLine{view_id='item2', frame={t=2, l=0}, idx=2}, + ItemLine{view_id='item3', frame={t=4, l=0}, idx=3}, + ItemLine{view_id='item4', frame={t=6, l=0}, idx=4}, widgets.CycleHotkeyLabel{ view_id="stairs_top_subtype", frame={t=3, l=0}, @@ -268,13 +303,22 @@ function PlannerOverlay:init() } end -function PlannerOverlay:do_config() - dfhack.run_script('gui/buildingplan') +function PlannerOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_counts_flag = false end function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + if uibs.selection_pos:isValid() then + uibs.selection_pos:clear() + return true + end + self:reset() return false end if PlannerOverlay.super.onInput(self, keys) then @@ -290,6 +334,10 @@ function PlannerOverlay:onInput(keys) if #get_cur_filters() == 0 then return false -- we don't add value; let the game place it end + self.subviews.item1:reduce_quantity() + self.subviews.item2:reduce_quantity() + self.subviews.item3:reduce_quantity() + self.subviews.item4:reduce_quantity() self:place_building() uibs.selection_pos:clear() return true @@ -315,6 +363,10 @@ local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) + if reset_counts_flag then + self:reset() + end + if not is_choosing_area() then return end local bounds = { From 18ad29dde4b09335730550b1f21d3b19f3504cc0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Feb 2023 19:10:42 -0800 Subject: [PATCH 11/47] show queue position --- plugins/buildingplan/buildingplan.cpp | 47 ++++++++- plugins/buildingplan/buildingplan.h | 1 + plugins/buildingplan/plannedbuilding.cpp | 62 +++++++++++- plugins/buildingplan/plannedbuilding.h | 6 +- plugins/lua/buildingplan.lua | 120 +++++++++++++++-------- 5 files changed, 189 insertions(+), 47 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 2c5d96b94..17db21cd2 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -175,7 +175,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); for (size_t idx = 0; idx < num_building_configs; ++idx) { - PlannedBuilding pb(building_configs[idx]); + PlannedBuilding pb(out, building_configs[idx]); registerPlannedBuilding(out, pb); } @@ -279,7 +279,7 @@ static string getBucket(const df::job_item & ji) { } // get a list of item vectors that we should search for matches -static vector getVectorIds(color_ostream &out, df::job_item *job_item) { +vector getVectorIds(color_ostream &out, df::job_item *job_item) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -310,6 +310,7 @@ static vector getVectorIds(color_ostream &out, df::job_i ret.push_back(df::job_item_vector_id::IN_PLAY); return ret; } + static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { df::building * bld = pb.getBuildingIfValidOrRemoveIfNot(out); if (!bld) @@ -385,7 +386,10 @@ static void printStatus(color_ostream &out) { auto *jitem = bld->jobs[0]->job_items[task.second]; int32_t quantity = jitem->quantity; if (quantity) { - string desc = toLower(ENUM_KEY_STR(item_type, jitem->item_type)); + string desc = "none"; + call_buildingplan_lua(&out, "get_desc", 1, 1, + [&](lua_State *L) { Lua::Push(L, jitem); }, + [&](lua_State *L) { desc = lua_tostring(L, -1); }); counts[desc] += quantity; total += quantity; } @@ -510,6 +514,42 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } +static int getQueuePosition(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getQueuePosition\n"); + if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) + return 0; + + auto &job_items = bld->jobs[0]->job_items; + if (job_items.size() <= index) + return 0; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + if (pb.vector_ids.size() <= index) + return 0; + + auto &job_item = job_items[index]; + + int min_pos = -1; + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_item); + if (!buckets.count(bucket_id)) + continue; + int bucket_pos = -1; + for (auto &task : buckets.at(bucket_id)) { + ++bucket_pos; + if (bld->id == task.first && index == task.second) + break; + } + if (bucket_pos++ >= 0) + min_pos = min_pos < 0 ? bucket_pos : std::min(min_pos, bucket_pos); + } + + return min_pos < 0 ? 0 : min_pos; +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(printStatus), DFHACK_LUA_FUNCTION(setSetting), @@ -519,5 +559,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 0e7e288ac..7fe2478aa 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -28,5 +28,6 @@ bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); +std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); bool matchesFilters(df::item * item, df::job_item * job_item); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index c03f56161..f4f3564b7 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -2,25 +2,79 @@ #include "buildingplan.h" #include "Debug.h" +#include "MiscUtils.h" #include "modules/World.h" +#include "df/job.h" + namespace DFHack { DBG_EXTERN(buildingplan, status); - DBG_EXTERN(buildingplan, cycle); } +using std::string; +using std::vector; using namespace DFHack; +static vector> get_vector_ids(color_ostream &out, int bld_id) { + vector> ret; + + df::building *bld = df::building::find(bld_id); + + if (!bld || bld->jobs.size() != 1) + return ret; + + auto &job = bld->jobs[0]; + for (auto &jitem : job->job_items) { + ret.emplace_back(getVectorIds(out, jitem)); + } + return ret; +} + +static vector> deserialize(color_ostream &out, PersistentDataItem &bld_config) { + vector> ret; + + DEBUG(status,out).print("deserializing state for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), bld_config.val().c_str()); + + vector joined; + split_string(&joined, bld_config.val(), "|"); + for (auto &str : joined) { + vector lst; + split_string(&lst, str, ","); + vector ids; + for (auto &s : lst) + ids.emplace_back(df::job_item_vector_id(string_to_int(s))); + ret.emplace_back(ids); + } + + if (!ret.size()) + ret = get_vector_ids(out, get_config_val(bld_config, BLD_CONFIG_ID)); + + return ret; +} + +static string serialize(const vector> &vector_ids) { + vector joined; + for (auto &vec_list : vector_ids) { + joined.emplace_back(join_strings(",", vec_list)); + } + return join_strings("|", joined); +} + PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) - : id(building->id) { + : id(building->id), vector_ids(get_vector_ids(out, id)) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); + bld_config.val() = serialize(vector_ids); + DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } -PlannedBuilding::PlannedBuilding(PersistentDataItem &bld_config) - : id(get_config_val(bld_config, BLD_CONFIG_ID)), bld_config(bld_config) { } +PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) + : id(get_config_val(bld_config, BLD_CONFIG_ID)), + vector_ids(deserialize(out, bld_config)), + bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear // for lots of reasons, such as running the game with the buildingplan plugin diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 9f0273b82..592f0e4b3 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -5,13 +5,17 @@ #include "modules/Persistence.h" #include "df/building.h" +#include "df/job_item_vector_id.h" class PlannedBuilding { public: const df::building::key_field_type id; + // job_item idx -> list of vectors the task is linked to + const std::vector> vector_ids; + PlannedBuilding(DFHack::color_ostream &out, df::building *building); - PlannedBuilding(DFHack::PersistentDataItem &bld_config); + PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 24ef90903..376787952 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -70,7 +70,7 @@ function reset_counts() end -------------------------------- --- Planner Overlay +-- PlannerOverlay -- local uibs = df.global.buildreq @@ -181,7 +181,7 @@ function ItemLine:reset() self.available = nil end -local function get_desc(filter) +function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use then desc = to_title_case(df.tool_uses[filter.has_tool_use]) @@ -190,13 +190,15 @@ local function get_desc(filter) desc = to_title_case(df.item_type[filter.item_type]) end if filter.flags2 and filter.flags2.building_material then - desc = "Generic material"; + desc = 'Generic material'; if filter.flags2.fire_safe then - desc = "Fire-safe material"; + desc = 'Fire-safe material'; end if filter.flags2.magma_safe then - desc = "Magma-safe material"; + desc = 'Magma-safe material'; end + elseif filter.flags2 and filter.flags2.screw then + desc = 'Screw' elseif filter.vector_id then desc = to_title_case(df.job_item_vector_id[filter.vector_id]) end @@ -249,27 +251,30 @@ PlannerOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=54, h=9}, - frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } function PlannerOverlay:init() self:addviews{ + widgets.Panel{ + frame={}, + frame_style=gui.MEDIUM_FRAME, + }, widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{view_id='item1', frame={t=0, l=0}, idx=1}, - ItemLine{view_id='item2', frame={t=2, l=0}, idx=2}, - ItemLine{view_id='item3', frame={t=4, l=0}, idx=3}, - ItemLine{view_id='item4', frame={t=6, l=0}, idx=4}, + ItemLine{view_id='item1', frame={t=1, l=1, r=1}, idx=1}, + ItemLine{view_id='item2', frame={t=3, l=1, r=1}, idx=2}, + ItemLine{view_id='item3', frame={t=5, l=1, r=1}, idx=3}, + ItemLine{view_id='item4', frame={t=7, l=1, r=1}, idx=4}, widgets.CycleHotkeyLabel{ - view_id="stairs_top_subtype", - frame={t=3, l=0}, - key="CUSTOM_R", - label="Top Stair Type: ", + view_id='stairs_top_subtype', + frame={t=4, l=1}, + key='CUSTOM_R', + label='Top Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, @@ -278,10 +283,10 @@ function PlannerOverlay:init() }, }, widgets.CycleHotkeyLabel { - view_id="stairs_bottom_subtype", - frame={t=4, l=0}, - key="CUSTOM_B", - label="Bottom Stair Type: ", + view_id='stairs_bottom_subtype', + frame={t=5, l=1}, + key='CUSTOM_B', + label='Bottom Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, @@ -290,7 +295,7 @@ function PlannerOverlay:init() }, }, widgets.Label{ - frame={b=0, l=17}, + frame={b=1, l=17}, text={ 'Selected area: ', {text=function() @@ -300,6 +305,17 @@ function PlannerOverlay:init() }, visible=is_choosing_area, }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=0, l=1}, + key='CUSTOM_F', + label='Extra safety: ', + options={ + {label='None', value='none'}, + {label='Magma', value='magma'}, + {label='Fire', value='fire'}, + }, + }, } end @@ -473,12 +489,50 @@ function PlannerOverlay:place_building() scheduleCycle() end +-------------------------------- +-- InspectorOverlay +-- + +local function get_building_filters() + local bld = dfhack.gui.getSelectedBuilding() + return dfhack.buildings.getFiltersByType({}, + bld:getType(), bld:getSubtype(), bld:getCustomType()) +end + +InspectorLine = defclass(InspectorLine, widgets.Panel) +InspectorLine.ATTRS{ + idx=DEFAULT_NIL, +} + +function InspectorLine:init() + self.frame.h = 2 + self.visible = function() return #get_building_filters() >= self.idx end + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={{text=function() return get_desc(get_building_filters()[self.idx]) end}}, + }, + widgets.Label{ + frame={t=1, l=2}, + text={{text=self:callback('get_status_line')}}, + }, + } +end + +function InspectorLine:get_status_line() + local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1) + if queue_pos <= 0 then + return 'Item attached' + end + return ('Position in line: %d'):format(queue_pos) +end + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=9}, + frame={w=30, h=14}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -489,29 +543,17 @@ function InspectorOverlay:init() frame={t=0, l=0}, text='Waiting for items:', }, - widgets.Label{ - frame={t=1, l=0}, - text='item1', - }, - widgets.Label{ - frame={t=2, l=0}, - text='item2', - }, - widgets.Label{ - frame={t=3, l=0}, - text='item3', - }, - widgets.Label{ - frame={t=4, l=0}, - text='item4', - }, + InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1}, + InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2}, + InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, + InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, widgets.HotkeyLabel{ - frame={t=5, l=0}, + frame={t=10, l=0}, label='adjust filters', key='CUSTOM_CTRL_F', }, widgets.HotkeyLabel{ - frame={t=6, l=0}, + frame={t=11, l=0}, label='make top priority', key='CUSTOM_CTRL_T', }, @@ -578,7 +620,7 @@ end -- does not need the core suspended. function show_global_settings_dialog(settings) GlobalSettings{ - frame_title="Buildingplan Global Settings", + frame_title='Buildingplan Global Settings', settings=settings, }:show() end From 56c8927316947ddbd494b36a25566b9e10cd9bd3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 03:08:55 -0800 Subject: [PATCH 12/47] better description string for inspection overlay --- plugins/buildingplan/buildingplan.cpp | 86 +++++++++++++++++---------- plugins/lua/buildingplan.lua | 20 +++---- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 17db21cd2..579c1e83f 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -357,6 +357,20 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { return true; } +static string get_desc_string(color_ostream &out, df::job_item *jitem, + const vector &vec_ids) { + vector descs; + for (auto &vec_id : vec_ids) { + df::job_item jitem_copy = *jitem; + jitem_copy.vector_id = vec_id; + call_buildingplan_lua(&out, "get_desc", 1, 1, + [&](lua_State *L) { Lua::Push(L, &jitem_copy); }, + [&](lua_State *L) { + descs.emplace_back(lua_tostring(L, -1)); }); + } + return join_strings(" or ", descs); +} + static void printStatus(color_ostream &out) { DEBUG(status,out).print("entering buildingplan_printStatus\n"); out.print("buildingplan is %s\n\n", is_enabled ? "enabled" : "disabled"); @@ -369,31 +383,21 @@ static void printStatus(color_ostream &out) { map counts; int32_t total = 0; - for (auto &buckets : tasks) { - for (auto &bucket_queue : buckets.second) { - Bucket &tqueue = bucket_queue.second; - for (auto it = tqueue.begin(); it != tqueue.end();) { - auto & task = *it; - auto id = task.first; - df::building *bld = NULL; - if (!planned_buildings.count(id) || - !(bld = planned_buildings.at(id).getBuildingIfValidOrRemoveIfNot(out))) { - DEBUG(status,out).print("discarding invalid task: bld=%d, job_item_idx=%d\n", - id, task.second); - it = tqueue.erase(it); - continue; - } - auto *jitem = bld->jobs[0]->job_items[task.second]; - int32_t quantity = jitem->quantity; - if (quantity) { - string desc = "none"; - call_buildingplan_lua(&out, "get_desc", 1, 1, - [&](lua_State *L) { Lua::Push(L, jitem); }, - [&](lua_State *L) { desc = lua_tostring(L, -1); }); - counts[desc] += quantity; - total += quantity; - } - ++it; + for (auto &entry : planned_buildings) { + auto &pb = entry.second; + auto bld = pb.getBuildingIfValidOrRemoveIfNot(out); + if (!bld || bld->jobs.size() != 1) + continue; + auto &job_items = bld->jobs[0]->job_items; + if (job_items.size() != pb.vector_ids.size()) + continue; + int job_item_idx = 0; + for (auto &vec_ids : pb.vector_ids) { + auto &jitem = job_items[job_item_idx++]; + int32_t quantity = jitem->quantity; + if (quantity) { + counts[get_desc_string(out, jitem, vec_ids)] += quantity; + total += quantity; } } } @@ -514,20 +518,41 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } -static int getQueuePosition(color_ostream &out, df::building *bld, int index) { - DEBUG(status,out).print("entering getQueuePosition\n"); +static bool validate_pb(color_ostream &out, df::building *bld, int index) { if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) - return 0; + return false; auto &job_items = bld->jobs[0]->job_items; if (job_items.size() <= index) - return 0; + return false; PlannedBuilding &pb = planned_buildings.at(bld->id); if (pb.vector_ids.size() <= index) + return false; + + return true; +} + +static string getDescString(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getDescString\n"); + if (!validate_pb(out, bld, index)) return 0; - auto &job_item = job_items[index]; + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &jitem = bld->jobs[0]->job_items[index]; + return get_desc_string(out, jitem, pb.vector_ids[index]); +} + +static int getQueuePosition(color_ostream &out, df::building *bld, int index) { + DEBUG(status,out).print("entering getQueuePosition\n"); + if (!validate_pb(out, bld, index)) + return 0; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_item = bld->jobs[0]->job_items[index]; + + if (job_item->quantity <= 0) + return 0; int min_pos = -1; for (auto &vec_id : pb.vector_ids[index]) { @@ -559,6 +584,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 376787952..d0b3417fb 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -183,13 +183,15 @@ end function get_desc(filter) local desc = 'Unknown' - if filter.has_tool_use then + if filter.has_tool_use and filter.has_tool_use > -1 then desc = to_title_case(df.tool_uses[filter.has_tool_use]) - end - if filter.item_type then + elseif filter.flags2 and filter.flags2.screw then + desc = 'Screw' + elseif filter.item_type and filter.item_type > -1 then desc = to_title_case(df.item_type[filter.item_type]) - end - if filter.flags2 and filter.flags2.building_material then + elseif filter.vector_id and filter.vector_id > -1 then + desc = to_title_case(df.job_item_vector_id[filter.vector_id]) + elseif filter.flags2 and filter.flags2.building_material then desc = 'Generic material'; if filter.flags2.fire_safe then desc = 'Fire-safe material'; @@ -197,10 +199,6 @@ function get_desc(filter) if filter.flags2.magma_safe then desc = 'Magma-safe material'; end - elseif filter.flags2 and filter.flags2.screw then - desc = 'Screw' - elseif filter.vector_id then - desc = to_title_case(df.job_item_vector_id[filter.vector_id]) end if desc:endswith('s') then @@ -208,6 +206,8 @@ function get_desc(filter) end if desc == 'Trappart' then desc = 'Mechanism' + elseif desc == 'Wood' then + desc = 'Log' end return desc end @@ -510,7 +510,7 @@ function InspectorLine:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={{text=function() return get_desc(get_building_filters()[self.idx]) end}}, + text={{text=function() return getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) end}}, }, widgets.Label{ frame={t=1, l=2}, From 0d3285678c3a0e3e058ad4fc742f0a0b4ed16b91 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 17:25:15 -0800 Subject: [PATCH 13/47] separate errors panel, fix pb vectors on load --- plugins/buildingplan/buildingplan.cpp | 15 +++-- plugins/lua/buildingplan.lua | 90 +++++++++++++++++++++------ 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 579c1e83f..e2db9c367 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -330,12 +330,11 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { auto job_item = job_items[job_item_idx]; auto bucket = getBucket(*job_item); - auto vector_ids = getVectorIds(out, job_item); // if there are multiple vector_ids, schedule duplicate tasks. after // the correct number of items are matched, the extras will get popped // as invalid - for (auto vector_id : vector_ids) { + for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { tasks[vector_id][bucket].push_back(std::make_pair(id, job_item_idx)); DEBUG(status,out).print("added task: %s/%s/%d,%d; " @@ -402,10 +401,14 @@ static void printStatus(color_ostream &out) { } } - out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", - total, planned_buildings.size()); - for (auto &count : counts) - out.print(" %3d %s\n", count.second, count.first.c_str()); + if (planned_buildings.size()) { + out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", + total, planned_buildings.size()); + for (auto &count : counts) + out.print(" %3d %s\n", count.second, count.first.c_str()); + } else { + out.print("Currently no planned buildings\n"); + } out.print("\n"); } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index d0b3417fb..5a12292ea 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -192,7 +192,7 @@ function get_desc(filter) elseif filter.vector_id and filter.vector_id > -1 then desc = to_title_case(df.job_item_vector_id[filter.vector_id]) elseif filter.flags2 and filter.flags2.building_material then - desc = 'Generic material'; + desc = 'Building material'; if filter.flags2.fire_safe then desc = 'Fire-safe material'; end @@ -236,7 +236,7 @@ function ItemLine:get_item_line_text() local note = self.available >= quantity and 'Can build now' or 'Will wait for item' - return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(13), note) + return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(14), note) end function ItemLine:reduce_quantity() @@ -245,34 +245,44 @@ function ItemLine:reduce_quantity() self.available = math.max(0, self.available - get_quantity(filter)) end +local function get_placement_errors() + local out = '' + for _,str in ipairs(uibs.errors) do + if #out > 0 then out = out .. NEWLINE end + out = out .. str.value + end + return out +end + PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ - default_pos={x=6,y=9}, + default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=54, h=9}, - frame_background=gui.CLEAR_PEN, + frame={w=56, h=18}, } function PlannerOverlay:init() - self:addviews{ - widgets.Panel{ - frame={}, - frame_style=gui.MEDIUM_FRAME, - }, + local main_panel = widgets.Panel{ + frame={t=0, l=0, r=0, h=14}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + main_panel:addviews{ widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{view_id='item1', frame={t=1, l=1, r=1}, idx=1}, - ItemLine{view_id='item2', frame={t=3, l=1, r=1}, idx=2}, - ItemLine{view_id='item3', frame={t=5, l=1, r=1}, idx=3}, - ItemLine{view_id='item4', frame={t=7, l=1, r=1}, idx=4}, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4}, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', - frame={t=4, l=1}, + frame={t=4, l=4}, key='CUSTOM_R', label='Top Stair Type: ', visible=is_stairs, @@ -284,7 +294,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', - frame={t=5, l=1}, + frame={t=5, l=4}, key='CUSTOM_B', label='Bottom Stair Type: ', visible=is_stairs, @@ -295,7 +305,7 @@ function PlannerOverlay:init() }, }, widgets.Label{ - frame={b=1, l=17}, + frame={b=3, l=17}, text={ 'Selected area: ', {text=function() @@ -307,15 +317,54 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=1}, - key='CUSTOM_F', - label='Extra safety: ', + frame={b=0, l=2}, + key='CUSTOM_G', + label='Safety: ', options={ {label='None', value='none'}, {label='Magma', value='magma'}, {label='Fire', value='fire'}, }, }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SELECT', + label='Choose item', + }, + widgets.HotkeyLabel{ + frame={b=1, l=21}, + key='CUSTOM_F', + label='Filter', + }, + widgets.HotkeyLabel{ + frame={b=1, l=33}, + key='CUSTOM_X', + label='Clear filter', + }, + } + + local error_panel = widgets.ResizingPanel{ + view_id='errors', + frame={t=14, l=0, r=0, h=3}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, + } + + error_panel:addviews{ + widgets.WrappedLabel{ + text_pen=COLOR_LIGHTRED, + text_to_wrap=get_placement_errors, + }, + widgets.Label{ + text_pen=COLOR_GREEN, + text='OK to build', + visible=function() return #uibs.errors == 0 end, + }, + } + + self:addviews{ + main_panel, + error_panel, } end @@ -367,6 +416,7 @@ end function PlannerOverlay:render(dc) if not is_plannable() then return end + self.subviews.errors:updateLayout() PlannerOverlay.super.render(self, dc) end From 3f8be2cd9e579ecf912ddb12eff9e68ba13cd30b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 18:02:15 -0800 Subject: [PATCH 14/47] implement make_top_priority, cache inspector data --- plugins/buildingplan/buildingplan.cpp | 33 ++++++++++++++++++++++- plugins/lua/buildingplan.lua | 38 +++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index e2db9c367..dff04f0c2 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -336,7 +336,7 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { // as invalid for (auto vector_id : pb.vector_ids[job_item_idx]) { for (int item_num = 0; item_num < job_item->quantity; ++item_num) { - tasks[vector_id][bucket].push_back(std::make_pair(id, job_item_idx)); + tasks[vector_id][bucket].emplace_back(id, job_item_idx); DEBUG(status,out).print("added task: %s/%s/%d,%d; " "%zu vector(s), %zu filter bucket(s), %zu task(s) in bucket", ENUM_KEY_STR(job_item_vector_id, vector_id).c_str(), @@ -578,6 +578,36 @@ static int getQueuePosition(color_ostream &out, df::building *bld, int index) { return min_pos < 0 ? 0 : min_pos; } +static void makeTopPriority(color_ostream &out, df::building *bld) { + DEBUG(status,out).print("entering makeTopPriority\n"); + if (!validate_pb(out, bld, 0)) + return; + + PlannedBuilding &pb = planned_buildings.at(bld->id); + auto &job_items = bld->jobs[0]->job_items; + + for (int index = 0; index < job_items.size(); ++index) { + for (auto &vec_id : pb.vector_ids[index]) { + if (!tasks.count(vec_id)) + continue; + auto &buckets = tasks.at(vec_id); + string bucket_id = getBucket(*job_items[index]); + if (!buckets.count(bucket_id)) + continue; + auto &bucket = buckets.at(bucket_id); + for (auto taskit = bucket.begin(); taskit != bucket.end(); ++taskit) { + if (bld->id == taskit->first && index == taskit->second) { + auto task_bld_id = taskit->first; + auto task_job_item_idx = taskit->second; + bucket.erase(taskit); + bucket.emplace_front(task_bld_id, task_job_item_idx); + break; + } + } + } + } +} + DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(printStatus), DFHACK_LUA_FUNCTION(setSetting), @@ -589,5 +619,6 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(countAvailableItems), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), + DFHACK_LUA_FUNCTION(makeTopPriority), DFHACK_LUA_END }; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 5a12292ea..6bf35bc2a 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -65,8 +65,10 @@ function get_job_item(btype, subtype, custom, index) end local reset_counts_flag = false +local reset_inspector_flag = false function reset_counts() reset_counts_flag = true + reset_inspector_flag = true end -------------------------------- @@ -560,7 +562,7 @@ function InspectorLine:init() self:addviews{ widgets.Label{ frame={t=0, l=0}, - text={{text=function() return getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) end}}, + text={{text=self:callback('get_desc_string')}}, }, widgets.Label{ frame={t=1, l=2}, @@ -569,12 +571,24 @@ function InspectorLine:init() } end +function InspectorLine:get_desc_string() + if self.desc then return self.desc end + self.desc = getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1) + return self.desc +end + function InspectorLine:get_status_line() + if self.status then return self.status end local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1) if queue_pos <= 0 then return 'Item attached' end - return ('Position in line: %d'):format(queue_pos) + self.status = ('Position in line: %d'):format(queue_pos) + return self.status +end + +function InspectorLine:reset() + self.status = nil end InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) @@ -606,14 +620,31 @@ function InspectorOverlay:init() frame={t=11, l=0}, label='make top priority', key='CUSTOM_CTRL_T', + on_activate=self:callback('make_top_priority'), }, } end +function InspectorOverlay:reset() + self.subviews.item1:reset() + self.subviews.item2:reset() + self.subviews.item3:reset() + self.subviews.item4:reset() + reset_inspector_flag = false +end + +function InspectorOverlay:make_top_priority() + makeTopPriority(dfhack.gui.getSelectedBuilding()) + self:reset() +end + function InspectorOverlay:onInput(keys) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return false end + if keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then + self:reset() + end return InspectorOverlay.super.onInput(self, keys) end @@ -621,6 +652,9 @@ function InspectorOverlay:render(dc) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return end + if reset_inspector_flag then + self:reset() + end InspectorOverlay.super.render(self, dc) end From 96fa7fa1e2951bb7c96b7de3bca6715aaf5a50ad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 18:23:14 -0800 Subject: [PATCH 15/47] fix position of errors panel --- plugins/lua/buildingplan.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 6bf35bc2a..82172613d 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -261,7 +261,7 @@ PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', - frame={w=56, h=18}, + frame={w=56, h=20}, } function PlannerOverlay:init() @@ -347,17 +347,20 @@ function PlannerOverlay:init() local error_panel = widgets.ResizingPanel{ view_id='errors', - frame={t=14, l=0, r=0, h=3}, + frame={t=14, l=0, r=0}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } error_panel:addviews{ widgets.WrappedLabel{ + frame={t=0, l=0, r=0}, text_pen=COLOR_LIGHTRED, text_to_wrap=get_placement_errors, + visible=function() return #uibs.errors > 0 end, }, widgets.Label{ + frame={t=0, l=0, r=0}, text_pen=COLOR_GREEN, text='OK to build', visible=function() return #uibs.errors == 0 end, From b3198c88a0aa6885c6ca09f8fea5d63b4e0de615 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 18:43:06 -0800 Subject: [PATCH 16/47] only block mouse clicks over exactly the panel area --- plugins/lua/buildingplan.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 82172613d..b7ed789e4 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -266,6 +266,7 @@ PlannerOverlay.ATTRS{ function PlannerOverlay:init() local main_panel = widgets.Panel{ + view_id='main', frame={t=0, l=0, r=0, h=14}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, @@ -396,7 +397,14 @@ function PlannerOverlay:onInput(keys) end if keys._MOUSE_L_DOWN then if is_over_options_panel() then return false end - if self:getMouseFramePos() then return true end + local detect_rect = copyall(self.frame_rect) + detect_rect.height = self.subviews.main.frame_rect.height + + self.subviews.errors.frame_rect.height + detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1 + if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect}) + or self.subviews.errors:getMousePos() then + return true + end if #uibs.errors > 0 then return true end local pos = dfhack.gui.getMousePos() if pos then From e92a54deaa0ec4a48850b9c204839183f5864406 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 21:17:55 -0800 Subject: [PATCH 17/47] beginning of textures --- plugins/buildingplan/buildingplan.cpp | 4 ++-- plugins/lua/buildingplan.lua | 29 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index dff04f0c2..080941823 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -220,7 +220,7 @@ static void do_cycle(color_ostream &out) { cycle_requested = false; buildingplan_cycle(out, tasks, planned_buildings); - call_buildingplan_lua(&out, "reset_counts"); + call_buildingplan_lua(&out, "signal_reset"); } DFhackCExport command_result plugin_onupdate(color_ostream &out) { @@ -428,7 +428,7 @@ static bool setSetting(color_ostream &out, string name, bool value) { } validate_config(out, true); - call_buildingplan_lua(&out, "reset_counts"); + call_buildingplan_lua(&out, "signal_reset"); return true; } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b7ed789e4..2123f636e 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -64,9 +64,11 @@ function get_job_item(btype, subtype, custom, index) return obj end +local texpos_base = -1 local reset_counts_flag = false local reset_inspector_flag = false -function reset_counts() +function signal_reset() + texpos_base = dfhack.textures.getControlPanelTexposStart() reset_counts_flag = true reset_inspector_flag = true end @@ -168,12 +170,22 @@ function ItemLine:init() self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ - frame={t=0, l=0}, - text={{text=function() return self:get_item_line_text() end}}, + frame={t=0, l=23}, + text={ + {tile=2600}, + {gap=6, tile=2602}, + {tile=2600}, + {gap=1, tile=2602}, + }, }, widgets.Label{ - frame={t=0, l=22}, - text='[filter][x]', + frame={t=0, l=0}, + text={ + {width=21, text=function() return self:get_item_line_text() end}, + {gap=3, text='filter'}, + {gap=2, text='x'}, + {gap=3, text=function() return self.note end}, + }, }, } end @@ -231,14 +243,13 @@ function ItemLine:get_item_line_text() local quantity = get_quantity(filter) self.desc = self.desc or get_desc(filter) - local line = ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') self.available = self.available or countAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - local note = self.available >= quantity and - 'Can build now' or 'Will wait for item' + self.note = self.available >= quantity and + 'Can build now' or 'Will build later' - return ('%-21s%s%s'):format(line:sub(1,21), (' '):rep(14), note) + return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end function ItemLine:reduce_quantity() From aa4ebe6398ed199bf9b8f3516cedfb9aa0570abd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 03:16:34 -0800 Subject: [PATCH 18/47] remove some cruft --- plugins/lua/buildingplan.lua | 130 ----------------------------------- 1 file changed, 130 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 2123f636e..7f8d996c8 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -685,10 +685,6 @@ OVERLAY_WIDGETS = { inspector=InspectorOverlay, } - -local dialogs = require('gui.dialogs') -local guidm = require('gui.dwarfmode') - -- returns whether the items matched by the specified filter can have a quality -- rating. This also conveniently indicates whether an item can be decorated. -- does not need the core suspended @@ -704,130 +700,4 @@ function item_can_be_improved(btype, subtype, custom, reverse_idx) filter.item_type ~= df.item_type.BOULDER end - --- --- GlobalSettings dialog --- - -local GlobalSettings = defclass(GlobalSettings, dialogs.MessageBox) -GlobalSettings.focus_path = 'buildingplan_globalsettings' - -GlobalSettings.ATTRS{ - settings = {} -} - -function GlobalSettings:onDismiss() - for k,v in pairs(self.settings) do - -- call back into C++ to save changes - setSetting(k, v) - end -end - --- does not need the core suspended. -function show_global_settings_dialog(settings) - GlobalSettings{ - frame_title='Buildingplan Global Settings', - settings=settings, - }:show() -end - -function GlobalSettings:toggle_setting(name) - self.settings[name] = not self.settings[name] -end - -function GlobalSettings:get_setting_string(name) - if self.settings[name] then return 'On' end - return 'Off' -end - -function GlobalSettings:get_setting_pen(name) - if self.settings[name] then return COLOR_LIGHTGREEN end - return COLOR_LIGHTRED -end - -function GlobalSettings:is_setting_enabled(name) - return self.settings[name] -end - -function GlobalSettings:make_setting_label_token(text, key, name, width) - return {text=text, key=key, key_sep=': ', key_pen=COLOR_LIGHTGREEN, - on_activate=self:callback('toggle_setting', name), width=width} -end - -function GlobalSettings:make_setting_value_token(name) - return {text=self:callback('get_setting_string', name), - enabled=self:callback('is_setting_enabled', name), - pen=self:callback('get_setting_pen', name), - dpen=COLOR_GRAY} -end - --- mockup: ---[[ - Buildingplan Global Settings - - e: Enable all: Off - Enables buildingplan for all building types. Use this to avoid having to - manually enable buildingplan for each building type that you want to plan. - Note that DFHack quickfort will use buildingplan to manage buildings - regardless of whether buildingplan is "enabled" for the building type. - - Allowed types for generic, fire-safe, and magma-safe building material: - b: Blocks: On - s: Boulders: On - w: Wood: On - r: Bars: Off - Changes to these settings will be applied to newly-planned buildings. - - A: Apply building material filter settings to existing planned buildings - Use this if your planned buildings can't be completed because the settings - above were too restrictive when the buildings were originally planned. - - M: Edit list of materials to avoid - potash - pearlash - ash - coal - Buildingplan will avoid using these material types when a planned building's - material filter is set to 'any'. They can stil be matched when they are - explicitly allowed by a planned building's material filter. Changes to this - list take effect for existing buildings immediately. - - g: Allow bags: Off - This allows bags to be placed where a 'coffer' is planned. - - f: Legacy Quickfort Mode: Off - Compatibility mode for the legacy Python-based Quickfort application. This - setting is not needed for DFHack quickfort. ---]] -function GlobalSettings:init() - - self.subviews.label:setText{ - self:make_setting_label_token('Enable all', 'CUSTOM_E', 'all_enabled', 12), - self:make_setting_value_token('all_enabled'), '\n', - ' Enables buildingplan for all building types. Use this to avoid having\n', - ' to manually enable buildingplan for each building type that you want\n', - ' to plan. Note that DFHack quickfort will use buildingplan to manage\n', - ' buildings regardless of whether buildingplan is "enabled" for the\n', - ' building type.\n', - '\n', - 'Allowed types for generic, fire-safe, and magma-safe building material:\n', - self:make_setting_label_token('Blocks', 'CUSTOM_B', 'blocks', 10), - self:make_setting_value_token('blocks'), '\n', - self:make_setting_label_token('Boulders', 'CUSTOM_S', 'boulders', 10), - self:make_setting_value_token('boulders'), '\n', - self:make_setting_label_token('Wood', 'CUSTOM_W', 'logs', 10), - self:make_setting_value_token('logs'), '\n', - self:make_setting_label_token('Bars', 'CUSTOM_R', 'bars', 10), - self:make_setting_value_token('bars'), '\n', - ' Changes to these settings will be applied to newly-planned buildings.\n', - ' If no types are enabled above, then any building material is allowed.\n', - '\n', - self:make_setting_label_token('Legacy Quickfort Mode', 'CUSTOM_F', - 'quickfort_mode', 23), - self:make_setting_value_token('quickfort_mode'), '\n', - ' Compatibility mode for the legacy Python-based Quickfort application.\n', - ' This setting is not needed for DFHack quickfort.' - } -end - return _ENV From c0cdd58b5080a43f94ec175e552c8c8c982e25ca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 21:42:58 -0800 Subject: [PATCH 19/47] fix signed-unsigned compare --- plugins/buildingplan/buildingplan.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 080941823..e31914372 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -481,7 +481,7 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); auto &job_items = job_item_repo[key]; - if (index >= job_items.size()) { + if (index >= (int)job_items.size()) { for (int i = job_items.size(); i <= index; ++i) { bool failed = false; if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, @@ -526,11 +526,11 @@ static bool validate_pb(color_ostream &out, df::building *bld, int index) { return false; auto &job_items = bld->jobs[0]->job_items; - if (job_items.size() <= index) + if ((int)job_items.size() <= index) return false; PlannedBuilding &pb = planned_buildings.at(bld->id); - if (pb.vector_ids.size() <= index) + if ((int)pb.vector_ids.size() <= index) return false; return true; @@ -586,7 +586,7 @@ static void makeTopPriority(color_ostream &out, df::building *bld) { PlannedBuilding &pb = planned_buildings.at(bld->id); auto &job_items = bld->jobs[0]->job_items; - for (int index = 0; index < job_items.size(); ++index) { + for (int index = 0; index < (int)job_items.size(); ++index) { for (auto &vec_id : pb.vector_ids[index]) { if (!tasks.count(vec_id)) continue; From c59ad78f40ab72dedd699c10cd8e0cc2e34d4906 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Feb 2023 23:02:34 -0800 Subject: [PATCH 20/47] more tokens, textures, and colors --- plugins/buildingplan/buildingplan.cpp | 53 +++++++++++++-------------- plugins/lua/buildingplan.lua | 52 +++++++++++++++++++------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index e31914372..687f46705 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -148,7 +148,30 @@ static void validate_config(color_ostream &out, bool verbose = false) { set_config_bool(config, CONFIG_BARS, false); } -static void clear_job_item_repo() { +static bool call_buildingplan_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 buildingplan lua function: '%s'\n", 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.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static void clear_state(color_ostream &out) { + call_buildingplan_lua(&out, "signal_reset"); + planned_buildings.clear(); + tasks.clear(); for (auto &entry : job_item_repo) { for (auto &jitem : entry.second) { delete jitem; @@ -168,9 +191,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { validate_config(out); DEBUG(status,out).print("loading persisted state\n"); - planned_buildings.clear(); - tasks.clear(); - clear_job_item_repo(); + clear_state(out); vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); @@ -185,33 +206,11 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == SC_WORLD_UNLOADED) { DEBUG(status,out).print("world unloaded; clearing state for %s\n", plugin_name); - planned_buildings.clear(); - tasks.clear(); - clear_job_item_repo(); + clear_state(out); } return CR_OK; } -static bool call_buildingplan_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 buildingplan lua function: '%s'\n", 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.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - static bool cycle_requested = false; static void do_cycle(color_ostream &out) { diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 7f8d996c8..6b1abb84e 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -64,15 +64,34 @@ function get_job_item(btype, subtype, custom, index) return obj end -local texpos_base = -1 +local BUTTON_START_PEN, BUTTON_END_PEN = nil, nil local reset_counts_flag = false local reset_inspector_flag = false function signal_reset() - texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_START_PEN = nil + BUTTON_END_PEN = nil reset_counts_flag = true reset_inspector_flag = true end +local to_pen = dfhack.pen.parse +local function get_button_start_pen() + if not BUTTON_START_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_START_PEN = to_pen{ch='[', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 13 or nil} + end + return BUTTON_START_PEN +end +local function get_button_end_pen() + if not BUTTON_END_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + BUTTON_END_PEN = to_pen{ch=']', fg=COLOR_YELLOW, + tile=texpos_base > 0 and texpos_base + 15 or nil} + end + return BUTTON_END_PEN +end + -------------------------------- -- PlannerOverlay -- @@ -172,19 +191,20 @@ function ItemLine:init() widgets.Label{ frame={t=0, l=23}, text={ - {tile=2600}, - {gap=6, tile=2602}, - {tile=2600}, - {gap=1, tile=2602}, + {tile=get_button_start_pen}, + {gap=6, tile=get_button_end_pen}, + {tile=get_button_start_pen}, + {gap=1, tile=get_button_end_pen}, }, }, widgets.Label{ frame={t=0, l=0}, text={ - {width=21, text=function() return self:get_item_line_text() end}, - {gap=3, text='filter'}, - {gap=2, text='x'}, - {gap=3, text=function() return self.note end}, + {width=21, text=self:callback('get_item_line_text')}, + {gap=3, text='filter', pen=COLOR_GREEN}, + {gap=2, text='x', pen=COLOR_GREEN}, + {gap=3, text=function() return self.note end, + pen=function() return self.note_pen end}, }, }, } @@ -246,8 +266,13 @@ function ItemLine:get_item_line_text() self.available = self.available or countAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1) - self.note = self.available >= quantity and - 'Can build now' or 'Will build later' + if self.available >= quantity then + self.note_pen = COLOR_GREEN + self.note = 'Available now' + else + self.note_pen = COLOR_YELLOW + self.note = 'Will link later' + end return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end @@ -298,7 +323,7 @@ function PlannerOverlay:init() view_id='stairs_top_subtype', frame={t=4, l=4}, key='CUSTOM_R', - label='Top Stair Type: ', + label='Top Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, @@ -444,7 +469,6 @@ function PlannerOverlay:render(dc) PlannerOverlay.super.render(self, dc) end -local to_pen = dfhack.pen.parse local GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, From daf691839fd73f4c40f3cc3cc4ae149d74b10e04 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Feb 2023 14:24:21 -0800 Subject: [PATCH 21/47] item selection, callback skeleton --- plugins/lua/buildingplan.lua | 139 +++++++++++++++++++++++++++-------- 1 file changed, 109 insertions(+), 30 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 6b1abb84e..b9be7e800 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -182,6 +182,10 @@ end ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, + is_selected_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, + on_filter=DEFAULT_NIL, + on_clear_filter=DEFAULT_NIL, } function ItemLine:init() @@ -189,16 +193,38 @@ function ItemLine:init() self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ - frame={t=0, l=23}, + frame={t=0, l=0}, + text='*', + auto_width=true, + visible=self.is_selected_fn, + }, + widgets.Label{ + frame={t=0, r=0}, + text='*', + auto_width=true, + visible=self.is_selected_fn, + on_click=self.on_filter, + }, + widgets.Label{ + frame={t=0, l=25}, text={ {tile=get_button_start_pen}, {gap=6, tile=get_button_end_pen}, + }, + auto_width=true, + on_click=function() self.on_filter(self.idx) end, + }, + widgets.Label{ + frame={t=0, l=33}, + text={ {tile=get_button_start_pen}, {gap=1, tile=get_button_end_pen}, }, + auto_width=true, + on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ - frame={t=0, l=0}, + frame={t=0, l=2}, text={ {width=21, text=self:callback('get_item_line_text')}, {gap=3, text='filter', pen=COLOR_GREEN}, @@ -215,6 +241,13 @@ function ItemLine:reset() self.available = nil end +function ItemLine:onInput(keys) + if keys._MOUSE_L_DOWN and self:getMousePos() then + self.on_select(self.idx) + end + return ItemLine.super.onInput(self, keys) +end + function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use and filter.has_tool_use > -1 then @@ -301,6 +334,8 @@ PlannerOverlay.ATTRS{ } function PlannerOverlay:init() + self.selected = 1 + local main_panel = widgets.Panel{ view_id='main', frame={t=0, l=0, r=0, h=14}, @@ -308,6 +343,14 @@ function PlannerOverlay:init() frame_background=gui.CLEAR_PEN, } + local function make_is_selected_fn(idx) + return function() return self.selected == idx end + end + + local function on_select_fn(idx) + self.selected = idx + end + main_panel:addviews{ widgets.Label{ frame={}, @@ -315,10 +358,22 @@ function PlannerOverlay:init() text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, - ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1}, - ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2}, - ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3}, - ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4}, + ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, + is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, + is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, + is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, + ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, + is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, + on_filter=self:callback('filter'), + on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={t=4, l=4}, @@ -354,32 +409,43 @@ function PlannerOverlay:init() }, visible=is_choosing_area, }, - widgets.CycleHotkeyLabel{ - view_id='safety', - frame={b=0, l=2}, - key='CUSTOM_G', - label='Safety: ', - options={ - {label='None', value='none'}, - {label='Magma', value='magma'}, - {label='Fire', value='fire'}, + widgets.Panel{ + visible=function() return #get_cur_filters() > 0 end, + subviews={ + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SELECT', + label='Choose item', + on_activate=function() self:choose(self.selected) end, + enabled=function() + return (self.subviews['item'..self.selected].available or 0) > 0 + end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=21}, + key='CUSTOM_F', + label='Filter', + on_activate=function() self:filter(self.selected) end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=33}, + key='CUSTOM_X', + label='Clear filter', + on_activate=function() self:clear_filter(self.selected) end, + }, + widgets.CycleHotkeyLabel{ + view_id='safety', + frame={b=0, l=2}, + key='CUSTOM_G', + label='Safety: ', + options={ + {label='None', value='none'}, + {label='Magma', value='magma'}, + {label='Fire', value='fire'}, + }, + }, }, }, - widgets.HotkeyLabel{ - frame={b=1, l=0}, - key='SELECT', - label='Choose item', - }, - widgets.HotkeyLabel{ - frame={b=1, l=21}, - key='CUSTOM_F', - label='Filter', - }, - widgets.HotkeyLabel{ - frame={b=1, l=33}, - key='CUSTOM_X', - label='Clear filter', - }, } local error_panel = widgets.ResizingPanel{ @@ -418,6 +484,18 @@ function PlannerOverlay:reset() reset_counts_flag = false end +function PlannerOverlay:choose(idx) + print('choose', idx) +end + +function PlannerOverlay:filter(idx) + print('filter', idx) +end + +function PlannerOverlay:clear_filter(idx) + print('clear_filter', idx) +end + function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then @@ -425,6 +503,7 @@ function PlannerOverlay:onInput(keys) uibs.selection_pos:clear() return true end + self.selected = 1 self:reset() return false end From 66a14ecc7471e147a98c07b4466233e997021166 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Feb 2023 19:16:45 -0800 Subject: [PATCH 22/47] get UI semi-finalized, prep for item choosing --- plugins/buildingplan/buildingplan.cpp | 27 ++++++-- plugins/buildingplan/buildingplan.h | 2 + plugins/buildingplan/buildingplan_cycle.cpp | 8 +-- plugins/lua/buildingplan.lua | 75 +++++++++++++-------- 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 687f46705..2dbceba1f 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -319,12 +319,15 @@ static bool registerPlannedBuilding(color_ostream &out, PlannedBuilding & pb) { DEBUG(status,out).print("unexpected number of jobs: want 1, got %zu\n", bld->jobs.size()); return false; } + auto job_items = bld->jobs[0]->job_items; - int num_job_items = job_items.size(); - if (num_job_items < 1) { - DEBUG(status,out).print("unexpected number of job items: want >0, got %d\n", num_job_items); - return false; + if (isJobReady(out, job_items)) { + // all items are already attached + finalizeBuilding(out, bld); + return true; } + + int num_job_items = job_items.size(); int32_t id = bld->id; for (int job_item_idx = 0; job_item_idx < num_job_items; ++job_item_idx) { auto job_item = job_items[job_item_idx]; @@ -520,6 +523,19 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } +static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering hasFilter\n"); + return false; +} + +static void setFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering setFilter\n"); +} + +static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering clearFilter\n"); +} + static bool validate_pb(color_ostream &out, df::building *bld, int index) { if (!isPlannedBuilding(out, bld) || bld->jobs.size() != 1) return false; @@ -616,6 +632,9 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), + DFHACK_LUA_FUNCTION(hasFilter), + DFHACK_LUA_FUNCTION(setFilter), + DFHACK_LUA_FUNCTION(clearFilter), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_FUNCTION(makeTopPriority), diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 7fe2478aa..01c72e370 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -31,3 +31,5 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); bool matchesFilters(df::item * item, df::job_item * job_item); +bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); +void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 6d5e4a405..069787f39 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -72,9 +72,9 @@ bool matchesFilters(df::item * item, df::job_item * job_item) { item->getType()); } -static bool isJobReady(color_ostream &out, df::job * job) { +bool isJobReady(color_ostream &out, const std::vector &jitems) { int needed_items = 0; - for (auto job_item : job->job_items) { needed_items += job_item->quantity; } + for (auto job_item : jitems) { needed_items += job_item->quantity; } if (needed_items) { DEBUG(cycle,out).print("building needs %d more item(s)\n", needed_items); return false; @@ -91,7 +91,7 @@ static bool job_item_idx_lt(df::job_item_ref *a, df::job_item_ref *b) { // now all at 0, so there is no risk of having extra items attached. we don't // remove them to keep the "finalize with buildingplan active" path as similar // as possible to the "finalize with buildingplan disabled" path. -static void finalizeBuilding(color_ostream &out, df::building * bld) { +void finalizeBuilding(color_ostream &out, df::building *bld) { DEBUG(cycle,out).print("finalizing building %d\n", bld->id); auto job = bld->jobs[0]; @@ -194,7 +194,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, // be completed with the correct number of items. --job->job_items[filter_idx]->quantity; task_queue.pop_front(); - if (isJobReady(out, job)) { + if (isJobReady(out, job->job_items)) { finalizeBuilding(out, bld); planned_buildings.at(id).remove(out); } diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b9be7e800..b6c13d5a7 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -198,13 +198,6 @@ function ItemLine:init() auto_width=true, visible=self.is_selected_fn, }, - widgets.Label{ - frame={t=0, r=0}, - text='*', - auto_width=true, - visible=self.is_selected_fn, - on_click=self.on_filter, - }, widgets.Label{ frame={t=0, l=25}, text={ @@ -228,7 +221,7 @@ function ItemLine:init() text={ {width=21, text=self:callback('get_item_line_text')}, {gap=3, text='filter', pen=COLOR_GREEN}, - {gap=2, text='x', pen=COLOR_GREEN}, + {gap=2, text='x', pen=self:callback('get_x_pen')}, {gap=3, text=function() return self.note end, pen=function() return self.note_pen end}, }, @@ -248,6 +241,10 @@ function ItemLine:onInput(keys) return ItemLine.super.onInput(self, keys) end +function ItemLine:get_x_pen() + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and COLOR_GREEN or COLOR_GREY +end + function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use and filter.has_tool_use > -1 then @@ -414,32 +411,52 @@ function PlannerOverlay:init() subviews={ widgets.HotkeyLabel{ frame={b=1, l=0}, - key='SELECT', - label='Choose item', - on_activate=function() self:choose(self.selected) end, - enabled=function() - return (self.subviews['item'..self.selected].available or 0) > 0 - end, + key='STRING_A042', + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=1}, + key='STRING_A047', + label='Prev/next item', + enabled=function() return #get_cur_filters() > 1 end, + on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, widgets.HotkeyLabel{ frame={b=1, l=21}, key='CUSTOM_F', - label='Filter', + label='Set filter', on_activate=function() self:filter(self.selected) end, }, widgets.HotkeyLabel{ - frame={b=1, l=33}, + frame={b=1, l=37}, key='CUSTOM_X', label='Clear filter', on_activate=function() self:clear_filter(self.selected) end, }, + widgets.CycleHotkeyLabel{ + view_id='choose', + frame={b=0, l=0}, + key='CUSTOM_I', + label='Choose exact items:', + options={{label='Yes', value=true}, + {label='No', value=false}}, + initial_option=false, + enabled=function() + for idx = 1,4 do + if (self.subviews['item'..idx].available or 0) > 0 then + return true + end + end + end, + }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=2}, + frame={b=0, l=29}, key='CUSTOM_G', - label='Safety: ', + label='Building safety:', options={ - {label='None', value='none'}, + {label='Any', value='none'}, {label='Magma', value='magma'}, {label='Fire', value='fire'}, }, @@ -484,10 +501,6 @@ function PlannerOverlay:reset() reset_counts_flag = false end -function PlannerOverlay:choose(idx) - print('choose', idx) -end - function PlannerOverlay:filter(idx) print('filter', idx) end @@ -504,6 +517,8 @@ function PlannerOverlay:onInput(keys) return true end self.selected = 1 + self.subviews.choose:setOption(false) + self.subviews.safety:setOption('none') self:reset() return false end @@ -527,10 +542,6 @@ function PlannerOverlay:onInput(keys) if #get_cur_filters() == 0 then return false -- we don't add value; let the game place it end - self.subviews.item1:reduce_quantity() - self.subviews.item2:reduce_quantity() - self.subviews.item3:reduce_quantity() - self.subviews.item4:reduce_quantity() self:place_building() uibs.selection_pos:clear() return true @@ -607,6 +618,11 @@ function PlannerOverlay:place_building() max_y = min_y + height - 1 max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end + if self.subviews.choose:getOptionValue() then + -- TODO + -- open dialog, showing all items (restricted to current filters) + -- select items (doesn't have to be all required items) + end local blds = {} local subtype = uibs.building_subtype for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do @@ -660,7 +676,12 @@ function PlannerOverlay:place_building() end table.insert(blds, bld) end end end + self.subviews.item1:reduce_quantity() + self.subviews.item2:reduce_quantity() + self.subviews.item3:reduce_quantity() + self.subviews.item4:reduce_quantity() for _,bld in ipairs(blds) do + -- TODO: attach chosen items and reduce job_item quantity addPlannedBuilding(bld) end scheduleCycle() From 4001ef381508eb1a04ae9ecb82b73a3d2d36a05c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:09:54 -0800 Subject: [PATCH 23/47] implement selecting specific items --- plugins/buildingplan/buildingplan.cpp | 38 +++- plugins/lua/buildingplan.lua | 294 ++++++++++++++++++++++---- 2 files changed, 282 insertions(+), 50 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 2dbceba1f..d1e78b675 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -476,8 +476,8 @@ static void scheduleCycle(color_ostream &out) { cycle_requested = true; } -static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering countAvailableItems\n"); +static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, + int32_t custom, int index, vector *item_ids = NULL) { DEBUG(status,out).print( "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); @@ -514,8 +514,11 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem)) + if (itemPassesScreen(item) && matchesFilters(item, jitem)) { + if (item_ids) + item_ids->emplace_back(item->id); ++count; + } } } @@ -523,6 +526,30 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return count; } +static int getAvailableItems(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector item_ids; + scanAvailableItems(*out, type, subtype, custom, index, &item_ids); + Lua::PushVector(L, item_ids); + return 1; +} + +static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print( + "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + return scanAvailableItems(out, type, subtype, custom, index); +} + static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { DEBUG(status,out).print("entering hasFilter\n"); return false; @@ -640,3 +667,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(makeTopPriority), DFHACK_LUA_END }; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_END +}; diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b6c13d5a7..46d27d626 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -20,6 +20,8 @@ local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') +local uibs = df.global.buildreq + local function process_args(opts, args) if args[1] == 'help' then opts.help = true @@ -64,12 +66,40 @@ function get_job_item(btype, subtype, custom, index) return obj end -local BUTTON_START_PEN, BUTTON_END_PEN = nil, nil +local function get_cur_filters() + return dfhack.buildings.getFiltersByType({}, uibs.building_type, + uibs.building_subtype, uibs.custom_type) +end + +local function is_choosing_area() + return uibs.selection_pos.x >= 0 +end + +local function get_cur_area_dims() + if not is_choosing_area() then return 1, 1, 1 end + return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, + math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, + math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 +end + +local function get_quantity(filter) + local quantity = filter.quantity or 1 + local dimx, dimy, dimz = get_cur_area_dims() + if quantity < 1 then + quantity = (((dimx * dimy) // 4) + 1) * dimz + else + quantity = quantity * dimx * dimy * dimz + end + return quantity +end + +local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil local reset_counts_flag = false local reset_inspector_flag = false function signal_reset() BUTTON_START_PEN = nil BUTTON_END_PEN = nil + SELECTED_ITEM_PEN = nil reset_counts_flag = true reset_inspector_flag = true end @@ -91,12 +121,169 @@ local function get_button_end_pen() end return BUTTON_END_PEN end +local function get_selected_item_pen() + if not SELECTED_ITEM_PEN then + local texpos_base = dfhack.textures.getControlPanelTexposStart() + SELECTED_ITEM_PEN = to_pen{ch='x', fg=COLOR_GREEN, + tile=texpos_base > 0 and texpos_base + 9 or nil} + end + return SELECTED_ITEM_PEN +end -------------------------------- --- PlannerOverlay +-- ItemSelection -- -local uibs = df.global.buildreq +ItemSelection = defclass(ItemSelection, widgets.Window) +ItemSelection.ATTRS{ + frame_title='Choose items', + frame={w=60, h=30, l=4, t=8}, + resizable=true, + resize_min={w=56, h=20}, + index=DEFAULT_NIL, + selected_set=DEFAULT_NIL, +} + +function ItemSelection:init() + local filter = get_cur_filters()[self.index] + self.quantity = get_quantity(filter) + self.num_selected = 0 + + self:addviews{ + widgets.Label{ + frame={t=0}, + text={ + get_desc(filter), + self.quantity == 1 and '' or 's', + NEWLINE, + ('Select up to %d items ('):format(self.quantity), + {text=function() return self.num_selected end}, + ' selected)', + }, + }, + widgets.FilteredList{ + frame={t=3, l=0, r=0, b=0}, + case_sensitive=false, + choices=self:get_choices(), + icon_width=2, + on_submit=self:callback('toggle_item'), + }, + } +end + +local function make_search_key(str) + local out = '' + for c in str:gmatch("[%w%s]") do + out = out .. c + end + return out +end + +function ItemSelection:get_choices() + local item_ids = getAvailableItems(uibs.building_type, + uibs.building_subtype, uibs.custom_type, self.index - 1) + local buckets, selected_buckets = {}, {} + for _,item_id in ipairs(item_ids) do + local item = df.item.find(item_id) + if not item then goto continue end + local desc = dfhack.items.getDescription(item, 0, true) + if buckets[desc] then + local bucket = buckets[desc] + table.insert(bucket.item_ids, item_id) + bucket.quantity = bucket.quantity + 1 + else + local entry = { + text=desc, + search_key=make_search_key(desc), + icon=self:callback('get_entry_icon', item_id), + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + selected=false, + } + buckets[desc] = entry + end + ::continue:: + end + local selected_qty = 0 + for bucket in pairs(selected_buckets) do + for _,item_id in ipairs(bucket.item_ids) do + self.selected_set[item_id] = true + end + selected_qty = selected_qty + bucket.quantity + bucket.selected = true + if selected_qty >= self.quantity then break end + end + self.num_selected = selected_qty + local choices = {} + for _,choice in pairs(buckets) do + choice.text = ('(%d) %s'):format(choice.quantity, choice.text) + table.insert(choices, choice) + end + local function choice_sort(a, b) + return a.item_type < b.item_type or + (a.item_type == b.item_type and a.item_subtype < b.item_subtype) or + (a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key) + end + table.sort(choices, choice_sort) + return choices +end + +function ItemSelection:toggle_item(_, choice) + if choice.selected then + for _,item_id in ipairs(choice.item_ids) do + self.selected_set[item_id] = nil + end + self.num_selected = self.num_selected - choice.quantity + choice.selected = false + elseif self.quantity > self.num_selected then + for _,item_id in ipairs(choice.item_ids) do + self.selected_set[item_id] = true + end + self.num_selected = self.num_selected + choice.quantity + choice.selected = true + end +end + +function ItemSelection:get_entry_icon(item_id) + return self.selected_set[item_id] and get_selected_item_pen() or nil +end + +ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen) +ItemSelectionScreen.ATTRS { + focus_path='buildingplan/itemselection', + force_pause=true, + pass_pause=false, + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, + index=DEFAULT_NIL, + on_submit=DEFAULT_NIL, +} + +function ItemSelectionScreen:init() + self.selected_set = {} + + self:addviews{ + ItemSelection{ + index=self.index, + selected_set=self.selected_set, + } + } +end + +function ItemSelectionScreen:onDismiss() + local selected_items = {} + for item_id in pairs(self.selected_set) do + table.insert(selected_items, item_id) + end + self.on_submit(selected_items) +end + +-------------------------------- +-- PlannerOverlay +-- local function cur_building_has_no_area() if uibs.building_type == df.building_type.Construction then return false end @@ -107,22 +294,6 @@ local function cur_building_has_no_area() return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) end -local function is_choosing_area() - return uibs.selection_pos.x >= 0 -end - -local function get_cur_area_dims() - if not is_choosing_area() then return 1, 1, 1 end - return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, - math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, - math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 -end - -local function get_cur_filters() - return dfhack.buildings.getFiltersByType({}, uibs.building_type, - uibs.building_subtype, uibs.custom_type) -end - local function is_plannable() return get_cur_filters() and not (uibs.building_type == df.building_type.Construction @@ -276,17 +447,6 @@ function get_desc(filter) return desc end -local function get_quantity(filter) - local quantity = filter.quantity or 1 - local dimx, dimy, dimz = get_cur_area_dims() - if quantity < 1 then - quantity = (((dimx * dimy) // 4) + 1) * dimz - else - quantity = quantity * dimx * dimy * dimz - end - return quantity -end - function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] @@ -357,19 +517,19 @@ function PlannerOverlay:init() }, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, - on_filter=self:callback('filter'), + on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', @@ -426,7 +586,7 @@ function PlannerOverlay:init() frame={b=1, l=21}, key='CUSTOM_F', label='Set filter', - on_activate=function() self:filter(self.selected) end, + on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, @@ -501,8 +661,8 @@ function PlannerOverlay:reset() reset_counts_flag = false end -function PlannerOverlay:filter(idx) - print('filter', idx) +function PlannerOverlay:set_filter(idx) + print('set_filter', idx) end function PlannerOverlay:clear_filter(idx) @@ -539,11 +699,34 @@ function PlannerOverlay:onInput(keys) local pos = dfhack.gui.getMousePos() if pos then if is_choosing_area() or cur_building_has_no_area() then - if #get_cur_filters() == 0 then + local num_filters = #get_cur_filters() + if num_filters == 0 then return false -- we don't add value; let the game place it end - self:place_building() - uibs.selection_pos:clear() + local choose = self.subviews.choose + if choose.enabled() and choose:getOptionValue() then + local chosen_items = {} + local pending = num_filters + for idx = num_filters,1,-1 do + chosen_items[idx] = {} + if (self.subviews['item'..idx].available or 0) > 0 then + ItemSelectionScreen{ + index=self.selected, + on_submit=function(items) + chosen_items[idx] = items + pending = pending - 1 + if pending == 0 then + self:place_building(chosen_items) + end + end, + }:show() + else + pending = pending - 1 + end + end + else + self:place_building() + end return true elseif not is_choosing_area() then return false @@ -589,7 +772,7 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:place_building() +function PlannerOverlay:place_building(chosen_items) local direction = uibs.direction local width, height, depth = get_cur_area_dims() local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( @@ -618,11 +801,6 @@ function PlannerOverlay:place_building() max_y = min_y + height - 1 max_z = math.max(uibs.selection_pos.z, uibs.pos.z) end - if self.subviews.choose:getOptionValue() then - -- TODO - -- open dialog, showing all items (restricted to current filters) - -- select items (doesn't have to be all required items) - end local blds = {} local subtype = uibs.building_subtype for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do @@ -681,10 +859,32 @@ function PlannerOverlay:place_building() self.subviews.item3:reduce_quantity() self.subviews.item4:reduce_quantity() for _,bld in ipairs(blds) do - -- TODO: attach chosen items and reduce job_item quantity + -- attach chosen items and reduce job_item quantity + if chosen_items then + local job = bld.jobs[0] + local jitems = job.job_items + for idx=1,#get_cur_filters() do + local item_ids = chosen_items[idx] + while jitems[idx-1].quantity > 0 and #item_ids > 0 do + local item_id = item_ids[#item_ids] + local item = df.item.find(item_id) + if not item then + dfhack.printerr(('item no longer available: %d'):format(item_id)) + break + end + if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then + dfhack.printerr(('cannot attach item: %d'):format(item_id)) + break + end + jitems[idx-1].quantity = jitems[idx-1].quantity - 1 + item_ids[#item_ids] = nil + end + end + end addPlannedBuilding(bld) end scheduleCycle() + uibs.selection_pos:clear() end -------------------------------- From ee827f5ca19cc4c90860fe7de1c42f11d308373e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:18:15 -0800 Subject: [PATCH 24/47] remember mouse pos from before item choosing --- plugins/lua/buildingplan.lua | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 46d27d626..65bf918ce 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -75,11 +75,12 @@ local function is_choosing_area() return uibs.selection_pos.x >= 0 end -local function get_cur_area_dims() +local function get_cur_area_dims(pos) if not is_choosing_area() then return 1, 1, 1 end - return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1, - math.abs(uibs.selection_pos.y - uibs.pos.y) + 1, - math.abs(uibs.selection_pos.z - uibs.pos.z) + 1 + pos = pos or uibs.pos + return math.abs(uibs.selection_pos.x - pos.x) + 1, + math.abs(uibs.selection_pos.y - pos.y) + 1, + math.abs(uibs.selection_pos.z - pos.z) + 1 end local function get_quantity(filter) @@ -716,7 +717,7 @@ function PlannerOverlay:onInput(keys) chosen_items[idx] = items pending = pending - 1 if pending == 0 then - self:place_building(chosen_items) + self:place_building(pos, chosen_items) end end, }:show() @@ -725,7 +726,7 @@ function PlannerOverlay:onInput(keys) end end else - self:place_building() + self:place_building(pos) end return true elseif not is_choosing_area() then @@ -756,11 +757,12 @@ function PlannerOverlay:onRenderFrame(dc, rect) if not is_choosing_area() then return end + local pos = uibs.pos local bounds = { - x1 = math.min(uibs.selection_pos.x, uibs.pos.x), - x2 = math.max(uibs.selection_pos.x, uibs.pos.x), - y1 = math.min(uibs.selection_pos.y, uibs.pos.y), - y2 = math.max(uibs.selection_pos.y, uibs.pos.y), + x1 = math.min(uibs.selection_pos.x, pos.x), + x2 = math.max(uibs.selection_pos.x, pos.x), + y1 = math.min(uibs.selection_pos.y, pos.y), + y2 = math.max(uibs.selection_pos.y, pos.y), } local pen = #uibs.errors > 0 and BAD_PEN or GOOD_PEN @@ -772,18 +774,18 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:place_building(chosen_items) +function PlannerOverlay:place_building(pos, chosen_items) local direction = uibs.direction - local width, height, depth = get_cur_area_dims() + local width, height, depth = get_cur_area_dims(pos) local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( width, height, uibs.building_type, uibs.building_subtype, uibs.custom_type, direction) -- get the upper-left corner of the building/area at min z-level local has_selection = is_choosing_area() local start_pos = xyz2pos( - has_selection and math.min(uibs.selection_pos.x, uibs.pos.x) or uibs.pos.x - adjusted_width//2, - has_selection and math.min(uibs.selection_pos.y, uibs.pos.y) or uibs.pos.y - adjusted_height//2, - has_selection and math.min(uibs.selection_pos.z, uibs.pos.z) or uibs.pos.z + has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, + has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z ) if uibs.building_type == df.building_type.ScrewPump then if direction == df.screw_pump_direction.FromSouth then @@ -799,7 +801,7 @@ function PlannerOverlay:place_building(chosen_items) and (width > 1 or height > 1 or depth > 1) then max_x = min_x + width - 1 max_y = min_y + height - 1 - max_z = math.max(uibs.selection_pos.z, uibs.pos.z) + max_z = math.max(uibs.selection_pos.z, pos.z) end local blds = {} local subtype = uibs.building_subtype From 2477a239724258d632cb41dab54ce6cac0cc6843 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:25:07 -0800 Subject: [PATCH 25/47] pass correct job_item index for item selection --- plugins/lua/buildingplan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 65bf918ce..327f9f685 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -712,7 +712,7 @@ function PlannerOverlay:onInput(keys) chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then ItemSelectionScreen{ - index=self.selected, + index=idx, on_submit=function(items) chosen_items[idx] = items pending = pending - 1 From daa812b21eab7fa86e9d74c919f6c9e27d304e50 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Feb 2023 01:25:24 -0800 Subject: [PATCH 26/47] pluralize plural plurals --- plugins/buildingplan/buildingplan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index d1e78b675..ad21ada94 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -407,7 +407,7 @@ static void printStatus(color_ostream &out) { out.print("Waiting for %d item(s) to be produced for %zd building(s):\n", total, planned_buildings.size()); for (auto &count : counts) - out.print(" %3d %s\n", count.second, count.first.c_str()); + out.print(" %3d %s%s\n", count.second, count.first.c_str(), count.second == 1 ? "" : "s"); } else { out.print("Currently no planned buildings\n"); } From a0785bded456708b49d6a174d7d01c3034c4bf8e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 00:57:30 -0800 Subject: [PATCH 27/47] implement heat safety --- plugins/buildingplan/buildingplan.cpp | 72 +++++++++++++++++---- plugins/buildingplan/buildingplan.h | 10 ++- plugins/buildingplan/buildingplan_cycle.cpp | 15 +++-- plugins/buildingplan/plannedbuilding.cpp | 6 +- plugins/buildingplan/plannedbuilding.h | 6 +- plugins/lua/buildingplan.lua | 32 +++++---- 6 files changed, 111 insertions(+), 30 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index ad21ada94..0a05c4ed5 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -74,6 +74,7 @@ struct BuildingTypeKeyHash { static PersistentDataItem config; // for use in counting available materials for the UI static unordered_map, BuildingTypeKeyHash> job_item_repo; +static unordered_map cur_heat_safety; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -456,13 +457,20 @@ static bool isPlannedBuilding(color_ostream &out, df::building *bld) { return bld && planned_buildings.count(bld->id) > 0; } +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} + static bool addPlannedBuilding(color_ostream &out, df::building *bld) { DEBUG(status,out).print("entering addPlannedBuilding\n"); if (!bld || planned_buildings.count(bld->id) || !isPlannableBuilding(out, bld->getType(), bld->getSubtype(), bld->getCustomType())) return false; - PlannedBuilding pb(out, bld); + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key)); return registerPlannedBuilding(out, pb); } @@ -482,6 +490,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); auto &job_items = job_item_repo[key]; if (index >= (int)job_items.size()) { for (int i = job_items.size(); i <= index; ++i) { @@ -514,7 +523,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem)) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat)) { if (item_ids) item_ids->emplace_back(item->id); ++count; @@ -550,17 +559,56 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return scanAvailableItems(out, type, subtype, custom, index); } -static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering hasFilter\n"); +static bool hasMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering hasMaterialFilter\n"); return false; } -static void setFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering setFilter\n"); +static void setMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index, string filter) { + DEBUG(status,out).print("entering setMaterialFilter\n"); + call_buildingplan_lua(&out, "signal_reset"); +} + +static int getMaterialFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + int index = luaL_checkint(L, 4); + DEBUG(status,*out).print( + "entering getMaterialFilter building_type=%d subtype=%d custom=%d index=%d\n", + type, subtype, custom, index); + vector filter; + Lua::PushVector(L, filter); + return 1; +} + +static void setHeatSafetyFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int heat) { + DEBUG(status,out).print("entering setHeatSafetyFilter\n"); + BuildingTypeKey key(type, subtype, custom); + if (heat == HEAT_SAFETY_FIRE || heat == HEAT_SAFETY_MAGMA) + cur_heat_safety[key] = (HeatSafety)heat; + else + cur_heat_safety.erase(key); + call_buildingplan_lua(&out, "signal_reset"); } -static void clearFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering clearFilter\n"); +static int getHeatSafetyFilter(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + df::building_type type = (df::building_type)luaL_checkint(L, 1); + int16_t subtype = luaL_checkint(L, 2); + int32_t custom = luaL_checkint(L, 3); + DEBUG(status,*out).print( + "entering getHeatSafetyFilter building_type=%d subtype=%d custom=%d\n", + type, subtype, custom); + BuildingTypeKey key(type, subtype, custom); + HeatSafety heat = get_heat_safety_filter(key); + Lua::Push(L, heat); + return 1; } static bool validate_pb(color_ostream &out, df::building *bld, int index) { @@ -659,9 +707,9 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), - DFHACK_LUA_FUNCTION(hasFilter), - DFHACK_LUA_FUNCTION(setFilter), - DFHACK_LUA_FUNCTION(clearFilter), + DFHACK_LUA_FUNCTION(hasMaterialFilter), + DFHACK_LUA_FUNCTION(setMaterialFilter), + DFHACK_LUA_FUNCTION(setHeatSafetyFilter), DFHACK_LUA_FUNCTION(getDescString), DFHACK_LUA_FUNCTION(getQueuePosition), DFHACK_LUA_FUNCTION(makeTopPriority), @@ -670,5 +718,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getAvailableItems), + DFHACK_LUA_COMMAND(getMaterialFilter), + DFHACK_LUA_COMMAND(getHeatSafetyFilter), DFHACK_LUA_END }; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 01c72e370..787987586 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -2,6 +2,7 @@ #include "modules/Persistence.h" +#include "df/building.h" #include "df/job_item.h" #include "df/job_item_vector_id.h" @@ -21,6 +22,13 @@ enum ConfigValues { enum BuildingConfigValues { BLD_CONFIG_ID = 0, + BLD_CONFIG_HEAT = 1, +}; + +enum HeatSafety { + HEAT_SAFETY_ANY = 0, + HEAT_SAFETY_FIRE = 1, + HEAT_SAFETY_MAGMA = 2, }; int get_config_val(DFHack::PersistentDataItem &c, int index); @@ -30,6 +38,6 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item); +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 069787f39..703bab9b0 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item) { +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; @@ -65,10 +65,17 @@ bool matchesFilters(df::item * item, df::job_item * job_item) { && !item->hasToolUse(job_item->has_tool_use)) return false; + df::job_item jitem = *job_item; + if (heat == HEAT_SAFETY_MAGMA) { + jitem.flags2.bits.magma_safe = true; + jitem.flags2.bits.fire_safe = false; + } else if (heat == HEAT_SAFETY_FIRE && !jitem.flags2.bits.magma_safe) + jitem.flags2.bits.fire_safe = true; + return Job::isSuitableItem( - job_item, item->getType(), item->getSubtype()) + &jitem, item->getType(), item->getSubtype()) && Job::isSuitableMaterial( - job_item, item->getMaterial(), item->getMaterialIndex(), + &jitem, item->getMaterial(), item->getMaterialIndex(), item->getType()); } @@ -173,7 +180,7 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto id = task.first; auto job = bld->jobs[0]; auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx]) + if (matchesFilters(item, job->job_items[filter_idx], planned_buildings.at(id).heat_safety) && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index f4f3564b7..eb55a95b4 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -62,11 +62,12 @@ static string serialize(const vector> &vector_ids return join_strings("|", joined); } -PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) - : id(building->id), vector_ids(get_vector_ids(out, id)) { +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); + set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); bld_config.val() = serialize(vector_ids); DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } @@ -74,6 +75,7 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *building) PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) : id(get_config_val(bld_config, BLD_CONFIG_ID)), vector_ids(deserialize(out, bld_config)), + heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 592f0e4b3..0a67e0edc 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -1,5 +1,7 @@ #pragma once +#include "buildingplan.h" + #include "Core.h" #include "modules/Persistence.h" @@ -14,7 +16,9 @@ public: // job_item idx -> list of vectors the task is linked to const std::vector> vector_ids; - PlannedBuilding(DFHack::color_ostream &out, df::building *building); + const HeatSafety heat_safety; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat); PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 327f9f685..e4aee907b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -251,14 +251,18 @@ function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end -ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen) -ItemSelectionScreen.ATTRS { - focus_path='buildingplan/itemselection', +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { force_pause=true, pass_pause=false, pass_movement_keys=true, pass_mouse_clicks=false, defocusable=false, +} + +ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) +ItemSelectionScreen.ATTRS { + focus_path='buildingplan/itemselection', index=DEFAULT_NIL, on_submit=DEFAULT_NIL, } @@ -414,7 +418,8 @@ function ItemLine:onInput(keys) end function ItemLine:get_x_pen() - return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and COLOR_GREEN or COLOR_GREY + return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and + COLOR_GREEN or COLOR_GREY end function get_desc(filter) @@ -599,7 +604,7 @@ function PlannerOverlay:init() view_id='choose', frame={b=0, l=0}, key='CUSTOM_I', - label='Choose exact items:', + label='Choose from items:', options={{label='Yes', value=true}, {label='No', value=false}}, initial_option=false, @@ -617,10 +622,13 @@ function PlannerOverlay:init() key='CUSTOM_G', label='Building safety:', options={ - {label='Any', value='none'}, - {label='Magma', value='magma'}, - {label='Fire', value='fire'}, + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, + on_change=function(heat) + setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) + end, }, }, }, @@ -663,11 +671,11 @@ function PlannerOverlay:reset() end function PlannerOverlay:set_filter(idx) - print('set_filter', idx) + print('TODO: set_filter', idx) end function PlannerOverlay:clear_filter(idx) - print('clear_filter', idx) + setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx, "") end function PlannerOverlay:onInput(keys) @@ -679,8 +687,8 @@ function PlannerOverlay:onInput(keys) end self.selected = 1 self.subviews.choose:setOption(false) - self.subviews.safety:setOption('none') self:reset() + reset_counts_flag = true return false end if PlannerOverlay.super.onInput(self, keys) then @@ -753,6 +761,8 @@ function PlannerOverlay:onRenderFrame(dc, rect) if reset_counts_flag then self:reset() + self.subviews.safety:setOption(getHeatSafetyFilter( + uibs.building_type, uibs.building_subtype, uibs.custom_type)) end if not is_choosing_area() then return end From 273183e864ac2b19bb2741fddc5cc21b8bf03b15 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 01:58:17 -0800 Subject: [PATCH 28/47] allow cancel when choosing items --- plugins/lua/buildingplan.lua | 65 ++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index e4aee907b..7283d805f 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -135,24 +135,29 @@ end -- ItemSelection -- +local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} +local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} + ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', - frame={w=60, h=30, l=4, t=8}, + frame={w=56, h=20, l=4, t=8}, + draggable=false, resizable=true, - resize_min={w=56, h=20}, index=DEFAULT_NIL, - selected_set=DEFAULT_NIL, + on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] self.quantity = get_quantity(filter) self.num_selected = 0 + self.selected_set = {} self:addviews{ widgets.Label{ - frame={t=0}, + frame={t=0, l=0, r=10}, text={ get_desc(filter), self.quantity == 1 and '' or 's', @@ -162,6 +167,17 @@ function ItemSelection:init() ' selected)', }, }, + widgets.Label{ + frame={r=0, w=9, t=0, h=3}, + text_pen=BUILD_TEXT_PEN, + text_hpen=BUILD_TEXT_HPEN, + text={ + ' ', NEWLINE, + ' Build ', NEWLINE, + ' ', + }, + on_click=self:callback('submit'), + }, widgets.FilteredList{ frame={t=3, l=0, r=0, b=0}, case_sensitive=false, @@ -251,6 +267,22 @@ function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end +function ItemSelection:submit() + local selected_items = {} + for item_id in pairs(self.selected_set) do + table.insert(selected_items, item_id) + end + self.on_submit(selected_items) +end + +function ItemSelection:onInput(keys) + if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then + self.on_cancel() + return true + end + return ItemSelection.super.onInput(self, keys) +end + BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) BuildingplanScreen.ATTRS { force_pause=true, @@ -265,27 +297,19 @@ ItemSelectionScreen.ATTRS { focus_path='buildingplan/itemselection', index=DEFAULT_NIL, on_submit=DEFAULT_NIL, + on_cancel=DEFAULT_NIL, } function ItemSelectionScreen:init() - self.selected_set = {} - self:addviews{ ItemSelection{ index=self.index, - selected_set=self.selected_set, + on_submit=self.on_submit, + on_cancel=self.on_cancel, } } end -function ItemSelectionScreen:onDismiss() - local selected_items = {} - for item_id in pairs(self.selected_set) do - table.insert(selected_items, item_id) - end - self.on_submit(selected_items) -end - -------------------------------- -- PlannerOverlay -- @@ -714,20 +738,27 @@ function PlannerOverlay:onInput(keys) end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then - local chosen_items = {} + local chosen_items, active_screens = {}, {} local pending = num_filters for idx = num_filters,1,-1 do chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then - ItemSelectionScreen{ + active_screens[idx] = ItemSelectionScreen{ index=idx, on_submit=function(items) chosen_items[idx] = items + active_screens[idx]:dismiss() + active_screens[idx] = nil pending = pending - 1 if pending == 0 then self:place_building(pos, chosen_items) end end, + on_cancel=function() + for i,scr in pairs(active_screens) do + scr:dismiss() + end + end, }:show() else pending = pending - 1 From e9555c29be83c3b21278aea082dffdb3f9fb2eeb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 02:03:39 -0800 Subject: [PATCH 29/47] initialize heat safety option to 'Any' --- plugins/lua/buildingplan.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 7283d805f..fcd7b83fd 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -650,6 +650,7 @@ function PlannerOverlay:init() {label='Magma', value=2, pen=COLOR_RED}, {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, + initial_option=0, on_change=function(heat) setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, From 348ac55f4cb5160122f073814de88dfb579e28d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 21:17:03 -0800 Subject: [PATCH 30/47] allow singleton selection for items --- plugins/lua/buildingplan.lua | 238 +++++++++++++++++++++++++---------- 1 file changed, 171 insertions(+), 67 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index fcd7b83fd..cb5926723 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -131,6 +131,15 @@ local function get_selected_item_pen() return SELECTED_ITEM_PEN end +BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) +BuildingplanScreen.ATTRS { + force_pause=true, + pass_pause=false, + pass_movement_keys=true, + pass_mouse_clicks=false, + defocusable=false, +} + -------------------------------- -- ItemSelection -- @@ -154,15 +163,16 @@ function ItemSelection:init() self.quantity = get_quantity(filter) self.num_selected = 0 self.selected_set = {} + local plural = self.quantity == 1 and '' or 's' self:addviews{ widgets.Label{ frame={t=0, l=0, r=10}, text={ get_desc(filter), - self.quantity == 1 and '' or 's', + plural, NEWLINE, - ('Select up to %d items ('):format(self.quantity), + ('Select up to %d item%s ('):format(self.quantity, plural), {text=function() return self.num_selected end}, ' selected)', }, @@ -179,11 +189,52 @@ function ItemSelection:init() on_click=self:callback('submit'), }, widgets.FilteredList{ - frame={t=3, l=0, r=0, b=0}, + view_id='flist', + frame={t=3, l=0, r=0, b=4}, case_sensitive=false, choices=self:get_choices(), icon_width=2, - on_submit=self:callback('toggle_item'), + on_submit=self:callback('toggle_group'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=2}, + key='SELECT', + label='Use all/none selected', + auto_width=true, + on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.HotkeyLabel{ + frame={l=32, b=2}, + key='LEAVESCREEN', + label='Cancel build', + auto_width=true, + on_activate=function() self.on_cancel() end, + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, + key='KEYBOARD_CURSOR_RIGHT_FAST', + key_sep=' : ', + label='Use one selected', + auto_width=true, + on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=1, w=5}, + text_pen=COLOR_LIGHTGREEN, + text='Right', + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='KEYBOARD_CURSOR_LEFT_FAST', + key_sep=' : ', + label='Use one fewer selected', + auto_width=true, + on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, + }, + widgets.Label{ + frame={l=6, b=0, w=4}, + text_pen=COLOR_LIGHTGREEN, + text='Left', }, } end @@ -199,67 +250,79 @@ end function ItemSelection:get_choices() local item_ids = getAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index - 1) - local buckets, selected_buckets = {}, {} + local buckets = {} for _,item_id in ipairs(item_ids) do local item = df.item.find(item_id) if not item then goto continue end local desc = dfhack.items.getDescription(item, 0, true) if buckets[desc] then local bucket = buckets[desc] - table.insert(bucket.item_ids, item_id) - bucket.quantity = bucket.quantity + 1 + table.insert(bucket.data.item_ids, item_id) + bucket.data.quantity = bucket.data.quantity + 1 else local entry = { - text=desc, search_key=make_search_key(desc), icon=self:callback('get_entry_icon', item_id), - item_ids={item_id}, - item_type=item:getType(), - item_subtype=item:getSubtype(), - quantity=1, - selected=false, + data={ + item_ids={item_id}, + item_type=item:getType(), + item_subtype=item:getSubtype(), + quantity=1, + quality=item:getQuality(), + selected=0, + }, } buckets[desc] = entry end ::continue:: end - local selected_qty = 0 - for bucket in pairs(selected_buckets) do - for _,item_id in ipairs(bucket.item_ids) do - self.selected_set[item_id] = true - end - selected_qty = selected_qty + bucket.quantity - bucket.selected = true - if selected_qty >= self.quantity then break end - end - self.num_selected = selected_qty local choices = {} - for _,choice in pairs(buckets) do - choice.text = ('(%d) %s'):format(choice.quantity, choice.text) + for desc,choice in pairs(buckets) do + local data = choice.data + choice.text = { + {width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end}, + {gap=2, text=desc}, + } table.insert(choices, choice) end local function choice_sort(a, b) - return a.item_type < b.item_type or - (a.item_type == b.item_type and a.item_subtype < b.item_subtype) or - (a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) end table.sort(choices, choice_sort) return choices end -function ItemSelection:toggle_item(_, choice) - if choice.selected then - for _,item_id in ipairs(choice.item_ids) do - self.selected_set[item_id] = nil - end - self.num_selected = self.num_selected - choice.quantity - choice.selected = false - elseif self.quantity > self.num_selected then - for _,item_id in ipairs(choice.item_ids) do - self.selected_set[item_id] = true - end - self.num_selected = self.num_selected + choice.quantity - choice.selected = true +function ItemSelection:increment_group(idx, choice) + local data = choice.data + if self.quantity <= self.num_selected then return false end + if data.selected >= data.quantity then return false end + data.selected = data.selected + 1 + self.num_selected = self.num_selected + 1 + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = true + return true +end + +function ItemSelection:decrement_group(idx, choice) + local data = choice.data + if data.selected <= 0 then return false end + local item_id = data.item_ids[data.selected] + self.selected_set[item_id] = nil + self.num_selected = self.num_selected - 1 + data.selected = data.selected - 1 + return true +end + +function ItemSelection:toggle_group(idx, choice) + local data = choice.data + if data.selected > 0 then + while self:decrement_group(idx, choice) do end + else + while self:increment_group(idx, choice) do end end end @@ -279,19 +342,26 @@ function ItemSelection:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then self.on_cancel() return true + elseif keys._MOUSE_L_DOWN then + local list = self.subviews.flist.list + local idx = list:getIdxUnderMouse() + if idx then + list:setSelected(idx) + local modstate = dfhack.internal.getModstate() + if modstate & 2 > 0 then -- ctrl + local choice = list:getChoices()[idx] + if modstate & 1 > 0 then -- shift + self:decrement_group(idx, choice) + else + self:increment_group(idx, choice) + end + return true + end + end end return ItemSelection.super.onInput(self, keys) end -BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) -BuildingplanScreen.ATTRS { - force_pause=true, - pass_pause=false, - pass_movement_keys=true, - pass_mouse_clicks=false, - defocusable=false, -} - ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { focus_path='buildingplan/itemselection', @@ -311,7 +381,48 @@ function ItemSelectionScreen:init() end -------------------------------- --- PlannerOverlay +-- FilterSelection +-- + +-- returns whether the items matched by the specified filter can have a quality +-- rating. This also conveniently indicates whether an item can be decorated. +local function can_be_improved(idx) + local filter = get_cur_filters()[idx] + if filter.flags2 and filter.flags2.building_material then + return false; + end + return filter.item_type ~= df.item_type.WOOD and + filter.item_type ~= df.item_type.BLOCKS and + filter.item_type ~= df.item_type.BAR and + filter.item_type ~= df.item_type.BOULDER +end + +FilterSelection = defclass(FilterSelection, widgets.Window) +FilterSelection.ATTRS{ + frame_title='Choose filters', + frame={w=60, h=40, l=4, t=8}, + draggable=false, + resizable=true, + index=DEFAULT_NIL, +} + +function FilterSelection:init() +end + +FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) +FilterSelectionScreen.ATTRS { + focus_path='buildingplan/filterselection', + index=DEFAULT_NIL, +} + +function FilterSelectionScreen:init() + self:addviews{ + FilterSelection{index=self.index} + } +end + +-------------------------------- +-- ItemLine -- local function cur_building_has_no_area() @@ -512,6 +623,10 @@ local function get_placement_errors() return out end +-------------------------------- +-- PlannerOverlay +-- + PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, @@ -932,7 +1047,7 @@ function PlannerOverlay:place_building(pos, chosen_items) end -------------------------------- --- InspectorOverlay +-- InspectorLine -- local function get_building_filters() @@ -981,6 +1096,10 @@ function InspectorLine:reset() self.status = nil end +-------------------------------- +-- InspectorOverlay +-- + InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, @@ -1053,19 +1172,4 @@ OVERLAY_WIDGETS = { inspector=InspectorOverlay, } --- returns whether the items matched by the specified filter can have a quality --- rating. This also conveniently indicates whether an item can be decorated. --- does not need the core suspended --- reverse_idx is 0-based and is expected to be counted from the *last* filter -function item_can_be_improved(btype, subtype, custom, reverse_idx) - local filter = get_filter(btype, subtype, custom, reverse_idx) - if filter.flags2 and filter.flags2.building_material then - return false; - end - return filter.item_type ~= df.item_type.WOOD and - filter.item_type ~= df.item_type.BLOCKS and - filter.item_type ~= df.item_type.BAR and - filter.item_type ~= df.item_type.BOULDER -end - return _ENV From 69e9da2e79c3305ca97a6c5aa7f721e8a3ba5856 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 19 Feb 2023 23:28:57 -0800 Subject: [PATCH 31/47] keep target area higlighted while choosing items --- plugins/buildingplan/buildingplan.cpp | 1 + plugins/lua/buildingplan.lua | 216 ++++++++++++++++---------- 2 files changed, 135 insertions(+), 82 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 0a05c4ed5..6c16b043e 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -171,6 +171,7 @@ static bool call_buildingplan_lua(color_ostream *out, const char *fn_name, static void clear_state(color_ostream &out) { call_buildingplan_lua(&out, "signal_reset"); + call_buildingplan_lua(&out, "reload_cursors"); planned_buildings.clear(); tasks.clear(); for (auto &entry : job_item_repo) { diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index cb5926723..b24f5893a 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -75,17 +75,18 @@ local function is_choosing_area() return uibs.selection_pos.x >= 0 end -local function get_cur_area_dims(pos) - if not is_choosing_area() then return 1, 1, 1 end - pos = pos or uibs.pos - return math.abs(uibs.selection_pos.x - pos.x) + 1, - math.abs(uibs.selection_pos.y - pos.y) + 1, - math.abs(uibs.selection_pos.z - pos.z) + 1 +local function get_cur_area_dims(placement_data) + if not placement_data and not is_choosing_area() then return 1, 1, 1 end + local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos + local pos = placement_data and placement_data.p2 or uibs.pos + return math.abs(selection_pos.x - pos.x) + 1, + math.abs(selection_pos.y - pos.y) + 1, + math.abs(selection_pos.z - pos.z) + 1 end -local function get_quantity(filter) +local function get_quantity(filter, placement_data) local quantity = filter.quantity or 1 - local dimx, dimy, dimz = get_cur_area_dims() + local dimx, dimy, dimz = get_cur_area_dims(placement_data) if quantity < 1 then quantity = (((dimx * dimy) // 4) + 1) * dimz else @@ -151,16 +152,16 @@ ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', frame={w=56, h=20, l=4, t=8}, - draggable=false, resizable=true, index=DEFAULT_NIL, + placement_data=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] - self.quantity = get_quantity(filter) + self.quantity = get_quantity(filter, self.placement_data) self.num_selected = 0 self.selected_set = {} local plural = self.quantity == 1 and '' or 's' @@ -364,8 +365,9 @@ end ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { - focus_path='buildingplan/itemselection', + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', index=DEFAULT_NIL, + placement_data=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } @@ -374,6 +376,7 @@ function ItemSelectionScreen:init() self:addviews{ ItemSelection{ index=self.index, + placement_data=self.placement_data, on_submit=self.on_submit, on_cancel=self.on_cancel, } @@ -401,7 +404,6 @@ FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', frame={w=60, h=40, l=4, t=8}, - draggable=false, resizable=true, index=DEFAULT_NIL, } @@ -411,7 +413,7 @@ end FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { - focus_path='buildingplan/filterselection', + focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', index=DEFAULT_NIL, } @@ -818,6 +820,73 @@ function PlannerOverlay:clear_filter(idx) setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx, "") end +local function get_placement_data() + local pos = uibs.pos + local direction = uibs.direction + local width, height, depth = get_cur_area_dims() + local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( + width, height, uibs.building_type, uibs.building_subtype, + uibs.custom_type, direction) + -- get the upper-left corner of the building/area at min z-level + local has_selection = is_choosing_area() + local start_pos = xyz2pos( + has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, + has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, + has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z + ) + if uibs.building_type == df.building_type.ScrewPump then + if direction == df.screw_pump_direction.FromSouth then + start_pos.y = start_pos.y + 1 + elseif direction == df.screw_pump_direction.FromEast then + start_pos.x = start_pos.x + 1 + end + end + local min_x, max_x = start_pos.x, start_pos.x + local min_y, max_y = start_pos.y, start_pos.y + local min_z, max_z = start_pos.z, start_pos.z + if adjusted_width == 1 and adjusted_height == 1 + and (width > 1 or height > 1 or depth > 1) then + max_x = min_x + width - 1 + max_y = min_y + height - 1 + max_z = math.max(uibs.selection_pos.z, pos.z) + end + return { + p1=xyz2pos(min_x, min_y, min_z), + p2=xyz2pos(max_x, max_y, max_z), + width=adjusted_width, + height=adjusted_height + } +end + +function PlannerOverlay:save_placement() + self.saved_placement = get_placement_data() + if (uibs.selection_pos:isValid()) then + self.saved_selection_pos_valid = true + self.saved_selection_pos = copyall(uibs.selection_pos) + self.saved_pos = copyall(uibs.pos) + uibs.selection_pos:clear() + else + self.saved_selection_pos = copyall(self.saved_placement.p1) + self.saved_pos = copyall(self.saved_placement.p2) + self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1 + self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1 + end +end + +function PlannerOverlay:restore_placement() + if self.saved_selection_pos_valid then + uibs.selection_pos = self.saved_selection_pos + self.saved_selection_pos_valid = nil + else + uibs.selection_pos:clear() + end + self.saved_selection_pos = nil + self.saved_pos = nil + local placement_data = self.saved_placement + self.saved_placement = nil + return placement_data +end + function PlannerOverlay:onInput(keys) if not is_plannable() then return false end if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then @@ -845,8 +914,7 @@ function PlannerOverlay:onInput(keys) return true end if #uibs.errors > 0 then return true end - local pos = dfhack.gui.getMousePos() - if pos then + if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then local num_filters = #get_cur_filters() if num_filters == 0 then @@ -854,6 +922,7 @@ function PlannerOverlay:onInput(keys) end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then + self:save_placement() local chosen_items, active_screens = {}, {} local pending = num_filters for idx = num_filters,1,-1 do @@ -861,19 +930,21 @@ function PlannerOverlay:onInput(keys) if (self.subviews['item'..idx].available or 0) > 0 then active_screens[idx] = ItemSelectionScreen{ index=idx, + placement_data=self.saved_placement, on_submit=function(items) chosen_items[idx] = items active_screens[idx]:dismiss() active_screens[idx] = nil pending = pending - 1 if pending == 0 then - self:place_building(pos, chosen_items) + self:place_building(self:restore_placement(), chosen_items) end end, on_cancel=function() for i,scr in pairs(active_screens) do scr:dismiss() end + self:restore_placement() end, }:show() else @@ -881,7 +952,7 @@ function PlannerOverlay:onInput(keys) end end else - self:place_building(pos) + self:place_building(get_placement_data()) end return true elseif not is_choosing_area() then @@ -898,10 +969,12 @@ function PlannerOverlay:render(dc) PlannerOverlay.super.render(self, dc) end -local GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, - tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} -local BAD_PEN = to_pen{ch='X', fg=COLOR_RED, - tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} +local GOOD_PEN, BAD_PEN +function reload_cursors() + GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} + BAD_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)} +end +reload_cursors() function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) @@ -912,17 +985,18 @@ function PlannerOverlay:onRenderFrame(dc, rect) uibs.building_type, uibs.building_subtype, uibs.custom_type)) end - if not is_choosing_area() then return end + local selection_pos = self.saved_selection_pos or uibs.selection_pos + if not selection_pos or selection_pos.x < 0 then return end - local pos = uibs.pos + local pos = self.saved_pos or uibs.pos local bounds = { - x1 = math.min(uibs.selection_pos.x, pos.x), - x2 = math.max(uibs.selection_pos.x, pos.x), - y1 = math.min(uibs.selection_pos.y, pos.y), - y2 = math.max(uibs.selection_pos.y, pos.y), + x1 = math.min(selection_pos.x, pos.x), + x2 = math.max(selection_pos.x, pos.x), + y1 = math.min(selection_pos.y, pos.y), + y2 = math.max(selection_pos.y, pos.y), } - local pen = #uibs.errors > 0 and BAD_PEN or GOOD_PEN + local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN local function get_overlay_pen(pos) return pen @@ -931,69 +1005,47 @@ function PlannerOverlay:onRenderFrame(dc, rect) guidm.renderMapOverlay(get_overlay_pen, bounds) end -function PlannerOverlay:place_building(pos, chosen_items) - local direction = uibs.direction - local width, height, depth = get_cur_area_dims(pos) - local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( - width, height, uibs.building_type, uibs.building_subtype, - uibs.custom_type, direction) - -- get the upper-left corner of the building/area at min z-level - local has_selection = is_choosing_area() - local start_pos = xyz2pos( - has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2, - has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2, - has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z - ) - if uibs.building_type == df.building_type.ScrewPump then - if direction == df.screw_pump_direction.FromSouth then - start_pos.y = start_pos.y + 1 - elseif direction == df.screw_pump_direction.FromEast then - start_pos.x = start_pos.x + 1 +function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2) + local subtype = uibs.building_subtype + if pos.z == corner1.z then + local opt = self.subviews.stairs_bottom_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_DOWN then + subtype = df.construction_type.UpStair + end + else + subtype = opt + end + elseif pos.z == corner2.z then + local opt = self.subviews.stairs_top_subtype:getOptionValue() + if opt == 'auto' then + local tt = dfhack.maps.getTileType(pos) + local shape = df.tiletype.attrs[tt].shape + if shape ~= df.tiletype_shape.STAIR_UP then + subtype = df.construction_type.DownStair + end + else + subtype = opt end end - local min_x, max_x = start_pos.x, start_pos.x - local min_y, max_y = start_pos.y, start_pos.y - local min_z, max_z = start_pos.z, start_pos.z - if adjusted_width == 1 and adjusted_height == 1 - and (width > 1 or height > 1 or depth > 1) then - max_x = min_x + width - 1 - max_y = min_y + height - 1 - max_z = math.max(uibs.selection_pos.z, pos.z) - end + return subtype +end + +function PlannerOverlay:place_building(placement_data, chosen_items) + local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} local subtype = uibs.building_subtype - for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do + for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do local pos = xyz2pos(x, y, z) if is_stairs() then - if z == min_z then - subtype = self.subviews.stairs_bottom_subtype:getOptionValue() - if subtype == 'auto' then - local tt = dfhack.maps.getTileType(pos) - local shape = df.tiletype.attrs[tt].shape - if shape == df.tiletype_shape.STAIR_DOWN then - subtype = uibs.building_subtype - else - subtype = df.construction_type.UpStair - end - end - elseif z == max_z then - subtype = self.subviews.stairs_top_subtype:getOptionValue() - if subtype == 'auto' then - local tt = dfhack.maps.getTileType(pos) - local shape = df.tiletype.attrs[tt].shape - if shape == df.tiletype_shape.STAIR_UP then - subtype = uibs.building_subtype - else - subtype = df.construction_type.DownStair - end - end - else - subtype = uibs.building_subtype - end + subtype = self:get_stairs_subtype(pos, p1, p2) end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, - width=adjusted_width, height=adjusted_height, direction=direction} + width=placement_data.width, height=placement_data.height, + direction=uibs.direction} if err then for _,b in ipairs(blds) do dfhack.buildings.deconstruct(b) From f09eeee864bfa8234a5fd95cdabd4c51733d5ed5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Feb 2023 00:11:05 -0800 Subject: [PATCH 32/47] only enable clear filter hotkey when a filter is set --- plugins/lua/buildingplan.lua | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b24f5893a..308afd797 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -555,7 +555,7 @@ function ItemLine:onInput(keys) end function ItemLine:get_x_pen() - return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx) and + return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and COLOR_GREEN or COLOR_GREY end @@ -707,11 +707,13 @@ function PlannerOverlay:init() text={ 'Selected area: ', {text=function() - return ('%dx%dx%d'):format(get_cur_area_dims()) + return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement)) end }, }, - visible=is_choosing_area, + visible=function() + return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area()) + end, }, widgets.Panel{ visible=function() return #get_cur_filters() > 0 end, @@ -719,6 +721,7 @@ function PlannerOverlay:init() widgets.HotkeyLabel{ frame={b=1, l=0}, key='STRING_A042', + auto_width=true, enabled=function() return #get_cur_filters() > 1 end, on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, }, @@ -726,6 +729,7 @@ function PlannerOverlay:init() frame={b=1, l=1}, key='STRING_A047', label='Prev/next item', + auto_width=true, enabled=function() return #get_cur_filters() > 1 end, on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, @@ -733,17 +737,22 @@ function PlannerOverlay:init() frame={b=1, l=21}, key='CUSTOM_F', label='Set filter', + auto_width=true, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, key='CUSTOM_X', label='Clear filter', + auto_width=true, on_activate=function() self:clear_filter(self.selected) end, + enabled=function() + return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + end }, widgets.CycleHotkeyLabel{ view_id='choose', - frame={b=0, l=0}, + frame={b=0, l=0, w=25}, key='CUSTOM_I', label='Choose from items:', options={{label='Yes', value=true}, @@ -759,7 +768,7 @@ function PlannerOverlay:init() }, widgets.CycleHotkeyLabel{ view_id='safety', - frame={b=0, l=29}, + frame={b=0, l=29, w=25}, key='CUSTOM_G', label='Building safety:', options={ @@ -817,7 +826,7 @@ function PlannerOverlay:set_filter(idx) end function PlannerOverlay:clear_filter(idx) - setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx, "") + setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1, "") end local function get_placement_data() From 1957ad4cdfeb353a4c69780e591c966578c8951e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Feb 2023 15:25:36 -0800 Subject: [PATCH 33/47] move the filter window a bit to the side, can pause --- plugins/lua/buildingplan.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 308afd797..5b048ff7c 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -134,8 +134,6 @@ end BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) BuildingplanScreen.ATTRS { - force_pause=true, - pass_pause=false, pass_movement_keys=true, pass_mouse_clicks=false, defocusable=false, @@ -366,6 +364,8 @@ end ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', + force_pause=true, + pass_pause=false, index=DEFAULT_NIL, placement_data=DEFAULT_NIL, on_submit=DEFAULT_NIL, @@ -403,7 +403,7 @@ end FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', - frame={w=60, h=40, l=4, t=8}, + frame={w=60, h=40, l=30, t=8}, resizable=true, index=DEFAULT_NIL, } @@ -822,7 +822,7 @@ function PlannerOverlay:reset() end function PlannerOverlay:set_filter(idx) - print('TODO: set_filter', idx) + FilterSelectionScreen{index=idx}:show() end function PlannerOverlay:clear_filter(idx) From 4f2d86f50af4a85f6c1062f5f55d5e6920e47b6c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 13:04:53 -0800 Subject: [PATCH 34/47] implement hollow area placement for constructions --- plugins/buildingplan/buildingplan.cpp | 6 ++--- plugins/lua/buildingplan.lua | 37 ++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 6c16b043e..828ae4954 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -560,8 +560,8 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 return scanAvailableItems(out, type, subtype, custom, index); } -static bool hasMaterialFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering hasMaterialFilter\n"); +static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { + DEBUG(status,out).print("entering hasFilter\n"); return false; } @@ -708,7 +708,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(doCycle), DFHACK_LUA_FUNCTION(scheduleCycle), DFHACK_LUA_FUNCTION(countAvailableItems), - DFHACK_LUA_FUNCTION(hasMaterialFilter), + DFHACK_LUA_FUNCTION(hasFilter), DFHACK_LUA_FUNCTION(setMaterialFilter), DFHACK_LUA_FUNCTION(setHeatSafetyFilter), DFHACK_LUA_FUNCTION(getDescString), diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 5b048ff7c..fe74ca7c0 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -146,6 +146,8 @@ BuildingplanScreen.ATTRS { local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} +local recently_selected = {} + ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', @@ -442,8 +444,12 @@ local function is_plannable() and uibs.building_subtype == df.construction_type.TrackNSEW) end -local function is_stairs() +local function is_construction() return uibs.building_type == df.building_type.Construction +end + +local function is_stairs() + return is_construction and uibs.building_subtype == df.construction_type.UpDownStair end @@ -555,7 +561,7 @@ function ItemLine:onInput(keys) end function ItemLine:get_x_pen() - return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and COLOR_GREEN or COLOR_GREY end @@ -678,6 +684,17 @@ function PlannerOverlay:init() is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, + widgets.CycleHotkeyLabel{ + view_id='hollow', + frame={t=3, l=4}, + key='CUSTOM_H', + label='Hollow area:', + visible=is_construction, + options={ + {label='No', value=false}, + {label='Yes', value=true}, + }, + }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={t=4, l=4}, @@ -747,7 +764,7 @@ function PlannerOverlay:init() auto_width=true, on_activate=function() self:clear_filter(self.selected) end, enabled=function() - return hasMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) + return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) end }, widgets.CycleHotkeyLabel{ @@ -904,6 +921,7 @@ function PlannerOverlay:onInput(keys) return true end self.selected = 1 + self.subviews.hollow:setOption(false) self.subviews.choose:setOption(false) self:reset() reset_counts_flag = true @@ -1005,10 +1023,16 @@ function PlannerOverlay:onRenderFrame(dc, rect) y2 = math.max(selection_pos.y, pos.y), } + local hollow = self.subviews.hollow:getOptionValue() local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN local function get_overlay_pen(pos) - return pen + if not hollow then return pen end + if pos.x == bounds.x1 or pos.x == bounds.x2 or + pos.y == bounds.y1 or pos.y == bounds.y2 then + return pen + end + return gui.TRANSPARENT_PEN end guidm.renderMapOverlay(get_overlay_pen, bounds) @@ -1045,8 +1069,12 @@ end function PlannerOverlay:place_building(placement_data, chosen_items) local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} + local hollow = self.subviews.hollow:getOptionValue() local subtype = uibs.building_subtype for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do + if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then + goto continue + end local pos = xyz2pos(x, y, z) if is_stairs() then subtype = self:get_stairs_subtype(pos, p1, p2) @@ -1073,6 +1101,7 @@ function PlannerOverlay:place_building(placement_data, chosen_items) if k == 'speed' then bld.speed = uibs.speed end end table.insert(blds, bld) + ::continue:: end end end self.subviews.item1:reduce_quantity() self.subviews.item2:reduce_quantity() From c52b2c27c8c5b67b5d8c5ebf68bc5c03cd9f2e1e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 15:05:06 -0800 Subject: [PATCH 35/47] implement automaterial in buildingplan --- plugins/lua/buildingplan.lua | 133 +++++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 22 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index fe74ca7c0..370ab5159 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -146,7 +146,40 @@ BuildingplanScreen.ATTRS { local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true} local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true} -local recently_selected = {} +-- map of building type -> {set=set of recently used, list=list of recently used} +-- most recent entries are at the *end* of the list +local recently_used = {} + +local function sort_by_type(a, b) + local ad, bd = a.data, b.data + return ad.item_type < bd.item_type or + (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or + (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) +end + +local function sort_by_recency(a, b) + local tracker = recently_used[uibs.building_type] + if not tracker then return sort_by_type(a, b) end + local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key] + -- if they're both in the set, return the one with the greater index, + -- indicating more recent + if recent_a and recent_b then return recent_a > recent_b end + if recent_a and not recent_b then return true end + if not recent_a and recent_b then return false end + return sort_by_type(a, b) +end + +local function sort_by_name(a, b) + return a.search_key < b.search_key or + (a.search_key == b.search_key and sort_by_type(a, b)) +end + +local function sort_by_quantity(a, b) + local ad, bd = a.data, b.data + return ad.quantity > bd.quantity or + (ad.quantity == bd.quantity and sort_by_type(a, b)) +end ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ @@ -193,53 +226,80 @@ function ItemSelection:init() view_id='flist', frame={t=3, l=0, r=0, b=4}, case_sensitive=false, - choices=self:get_choices(), + choices=self:get_choices(sort_by_recency), icon_width=2, on_submit=self:callback('toggle_group'), }, - widgets.HotkeyLabel{ + widgets.CycleHotkeyLabel{ frame={l=0, b=2}, + key='CUSTOM_CTRL_X', + label='Sort by:', + options={ + {label='Recently used', value=sort_by_recency}, + {label='Name', value=sort_by_name}, + {label='Amount', value=sort_by_quantity}, + }, + on_change=self:callback('on_sort'), + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, key='SELECT', - label='Use all/none selected', + label='Use all/none', auto_width=true, on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, }, widgets.HotkeyLabel{ - frame={l=32, b=2}, + frame={l=22, b=1}, + key='CUSTOM_CTRL_D', + label='Build', + auto_width=true, + on_activate=self:callback('submit'), + }, + widgets.HotkeyLabel{ + frame={l=38, b=1}, key='LEAVESCREEN', - label='Cancel build', + label='Go back', auto_width=true, - on_activate=function() self.on_cancel() end, + on_activate=self:callback('on_cancel'), }, widgets.HotkeyLabel{ - frame={l=0, b=1}, + frame={l=0, b=0}, key='KEYBOARD_CURSOR_RIGHT_FAST', key_sep=' : ', - label='Use one selected', + label='Use one', auto_width=true, on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, }, widgets.Label{ - frame={l=6, b=1, w=5}, + frame={l=6, b=0, w=5}, text_pen=COLOR_LIGHTGREEN, text='Right', }, widgets.HotkeyLabel{ - frame={l=0, b=0}, + frame={l=23, b=0}, key='KEYBOARD_CURSOR_LEFT_FAST', key_sep=' : ', - label='Use one fewer selected', + label='Use one fewer', auto_width=true, on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, }, widgets.Label{ - frame={l=6, b=0, w=4}, + frame={l=29, b=0, w=4}, text_pen=COLOR_LIGHTGREEN, text='Left', }, } end +-- resort and restore selection +function ItemSelection:on_sort(sort_fn) + local flist = self.subviews.flist + local saved_filter = flist:getFilter() + flist:setFilter('') + flist:setChoices(self:get_choices(sort_fn), flist:getSelected()) + flist:setFilter(saved_filter) +end + local function make_search_key(str) local out = '' for c in str:gmatch("[%w%s]") do @@ -248,7 +308,7 @@ local function make_search_key(str) return out end -function ItemSelection:get_choices() +function ItemSelection:get_choices(sort_fn) local item_ids = getAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index - 1) local buckets = {} @@ -286,14 +346,7 @@ function ItemSelection:get_choices() } table.insert(choices, choice) end - local function choice_sort(a, b) - local ad, bd = a.data, b.data - return ad.item_type < bd.item_type or - (ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or - (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or - (ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality) - end - table.sort(choices, choice_sort) + table.sort(choices, sort_fn) return choices end @@ -331,11 +384,47 @@ function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end +local function track_recently_used(choices) + -- use same set for all subtypes + local tracker = ensure_key(recently_used, uibs.building_type) + for _,choice in ipairs(choices) do + local data = choice.data + if data.selected <= 0 then goto continue end + local key = choice.search_key + local recent_set = ensure_key(tracker, 'set') + local recent_list = ensure_key(tracker, 'list') + if recent_set[key] then + if recent_list[#recent_list] ~= key then + for i,v in ipairs(recent_list) do + if v == key then + table.remove(recent_list, i) + table.insert(recent_list, key) + break + end + end + tracker.set = utils.invert(recent_list) + end + else + -- only keep most recent 10 + if #recent_list >= 10 then + -- remove least recently used from list and set + recent_set[table.remove(recent_list, 1)] = nil + end + table.insert(recent_list, key) + recent_set[key] = #recent_list + end + ::continue:: + end +end + function ItemSelection:submit() local selected_items = {} for item_id in pairs(self.selected_set) do table.insert(selected_items, item_id) end + if #selected_items > 0 then + track_recently_used(self.subviews.flist:getChoices()) + end self.on_submit(selected_items) end From a0798178a6380422bc1b6ee12678f0140d6a633f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 15:42:30 -0800 Subject: [PATCH 36/47] ensure item quantity is correct when hollow --- plugins/lua/buildingplan.lua | 50 +++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 370ab5159..9d8eb463b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -84,15 +84,16 @@ local function get_cur_area_dims(placement_data) math.abs(selection_pos.z - pos.z) + 1 end -local function get_quantity(filter, placement_data) +local function get_quantity(filter, hollow, placement_data) local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims(placement_data) if quantity < 1 then - quantity = (((dimx * dimy) // 4) + 1) * dimz - else - quantity = quantity * dimx * dimy * dimz + return (((dimx * dimy) // 4) + 1) * dimz + end + if hollow and dimx > 2 and dimy > 2 then + return quantity * (2*dimx + 2*dimy - 4) * dimz end - return quantity + return quantity * dimx * dimy * dimz end local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil @@ -187,14 +188,13 @@ ItemSelection.ATTRS{ frame={w=56, h=20, l=4, t=8}, resizable=true, index=DEFAULT_NIL, - placement_data=DEFAULT_NIL, + quantity=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] - self.quantity = get_quantity(filter, self.placement_data) self.num_selected = 0 self.selected_set = {} local plural = self.quantity == 1 and '' or 's' @@ -458,7 +458,7 @@ ItemSelectionScreen.ATTRS { force_pause=true, pass_pause=false, index=DEFAULT_NIL, - placement_data=DEFAULT_NIL, + quantity=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } @@ -467,7 +467,7 @@ function ItemSelectionScreen:init() self:addviews{ ItemSelection{ index=self.index, - placement_data=self.placement_data, + quantity=self.quantity, on_submit=self.on_submit, on_cancel=self.on_cancel, } @@ -591,6 +591,7 @@ ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL, + is_hollow_fn=DEFAULT_NIL, on_select=DEFAULT_NIL, on_filter=DEFAULT_NIL, on_clear_filter=DEFAULT_NIL, @@ -688,7 +689,7 @@ end function ItemLine:get_item_line_text() local idx = self.idx local filter = get_cur_filters()[idx] - local quantity = get_quantity(filter) + local quantity = get_quantity(filter, self.is_hollow_fn()) self.desc = self.desc or get_desc(filter) @@ -708,7 +709,7 @@ end function ItemLine:reduce_quantity() if not self.available then return end local filter = get_cur_filters()[self.idx] - self.available = math.max(0, self.available - get_quantity(filter)) + self.available = math.max(0, self.available - get_quantity(filter, self.is_hollow_fn())) end local function get_placement_errors() @@ -750,6 +751,10 @@ function PlannerOverlay:init() self.selected = idx end + local function is_hollow_fn() + return self.subviews.hollow:getOptionValue() + end + main_panel:addviews{ widgets.Label{ frame={}, @@ -758,20 +763,20 @@ function PlannerOverlay:init() visible=function() return #get_cur_filters() == 0 end, }, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, - is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, - is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, - is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, - is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, - on_filter=self:callback('set_filter'), + is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn, + on_select=on_select_fn, on_filter=self:callback('set_filter'), on_clear_filter=self:callback('clear_filter')}, widgets.CycleHotkeyLabel{ view_id='hollow', @@ -1032,13 +1037,15 @@ function PlannerOverlay:onInput(keys) if #uibs.errors > 0 then return true end if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then - local num_filters = #get_cur_filters() + local filters = get_cur_filters() + local num_filters = #filters if num_filters == 0 then return false -- we don't add value; let the game place it end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then self:save_placement() + local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters for idx = num_filters,1,-1 do @@ -1046,7 +1053,8 @@ function PlannerOverlay:onInput(keys) if (self.subviews['item'..idx].available or 0) > 0 then active_screens[idx] = ItemSelectionScreen{ index=idx, - placement_data=self.saved_placement, + quantity=get_quantity(filters[idx], is_hollow, + self.saved_placement), on_submit=function(items) chosen_items[idx] = items active_screens[idx]:dismiss() From 097e955796269e07b17e24edfb81875d1857cb59 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 21 Feb 2023 18:05:15 -0800 Subject: [PATCH 37/47] infrastructure for item filtering --- plugins/buildingplan/buildingplan.cpp | 174 ++++++++++-------- plugins/buildingplan/buildingplan.h | 4 +- plugins/buildingplan/buildingplan_cycle.cpp | 9 +- plugins/buildingplan/itemfilter.cpp | 189 ++++++++++++++++++++ plugins/buildingplan/itemfilter.h | 38 ++++ plugins/buildingplan/plannedbuilding.cpp | 56 ++++-- plugins/buildingplan/plannedbuilding.h | 5 +- plugins/lua/buildingplan.lua | 6 +- 8 files changed, 393 insertions(+), 88 deletions(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 828ae4954..7359d842c 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -73,8 +73,9 @@ struct BuildingTypeKeyHash { static PersistentDataItem config; // for use in counting available materials for the UI -static unordered_map, BuildingTypeKeyHash> job_item_repo; +static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; +static unordered_map, BuildingTypeKeyHash> cur_item_filters; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -96,6 +97,87 @@ void PlannedBuilding::remove(color_ostream &out) { static const int32_t CYCLE_TICKS = 600; // twice per game day static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle +static bool call_buildingplan_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 buildingplan lua function: '%s'\n", 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.buildingplan", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static int get_num_filters(color_ostream &out, BuildingTypeKey key) { + int num_filters = 0; + if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + }, + [&](lua_State *L) { + num_filters = lua_tonumber(L, -1); + })) { + return 0; + } + return num_filters; +} + +static vector & get_job_items(color_ostream &out, BuildingTypeKey key) { + if (job_item_cache.count(key)) + return job_item_cache[key]; + const int num_filters = get_num_filters(out, key); + auto &jitems = job_item_cache[key]; + for (int index = 0; index < num_filters; ++index) { + bool failed = false; + if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, + [&](lua_State *L) { + Lua::Push(L, std::get<0>(key)); + Lua::Push(L, std::get<1>(key)); + Lua::Push(L, std::get<2>(key)); + Lua::Push(L, index+1); + }, + [&](lua_State *L) { + df::job_item *jitem = Lua::GetDFObject(L, -1); + DEBUG(status,out).print("retrieving job_item for (%d, %d, %d) index=%d: %p\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index, jitem); + if (!jitem) + failed = true; + else + jitems.emplace_back(jitem); + }) || failed) { + jitems.clear(); + break; + } + } + return jitems; +} + +static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { + if (cur_heat_safety.count(key)) + return cur_heat_safety.at(key); + return HEAT_SAFETY_ANY; +} + +static vector & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { + if (cur_item_filters.count(key)) + return cur_item_filters[key]; + + vector &filters = cur_item_filters[key]; + filters.resize(get_job_items(out, key).size()); + return filters; +} + static command_result do_command(color_ostream &out, vector ¶meters); void buildingplan_cycle(color_ostream &out, Tasks &tasks, unordered_map &planned_buildings); @@ -149,37 +231,19 @@ static void validate_config(color_ostream &out, bool verbose = false) { set_config_bool(config, CONFIG_BARS, false); } -static bool call_buildingplan_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 buildingplan lua function: '%s'\n", 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.buildingplan", fn_name, - nargs, nres, - std::forward(args_lambda), - std::forward(res_lambda)); -} - static void clear_state(color_ostream &out) { call_buildingplan_lua(&out, "signal_reset"); call_buildingplan_lua(&out, "reload_cursors"); planned_buildings.clear(); tasks.clear(); - for (auto &entry : job_item_repo) { + cur_heat_safety.clear(); + cur_item_filters.clear(); + for (auto &entry : job_item_cache ) { for (auto &jitem : entry.second) { delete jitem; } } - job_item_repo.clear(); + job_item_cache.clear(); } DFhackCExport command_result plugin_load_data (color_ostream &out) { @@ -199,7 +263,15 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { const size_t num_building_configs = building_configs.size(); for (size_t idx = 0; idx < num_building_configs; ++idx) { PlannedBuilding pb(out, building_configs[idx]); - registerPlannedBuilding(out, pb); + df::building *bld = df::building::find(pb.id); + if (!bld) { + WARN(status).print("cannot find building %d; halting load\n", pb.id); + } + BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); + if (pb.item_filters.size() != get_item_filters(out, key).size()) + WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); + else + registerPlannedBuilding(out, pb); } return CR_OK; @@ -438,30 +510,12 @@ static bool setSetting(color_ostream &out, string name, bool value) { static bool isPlannableBuilding(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom) { DEBUG(status,out).print("entering isPlannableBuilding\n"); - int num_filters = 0; - if (!call_buildingplan_lua(&out, "get_num_filters", 3, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - }, - [&](lua_State *L) { - num_filters = lua_tonumber(L, -1); - })) { - return false; - } - return num_filters >= 1; + return get_num_filters(out, BuildingTypeKey(type, subtype, custom)) >= 1; } static bool isPlannedBuilding(color_ostream &out, df::building *bld) { TRACE(status,out).print("entering isPlannedBuilding\n"); - return bld && planned_buildings.count(bld->id) > 0; -} - -static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { - if (cur_heat_safety.count(key)) - return cur_heat_safety.at(key); - return HEAT_SAFETY_ANY; + return bld && planned_buildings.count(bld->id); } static bool addPlannedBuilding(color_ostream &out, df::building *bld) { @@ -471,7 +525,7 @@ static bool addPlannedBuilding(color_ostream &out, df::building *bld) { bld->getCustomType())) return false; BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - PlannedBuilding pb(out, bld, get_heat_safety_filter(key)); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key)); return registerPlannedBuilding(out, pb); } @@ -492,30 +546,10 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ type, subtype, custom, index); BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); - auto &job_items = job_item_repo[key]; - if (index >= (int)job_items.size()) { - for (int i = job_items.size(); i <= index; ++i) { - bool failed = false; - if (!call_buildingplan_lua(&out, "get_job_item", 4, 1, - [&](lua_State *L) { - Lua::Push(L, type); - Lua::Push(L, subtype); - Lua::Push(L, custom); - Lua::Push(L, index+1); - }, - [&](lua_State *L) { - df::job_item *jitem = Lua::GetDFObject(L, -1); - DEBUG(status,out).print("retrieving job_item for index=%d: %p\n", - index, jitem); - if (!jitem) - failed = true; - else - job_items.emplace_back(jitem); - }) || failed) { - return 0; - } - } - } + auto &job_items = get_job_items(out, key); + if (job_items.size() <= index) + return 0; + auto &item_filters = get_item_filters(out, key); auto &jitem = job_items[index]; auto vector_ids = getVectorIds(out, jitem); @@ -524,7 +558,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ for (auto vector_id : vector_ids) { auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); for (auto &item : df::global::world->items.other[other_id]) { - if (itemPassesScreen(item) && matchesFilters(item, jitem, heat)) { + if (itemPassesScreen(item) && matchesFilters(item, jitem, heat, item_filters[index])) { if (item_ids) item_ids->emplace_back(item->id); ++count; diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 787987586..4f0d374e7 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -1,5 +1,7 @@ #pragma once +#include "itemfilter.h" + #include "modules/Persistence.h" #include "df/building.h" @@ -38,6 +40,6 @@ void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat); +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index 703bab9b0..a904bc5a8 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { +bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; @@ -76,7 +76,8 @@ bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat) { &jitem, item->getType(), item->getSubtype()) && Job::isSuitableMaterial( &jitem, item->getMaterial(), item->getMaterialIndex(), - item->getType()); + item->getType()) + && item_filter.matches(item); } bool isJobReady(color_ostream &out, const std::vector &jitems) { @@ -180,7 +181,9 @@ static void doVector(color_ostream &out, df::job_item_vector_id vector_id, auto id = task.first; auto job = bld->jobs[0]; auto filter_idx = task.second; - if (matchesFilters(item, job->job_items[filter_idx], planned_buildings.at(id).heat_safety) + auto &pb = planned_buildings.at(id); + if (matchesFilters(item, job->job_items[filter_idx], pb.heat_safety, + pb.item_filters[filter_idx]) && Job::attachJobItem(job, item, df::job_item_ref::Hauled, filter_idx)) { diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index e69de29bb..bd35c848a 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -0,0 +1,189 @@ +#include "itemfilter.h" + +#include "Debug.h" + +#include "df/item.h" + +using namespace DFHack; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +ItemFilter::ItemFilter() { + clear(); +} + +void ItemFilter::clear() { + min_quality = df::item_quality::Ordinary; + max_quality = df::item_quality::Masterful; + decorated_only = false; + mat_mask.whole = 0; + materials.clear(); +} + +bool ItemFilter::isEmpty() { + return min_quality == df::item_quality::Ordinary + && max_quality == df::item_quality::Masterful + && !decorated_only + && !mat_mask.whole + && materials.empty(); +} + +static bool deserializeMaterialMask(std::string ser, df::dfhack_material_category mat_mask) { + if (ser.empty()) + return true; + + if (!parseJobMaterialCategory(&mat_mask, ser)) { + DEBUG(status).print("invalid job material category serialization: '%s'", ser.c_str()); + return false; + } + return true; +} + +static bool deserializeMaterials(std::string ser, std::vector &materials) { + if (ser.empty()) + return true; + + std::vector mat_names; + split_string(&mat_names, ser, ","); + for (auto m = mat_names.begin(); m != mat_names.end(); m++) { + DFHack::MaterialInfo material; + if (!material.find(*m) || !material.isValid()) { + DEBUG(status).print("invalid material name serialization: '%s'", ser.c_str()); + return false; + } + materials.push_back(material); + } + return true; +} + +ItemFilter::ItemFilter(std::string serialized) { + clear(); + + std::vector tokens; + split_string(&tokens, serialized, "/"); + if (tokens.size() != 5) { + DEBUG(status).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + return; + } + + if (!deserializeMaterialMask(tokens[0], mat_mask) || !deserializeMaterials(tokens[1], materials)) + return; + + setMinQuality(atoi(tokens[2].c_str())); + setMaxQuality(atoi(tokens[3].c_str())); + decorated_only = static_cast(atoi(tokens[4].c_str())); +} + +// format: mat,mask,elements/materials,list/minq/maxq/decorated +std::string ItemFilter::serialize() const { + std::ostringstream ser; + ser << bitfield_to_string(mat_mask, ",") << "/"; + if (!materials.empty()) { + ser << materials[0].getToken(); + for (size_t i = 1; i < materials.size(); ++i) + ser << "," << materials[i].getToken(); + } + ser << "/" << static_cast(min_quality); + ser << "/" << static_cast(max_quality); + ser << "/" << static_cast(decorated_only); + return ser.str(); +} + +static void clampItemQuality(df::item_quality *quality) { + if (*quality > df::item_quality::Artifact) { + DEBUG(status).print("clamping quality to Artifact"); + *quality = df::item_quality::Artifact; + } + if (*quality < df::item_quality::Ordinary) { + DEBUG(status).print("clamping quality to Ordinary"); + *quality = df::item_quality::Ordinary; + } +} + +void ItemFilter::setMinQuality(int quality) { + min_quality = static_cast(quality); + clampItemQuality(&min_quality); + if (max_quality < min_quality) + max_quality = min_quality; +} + +void ItemFilter::setMaxQuality(int quality) { + max_quality = static_cast(quality); + clampItemQuality(&max_quality); + if (max_quality < min_quality) + min_quality = max_quality; +} + +void ItemFilter::setDecoratedOnly(bool decorated) { + decorated_only = decorated; +} + +void ItemFilter::setMaterialMask(uint32_t mask) { + mat_mask.whole = mask; +} + +void ItemFilter::setMaterials(const std::vector &materials) { + this->materials = materials; +} + +std::string ItemFilter::getMinQuality() const { + return ENUM_KEY_STR(item_quality, min_quality); +} + +std::string ItemFilter::getMaxQuality() const { + return ENUM_KEY_STR(item_quality, max_quality); +} + +bool ItemFilter::getDecoratedOnly() const { + return decorated_only; +} + +uint32_t ItemFilter::getMaterialMask() const { + return mat_mask.whole; +} + +static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } + +std::vector ItemFilter::getMaterials() const { + std::vector descriptions; + transform_(materials, descriptions, material_to_string_fn); + + if (descriptions.size() == 0) + bitfield_to_string(&descriptions, mat_mask); + + if (descriptions.size() == 0) + descriptions.push_back("any"); + + return descriptions; +} + +static bool matchesMask(DFHack::MaterialInfo &mat, df::dfhack_material_category mat_mask) { + return mat_mask.whole ? mat.matches(mat_mask) : true; +} + +bool ItemFilter::matches(df::dfhack_material_category mask) const { + return mask.whole & mat_mask.whole; +} + +bool ItemFilter::matches(DFHack::MaterialInfo &material) const { + for (auto it = materials.begin(); it != materials.end(); ++it) + if (material.matches(*it)) + return true; + return false; +} + +bool ItemFilter::matches(df::item *item) const { + if (item->getQuality() < min_quality || item->getQuality() > max_quality) + return false; + + if (decorated_only && !item->hasImprovements()) + return false; + + auto imattype = item->getActualMaterial(); + auto imatindex = item->getActualMaterialIndex(); + auto item_mat = DFHack::MaterialInfo(imattype, imatindex); + + return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 6f70f09be..134d3b249 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -1 +1,39 @@ #pragma once + +#include "modules/Materials.h" + +#include "df/dfhack_material_category.h" +#include "df/item_quality.h" + +class ItemFilter { +public: + ItemFilter(); + ItemFilter(std::string serialized); + + void clear(); + bool isEmpty(); + std::string serialize() const; + + void setMinQuality(int quality); + void setMaxQuality(int quality); + void setDecoratedOnly(bool decorated); + void setMaterialMask(uint32_t mask); + void setMaterials(const std::vector &materials); + + std::string getMinQuality() const; + std::string getMaxQuality() const; + bool getDecoratedOnly() const; + uint32_t getMaterialMask() const; + std::vector getMaterials() const; + + bool matches(df::dfhack_material_category mask) const; + bool matches(DFHack::MaterialInfo &material) const; + bool matches(df::item *item) const; + +private: + df::item_quality min_quality; + df::item_quality max_quality; + bool decorated_only; + df::dfhack_material_category mat_mask; + std::vector materials; +}; diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index eb55a95b4..c68d668bf 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -31,14 +31,18 @@ static vector> get_vector_ids(color_ostream &out, return ret; } -static vector> deserialize(color_ostream &out, PersistentDataItem &bld_config) { +static vector> deserialize_vector_ids(color_ostream &out, PersistentDataItem &bld_config) { vector> ret; - DEBUG(status,out).print("deserializing state for building %d: %s\n", - get_config_val(bld_config, BLD_CONFIG_ID), bld_config.val().c_str()); + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + const string &serialized = rawstrs[0]; + + DEBUG(status,out).print("deserializing vector ids for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); vector joined; - split_string(&joined, bld_config.val(), "|"); + split_string(&joined, serialized, ";"); for (auto &str : joined) { vector lst; split_string(&lst, str, ","); @@ -54,28 +58,60 @@ static vector> deserialize(color_ostream &out, Pe return ret; } -static string serialize(const vector> &vector_ids) { +static std::vector deserialize_item_filters(color_ostream &out, PersistentDataItem &bld_config) { + std::vector ret; + + vector rawstrs; + split_string(&rawstrs, bld_config.val(), "|"); + if (rawstrs.size() < 2) + return ret; + const string &serialized = rawstrs[1]; + + DEBUG(status,out).print("deserializing item filters for building %d: %s\n", + get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); + + vector filterstrs; + split_string(&filterstrs, serialized, ";"); + for (auto &str : filterstrs) { + ret.emplace_back(str); + } + + return ret; +} + +static string serialize(const vector> &vector_ids, const vector &item_filters) { vector joined; for (auto &vec_list : vector_ids) { joined.emplace_back(join_strings(",", vec_list)); } - return join_strings("|", joined); + std::ostringstream out; + out << join_strings(";", joined) << "|"; + + joined.clear(); + for (auto &filter : item_filters) { + joined.emplace_back(filter.serialize()); + } + out << join_strings(";", joined); + + return out.str(); } -PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat) - : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat) { +PlannedBuilding::PlannedBuilding(color_ostream &out, df::building *bld, HeatSafety heat, const vector &item_filters) + : id(bld->id), vector_ids(get_vector_ids(out, id)), heat_safety(heat), + item_filters(item_filters) { DEBUG(status,out).print("creating persistent data for building %d\n", id); bld_config = World::AddPersistentData(BLD_CONFIG_KEY); set_config_val(bld_config, BLD_CONFIG_ID, id); set_config_val(bld_config, BLD_CONFIG_HEAT, heat_safety); - bld_config.val() = serialize(vector_ids); + bld_config.val() = serialize(vector_ids, item_filters); DEBUG(status,out).print("serialized state for building %d: %s\n", id, bld_config.val().c_str()); } PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_config) : id(get_config_val(bld_config, BLD_CONFIG_ID)), - vector_ids(deserialize(out, bld_config)), + vector_ids(deserialize_vector_ids(out, bld_config)), heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), + item_filters(deserialize_item_filters(out, bld_config)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear diff --git a/plugins/buildingplan/plannedbuilding.h b/plugins/buildingplan/plannedbuilding.h index 0a67e0edc..5bd09ba5a 100644 --- a/plugins/buildingplan/plannedbuilding.h +++ b/plugins/buildingplan/plannedbuilding.h @@ -1,6 +1,7 @@ #pragma once #include "buildingplan.h" +#include "itemfilter.h" #include "Core.h" @@ -18,7 +19,9 @@ public: const HeatSafety heat_safety; - PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat); + const std::vector item_filters; + + PlannedBuilding(DFHack::color_ostream &out, df::building *bld, HeatSafety heat, const std::vector &item_filters); PlannedBuilding(DFHack::color_ostream &out, DFHack::PersistentDataItem &bld_config); void remove(DFHack::color_ostream &out); diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 9d8eb463b..b6cbb383b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1292,7 +1292,7 @@ InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', - frame={w=30, h=14}, + frame={w=30, h=15}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } @@ -1308,12 +1308,12 @@ function InspectorOverlay:init() InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3}, InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4}, widgets.HotkeyLabel{ - frame={t=10, l=0}, + frame={t=11, l=0}, label='adjust filters', key='CUSTOM_CTRL_F', }, widgets.HotkeyLabel{ - frame={t=11, l=0}, + frame={t=12, l=0}, label='make top priority', key='CUSTOM_CTRL_T', on_activate=self:callback('make_top_priority'), From 60de4619a294ab84cf82c61d8839f540ef326534 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 08:34:55 -0800 Subject: [PATCH 38/47] fix signed unsigned compare --- plugins/buildingplan/buildingplan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 7359d842c..3a73372c3 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -547,7 +547,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ BuildingTypeKey key(type, subtype, custom); HeatSafety heat = get_heat_safety_filter(key); auto &job_items = get_job_items(out, key); - if (job_items.size() <= index) + if (index < 0 || job_items.size() <= (size_t)index) return 0; auto &item_filters = get_item_filters(out, key); From 4cc262c796f58126a964cfe12fdee17f19280bc4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 15:08:11 -0800 Subject: [PATCH 39/47] overhaul serialization; persist item filters --- plugins/buildingplan/CMakeLists.txt | 5 +- plugins/buildingplan/buildingplan.cpp | 79 ++++++++++----------- plugins/buildingplan/buildingplan.h | 11 ++- plugins/buildingplan/buildingplan_cycle.cpp | 2 +- plugins/buildingplan/buildingtypekey.cpp | 59 +++++++++++++++ plugins/buildingplan/buildingtypekey.h | 22 ++++++ plugins/buildingplan/defaultitemfilters.cpp | 60 ++++++++++++++++ plugins/buildingplan/defaultitemfilters.h | 24 +++++++ plugins/buildingplan/itemfilter.cpp | 55 +++++++++----- plugins/buildingplan/itemfilter.h | 7 +- plugins/buildingplan/plannedbuilding.cpp | 26 ++----- 11 files changed, 264 insertions(+), 86 deletions(-) create mode 100644 plugins/buildingplan/buildingtypekey.cpp create mode 100644 plugins/buildingplan/buildingtypekey.h create mode 100644 plugins/buildingplan/defaultitemfilters.cpp create mode 100644 plugins/buildingplan/defaultitemfilters.h diff --git a/plugins/buildingplan/CMakeLists.txt b/plugins/buildingplan/CMakeLists.txt index 85475edaa..118b2a1d1 100644 --- a/plugins/buildingplan/CMakeLists.txt +++ b/plugins/buildingplan/CMakeLists.txt @@ -2,12 +2,15 @@ project(buildingplan) set(COMMON_HDRS buildingplan.h + buildingtypekey.h + defaultitemfilters.h itemfilter.h plannedbuilding.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) dfhack_plugin(buildingplan - buildingplan.cpp buildingplan_cycle.cpp itemfilter.cpp plannedbuilding.cpp + buildingplan.cpp buildingplan_cycle.cpp buildingtypekey.cpp + defaultitemfilters.cpp itemfilter.cpp plannedbuilding.cpp ${COMMON_HDRS} LINK_LIBRARIES lua) diff --git a/plugins/buildingplan/buildingplan.cpp b/plugins/buildingplan/buildingplan.cpp index 3a73372c3..4fa119ebc 100644 --- a/plugins/buildingplan/buildingplan.cpp +++ b/plugins/buildingplan/buildingplan.cpp @@ -1,5 +1,7 @@ -#include "plannedbuilding.h" #include "buildingplan.h" +#include "buildingtypekey.h" +#include "defaultitemfilters.h" +#include "plannedbuilding.h" #include "Debug.h" #include "LuaTools.h" @@ -29,6 +31,7 @@ namespace DFHack { } static const string CONFIG_KEY = string(plugin_name) + "/config"; +const string FILTER_CONFIG_KEY = string(plugin_name) + "/filter"; const string BLD_CONFIG_KEY = string(plugin_name) + "/building"; int get_config_val(PersistentDataItem &c, int index) { @@ -47,35 +50,11 @@ void set_config_bool(PersistentDataItem &c, int index, bool value) { set_config_val(c, index, value ? 1 : 0); } -// building type, subtype, custom -typedef std::tuple BuildingTypeKey; - -// rotates a size_t value left by count bits -// assumes count is not 0 or >= size_t_bits -// replace this with std::rotl when we move to C++20 -static std::size_t rotl_size_t(size_t val, uint32_t count) -{ - static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); - return val << count | val >> (size_t_bits - count); -} - -struct BuildingTypeKeyHash { - std::size_t operator() (const BuildingTypeKey & key) const { - // cast first param to appease gcc-4.8, which is missing the enum - // specializations for std::hash - std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); - std::size_t h2 = std::hash()(std::get<1>(key)); - std::size_t h3 = std::hash()(std::get<2>(key)); - - return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); - } -}; - static PersistentDataItem config; // for use in counting available materials for the UI -static unordered_map, BuildingTypeKeyHash> job_item_cache; +static unordered_map, BuildingTypeKeyHash> job_item_cache; static unordered_map cur_heat_safety; -static unordered_map, BuildingTypeKeyHash> cur_item_filters; +static unordered_map cur_item_filters; // building id -> PlannedBuilding static unordered_map planned_buildings; // vector id -> filter bucket -> queue of (building id, job_item index) @@ -133,7 +112,7 @@ static int get_num_filters(color_ostream &out, BuildingTypeKey key) { return num_filters; } -static vector & get_job_items(color_ostream &out, BuildingTypeKey key) { +static const vector & get_job_items(color_ostream &out, BuildingTypeKey key) { if (job_item_cache.count(key)) return job_item_cache[key]; const int num_filters = get_num_filters(out, key); @@ -169,13 +148,11 @@ static HeatSafety get_heat_safety_filter(const BuildingTypeKey &key) { return HEAT_SAFETY_ANY; } -static vector & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { +static DefaultItemFilters & get_item_filters(color_ostream &out, const BuildingTypeKey &key) { if (cur_item_filters.count(key)) - return cur_item_filters[key]; - - vector &filters = cur_item_filters[key]; - filters.resize(get_job_items(out, key).size()); - return filters; + return cur_item_filters.at(key); + cur_item_filters.emplace(key, DefaultItemFilters(out, key, get_job_items(out, key))); + return cur_item_filters.at(key); } static command_result do_command(color_ostream &out, vector ¶meters); @@ -258,6 +235,14 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(status,out).print("loading persisted state\n"); clear_state(out); + + vector filter_configs; + World::GetPersistentData(&filter_configs, FILTER_CONFIG_KEY); + for (auto &cfg : filter_configs) { + BuildingTypeKey key = DefaultItemFilters::getKey(cfg); + cur_item_filters.emplace(key, DefaultItemFilters(out, cfg, get_job_items(out, key))); + } + vector building_configs; World::GetPersistentData(&building_configs, BLD_CONFIG_KEY); const size_t num_building_configs = building_configs.size(); @@ -265,13 +250,17 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { PlannedBuilding pb(out, building_configs[idx]); df::building *bld = df::building::find(pb.id); if (!bld) { - WARN(status).print("cannot find building %d; halting load\n", pb.id); + INFO(status).print("building %d no longer exists; skipping\n", pb.id); + pb.remove(out); + continue; } BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - if (pb.item_filters.size() != get_item_filters(out, key).size()) + if (pb.item_filters.size() != get_item_filters(out, key).getItemFilters().size()) { WARN(status).print("loaded state for building %d doesn't match world\n", pb.id); - else - registerPlannedBuilding(out, pb); + pb.remove(out); + continue; + } + registerPlannedBuilding(out, pb); } return CR_OK; @@ -352,7 +341,7 @@ static string getBucket(const df::job_item & ji) { } // get a list of item vectors that we should search for matches -vector getVectorIds(color_ostream &out, df::job_item *job_item) { +vector getVectorIds(color_ostream &out, const df::job_item *job_item) { std::vector ret; // if the filter already has the vector_id set to something specific, use it @@ -525,7 +514,7 @@ static bool addPlannedBuilding(color_ostream &out, df::building *bld) { bld->getCustomType())) return false; BuildingTypeKey key(bld->getType(), bld->getSubtype(), bld->getCustomType()); - PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key)); + PlannedBuilding pb(out, bld, get_heat_safety_filter(key), get_item_filters(out, key).getItemFilters()); return registerPlannedBuilding(out, pb); } @@ -549,7 +538,7 @@ static int scanAvailableItems(color_ostream &out, df::building_type type, int16_ auto &job_items = get_job_items(out, key); if (index < 0 || job_items.size() <= (size_t)index) return 0; - auto &item_filters = get_item_filters(out, key); + auto &item_filters = get_item_filters(out, key).getItemFilters(); auto &jitem = job_items[index]; auto vector_ids = getVectorIds(out, jitem); @@ -595,7 +584,13 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16 } static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { - DEBUG(status,out).print("entering hasFilter\n"); + TRACE(status,out).print("entering hasFilter\n"); + BuildingTypeKey key(type, subtype, custom); + auto &filters = get_item_filters(out, key); + for (auto &filter : filters.getItemFilters()) { + if (filter.isEmpty()) + return true; + } return false; } diff --git a/plugins/buildingplan/buildingplan.h b/plugins/buildingplan/buildingplan.h index 4f0d374e7..eef9808e6 100644 --- a/plugins/buildingplan/buildingplan.h +++ b/plugins/buildingplan/buildingplan.h @@ -13,6 +13,7 @@ typedef std::deque> Bucket; typedef std::map> Tasks; +extern const std::string FILTER_CONFIG_KEY; extern const std::string BLD_CONFIG_KEY; enum ConfigValues { @@ -22,6 +23,12 @@ enum ConfigValues { CONFIG_BARS = 4, }; +enum FilterConfigValues { + FILTER_CONFIG_TYPE = 0, + FILTER_CONFIG_SUBTYPE = 1, + FILTER_CONFIG_CUSTOM = 2, +}; + enum BuildingConfigValues { BLD_CONFIG_ID = 0, BLD_CONFIG_HEAT = 1, @@ -38,8 +45,8 @@ bool get_config_bool(DFHack::PersistentDataItem &c, int index); void set_config_val(DFHack::PersistentDataItem &c, int index, int value); void set_config_bool(DFHack::PersistentDataItem &c, int index, bool value); -std::vector getVectorIds(DFHack::color_ostream &out, df::job_item *job_item); +std::vector getVectorIds(DFHack::color_ostream &out, const df::job_item *job_item); bool itemPassesScreen(df::item * item); -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter); bool isJobReady(DFHack::color_ostream &out, const std::vector &jitems); void finalizeBuilding(DFHack::color_ostream &out, df::building *bld); diff --git a/plugins/buildingplan/buildingplan_cycle.cpp b/plugins/buildingplan/buildingplan_cycle.cpp index a904bc5a8..655dc8c1a 100644 --- a/plugins/buildingplan/buildingplan_cycle.cpp +++ b/plugins/buildingplan/buildingplan_cycle.cpp @@ -46,7 +46,7 @@ bool itemPassesScreen(df::item * item) { && !item->isAssignedToStockpile(); } -bool matchesFilters(df::item * item, df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { +bool matchesFilters(df::item * item, const df::job_item * job_item, HeatSafety heat, const ItemFilter &item_filter) { // check the properties that are not checked by Job::isSuitableItem() if (job_item->item_type > -1 && job_item->item_type != item->getType()) return false; diff --git a/plugins/buildingplan/buildingtypekey.cpp b/plugins/buildingplan/buildingtypekey.cpp new file mode 100644 index 000000000..664fdf27d --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.cpp @@ -0,0 +1,59 @@ +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "Debug.h" +#include "MiscUtils.h" + +using std::string; +using std::vector; + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using namespace DFHack; + +// building type, subtype, custom +BuildingTypeKey::BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom) + : tuple(type, subtype, custom) { } + +static BuildingTypeKey deserialize(color_ostream &out, const std::string &serialized) { + vector key_parts; + split_string(&key_parts, serialized, ","); + if (key_parts.size() != 3) { + WARN(status,out).print("invalid key_str: '%s'\n", serialized.c_str()); + return BuildingTypeKey(df::building_type::NONE, -1, -1); + } + return BuildingTypeKey((df::building_type)string_to_int(key_parts[0]), + string_to_int(key_parts[1]), string_to_int(key_parts[2])); +} + +BuildingTypeKey::BuildingTypeKey(color_ostream &out, const std::string &serialized) + :tuple(deserialize(out, serialized)) { } + +string BuildingTypeKey::serialize() const { + std::ostringstream ser; + ser << std::get<0>(*this) << ","; + ser << std::get<1>(*this) << ","; + ser << std::get<2>(*this); + return ser.str(); +} + +// rotates a size_t value left by count bits +// assumes count is not 0 or >= size_t_bits +// replace this with std::rotl when we move to C++20 +static std::size_t rotl_size_t(size_t val, uint32_t count) +{ + static const int size_t_bits = CHAR_BIT * sizeof(std::size_t); + return val << count | val >> (size_t_bits - count); +} + +std::size_t BuildingTypeKeyHash::operator() (const BuildingTypeKey & key) const { + // cast first param to appease gcc-4.8, which is missing the enum + // specializations for std::hash + std::size_t h1 = std::hash()(static_cast(std::get<0>(key))); + std::size_t h2 = std::hash()(std::get<1>(key)); + std::size_t h3 = std::hash()(std::get<2>(key)); + + return h1 ^ rotl_size_t(h2, 8) ^ rotl_size_t(h3, 16); +} diff --git a/plugins/buildingplan/buildingtypekey.h b/plugins/buildingplan/buildingtypekey.h new file mode 100644 index 000000000..81bb043c5 --- /dev/null +++ b/plugins/buildingplan/buildingtypekey.h @@ -0,0 +1,22 @@ +#pragma once + +#include "df/building_type.h" + +#include +#include + +namespace DFHack { + class color_ostream; +} + +// building type, subtype, custom +struct BuildingTypeKey : public std::tuple { + BuildingTypeKey(df::building_type type, int16_t subtype, int32_t custom); + BuildingTypeKey(DFHack::color_ostream &out, const std::string & serialized); + + std::string serialize() const; +}; + +struct BuildingTypeKeyHash { + std::size_t operator() (const BuildingTypeKey & key) const; +}; diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp new file mode 100644 index 000000000..4cc6f11cf --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -0,0 +1,60 @@ +#include "defaultitemfilters.h" + +#include "Debug.h" +#include "MiscUtils.h" + +#include "modules/World.h" + +namespace DFHack { + DBG_EXTERN(buildingplan, status); +} + +using std::string; +using std::vector; +using namespace DFHack; + +BuildingTypeKey DefaultItemFilters::getKey(PersistentDataItem &filter_config) { + return BuildingTypeKey( + (df::building_type)get_config_val(filter_config, FILTER_CONFIG_TYPE), + get_config_val(filter_config, FILTER_CONFIG_SUBTYPE), + get_config_val(filter_config, FILTER_CONFIG_CUSTOM)); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, BuildingTypeKey key, const std::vector &jitems) + : key(key) { + DEBUG(status,out).print("creating persistent data for filter key %d,%d,%d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key)); + filter_config = World::AddPersistentData(FILTER_CONFIG_KEY); + set_config_val(filter_config, FILTER_CONFIG_TYPE, std::get<0>(key)); + set_config_val(filter_config, FILTER_CONFIG_SUBTYPE, std::get<1>(key)); + set_config_val(filter_config, FILTER_CONFIG_CUSTOM, std::get<2>(key)); + item_filters.resize(jitems.size()); + filter_config.val() = serialize_item_filters(item_filters); +} + +DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &filter_config, const std::vector &jitems) + : key(getKey(filter_config)), filter_config(filter_config) { + auto &serialized = filter_config.val(); + DEBUG(status,out).print("deserializing item filters for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + std::vector filters = deserialize_item_filters(out, serialized); + if (filters.size() != jitems.size()) { + WARN(status,out).print("ignoring invalid filters_str for key %d,%d,%d: '%s'\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), serialized.c_str()); + item_filters.resize(jitems.size()); + } else + item_filters = filters; +} + +void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { + if (item_filters.size() <= index) { + WARN(status,out).print("invalid index for filter key %d,%d,%d: %d\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), index); + return; + } + + item_filters[index] = filter; + filter_config.val() = serialize_item_filters(item_filters); + DEBUG(status,out).print("updated item filter and persisted for key %d,%d,%d: %s\n", + std::get<0>(key), std::get<1>(key), std::get<2>(key), filter_config.val().c_str()); +} diff --git a/plugins/buildingplan/defaultitemfilters.h b/plugins/buildingplan/defaultitemfilters.h new file mode 100644 index 000000000..4d1d5cbd2 --- /dev/null +++ b/plugins/buildingplan/defaultitemfilters.h @@ -0,0 +1,24 @@ +#pragma once + +#include "buildingplan.h" +#include "buildingtypekey.h" + +#include "modules/Persistence.h" + +class DefaultItemFilters { +public: + static BuildingTypeKey getKey(DFHack::PersistentDataItem &filter_config); + + const BuildingTypeKey key; + + DefaultItemFilters(DFHack::color_ostream &out, BuildingTypeKey key, const std::vector &jitems); + DefaultItemFilters(DFHack::color_ostream &out, DFHack::PersistentDataItem &filter_config, const std::vector &jitems); + + void setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index); + + const std::vector & getItemFilters() const { return item_filters; } + +private: + DFHack::PersistentDataItem filter_config; + std::vector item_filters; +}; diff --git a/plugins/buildingplan/itemfilter.cpp b/plugins/buildingplan/itemfilter.cpp index bd35c848a..a714b62d4 100644 --- a/plugins/buildingplan/itemfilter.cpp +++ b/plugins/buildingplan/itemfilter.cpp @@ -4,12 +4,15 @@ #include "df/item.h" -using namespace DFHack; - namespace DFHack { DBG_EXTERN(buildingplan, status); } +using std::string; +using std::vector; + +using namespace DFHack; + ItemFilter::ItemFilter() { clear(); } @@ -22,7 +25,7 @@ void ItemFilter::clear() { materials.clear(); } -bool ItemFilter::isEmpty() { +bool ItemFilter::isEmpty() const { return min_quality == df::item_quality::Ordinary && max_quality == df::item_quality::Masterful && !decorated_only @@ -30,7 +33,7 @@ bool ItemFilter::isEmpty() { && materials.empty(); } -static bool deserializeMaterialMask(std::string ser, df::dfhack_material_category mat_mask) { +static bool deserializeMaterialMask(string ser, df::dfhack_material_category mat_mask) { if (ser.empty()) return true; @@ -41,11 +44,11 @@ static bool deserializeMaterialMask(std::string ser, df::dfhack_material_categor return true; } -static bool deserializeMaterials(std::string ser, std::vector &materials) { +static bool deserializeMaterials(string ser, vector &materials) { if (ser.empty()) return true; - std::vector mat_names; + vector mat_names; split_string(&mat_names, ser, ","); for (auto m = mat_names.begin(); m != mat_names.end(); m++) { DFHack::MaterialInfo material; @@ -58,13 +61,13 @@ static bool deserializeMaterials(std::string ser, std::vector tokens; + vector tokens; split_string(&tokens, serialized, "/"); if (tokens.size() != 5) { - DEBUG(status).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); + DEBUG(status,out).print("invalid ItemFilter serialization: '%s'", serialized.c_str()); return; } @@ -77,7 +80,7 @@ ItemFilter::ItemFilter(std::string serialized) { } // format: mat,mask,elements/materials,list/minq/maxq/decorated -std::string ItemFilter::serialize() const { +string ItemFilter::serialize() const { std::ostringstream ser; ser << bitfield_to_string(mat_mask, ",") << "/"; if (!materials.empty()) { @@ -124,15 +127,15 @@ void ItemFilter::setMaterialMask(uint32_t mask) { mat_mask.whole = mask; } -void ItemFilter::setMaterials(const std::vector &materials) { +void ItemFilter::setMaterials(const vector &materials) { this->materials = materials; } -std::string ItemFilter::getMinQuality() const { +string ItemFilter::getMinQuality() const { return ENUM_KEY_STR(item_quality, min_quality); } -std::string ItemFilter::getMaxQuality() const { +string ItemFilter::getMaxQuality() const { return ENUM_KEY_STR(item_quality, max_quality); } @@ -144,10 +147,10 @@ uint32_t ItemFilter::getMaterialMask() const { return mat_mask.whole; } -static std::string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } +static string material_to_string_fn(const MaterialInfo &m) { return m.toString(); } -std::vector ItemFilter::getMaterials() const { - std::vector descriptions; +vector ItemFilter::getMaterials() const { + vector descriptions; transform_(materials, descriptions, material_to_string_fn); if (descriptions.size() == 0) @@ -187,3 +190,23 @@ bool ItemFilter::matches(df::item *item) const { return (materials.size() == 0) ? matchesMask(item_mat, mat_mask) : matches(item_mat); } + +vector deserialize_item_filters(color_ostream &out, const string &serialized) { + std::vector filters; + + vector filter_strs; + split_string(&filter_strs, serialized, ";"); + for (auto &str : filter_strs) { + filters.emplace_back(out, str); + } + + return filters; +} + +string serialize_item_filters(const vector &filters) { + vector strs; + for (auto &filter : filters) { + strs.emplace_back(filter.serialize()); + } + return join_strings(";", strs); +} diff --git a/plugins/buildingplan/itemfilter.h b/plugins/buildingplan/itemfilter.h index 134d3b249..6eb7551b4 100644 --- a/plugins/buildingplan/itemfilter.h +++ b/plugins/buildingplan/itemfilter.h @@ -8,10 +8,10 @@ class ItemFilter { public: ItemFilter(); - ItemFilter(std::string serialized); + ItemFilter(DFHack::color_ostream &out, std::string serialized); void clear(); - bool isEmpty(); + bool isEmpty() const; std::string serialize() const; void setMinQuality(int quality); @@ -37,3 +37,6 @@ private: df::dfhack_material_category mat_mask; std::vector materials; }; + +std::vector deserialize_item_filters(DFHack::color_ostream &out, const std::string &serialized); +std::string serialize_item_filters(const std::vector &filters); diff --git a/plugins/buildingplan/plannedbuilding.cpp b/plugins/buildingplan/plannedbuilding.cpp index c68d668bf..27be36a5b 100644 --- a/plugins/buildingplan/plannedbuilding.cpp +++ b/plugins/buildingplan/plannedbuilding.cpp @@ -58,25 +58,14 @@ static vector> deserialize_vector_ids(color_ostre return ret; } -static std::vector deserialize_item_filters(color_ostream &out, PersistentDataItem &bld_config) { +static std::vector get_item_filters(color_ostream &out, PersistentDataItem &bld_config) { std::vector ret; vector rawstrs; split_string(&rawstrs, bld_config.val(), "|"); if (rawstrs.size() < 2) return ret; - const string &serialized = rawstrs[1]; - - DEBUG(status,out).print("deserializing item filters for building %d: %s\n", - get_config_val(bld_config, BLD_CONFIG_ID), serialized.c_str()); - - vector filterstrs; - split_string(&filterstrs, serialized, ";"); - for (auto &str : filterstrs) { - ret.emplace_back(str); - } - - return ret; + return deserialize_item_filters(out, rawstrs[1]); } static string serialize(const vector> &vector_ids, const vector &item_filters) { @@ -85,14 +74,7 @@ static string serialize(const vector> &vector_ids joined.emplace_back(join_strings(",", vec_list)); } std::ostringstream out; - out << join_strings(";", joined) << "|"; - - joined.clear(); - for (auto &filter : item_filters) { - joined.emplace_back(filter.serialize()); - } - out << join_strings(";", joined); - + out << join_strings(";", joined) << "|" << serialize_item_filters(item_filters); return out.str(); } @@ -111,7 +93,7 @@ PlannedBuilding::PlannedBuilding(color_ostream &out, PersistentDataItem &bld_con : id(get_config_val(bld_config, BLD_CONFIG_ID)), vector_ids(deserialize_vector_ids(out, bld_config)), heat_safety((HeatSafety)get_config_val(bld_config, BLD_CONFIG_HEAT)), - item_filters(deserialize_item_filters(out, bld_config)), + item_filters(get_item_filters(out, bld_config)), bld_config(bld_config) { } // Ensure the building still exists and is in a valid state. It can disappear From 20a0390c50c3386ba8b16a824814bd057241acfd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:06:30 -0800 Subject: [PATCH 40/47] no building shadow when other windows are up --- plugins/lua/buildingplan.lua | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index b6cbb383b..f3a6ef6ce 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -500,6 +500,70 @@ FilterSelection.ATTRS{ } function FilterSelection:init() + self:addviews{ + widgets.Panel{ + view_id='options_panel', + frame={l=0, t=0, b=5, w=10}, + autoarrange_subviews=true, + subviews={ + widgets.Panel{ + view_id='quality_panel', + frame={}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Item quality', + subviews={ + }, + }, + widgets.Panel{ + view_id='building_panel', + frame={}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Building options', + subviews={ + }, + }, + widgets.Panel{ + view_id='global_panel', + frame={}, + frame_style=gui.MEDIUM_FRAME, + frame_title='Global options', + subviews={ + }, + }, + }, + }, + widgets.Panel{ + view_id='materials_panel', + frame={l=10, t=0, b=5, r=0}, + subviews={ + widgets.Panel{ + view_id='materials_top', + frame={l=0, t=0, r=0, h=5}, + subviews={ + }, + }, + widgets.Panel{ + view_id='materials_lists', + frame={l=0, t=5, r=0, b=0}, + frame_style=gui.MEDIUM_FRAME, + subviews={ + widgets.Panel{ + view_id='materials_categories', + frame={l=0, t=0, b=0, w=20}, + subviews={ + }, + }, + widgets.Panel{ + view_id='materials_mats', + frame={l=21, t=0, r=0, b=0}, + subviews={ + }, + }, + }, + }, + }, + }, + } end FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) @@ -514,6 +578,14 @@ function FilterSelectionScreen:init() } end +function FilterSelectionScreen:onShow() + df.global.game.main_interface.bottom_mode_selected = -1 +end + +function FilterSelectionScreen:onDismiss() + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT +end + -------------------------------- -- ItemLine -- @@ -1048,6 +1120,7 @@ function PlannerOverlay:onInput(keys) local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters + df.global.game.main_interface.bottom_mode_selected = -1 for idx = num_filters,1,-1 do chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then @@ -1061,6 +1134,7 @@ function PlannerOverlay:onInput(keys) active_screens[idx] = nil pending = pending - 1 if pending == 0 then + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:place_building(self:restore_placement(), chosen_items) end end, @@ -1068,6 +1142,7 @@ function PlannerOverlay:onInput(keys) for i,scr in pairs(active_screens) do scr:dismiss() end + df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT self:restore_placement() end, }:show() From dadecdcf45f9a2aa914b8a5caa13ba57013fb73f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:14:11 -0800 Subject: [PATCH 41/47] fix inspector screen not resetting the description --- plugins/lua/buildingplan.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index f3a6ef6ce..ee2aff21b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -1355,6 +1355,7 @@ function InspectorLine:get_status_line() end function InspectorLine:reset() + self.desc = nil self.status = nil end From 4b2645469686e678846811a0c66769b8ec9f8a9d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 18:59:15 -0800 Subject: [PATCH 42/47] start of filters dialog --- plugins/lua/buildingplan.lua | 106 +++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index ee2aff21b..aef04e020 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -494,7 +494,7 @@ end FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', - frame={w=60, h=40, l=30, t=8}, + frame={w=80, h=53, l=30, t=8}, resizable=true, index=DEFAULT_NIL, } @@ -503,38 +503,122 @@ function FilterSelection:init() self:addviews{ widgets.Panel{ view_id='options_panel', - frame={l=0, t=0, b=5, w=10}, + frame={l=0, t=0, b=5, w=30}, autoarrange_subviews=true, subviews={ widgets.Panel{ view_id='quality_panel', - frame={}, - frame_style=gui.MEDIUM_FRAME, + frame={l=0, r=0, h=23}, + frame_style=gui.INTERIOR_FRAME, frame_title='Item quality', subviews={ + widgets.Label{ + frame={l=0, t=0}, + text='updown hotkeys', + }, + widgets.Panel{ + view_id='quality_slider', + frame={l=0, t=2, w=3, h=15}, + frame_background=to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '}, + }, + widgets.Label{ + frame={l=3, t=3}, + text='- Artifact (num)', + }, + widgets.Label{ + frame={l=3, t=5}, + text='- Masterful (num)', + }, + widgets.Label{ + frame={l=3, t=7}, + text='- Exceptional (num)', + }, + widgets.Label{ + frame={l=3, t=9}, + text='- Superior (num)', + }, + widgets.Label{ + frame={l=3, t=11}, + text='- FinelyCrafted (num)', + }, + widgets.Label{ + frame={l=3, t=13}, + text='- WellCrafted (num)', + }, + widgets.Label{ + frame={l=3, t=15}, + text='- Ordinary (num)', + }, + widgets.Label{ + frame={l=0, t=18}, + text='updown hotkeys', + }, + widgets.CycleHotkeyLabel{ + frame={l=0, t=20}, + label='Decorated only:', + options={'No', 'Yes'}, + }, }, }, - widgets.Panel{ + widgets.ResizingPanel{ view_id='building_panel', - frame={}, - frame_style=gui.MEDIUM_FRAME, + frame={l=0, r=0}, + frame_style=gui.INTERIOR_FRAME, frame_title='Building options', + autoarrange_subviews=true, + autoarrange_gap=1, subviews={ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect all items for the current building type.', + }, + widgets.CycleHotkeyLabel{ + frame={l=0}, + key='CUSTOM_G', + label='Building safety:', + options={ + {label='Any', value=0}, + {label='Magma', value=2, pen=COLOR_RED}, + {label='Fire', value=1, pen=COLOR_LIGHTRED}, + }, + }, }, }, widgets.Panel{ view_id='global_panel', - frame={}, - frame_style=gui.MEDIUM_FRAME, + frame={l=0, r=0, b=0}, + frame_style=gui.INTERIOR_FRAME, frame_title='Global options', + autoarrange_subviews=true, + autoarrange_gap=1, subviews={ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Blocks', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Logs', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Boulders', + }, + widgets.ToggleHotkeyLabel{ + frame={l=0}, + label='Bars', + }, }, }, }, }, widgets.Panel{ view_id='materials_panel', - frame={l=10, t=0, b=5, r=0}, + frame={l=30, t=0, b=5, r=0}, subviews={ widgets.Panel{ view_id='materials_top', @@ -545,7 +629,7 @@ function FilterSelection:init() widgets.Panel{ view_id='materials_lists', frame={l=0, t=5, r=0, b=0}, - frame_style=gui.MEDIUM_FRAME, + frame_style=gui.INTERIOR_FRAME, subviews={ widgets.Panel{ view_id='materials_categories', From f0ca7ad4257b2ab7b69486831ed133908a9437b9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 20:34:54 -0800 Subject: [PATCH 43/47] fix all buildings being identified as constructions --- plugins/lua/buildingplan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index aef04e020..905a5558f 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -694,7 +694,7 @@ local function is_construction() end local function is_stairs() - return is_construction + return is_construction() and uibs.building_subtype == df.construction_type.UpDownStair end From d8e440806c8ad617f29ff2b462d2c0ce407735d2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 Feb 2023 23:19:04 -0800 Subject: [PATCH 44/47] fix signed/unsigned compare --- plugins/buildingplan/defaultitemfilters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildingplan/defaultitemfilters.cpp b/plugins/buildingplan/defaultitemfilters.cpp index 4cc6f11cf..36d074363 100644 --- a/plugins/buildingplan/defaultitemfilters.cpp +++ b/plugins/buildingplan/defaultitemfilters.cpp @@ -47,7 +47,7 @@ DefaultItemFilters::DefaultItemFilters(color_ostream &out, PersistentDataItem &f } void DefaultItemFilters::setItemFilter(DFHack::color_ostream &out, const ItemFilter &filter, int index) { - if (item_filters.size() <= index) { + if (index < 0 || item_filters.size() <= (size_t)index) { WARN(status,out).print("invalid index for filter key %d,%d,%d: %d\n", std::get<0>(key), std::get<1>(key), std::get<2>(key), index); return; From fbd3cd44d60b3f57d27d1b694fc720bb061ecd4f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Feb 2023 01:15:22 -0800 Subject: [PATCH 45/47] initial mock of filter dialog --- plugins/lua/buildingplan.lua | 183 ++++++++++++++++++++++++++++++----- 1 file changed, 157 insertions(+), 26 deletions(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 905a5558f..54e8c9d8d 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -491,6 +491,11 @@ local function can_be_improved(idx) filter.item_type ~= df.item_type.BOULDER end +local OPTIONS_COL_WIDTH = 28 +local TYPE_COL_WIDTH = 20 +local HEADER_HEIGHT = 5 +local FOOTER_HEIGHT = 4 + FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', @@ -499,62 +504,76 @@ FilterSelection.ATTRS{ index=DEFAULT_NIL, } +local STANDIN_PEN = to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '} + function FilterSelection:init() self:addviews{ widgets.Panel{ view_id='options_panel', - frame={l=0, t=0, b=5, w=30}, + frame={l=0, t=0, b=FOOTER_HEIGHT, w=OPTIONS_COL_WIDTH}, autoarrange_subviews=true, subviews={ widgets.Panel{ view_id='quality_panel', - frame={l=0, r=0, h=23}, + frame={l=0, r=0, h=24}, + frame_inset={t=1}, frame_style=gui.INTERIOR_FRAME, frame_title='Item quality', subviews={ - widgets.Label{ + widgets.HotkeyLabel{ frame={l=0, t=0}, - text='updown hotkeys', + key='CUSTOM_SHIFT_Q', + }, + widgets.HotkeyLabel{ + frame={l=1, t=0}, + key='CUSTOM_SHIFT_W', + label='Set max quality', }, widgets.Panel{ view_id='quality_slider', frame={l=0, t=2, w=3, h=15}, - frame_background=to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '}, + frame_background=STANDIN_PEN, }, widgets.Label{ frame={l=3, t=3}, - text='- Artifact (num)', + text='- Artifact (1)', }, widgets.Label{ frame={l=3, t=5}, - text='- Masterful (num)', + text='- Masterful (3)', }, widgets.Label{ frame={l=3, t=7}, - text='- Exceptional (num)', + text='- Exceptional (34)', }, widgets.Label{ frame={l=3, t=9}, - text='- Superior (num)', + text='- Superior (50)', }, widgets.Label{ frame={l=3, t=11}, - text='- FinelyCrafted (num)', + text='- FinelyCrafted (67)', }, widgets.Label{ frame={l=3, t=13}, - text='- WellCrafted (num)', + text='- WellCrafted (79)', }, widgets.Label{ frame={l=3, t=15}, - text='- Ordinary (num)', + text='- Ordinary (206)', }, - widgets.Label{ + widgets.HotkeyLabel{ frame={l=0, t=18}, - text='updown hotkeys', + key='CUSTOM_SHIFT_Z', + }, + widgets.HotkeyLabel{ + frame={l=1, t=18}, + key='CUSTOM_SHIFT_X', + label='Set min quality', }, widgets.CycleHotkeyLabel{ frame={l=0, t=20}, + key='CUSTOM_SHIFT_D', label='Decorated only:', options={'No', 'Yes'}, }, @@ -563,6 +582,7 @@ function FilterSelection:init() widgets.ResizingPanel{ view_id='building_panel', frame={l=0, r=0}, + frame_inset={t=1}, frame_style=gui.INTERIOR_FRAME, frame_title='Building options', autoarrange_subviews=true, @@ -574,7 +594,7 @@ function FilterSelection:init() }, widgets.CycleHotkeyLabel{ frame={l=0}, - key='CUSTOM_G', + key='CUSTOM_SHIFT_G', label='Building safety:', options={ {label='Any', value=0}, @@ -587,30 +607,41 @@ function FilterSelection:init() widgets.Panel{ view_id='global_panel', frame={l=0, r=0, b=0}, + frame_inset={t=1}, frame_style=gui.INTERIOR_FRAME, frame_title='Global options', autoarrange_subviews=true, - autoarrange_gap=1, subviews={ widgets.WrappedLabel{ frame={l=0}, text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.', }, + widgets.Panel{ + frame={h=1}, + }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_B', label='Blocks', + label_width=8, }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_L', label='Logs', + label_width=8, }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_O', label='Boulders', + label_width=8, }, widgets.ToggleHotkeyLabel{ frame={l=0}, + key='CUSTOM_SHIFT_P', label='Bars', + label_width=8, }, }, }, @@ -618,38 +649,138 @@ function FilterSelection:init() }, widgets.Panel{ view_id='materials_panel', - frame={l=30, t=0, b=5, r=0}, + frame={l=OPTIONS_COL_WIDTH, t=0, b=FOOTER_HEIGHT, r=0}, subviews={ widgets.Panel{ - view_id='materials_top', - frame={l=0, t=0, r=0, h=5}, + view_id='header', + frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, subviews={ + widgets.EditField{ + frame={l=1, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('%l') end, + }, + widgets.CycleHotkeyLabel{ + frame={l=1, t=2, w=21}, + label='Sort by:', + key='CUSTOM_SHIFT_R', + options={'name', 'available'}, + }, + widgets.ToggleHotkeyLabel{ + frame={l=24, t=2, w=24}, + label='Hide unavailable:', + key='CUSTOM_SHIFT_H', + initial_option=false, + }, + widgets.Label{ + frame={l=1, b=0}, + text='Type', + text_pen=COLOR_LIGHTRED, + }, + widgets.Label{ + frame={l=TYPE_COL_WIDTH, b=0}, + text='Material', + text_pen=COLOR_LIGHTRED, + }, }, }, widgets.Panel{ view_id='materials_lists', - frame={l=0, t=5, r=0, b=0}, + frame={l=0, t=HEADER_HEIGHT, r=0, b=0}, frame_style=gui.INTERIOR_FRAME, subviews={ - widgets.Panel{ + widgets.List{ view_id='materials_categories', - frame={l=0, t=0, b=0, w=20}, - subviews={ + frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, + scroll_keys={}, + choices={ + {text='Stone', key='CUSTOM_SHIFT_S'}, + {text='Wood', key='CUSTOM_SHIFT_W'}, + {text='Metal', key='CUSTOM_SHIFT_M'}, + {text='Other', key='CUSTOM_SHIFT_O'}, }, }, - widgets.Panel{ + widgets.List{ view_id='materials_mats', - frame={l=21, t=0, r=0, b=0}, - subviews={ + frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, + choices={ + {text='9 - granite'}, + {text='0 - graphite'}, }, }, }, }, + widgets.Panel{ + view_id='divider', + frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=0, w=1}, + on_render=self:callback('draw_divider'), + } }, }, + widgets.Panel{ + view_id='footer', + frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, + frame_inset={l=20, t=1}, + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0}, + label='Toggle', + auto_width=true, + key='SELECT', + }, + widgets.HotkeyLabel{ + frame={l=0, t=2}, + label='Done', + auto_width=true, + key='LEAVESCREEN', + }, + widgets.HotkeyLabel{ + frame={l=30, t=0}, + label='Select all', + auto_width=true, + key='CUSTOM_SHIFT_A', + }, + widgets.HotkeyLabel{ + frame={l=30, t=1}, + label='Invert selection', + auto_width=true, + key='CUSTOM_SHIFT_I', + }, + widgets.HotkeyLabel{ + frame={l=30, t=2}, + label='Clear selection', + auto_width=true, + key='CUSTOM_SHIFT_C', + }, + }, + } } end +local texpos = dfhack.textures.getThinBordersTexposStart() +local tp = function(offset) + if texpos == -1 then return nil end + return texpos + offset +end + +local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK} +local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK} +local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} + +function FilterSelection:draw_divider(dc) + local y2 = dc.height - 1 + for y=0,y2 do + dc:seek(0, y) + if y == 0 then + dc:char(nil, TOP_PEN) + elseif y == y2 then + dc:char(nil, BOT_PEN) + else + dc:char(nil, MID_PEN) + end + end +end + FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', From 63d752b3f836d0dd4d72344fade8b60756404e85 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 10:53:30 -0800 Subject: [PATCH 46/47] update docs --- docs/guides/quickfort-user-guide.rst | 2 +- docs/plugins/buildingplan.rst | 97 +++++++++++++++++----------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/docs/guides/quickfort-user-guide.rst b/docs/guides/quickfort-user-guide.rst index 6f606c43a..ecee46918 100644 --- a/docs/guides/quickfort-user-guide.rst +++ b/docs/guides/quickfort-user-guide.rst @@ -1308,7 +1308,7 @@ legacy Python Quickfort. This setting has no effect on DFHack Quickfort, which will use buildingplan to manage everything designated in a ``#build`` blueprint regardless of the buildingplan UI settings. -However, quickfort *does* use `buildingplan's filters ` +However, quickfort *does* use `buildingplan's filters ` for each building type. For example, you can use the buildingplan UI to set the type of stone you want your walls made out of. Or you can specify that all buildingplan-managed chairs and tables must be of Masterful quality. The current diff --git a/docs/plugins/buildingplan.rst b/docs/plugins/buildingplan.rst index a331e9eb8..f77e5b590 100644 --- a/docs/plugins/buildingplan.rst +++ b/docs/plugins/buildingplan.rst @@ -2,32 +2,73 @@ buildingplan ============ .. dfhack-tool:: - :summary: Plan building construction before you have materials. + :summary: Plan building layouts with or without materials. :tags: fort design buildings -This plugin adds a planning mode for building placement. You can then place -furniture, constructions, and other buildings before the required materials are -available, and they will be created in a suspended state. Buildingplan will -periodically scan for appropriate items, and the jobs will be unsuspended when -the items are available. - -This is very powerful when used with tools like `quickfort`, which allow you to -set a building plan according to a blueprint, and the buildings will simply be -built when you can build them. - -You can use manager work orders or `workflow` to ensure you always have one or -two doors/beds/tables/chairs/etc. available, and place as many as you like. -Materials are used to build the planned buildings as they are produced, with -minimal space dedicated to stockpiles. +Buildingplan allows you to place furniture, constructions, and other buildings, +regardless of whether the required materials are available. This allows you to +focus purely on design elements when you are laying out your fort, and defers +item production concerns to a more convenient time. + +Buildingplan is as an alternative to the vanilla building placement UI. It +appears after you have selected the type of building, furniture, or construction +that you want to place in the vanilla build menu. Buildingplan then takes over +for the actual placement step. If any building materials are not available yet +for the placed building, it will be created in a suspended state. Buildingplan +will periodically scan for appropriate items and attach them. Once all items are +attached, the construction job will be unsuspended and a dwarf will come and +build the building. If you have the `unsuspend` overlay enabled (it is enabled +by default), then buildingplan-suspended buildings will appear with a ``P`` marker +on the main map, as opposed to the usual ``x`` marker for "regular" suspended +buildings. + +If you want to impose restrictions on which items are chosen for the buildings, +buildingplan has full support for quality and material filters. Before you place +a building, you can select a component item in the list and hit ``f`` or click on +the ``filter`` button next to the item description. This will let you choose your +desired item quality range, whether the item must be decorated, and even which +specific materials the item must be made out of. This lets you create layouts +with a consistent color, if that is part of your design. + +If you just care about the heat sensitivity of the building, you can set the +building to be fire- or magma-proof in the placement UI screen or in any item +filter screen, and the restriction will apply to all building items. This makes it +very easy to create magma-safe pump stacks, for example. + +Buildingplan works very well in conjuction with other design tools like +`gui/quickfort`, which allow you to apply a building layout from a blueprint. You +can apply very large, complicated layouts, and the buildings will simply be built +when your dwarves get around to producing the needed materials. If you set filters +in the buildingplan UI before applying the blueprint, the filters will be applied +to the blueprint buildings, just as if you had planned them from the buildingplan +placement UI. + +One way to integrate buildingplan into your gameplay is to create manager +workorders to ensure you always have a few blocks/doors/beds/etc. available. You +can then place as many of each building as you like. Produced items will be used +to build the planned buildings as they are produced, with minimal space dedicated +to stockpiles. The DFHack `orders` library can help with setting up these manager +workorders for you. + +If you do not wish to use the ``buildingplan`` interface, you can turn off the +``buildingplan.planner`` overlay in `gui/overlay`. You should not disable the +``buildingplan`` service entirely in `gui/control-panel` since then existing +planned buildings in loaded forts will stop functioning. Usage ----- :: - enable buildingplan buildingplan [status] - buildingplan set true|false + buildingplan set (true|false) + +Examples +-------- + +``buildingplan`` + Print a report of current settings, which kinds of buildings are planned, + and what kinds of materials the buildings are waiting for. .. _buildingplan-settings: @@ -49,23 +90,5 @@ want to add this line to your ``dfhack-config/init/onMapLoad.init`` file to always configure `buildingplan` to just use blocks for buildings and constructions:: - on-new-fortress buildingplan set boulders false; buildingplan set logs false - -.. _buildingplan-filters: - -Item filtering --------------- - -While placing a building, you can set filters for what materials you want the -building made out of, what quality you want the component items to be, and -whether you want the items to be decorated. - -If a building type takes more than one item to construct, use -:kbd:`Ctrl`:kbd:`Left` and :kbd:`Ctrl`:kbd:`Right` to select the item that you -want to set filters for. Any filters that you set will be used for all buildings -of the selected type placed from that point onward (until you set a new filter -or clear the current one). Buildings placed before the filters were changed will -keep the filter values that were set when the building was placed. - -For example, you can be sure that all your constructed walls are the same color -by setting a filter to accept only certain types of stone. + on-new-fortress buildingplan set boulders false + on-new-fortress buildingplan set logs false From 97ee1022c7200b1c47b7c5e36c3260dcba50582c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 26 Feb 2023 10:55:50 -0800 Subject: [PATCH 47/47] note that filter page is a mock --- plugins/lua/buildingplan.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/buildingplan.lua b/plugins/lua/buildingplan.lua index 54e8c9d8d..0e1341a3b 100644 --- a/plugins/lua/buildingplan.lua +++ b/plugins/lua/buildingplan.lua @@ -498,7 +498,7 @@ local FOOTER_HEIGHT = 4 FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ - frame_title='Choose filters', + frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]', frame={w=80, h=53, l=30, t=8}, resizable=true, index=DEFAULT_NIL,