From fce63b7a713b955ad094853642f316daef653c43 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 22 Nov 2025 05:12:44 -0600 Subject: [PATCH 1/3] info sort overlay: support new "Siege engines" subtab Move the Places tab search widget: - in wide interfaces it was being drawn over the new "Siege engines" subtab; move it down so it is drawn in the blank line between the subtab group and the content area (like the Tasks search widget) - in narrow interfaces it was being drawn too far right to show its text field inside the bounds of DF's Info panel; move it left into the space made available now that DF is wrapping the subtab group due to the addition of the new subtab Register a handler for the new "Siege engine" subtab; generate search keys that match the text that DF displays. --- docs/changelog.txt | 2 + plugins/lua/sort/info.lua | 3 +- plugins/lua/sort/places.lua | 115 +++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 617a5849c8..a18405fd9b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,9 +57,11 @@ Template for new versions: ## New Tools ## New Features +- `sort`: Places search widget can search "Siege engines" subtab by name, loaded status, and operator status ## Fixes - `sort`: Using the squad unit selector will no longer cause Dwarf Fortress to crash on exit +- `sort`: Places search widget moved to account for DF's new "Siege engines" subtab ## Misc Improvements diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index feadee99c0..e2c728e0e6 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -299,8 +299,7 @@ function get_panel_offsets() if tabs_in_two_rows then t_offset = shift_right and 0 or 3 end - if info.current_mode == df.info_interface_mode_type.JOBS or - info.current_mode == df.info_interface_mode_type.BUILDINGS then + if info.current_mode == df.info_interface_mode_type.JOBS then t_offset = t_offset - 1 end return l_offset, t_offset diff --git a/plugins/lua/sort/places.lua b/plugins/lua/sort/places.lua index f2588cc0ff..c353d5a948 100644 --- a/plugins/lua/sort/places.lua +++ b/plugins/lua/sort/places.lua @@ -170,6 +170,117 @@ local function get_farmplot_search_key(farmplot) return table.concat(result, ' ') end +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_type(siege_engine) + if siege_engine.type == df.siegeengine_type.BoltThrower then + return 'Bolt Thrower' + end + return df.siegeengine_type[siege_engine.type] +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_status(siege_engine) + -- portions of return value with with underscores are to allow easier + -- word-anchored matching even when the DFHack full-text search mode is + -- enabled; e.g. + -- - "loaded" would match "Loaded" and "Unloaded", + -- - but "_loaded" would only match "_Loaded" + local count = 0 + local count_all = siege_engine.type == df.siegeengine_type.BoltThrower + for _, building_item in ipairs(siege_engine.contained_items) do + if building_item.use_mode == df.building_item_role_type.TEMP then + if not count_all then + return 'Loaded _Loaded' + end + count = count + building_item.item:getStackSize() + end + end + if count_all and count > 0 then + return ('%d bolts _%d_bolts'):format(count, count) + end + return 'Unloaded' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function siege_engine_job_status(siege_engine) + for _, job in ipairs(siege_engine.jobs) do + if job.job_type == df.job_type.LoadCatapult + or job.job_type == df.job_type.LoadBallista + or job.job_type == df.job_type.LoadBoltThrower + then + if dfhack.job.getWorker(job) ~= nil then + return 'Loading' + else + return 'Inactive load task' + end + end + local firing_bolt_thrower = job.job_type == df.job_type.FireBoltThrower + local firing = job.job_type == df.job_type.FireCatapult + or job.job_type == df.job_type.FireBallista + or firing_bolt_thrower + if firing then + local unit = dfhack.job.getWorker(job) + if unit == nil then + return 'No operator' + else + ---@type integer?, integer?, integer? + local x, y, z = dfhack.units.getPosition(unit) + -- DF shows "present" when the unit is inside the building's + -- footprint (or, for bolt throwers, next to it); the unit does + -- not need to be at the exact firing position tile (which + -- varies based on siege engine type and direction) + if x ~= nil and z == siege_engine.z then + ---@cast y integer + if firing_bolt_thrower then + if siege_engine.x1 - 1 <= x and x <= siege_engine.x2 + 1 + and siege_engine.y1 - 1 <= y and y <= siege_engine.y2 + 1 + then + return 'Operator present' + end + elseif dfhack.buildings.containsTile(siege_engine, x, y) then + return 'Operator present' + end + end + return 'Operator assigned' + end + end + end + return '' +end + +---@param siege_engine df.building_siegeenginest +---@return string +local function get_siege_engine_search_key(siege_engine) + -- DF 53.05 Info window, Places tab, Siege Engines subtab shows this info: + -- name: assigned name or siege engine type name + -- status: "Unloaded", "Loaded", " bolts" + -- job status: + -- - "Inactive load task" (load job unassigned), + -- - "Loading" (load job assigned), + -- - "No operator" (fire job unassigned), + -- - "Operator present" (fire job assigned), + -- - "Operator assigned" (fire job assigned, but not in position), + -- - blank + -- action: (icons) fire-at-will, practice, prepare-to-fire, keep-loaded, not-in-use + -- These have associated text blurbs that are shown in the + -- building info window, but those texts are not discoverable + -- from the Info > Places > Siege engine list view. + local result = {} + + if #siege_engine.name ~= 0 then table.insert(result, siege_engine.name) end + + table.insert(result, siege_engine_type(siege_engine)) + + table.insert(result, siege_engine_status(siege_engine)) + + table.insert(result, siege_engine_job_status(siege_engine)) + + return table.concat(result, ' ') +end + -- ---------------------- -- PlacesOverlay -- @@ -177,7 +288,8 @@ end PlacesOverlay = defclass(PlacesOverlay, sortoverlay.SortOverlay) PlacesOverlay.ATTRS{ desc='Adds search functionality to the places overview screens.', - default_pos={x=71, y=9}, + default_pos={x=52, y=9}, + version=2, viewscreens='dwarfmode/Info', frame={w=40, h=6} } @@ -205,6 +317,7 @@ function PlacesOverlay:init() self:register_handler('STOCKPILES', buildings.list[df.buildings_mode_type.STOCKPILES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_stockpile_search_key})) self:register_handler('WORKSHOPS', buildings.list[df.buildings_mode_type.WORKSHOPS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_workshop_search_key})) self:register_handler('FARMPLOTS', buildings.list[df.buildings_mode_type.FARMPLOTS], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_farmplot_search_key})) + self:register_handler('SIEGE_ENGINES', buildings.list[df.buildings_mode_type.SIEGE_ENGINES], curry(sortoverlay.single_vector_search, {get_search_key_fn=get_siege_engine_search_key})) end function PlacesOverlay:get_key() From 6eaf98b1d07c6735d1cd7e9f163ff9fa1fd79c63 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 22 Nov 2025 05:12:44 -0600 Subject: [PATCH 2/3] info sort overlay: use interface width in resize_overlay The DF info window is based on the interface area, not the full window area. So the sort info overlays should also work with the interface width instead of the window width. If DF is configured to use an interface percentage smaller than 100, then the interface area will be narrower than the full window once the window is resized beyond the minimum width. Basing the info sort overlay size on the window size can let some UI elements extend outside the footprint of the info window. For example, when the DF interface percentage is set to 90, the search box for the Task tab - is too close to the info window border at window widths 115 and 147 - shares a edge with the info window border at window widths 116 and 146 - extends beyond the info window border for window widths 117-145 --- plugins/lua/sort/info.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index e2c728e0e6..0b261da2d7 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -278,8 +278,8 @@ function InfoOverlay:get_key() end function resize_overlay(self) - local sw = dfhack.screen.getWindowSize() - local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) + local iw = gui.get_interface_rect().width + local overlay_width = math.min(40, iw - (self.frame_rect.x1 + 30)) if overlay_width ~= self.frame.w then self.frame.w = overlay_width return true From 30536a65bb2d59a6d0bcd1e83ea1278ac6818646 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Nov 2025 06:30:09 -0600 Subject: [PATCH 3/3] info sort overlay: remove check for unused Labor tab There hasn't been a LABOR tab sort overlay since 50.12. The WORK_DETAIL overlay was removed in 4c928766. That commit was part of 50.12-r1~34. The DF 50.12 release notes say that work detail sorting and searching was added, and the DFHack 50.12-r1 release notes say that search widgets were removed for screens that gained native search. https://store.steampowered.com/news/app/975370/view/4136064865380703752 https://docs.dfhack.org/en/stable/docs/NEWS.html#dfhack-50-12-r1 --- plugins/lua/sort/info.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 0b261da2d7..4f0462dfb7 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -292,8 +292,7 @@ end function get_panel_offsets() local tabs_in_two_rows = is_tabs_in_two_rows() - local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS or - info.current_mode == df.info_interface_mode_type.LABOR + local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS local l_offset = (not tabs_in_two_rows and shift_right) and 4 or 0 local t_offset = 1 if tabs_in_two_rows then