Skip to content

Commit 3c16db8

Browse files
committed
Add lever-interface script and documentation
1 parent ff1b95a commit 3c16db8

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed

docs/lever-interface.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Lever Interface
2+
===============
3+
4+
Overview
5+
--------
6+
The lever interface provides a consolidated list of all levers in the current map
7+
and lets you queue or remove pull tasks. The list is kept up to date automatically
8+
so queued, completed, and cancelled pulls are reflected without manual refreshes.
9+
10+
Main features
11+
-------------
12+
- Lists all levers with a status prefix (``[Pulled]`` or ``[Not Pulled]``).
13+
- Shows queued pull counts per lever and a global queued pull total.
14+
- Allows queuing a pull task for the selected lever.
15+
- Allows removing queued pull tasks from the selected lever.
16+
- Supports hover focus to pan the map to the lever without clicking.
17+
- Supports search filtering by lever name.
18+
19+
Using the interface
20+
-------------------
21+
- **Search**: Type in the search field to filter levers by name. Filtering is
22+
case-insensitive and matches substrings.
23+
- **Hover**: Move the mouse over a lever entry to pan and highlight the lever.
24+
- **Click**: Click a lever entry (or press Enter) to queue a pull task.
25+
- **Remove queued pulls**: Use the remove hotkey to clear queued pull jobs for
26+
the selected lever.
27+
28+
Hotkeys
29+
-------
30+
- ``P``: Queue a pull task for the selected lever.
31+
- ``X``: Remove queued pull tasks from the selected lever.
32+
- ``R``: Refresh the list.

lever-interface.lua

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
-- List and pull levers
2+
3+
local gui = require('gui')
4+
local guidm = require('gui.dwarfmode')
5+
local utils = require('utils')
6+
local widgets = require('gui.widgets')
7+
8+
local lever_script = reqscript('lever')
9+
10+
local REFRESH_MS = 1000
11+
12+
local function get_levers()
13+
local levers = {}
14+
for _, building in ipairs(df.global.world.buildings.other.TRAP) do
15+
if building.trap_type == df.trap_type.Lever then
16+
table.insert(levers, building)
17+
end
18+
end
19+
return levers
20+
end
21+
22+
local function get_lever_label(lever)
23+
local status = (lever.state == 1) and 'Pulled' or 'Not Pulled'
24+
local name = utils.getBuildingName(lever)
25+
local queued = 0
26+
for _, job in ipairs(lever.jobs) do
27+
if job.job_type == df.job_type.PullLever then
28+
queued = queued + 1
29+
end
30+
end
31+
local queued_text = queued > 0 and (' (queued: %d)'):format(queued) or ''
32+
return ('[%s] %s (#%d)%s'):format(status, name, lever.id, queued_text)
33+
end
34+
35+
local function get_queued_count(levers)
36+
local queued = 0
37+
for _, lever in ipairs(levers) do
38+
for _, job in ipairs(lever.jobs) do
39+
if job.job_type == df.job_type.PullLever then
40+
queued = queued + 1
41+
end
42+
end
43+
end
44+
return queued
45+
end
46+
47+
LeverWindow = defclass(LeverWindow, widgets.Window)
48+
LeverWindow.ATTRS{
49+
frame_title = 'Lever Tasks',
50+
frame = {w=60, h=18, r=2},
51+
}
52+
53+
function LeverWindow:init()
54+
local _, screen_height = dfhack.screen.getWindowSize()
55+
if screen_height then
56+
self.frame.t = math.max(0, math.floor((screen_height - self.frame.h) / 2))
57+
end
58+
self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS
59+
self.filter_text = ''
60+
self:addviews{
61+
widgets.EditField{
62+
view_id='search',
63+
frame={t=0, l=0, r=0},
64+
label_text='Search: ',
65+
on_change=self:callback('set_filter'),
66+
},
67+
widgets.List{
68+
view_id='lever_list',
69+
frame={t=1, l=0, r=0, b=4},
70+
on_submit=self:callback('queue_pull'),
71+
on_select=self:callback('focus_lever'),
72+
},
73+
widgets.Label{
74+
view_id='empty_message',
75+
frame={t=1, l=0, r=0},
76+
text='No levers found.',
77+
visible=false,
78+
},
79+
widgets.HotkeyLabel{
80+
frame={b=3, l=0},
81+
label='Pull selected lever',
82+
key='CUSTOM_P',
83+
on_activate=self:callback('queue_pull'),
84+
},
85+
widgets.HotkeyLabel{
86+
frame={b=2, l=0},
87+
label='Remove queued pulls',
88+
key='CUSTOM_X',
89+
on_activate=self:callback('remove_queued_pulls'),
90+
},
91+
widgets.Label{
92+
view_id='queued_count',
93+
frame={b=3, r=0},
94+
text='Queued pulls: 0',
95+
auto_width=true,
96+
},
97+
widgets.HotkeyLabel{
98+
frame={b=1, l=0},
99+
label='Refresh list',
100+
key='CUSTOM_R',
101+
on_activate=self:callback('refresh_list'),
102+
},
103+
}
104+
105+
self:refresh_list()
106+
end
107+
108+
function LeverWindow:set_filter(text)
109+
self.filter_text = text or ''
110+
self:refresh_list()
111+
end
112+
113+
function LeverWindow:refresh_list()
114+
local list = self.subviews.lever_list
115+
local selected_id
116+
if list then
117+
local _, selected = list:getSelected()
118+
if selected and selected.data then
119+
selected_id = selected.data.id
120+
end
121+
end
122+
123+
local choices = {}
124+
local levers = get_levers()
125+
table.sort(levers, function(a, b)
126+
if a.state == b.state then
127+
return a.id < b.id
128+
end
129+
return a.state > b.state
130+
end)
131+
local filter = (self.filter_text or ''):lower()
132+
local filtered_levers = {}
133+
if filter == '' then
134+
filtered_levers = levers
135+
else
136+
for _, lever in ipairs(levers) do
137+
local name = utils.getBuildingName(lever)
138+
if name:lower():find(filter, 1, true) then
139+
table.insert(filtered_levers, lever)
140+
end
141+
end
142+
end
143+
local selected_idx = 1
144+
for idx, lever in ipairs(filtered_levers) do
145+
table.insert(choices, {text=get_lever_label(lever), data=lever})
146+
if selected_id and lever.id == selected_id then
147+
selected_idx = idx
148+
end
149+
end
150+
list:setChoices(choices, selected_idx)
151+
self.subviews.empty_message.visible = #choices == 0
152+
self.subviews.queued_count:setText(('Queued pulls: %d'):format(get_queued_count(levers)))
153+
end
154+
155+
function LeverWindow:queue_pull()
156+
local _, choice = self.subviews.lever_list:getSelected()
157+
if not choice then
158+
return
159+
end
160+
lever_script.leverPullJob(choice.data, false)
161+
self:refresh_list()
162+
end
163+
164+
function LeverWindow:remove_queued_pulls()
165+
local _, choice = self.subviews.lever_list:getSelected()
166+
if not choice then
167+
return
168+
end
169+
local jobs = {}
170+
for _, job in ipairs(choice.data.jobs) do
171+
if job.job_type == df.job_type.PullLever then
172+
table.insert(jobs, job)
173+
end
174+
end
175+
for _, job in ipairs(jobs) do
176+
dfhack.job.removeJob(job)
177+
end
178+
self:refresh_list()
179+
end
180+
181+
function LeverWindow:onRenderFrame(dc, rect)
182+
LeverWindow.super.onRenderFrame(self, dc, rect)
183+
184+
local list = self.subviews.lever_list
185+
local hover_idx = list:getIdxUnderMouse()
186+
if hover_idx and hover_idx ~= self.hover_index then
187+
self.hover_index = hover_idx
188+
list:setSelected(hover_idx)
189+
local _, choice = list:getSelected()
190+
if choice then
191+
self:focus_lever(nil, choice)
192+
end
193+
end
194+
end
195+
196+
function LeverWindow:onRenderBody()
197+
if dfhack.getTickCount() >= self.next_refresh_ms then
198+
self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS
199+
self:refresh_list()
200+
end
201+
end
202+
203+
function LeverWindow:focus_lever(_, choice)
204+
if not choice then
205+
return
206+
end
207+
local lever = choice.data
208+
local pos = {x=lever.centerx, y=lever.centery, z=lever.z}
209+
dfhack.gui.revealInDwarfmodeMap(pos, true, true)
210+
guidm.setCursorPos(pos)
211+
end
212+
213+
LeverScreen = defclass(LeverScreen, gui.ZScreen)
214+
LeverScreen.ATTRS{focus_path='lever'}
215+
216+
function LeverScreen:init()
217+
self:addviews{LeverWindow{}}
218+
end
219+
220+
function LeverScreen:onDismiss()
221+
view = nil
222+
end
223+
224+
if not dfhack.isMapLoaded() then
225+
qerror('gui/lever requires a map to be loaded')
226+
end
227+
228+
view = view and view:raise() or LeverScreen{}:show()

0 commit comments

Comments
 (0)