Skip to content

Commit 3efcff3

Browse files
committed
Add autoceiling script and documentation
1 parent ff1b95a commit 3efcff3

File tree

2 files changed

+290
-0
lines changed

2 files changed

+290
-0
lines changed

autoceiling.lua

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
-- AutoCeiling.lua
2+
-- Purpose: flood-fill the connected dug area on the cursor z-level (z0)
3+
-- and place constructed floors directly above (z0+1). When the buildingplan
4+
-- plugin is enabled, planned constructions are created. Otherwise we fall back
5+
-- to native construction designations so dwarves get immediate jobs.
6+
-- The script skips tiles that already have a player-made construction or
7+
-- any existing building at the target tile on z0+1.
8+
9+
-------------------------
10+
-- Configuration defaults
11+
-------------------------
12+
local CONFIG = {
13+
MAX_FILL_TILES = 2000, -- positive integer; safety limit
14+
ALLOW_DIAGONALS = false, -- set true to allow 8-way fill
15+
MAX_LIMIT_HARD = 4000, -- hard clamp to avoid runaway fills
16+
}
17+
18+
-------------------------
19+
-- Utilities and guards
20+
-------------------------
21+
local function err(msg) qerror('AutoCeiling: ' .. tostring(msg)) end
22+
23+
local function xyz2pos(x, y, z)
24+
return { x = x, y = y, z = z }
25+
end
26+
27+
-- Cache frequently used modules/tables for readability
28+
local maps = dfhack.maps
29+
local constructions = dfhack.constructions
30+
local buildings = dfhack.buildings
31+
local tattrs = df.tiletype.attrs
32+
33+
-------------------------
34+
-- World and map helpers
35+
-------------------------
36+
local function in_bounds(x, y, z)
37+
return maps.isValidTilePos(x, y, z)
38+
end
39+
40+
local function get_tiletype(x, y, z)
41+
return maps.getTileType(x, y, z)
42+
end
43+
44+
local function tile_shape(tt)
45+
if not tt then return nil end
46+
local a = tattrs[tt]
47+
return (a and a.shape ~= df.tiletype_shape.NONE) and a.shape or nil
48+
end
49+
50+
-------------------------
51+
-- Predicates
52+
-------------------------
53+
local function is_walkable_dug(tt)
54+
local s = tile_shape(tt)
55+
if not s then return false end
56+
return s == df.tiletype_shape.FLOOR
57+
or s == df.tiletype_shape.RAMP
58+
or s == df.tiletype_shape.STAIR_UP
59+
or s == df.tiletype_shape.STAIR_DOWN
60+
or s == df.tiletype_shape.STAIR_UPDOWN
61+
or s == df.tiletype_shape.EMPTY
62+
end
63+
64+
local function is_constructed_tile(x, y, z)
65+
return constructions.findAtTile(x, y, z) ~= nil
66+
end
67+
68+
local function has_any_building(x, y, z)
69+
return buildings.findAtTile(xyz2pos(x, y, z)) ~= nil
70+
end
71+
72+
-------------------------
73+
-- Flood fill
74+
-------------------------
75+
local function flood_fill_footprint(seed_x, seed_y, z0)
76+
local footprint = {}
77+
local visited = {}
78+
local queue = { { seed_x, seed_y } }
79+
visited[seed_x .. ',' .. seed_y] = true
80+
local queue_pos = 1
81+
82+
local function push_if_ok(x, y)
83+
if not in_bounds(x, y, z0) then return end
84+
local key = x .. ',' .. y
85+
if visited[key] then return end
86+
local tt = get_tiletype(x, y, z0)
87+
if is_walkable_dug(tt) then
88+
visited[key] = true
89+
table.insert(queue, { x, y })
90+
end
91+
end
92+
93+
while queue_pos <= #queue and #footprint < CONFIG.MAX_FILL_TILES do
94+
local x, y = table.unpack(queue[queue_pos])
95+
queue_pos = queue_pos + 1
96+
table.insert(footprint, { x = x, y = y })
97+
push_if_ok(x + 1, y)
98+
push_if_ok(x - 1, y)
99+
push_if_ok(x, y + 1)
100+
push_if_ok(x, y - 1)
101+
if CONFIG.ALLOW_DIAGONALS then
102+
push_if_ok(x + 1, y + 1)
103+
push_if_ok(x + 1, y - 1)
104+
push_if_ok(x - 1, y + 1)
105+
push_if_ok(x - 1, y - 1)
106+
end
107+
end
108+
109+
if #queue > CONFIG.MAX_FILL_TILES then
110+
dfhack.printerr(('AutoCeiling: flood fill truncated at %d tiles'):format(CONFIG.MAX_FILL_TILES))
111+
end
112+
return footprint
113+
end
114+
115+
-------------------------
116+
-- Placement strategies
117+
-------------------------
118+
local function place_planned(bp, pos)
119+
local ok, bld = pcall(function()
120+
return dfhack.buildings.constructBuilding{
121+
type = df.building_type.Construction,
122+
subtype = df.construction_type.Floor,
123+
pos = pos
124+
}
125+
end)
126+
if not ok or not bld then return false, 'construct-error' end
127+
pcall(function() bp.addPlannedBuilding(bld) end)
128+
return true
129+
end
130+
131+
local function place_native(cons, pos)
132+
if not cons or not cons.designateNew then return false, 'no-constructions-api' end
133+
134+
local ok, res = pcall(function()
135+
return cons.designateNew(pos, df.construction_type.Floor, -1, -1)
136+
end)
137+
if ok and res then return true end
138+
139+
local ok2, res2 = pcall(function()
140+
return cons.designateNew(pos, df.construction_type.Floor, df.item_type.BOULDER, -1)
141+
end)
142+
if ok2 and res2 then return true end
143+
144+
return false, 'designate-error'
145+
end
146+
147+
-------------------------
148+
-- Main
149+
-------------------------
150+
local utils = require('utils')
151+
152+
local function main(...)
153+
local args = {...}
154+
155+
for _, raw in ipairs(args) do
156+
local s = tostring(raw):lower()
157+
local num = tonumber(s)
158+
if num then
159+
if num < 1 then err('MAX_FILL_TILES must be >= 1') end
160+
if num > CONFIG.MAX_LIMIT_HARD then
161+
dfhack.printerr(('clamping MAX_FILL_TILES from %d to %d'):format(num, CONFIG.MAX_LIMIT_HARD))
162+
num = CONFIG.MAX_LIMIT_HARD
163+
end
164+
CONFIG.MAX_FILL_TILES = math.floor(num)
165+
elseif s == 't' or s == 'true' then
166+
CONFIG.ALLOW_DIAGONALS = true
167+
elseif s == 'h' or s == 'help' then
168+
print('Usage: autoceiling [t] [<max_fill_tiles>]')
169+
print(' t: enable diagonal flood fill')
170+
print((' <max_fill_tiles>: positive integer, default %d, max %d')
171+
:format(CONFIG.MAX_FILL_TILES, CONFIG.MAX_LIMIT_HARD))
172+
return
173+
elseif s ~= '' then
174+
err('unknown argument: ' .. tostring(raw))
175+
end
176+
end
177+
178+
local cur = utils.clone(df.global.cursor)
179+
if cur.x == -30000 then err('cursor not set. Move to a dug tile and run again.') end
180+
local z0 = cur.z
181+
local seed_tt = get_tiletype(cur.x, cur.y, z0)
182+
if not is_walkable_dug(seed_tt) then err('cursor tile is not dug/open interior') end
183+
184+
local footprint = flood_fill_footprint(cur.x, cur.y, z0)
185+
if #footprint == 0 then
186+
print('AutoCeiling: nothing to do — no connected dug tiles found at cursor')
187+
return
188+
end
189+
local z_surface = z0 + 1
190+
191+
local ok, bp = pcall(require, 'plugins.buildingplan')
192+
if not ok then
193+
bp = nil
194+
elseif bp and (not bp.isEnabled or not bp.isEnabled()) then
195+
bp = nil
196+
end
197+
local cons = dfhack.constructions
198+
199+
local placed, skipped = 0, 0
200+
local reasons = {}
201+
local function skip(reason)
202+
skipped = skipped + 1
203+
reasons[reason] = (reasons[reason] or 0) + 1
204+
end
205+
206+
for i, foot in ipairs(footprint) do
207+
local x, y = foot.x, foot.y
208+
local pos = xyz2pos(x, y, z_surface)
209+
if not in_bounds(x, y, z_surface) then
210+
skip('oob')
211+
elseif is_constructed_tile(x, y, z_surface) then
212+
skip('constructed')
213+
elseif has_any_building(x, y, z_surface) then
214+
skip('building')
215+
else
216+
local ok_place, why
217+
if bp then
218+
ok_place, why = place_planned(bp, pos)
219+
else
220+
ok_place, why = place_native(cons, pos)
221+
end
222+
if ok_place then placed = placed + 1 else skip(why or 'unknown') end
223+
end
224+
end
225+
226+
if bp and bp.doCycle then pcall(function() bp.doCycle() end) end
227+
228+
print(('AutoCeiling: placed %d floor construction(s); skipped %d'):format(placed, skipped))
229+
if bp then
230+
print('buildingplan active: created planned floors that will auto-assign materials')
231+
elseif cons and cons.designateNew then
232+
print('used native construction designations')
233+
else
234+
print('no buildingplan and no constructions API available')
235+
end
236+
for k, v in pairs(reasons) do
237+
print((' skipped %-18s %d'):format(k, v))
238+
end
239+
end
240+
241+
main(...)

docs/autoceiling.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
autoceiling
2+
===========
3+
4+
.. dfhack-tool::
5+
:summary: Place floors above dug areas to seal surface openings.
6+
:tags: construction automation utility
7+
8+
**AutoCeiling** is a DFHack Lua script that automatically places constructed
9+
floors above any dug-out area. It uses a flood-fill algorithm to detect connected
10+
dug tiles on the selected Z-level, then creates planned floor constructions
11+
directly above them to seal the area. This prevents surface collapse and stops
12+
creatures from entering your fortress through unexpected openings. It’s
13+
especially useful when building farms directly below the surface, since those
14+
areas are prone to collapsing without warning and can leave open spaces that
15+
allow surface creatures to breach your fort.
16+
17+
Usage
18+
-----
19+
20+
::
21+
22+
autoceiling [t] [<max>]
23+
24+
Examples
25+
--------
26+
27+
``autoceiling``
28+
Run with default settings (4,000 tile flood-fill limit, no diagonal fill).
29+
30+
``autoceiling t``
31+
Enable diagonal flood-fill connections (8-way fill).
32+
33+
``autoceiling 500``
34+
Raise or lower flood-fill limits.
35+
36+
``autoceiling t 6000`` or ``autoceiling 6000 t``
37+
Allow diagonals and increase fill limit to 6,000 tiles.
38+
39+
Options
40+
-------
41+
42+
``t``
43+
Enables 8-directional (diagonal) flood fill mode.
44+
45+
``<max>``
46+
Sets the maximum number of tiles the flood fill can cover (default: 4000).
47+
48+
These are the only two options available for this command. Use ``t`` to toggle
49+
diagonal fill and ``<max>`` to control the tile limit for flood fill.

0 commit comments

Comments
 (0)