Skip to content

Commit 3242c73

Browse files
committed
Add door-toggle script and documentation
1 parent ff1b95a commit 3242c73

2 files changed

Lines changed: 285 additions & 0 deletions

File tree

docs/door-toggle.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Door Toggle
2+
===========
3+
4+
``door-toggle`` is a DFHack Lua tool that bulk locks or unlocks doors and
5+
hatches within a rectangular area. It opens a small GUI where you choose a
6+
mode and then select two corners on the map to apply the action to all targets
7+
in that rectangle.
8+
9+
Usage
10+
-----
11+
12+
- ``door-toggle``
13+
Opens the GUI and waits for the user to pick two corners.
14+
- ``door-toggle lock``
15+
Opens the GUI with the mode set to lock.
16+
- ``door-toggle open``
17+
Opens the GUI with the mode set to unlock.
18+
19+
Behavior
20+
--------
21+
22+
- The first click sets the starting corner.
23+
- Moving the mouse shows a live preview of the rectangle to be processed.
24+
- The second click applies the action to any doors or hatches within the
25+
rectangle on the current z-level.
26+
- Right-click clears the first corner if already set, or closes the tool if
27+
no corner is active.
28+
29+
Notes
30+
-----
31+
32+
- Locking is implemented by setting the building's ``door_flags.forbidden``
33+
to ``true``. Unlocking clears that flag.
34+
- The tool keeps selection mode active by default so you can perform multiple
35+
selections without reopening the UI.

door-toggle.lua

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
-- door-toggle.lua
2+
-- DFHack tool: bulk lock/unlock doors and hatches in a selected rectangle
3+
-- Usage:
4+
-- door-toggle -> opens GUI
5+
-- door-toggle lock -> preselect lock, start selection
6+
-- door-toggle open -> preselect unlock, start selection
7+
8+
local gui = require('gui')
9+
local guidm = require('gui.dwarfmode')
10+
local widgets = require('gui.widgets')
11+
12+
-- =============================
13+
-- Core logic
14+
-- =============================
15+
16+
local function bounds_from(pos1, pos2)
17+
return {
18+
x1=math.min(pos1.x, pos2.x),
19+
x2=math.max(pos1.x, pos2.x),
20+
y1=math.min(pos1.y, pos2.y),
21+
y2=math.max(pos1.y, pos2.y),
22+
z1=math.min(pos1.z, pos2.z),
23+
z2=math.max(pos1.z, pos2.z),
24+
}
25+
end
26+
27+
local function is_pos_in_bounds(pos, b)
28+
return pos.x >= b.x1 and pos.x <= b.x2
29+
and pos.y >= b.y1 and pos.y <= b.y2
30+
and pos.z >= b.z1 and pos.z <= b.z2
31+
end
32+
33+
local function is_toggle_target(bld)
34+
local t = bld:getType()
35+
return t == df.building_type.Door or t == df.building_type.Hatch
36+
end
37+
38+
local function apply_to_doors_in_rect(pos1, pos2, mode)
39+
local b = bounds_from(pos1, pos2)
40+
local changed = 0
41+
local skipped = 0
42+
43+
for _, bld in ipairs(df.global.world.buildings.all) do
44+
if is_toggle_target(bld) then
45+
local pos = {x=bld.centerx, y=bld.centery, z=bld.z}
46+
if is_pos_in_bounds(pos, b) then
47+
if bld.door_flags then
48+
if mode == 'lock' then
49+
bld.door_flags.forbidden = true
50+
else -- mode == 'open'
51+
bld.door_flags.forbidden = false
52+
end
53+
end
54+
changed = changed + 1
55+
else
56+
skipped = skipped + 1
57+
end
58+
end
59+
end
60+
61+
return changed, skipped
62+
end
63+
64+
local function get_action_text(mark)
65+
local str = mark and 'opposite' or 'first'
66+
return ('Select the %s corner with the mouse.'):format(str)
67+
end
68+
69+
-- =============================
70+
-- Preview overlay
71+
-- =============================
72+
73+
local to_pen = dfhack.pen.parse
74+
local SELECTION_PEN = to_pen{
75+
tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2),
76+
}
77+
78+
-- =============================
79+
-- Window
80+
-- =============================
81+
82+
DoorToggleWindow = defclass(DoorToggleWindow, widgets.Window)
83+
DoorToggleWindow.ATTRS{
84+
frame_title='Door Toggle',
85+
frame={w=44, h=13, r=2, t=18},
86+
resizable=true,
87+
autoarrange_subviews=true,
88+
autoarrange_gap=1,
89+
mode='lock',
90+
status_text='',
91+
selecting=true,
92+
mark=nil,
93+
on_cancel=DEFAULT_NIL,
94+
}
95+
96+
function DoorToggleWindow:init()
97+
if self.status_text == '' then
98+
self.status_text = 'Select the first corner with the mouse.'
99+
end
100+
self:addviews{
101+
widgets.WrappedLabel{
102+
view_id='status',
103+
text_to_wrap=function() return self.status_text end,
104+
},
105+
widgets.CycleHotkeyLabel{
106+
view_id='mode',
107+
label='Mode:',
108+
key='CUSTOM_S',
109+
options={
110+
{label='Lock', value='lock', pen=COLOR_RED},
111+
{label='Unlock', value='open', pen=COLOR_GREEN},
112+
},
113+
initial_option=(self.mode == 'open') and 2 or 1,
114+
},
115+
widgets.HotkeyLabel{
116+
label='Cancel',
117+
key='LEAVESCREEN',
118+
on_activate=function()
119+
if self.on_cancel then self.on_cancel() end
120+
end,
121+
},
122+
widgets.WrappedLabel{
123+
text_to_wrap=function()
124+
if not self.selecting then return '' end
125+
return get_action_text(self.mark)
126+
end,
127+
pen=COLOR_LIGHTCYAN,
128+
},
129+
}
130+
end
131+
132+
function DoorToggleWindow:onInput(keys)
133+
if DoorToggleWindow.super.onInput(self, keys) then return true end
134+
135+
if keys.LEAVESCREEN then
136+
if self.on_cancel then self.on_cancel() end
137+
return true
138+
end
139+
140+
if keys._MOUSE_R then
141+
if self.mark then
142+
self.mark = nil
143+
self.status_text = 'Select the first corner with the mouse.'
144+
self:updateLayout()
145+
return true
146+
end
147+
if self.on_cancel then self.on_cancel() end
148+
return true
149+
end
150+
self.selecting = true
151+
152+
local pos = nil
153+
if keys._MOUSE_L and not self:getMouseFramePos() then
154+
pos = dfhack.gui.getMousePos()
155+
end
156+
if not pos then return false end
157+
158+
if self.mark then
159+
local mode = self.subviews.mode:getOptionValue()
160+
local changed, skipped = apply_to_doors_in_rect(self.mark, pos, mode)
161+
self.status_text = string.format(
162+
'%s %d doors/hatches.',
163+
(mode == 'lock') and 'Locked' or 'Unlocked',
164+
changed
165+
)
166+
self.mark = nil
167+
self:updateLayout()
168+
else
169+
self.mark = pos
170+
self.status_text = get_action_text(self.mark)
171+
self:updateLayout()
172+
end
173+
174+
return true
175+
end
176+
177+
-- =============================
178+
-- Screen
179+
-- =============================
180+
181+
DoorToggleScreen = defclass(DoorToggleScreen, gui.ZScreen)
182+
DoorToggleScreen.ATTRS{
183+
focus_path='door-toggle',
184+
pass_movement_keys=true,
185+
pass_mouse_clicks=false,
186+
mode='lock',
187+
start_selection=false,
188+
}
189+
190+
function DoorToggleScreen:init()
191+
local screen = self
192+
self.window = DoorToggleWindow{
193+
mode=self.mode,
194+
on_cancel=function() screen:dismiss() end,
195+
}
196+
self:addviews{self.window}
197+
if self.start_selection then
198+
self.window.selecting = true
199+
self.window.status_text = 'Select the first corner with the mouse.'
200+
self.window:updateLayout()
201+
end
202+
end
203+
204+
function DoorToggleScreen:onRenderFrame(dc, rect)
205+
DoorToggleScreen.super.onRenderFrame(self, dc, rect)
206+
207+
if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then
208+
return
209+
end
210+
211+
if not self.window then return end
212+
if not self.window.selecting or not self.window.mark then return end
213+
if self.window:getMouseFramePos() then return end
214+
215+
local mouse_pos = dfhack.gui.getMousePos()
216+
if not mouse_pos then return end
217+
218+
local start_pos = self.window.mark
219+
local preview_pos = {x=mouse_pos.x, y=mouse_pos.y, z=start_pos.z}
220+
local bounds = bounds_from(start_pos, preview_pos)
221+
bounds.z1 = start_pos.z
222+
bounds.z2 = start_pos.z
223+
224+
local function get_overlay_pen(pos)
225+
if is_pos_in_bounds(pos, bounds) then
226+
return SELECTION_PEN
227+
end
228+
end
229+
230+
guidm.renderMapOverlay(get_overlay_pen, bounds)
231+
end
232+
233+
-- =============================
234+
-- Entrypoint
235+
-- =============================
236+
237+
local args = {...}
238+
239+
local function start_gui_with_mode(mode, start_selection)
240+
local screen = DoorToggleScreen{mode=mode or 'lock', start_selection=start_selection}
241+
dfhack.screen.show(screen)
242+
end
243+
244+
if #args == 0 then
245+
start_gui_with_mode('lock', false)
246+
elseif args[1] == 'lock' or args[1] == 'open' then
247+
start_gui_with_mode(args[1], true)
248+
else
249+
qerror('Usage: door-toggle [lock|open]')
250+
end

0 commit comments

Comments
 (0)