Skip to content

Commit 73a3892

Browse files
committed
Add hauling-route-search-filter script and documentation
1 parent ff1b95a commit 73a3892

2 files changed

Lines changed: 310 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Summary:
2+
Add a search box to the Hauling menu to filter hauling routes.
3+
This overlay adds a Filter field to the Hauling menu (opened with h). The list of hauling routes updates as you type. You can activate the filter by clicking the field or pressing Alt+S. Clearing the field shows the full list again, including any routes or stops added while the filter was active.

hauling-route-search-filter.lua

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
-- Search/filter hauling routes from the Hauling menu.
2+
--[====[
3+
4+
gui/hauling-search
5+
==================
6+
Activate in the :guilabel:`Hauling` menu (press :kbd:`h`) to
7+
filter the native hauling route list by name or id. The filter
8+
hides non-matching routes in the in-game list and restores the
9+
full list when cleared.
10+
11+
]====]
12+
--@ module = true
13+
14+
local overlay = require 'plugins.overlay'
15+
local widgets = require 'gui.widgets'
16+
17+
local last_filter = ''
18+
local STOP_KEY = {}
19+
20+
local function safe_field(obj, field)
21+
local ok, value = pcall(function() return obj[field] end)
22+
if ok then
23+
return value
24+
end
25+
return nil
26+
end
27+
28+
local function resolve_route(hauling, route_ref)
29+
if not hauling or not route_ref then return nil end
30+
local name = safe_field(route_ref, 'name')
31+
local id = safe_field(route_ref, 'id')
32+
if name ~= nil or id ~= nil then
33+
return route_ref
34+
end
35+
local route_id = safe_field(route_ref, 'route_id')
36+
if route_id ~= nil then
37+
local routes = safe_field(hauling, 'routes')
38+
if routes then return routes[route_id] end
39+
end
40+
return nil
41+
end
42+
43+
local function resolve_route_id(hauling, route_ref, stop_ref)
44+
local route = resolve_route(hauling, route_ref)
45+
if route and route.id ~= nil then
46+
return route.id
47+
end
48+
local stop_route_id = safe_field(stop_ref, 'route_id')
49+
if stop_route_id ~= nil then
50+
return stop_route_id
51+
end
52+
return nil
53+
end
54+
55+
local function get_route_name(route)
56+
if not route then return 'Route ?' end
57+
return route.name and #route.name > 0 and route.name or ('Route '..route.id)
58+
end
59+
60+
local function is_match(filter, route)
61+
if not route then return false end
62+
if filter == '' then return true end
63+
local needle = filter:lower()
64+
local name = get_route_name(route):lower()
65+
if name:find(needle, 1, true) then return true end
66+
if tostring(route.id):find(needle, 1, true) then return true end
67+
return false
68+
end
69+
70+
local function build_matching_route_ids(hauling, filter)
71+
local matching_route_ids = {}
72+
local routes = safe_field(hauling, 'routes')
73+
if not routes then return matching_route_ids end
74+
for i = 0, #routes - 1 do
75+
local route = routes[i]
76+
if is_match(filter, route) then
77+
if route and route.id ~= nil then
78+
matching_route_ids[route.id] = true
79+
end
80+
end
81+
end
82+
return matching_route_ids
83+
end
84+
85+
local function snapshot_rows(hauling)
86+
local view_routes = safe_field(hauling, 'view_routes')
87+
local view_stops = safe_field(hauling, 'view_stops')
88+
if not view_routes or not view_stops then return nil end
89+
local rows = {}
90+
for i = 0, #view_routes - 1 do
91+
local route_ref = view_routes[i]
92+
local stop_ref = view_stops[i]
93+
local route_id = resolve_route_id(hauling, route_ref, stop_ref)
94+
local stop_id = safe_field(stop_ref, 'id')
95+
table.insert(rows, {
96+
route=route_ref,
97+
stop=stop_ref,
98+
route_id=route_id,
99+
stop_id=stop_id,
100+
})
101+
end
102+
return rows
103+
end
104+
105+
local function snapshot_routes(hauling)
106+
local routes = safe_field(hauling, 'routes')
107+
if not routes then return nil end
108+
local rows = {}
109+
for i = 0, #routes - 1 do
110+
local route = routes[i]
111+
if route then
112+
table.insert(rows, {
113+
route=route,
114+
stop=nil,
115+
route_id=route.id,
116+
stop_id=nil,
117+
})
118+
local stops = safe_field(route, 'stops')
119+
if stops then
120+
for j = 0, #stops - 1 do
121+
local stop = stops[j]
122+
table.insert(rows, {
123+
route=route,
124+
stop=stop,
125+
route_id=route.id,
126+
stop_id=safe_field(stop, 'id'),
127+
})
128+
end
129+
end
130+
end
131+
end
132+
return rows
133+
end
134+
135+
local function get_route_signature(hauling)
136+
local routes = safe_field(hauling, 'routes')
137+
if not routes then return nil end
138+
local parts = {}
139+
for i = 0, #routes - 1 do
140+
local route = routes[i]
141+
local id = route and route.id or 'nil'
142+
local stops = route and safe_field(route, 'stops')
143+
local stop_count = stops and #stops or 0
144+
table.insert(parts, tostring(id) .. ':' .. tostring(stop_count))
145+
end
146+
return table.concat(parts, '|')
147+
end
148+
149+
local function rebuild_rows(hauling, rows)
150+
local view_routes = safe_field(hauling, 'view_routes')
151+
local view_stops = safe_field(hauling, 'view_stops')
152+
if not view_routes or not view_stops then return end
153+
view_routes:resize(0)
154+
view_stops:resize(0)
155+
for _, row in ipairs(rows) do
156+
view_routes:insert('#', row.route)
157+
view_stops:insert('#', row.stop)
158+
end
159+
end
160+
161+
local function merge_rows(existing_rows, hauling)
162+
local view_routes = safe_field(hauling, 'view_routes')
163+
local view_stops = safe_field(hauling, 'view_stops')
164+
if not view_routes or not view_stops then return existing_rows end
165+
local seen = {}
166+
for idx, row in ipairs(existing_rows) do
167+
local route_key = row.route_id or row.route or idx
168+
local stop_key = row.stop_id or row.stop or STOP_KEY
169+
seen[tostring(route_key) .. ':' .. tostring(stop_key)] = true
170+
end
171+
for i = 0, #view_routes - 1 do
172+
local route_ref = view_routes[i]
173+
local stop_ref = view_stops[i]
174+
local route_id = resolve_route_id(hauling, route_ref, stop_ref)
175+
local stop_id = safe_field(stop_ref, 'id')
176+
local route_key = route_id or route_ref or i
177+
local stop_key = stop_id or stop_ref or STOP_KEY
178+
local key = tostring(route_key) .. ':' .. tostring(stop_key)
179+
if not seen[key] then
180+
table.insert(existing_rows, {
181+
route=route_ref,
182+
stop=stop_ref,
183+
route_id=route_id,
184+
stop_id=stop_id,
185+
})
186+
seen[key] = true
187+
end
188+
end
189+
return existing_rows
190+
end
191+
192+
HaulingRouteFilterOverlay = defclass(HaulingRouteFilterOverlay, overlay.OverlayWidget)
193+
HaulingRouteFilterOverlay.ATTRS{
194+
desc='Adds an inline filter box to the hauling routes list.',
195+
default_enabled=true,
196+
default_pos={x=8, y=6},
197+
frame={w=46, h=1},
198+
viewscreens='dwarfmode/Hauling',
199+
}
200+
201+
function HaulingRouteFilterOverlay:init()
202+
self.hauling = df.global.plotinfo.hauling
203+
self:addviews{
204+
widgets.Panel{
205+
subviews={
206+
widgets.EditField{
207+
view_id='filter',
208+
frame={t=0, l=1, r=1},
209+
key='CUSTOM_ALT_S',
210+
label_text='Filter: ',
211+
text=last_filter,
212+
on_change=self:callback('on_filter_change'),
213+
},
214+
},
215+
},
216+
}
217+
end
218+
219+
function HaulingRouteFilterOverlay:overlay_onupdate()
220+
if self.filter_text then
221+
self:apply_filter(self.filter_text)
222+
end
223+
end
224+
225+
function HaulingRouteFilterOverlay:snapshot_rows()
226+
return snapshot_rows(self.hauling)
227+
end
228+
229+
function HaulingRouteFilterOverlay:restore_rows()
230+
if not self.unfiltered_rows then return end
231+
local refreshed = snapshot_routes(self.hauling)
232+
if refreshed then
233+
self.unfiltered_rows = refreshed
234+
end
235+
self.unfiltered_rows = merge_rows(self.unfiltered_rows, self.hauling)
236+
rebuild_rows(self.hauling, self.unfiltered_rows)
237+
self.unfiltered_rows = nil
238+
self.route_signature = nil
239+
end
240+
241+
function HaulingRouteFilterOverlay:apply_filter(filter)
242+
if filter == '' then
243+
self:restore_rows()
244+
return
245+
end
246+
if not self.unfiltered_rows then
247+
self.unfiltered_rows = self:snapshot_rows() or snapshot_routes(self.hauling)
248+
self.route_signature = get_route_signature(self.hauling)
249+
else
250+
local signature = get_route_signature(self.hauling)
251+
if signature and signature ~= self.route_signature then
252+
local refreshed = snapshot_routes(self.hauling)
253+
if refreshed then
254+
self.unfiltered_rows = refreshed
255+
self.route_signature = signature
256+
end
257+
end
258+
end
259+
if not self.unfiltered_rows then return end
260+
local matching_route_ids = build_matching_route_ids(self.hauling, filter)
261+
local filtered = {}
262+
for _, row in ipairs(self.unfiltered_rows) do
263+
local route_id = row.route_id or resolve_route_id(self.hauling, row.route, row.stop)
264+
local resolved_route = resolve_route(self.hauling, row.route)
265+
local is_match_id = route_id ~= nil and matching_route_ids[route_id]
266+
local is_match_route = is_match(filter, resolved_route)
267+
if is_match_id or is_match_route then
268+
table.insert(filtered, row)
269+
end
270+
end
271+
rebuild_rows(self.hauling, filtered)
272+
end
273+
274+
function HaulingRouteFilterOverlay:on_filter_change(text)
275+
self.filter_text = text
276+
last_filter = text
277+
self:apply_filter(text)
278+
end
279+
280+
function HaulingRouteFilterOverlay:overlay_onenable()
281+
if not self then return end
282+
local filter = self.subviews.filter
283+
if filter then
284+
filter:setFocus(false)
285+
end
286+
end
287+
288+
function HaulingRouteFilterOverlay:onInput(keys)
289+
if keys.SELECT then return false end
290+
return HaulingRouteFilterOverlay.super.onInput(self, keys)
291+
end
292+
293+
function HaulingRouteFilterOverlay:overlay_ondisable()
294+
self:restore_rows()
295+
end
296+
297+
OVERLAY_WIDGETS = {filter=HaulingRouteFilterOverlay}
298+
299+
if dfhack_flags.module then
300+
return
301+
end
302+
303+
if not dfhack.gui.matchFocusString('dwarfmode/Hauling') then
304+
qerror('This script must be run from the Hauling screen.')
305+
end
306+
307+
overlay.overlay_command({'enable', 'hauling-search.filter'})

0 commit comments

Comments
 (0)