Skip to content

Commit 26bf4de

Browse files
committed
Add autoceiling script and documentation
1 parent e50474e commit 26bf4de

2 files changed

Lines changed: 266 additions & 0 deletions

File tree

autoceiling.lua

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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 = 4000, -- safety limit
14+
ALLOW_DIAGONALS = false -- can be overridden by parameter
15+
}
16+
17+
-------------------------
18+
-- Utilities and guards
19+
-------------------------
20+
local function err(msg) qerror('AutoCeiling: ' .. tostring(msg)) end
21+
22+
local function try_require(modname)
23+
local ok, mod = pcall(require, modname)
24+
if ok and mod then return mod end
25+
return nil
26+
end
27+
28+
-------------------------
29+
-- World and map helpers
30+
-------------------------
31+
local W = df.global.world
32+
local XMAX, YMAX, ZMAX = W.map.x_count, W.map.y_count, W.map.z_count
33+
34+
local function in_bounds(x, y, z)
35+
return x >= 0 and y >= 0 and z >= 0 and x < XMAX and y < YMAX and z < ZMAX
36+
end
37+
38+
local function get_block(x, y, z)
39+
return dfhack.maps.getTileBlock(x, y, z)
40+
end
41+
42+
local function get_tiletype(x, y, z)
43+
local b = get_block(x, y, z)
44+
if not b then return nil end
45+
return b.tiletype[x % 16][y % 16]
46+
end
47+
48+
local function tile_shape(tt)
49+
if not tt then return nil end
50+
local a = df.tiletype.attrs[tt]
51+
return a and a.shape or nil
52+
end
53+
54+
local function tile_material(tt)
55+
if not tt then return nil end
56+
local a = df.tiletype.attrs[tt]
57+
return a and a.material or nil
58+
end
59+
60+
-------------------------
61+
-- Predicates
62+
-------------------------
63+
local function is_walkable_dug(tt)
64+
local s = tile_shape(tt)
65+
if not s then return false end
66+
return s == df.tiletype_shape.FLOOR
67+
or s == df.tiletype_shape.RAMP
68+
or s == df.tiletype_shape.STAIR_UP
69+
or s == df.tiletype_shape.STAIR_DOWN
70+
or s == df.tiletype_shape.STAIR_UPDOWN
71+
or s == df.tiletype_shape.EMPTY
72+
end
73+
74+
local function is_constructed_tile(x, y, z)
75+
local tt = get_tiletype(x, y, z)
76+
local mat = tile_material(tt)
77+
return mat == df.tiletype_material.CONSTRUCTION
78+
end
79+
80+
local function has_any_building(x, y, z)
81+
-- Also detects in-progress constructions as buildings
82+
return dfhack.buildings.findAtTile({ x = x, y = y, z = z }) ~= nil
83+
end
84+
85+
-------------------------
86+
-- Flood fill
87+
-------------------------
88+
local function push_if_ok(q, visited, x, y, z)
89+
if not in_bounds(x, y, z) then return end
90+
local key = x .. ',' .. y
91+
if visited[key] then return end
92+
local tt = get_tiletype(x, y, z)
93+
if is_walkable_dug(tt) then
94+
visited[key] = true
95+
q[#q + 1] = { x, y }
96+
end
97+
end
98+
99+
local function flood_fill_footprint(seed_x, seed_y, z0)
100+
local footprint = {}
101+
local visited = {}
102+
local q = { { seed_x, seed_y } }
103+
visited[seed_x .. ',' .. seed_y] = true
104+
local head = 1
105+
while head <= #q and #footprint < CONFIG.MAX_FILL_TILES do
106+
local x, y = table.unpack(q[head]); head = head + 1
107+
footprint[#footprint + 1] = { x = x, y = y }
108+
if CONFIG.ALLOW_DIAGONALS then
109+
push_if_ok(q, visited, x + 1, y, z0)
110+
push_if_ok(q, visited, x - 1, y, z0)
111+
push_if_ok(q, visited, x, y + 1, z0)
112+
push_if_ok(q, visited, x, y - 1, z0)
113+
push_if_ok(q, visited, x + 1, y + 1, z0)
114+
push_if_ok(q, visited, x + 1, y - 1, z0)
115+
push_if_ok(q, visited, x - 1, y + 1, z0)
116+
push_if_ok(q, visited, x - 1, y - 1, z0)
117+
else
118+
push_if_ok(q, visited, x + 1, y, z0)
119+
push_if_ok(q, visited, x - 1, y, z0)
120+
push_if_ok(q, visited, x, y + 1, z0)
121+
push_if_ok(q, visited, x, y - 1, z0)
122+
end
123+
end
124+
125+
if #q > CONFIG.MAX_FILL_TILES then
126+
dfhack.printerr(('AutoCeiling: flood fill truncated at %d tiles'):format(CONFIG.MAX_FILL_TILES))
127+
end
128+
return footprint
129+
end
130+
131+
-------------------------
132+
-- Placement strategies
133+
-------------------------
134+
local function place_planned(bp, x, y, z)
135+
local ok, bld = pcall(function()
136+
return dfhack.buildings.constructBuilding{
137+
type = df.building_type.Construction,
138+
subtype = df.construction_type.Floor,
139+
pos = { x = x, y = y, z = z }
140+
}
141+
end)
142+
if not ok or not bld then return false, 'construct-error' end
143+
pcall(function() bp.addPlannedBuilding(bld) end)
144+
return true
145+
end
146+
147+
local function place_native(cons, x, y, z)
148+
if not cons or not cons.designate then return false, 'no-constructions-api' end
149+
local ok, derr = pcall(function()
150+
cons.designate{ pos = { x = x, y = y, z = z }, type = df.construction_type.Floor }
151+
end)
152+
if not ok then return false, 'designate-error' end
153+
return true
154+
end
155+
156+
-------------------------
157+
-- Main
158+
-------------------------
159+
local function main(...)
160+
local args = {...}
161+
-- Allow user to set diagonals with parameter 't' or 'true'
162+
if #args > 0 and (args[1] == 't' or args[1] == 'true') then
163+
CONFIG.ALLOW_DIAGONALS = true
164+
end
165+
166+
-- Validate cursor and tile
167+
local cur = df.global.cursor
168+
if cur.x == -30000 then err('cursor not set. Move to a dug tile and run again.') end
169+
local z0 = cur.z
170+
local seed_tt = get_tiletype(cur.x, cur.y, z0)
171+
if not is_walkable_dug(seed_tt) then err('cursor tile is not dug/open interior') end
172+
173+
-- Discover footprint and target surface level
174+
local footprint = flood_fill_footprint(cur.x, cur.y, z0)
175+
local z_surface = z0 + 1
176+
177+
-- Load optional DFHack helpers
178+
local bp = try_require('plugins.buildingplan')
179+
if bp and (not bp.isEnabled or not bp.isEnabled()) then bp = nil end
180+
local cons = try_require('dfhack.constructions')
181+
182+
local placed, skipped = 0, 0
183+
local reasons = {}
184+
local function skip(reason)
185+
skipped = skipped + 1
186+
reasons[reason] = (reasons[reason] or 0) + 1
187+
end
188+
189+
-- Process each tile
190+
for i = 1, #footprint do
191+
local x, y = footprint[i].x, footprint[i].y
192+
if not in_bounds(x, y, z_surface) then
193+
skip('oob')
194+
elseif is_constructed_tile(x, y, z_surface) then
195+
skip('constructed')
196+
elseif has_any_building(x, y, z_surface) then
197+
skip('building')
198+
else
199+
local ok, why
200+
if bp then
201+
ok, why = place_planned(bp, x, y, z_surface)
202+
else
203+
ok, why = place_native(cons, x, y, z_surface)
204+
end
205+
if ok then placed = placed + 1 else skip(why or 'unknown') end
206+
end
207+
end
208+
209+
if bp and bp.doCycle then pcall(function() bp.doCycle() end) end
210+
211+
print(('AutoCeiling: placed %d floor construction(s); skipped %d'):format(placed, skipped))
212+
if bp then
213+
print('buildingplan active: created planned floors that will auto-assign materials')
214+
elseif cons and cons.designate then
215+
print('used native construction designations')
216+
else
217+
print('no buildingplan and no constructions API available')
218+
end
219+
for k, v in pairs(reasons) do
220+
print((' skipped %-18s %d'):format(k, v))
221+
end
222+
end
223+
224+
main(...)

docs/autoceiling.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
AutoCeiling
2+
=============
3+
4+
This is a DFHack Lua script for **Dwarf Fortress (Steam version)** that automatically places constructed floors above any dug-out area. It uses a flood-fill algorithm to detect connected dug tiles on the selected Z-level, then creates planned floor constructions directly above them to seal the area. This helps prevent surface collapse and creature intrusion when mining under open ground.
5+
6+
Features
7+
--------
8+
9+
- **Automatic Flood Fill Detection**: Finds all connected dug tiles from the cursor location.
10+
- **Smart Floor Placement**: Builds floors one level above the dug region.
11+
- **Buildingplan Integration**: When the `buildingplan` plugin is active, floors are added as planned constructions and will auto-assign materials.
12+
- **Native DF Construction Support**: Falls back to native designations if `buildingplan` is unavailable.
13+
- **Safety Checks**: Skips tiles that already have player-made constructions or any existing buildings.
14+
- **Parameter Input**: Run `autoceiling t` to enable diagonal flood fill (8-way). Default is 4-way fill.
15+
- **Performance Limit**: Caps flood-fill to a configurable number of tiles (default 4000) for safety.
16+
17+
Usage
18+
-----
19+
20+
1. Move the **game cursor** to a dug-out tile at the level you want to seal the ceiling.
21+
2. In the DFHack console, run:
22+
23+
```
24+
autoceiling
25+
```
26+
or, for diagonal (8-way) flood fill:
27+
```
28+
autoceiling t
29+
```
30+
31+
3. The script will automatically:
32+
- Scan connected walkable tiles at the current Z-level.
33+
- Attempt to place floor constructions one Z-level above.
34+
- Report how many tiles were placed and skipped.
35+
36+
4. If the `buildingplan` plugin is active, you’ll see a message confirming planned floor placement. Otherwise, the script will use standard construction designations.
37+
38+
Notes
39+
-----
40+
41+
- Ideal for use after large excavation projects to prevent breaches to the surface.
42+
- Works well in conjunction with the **buildingplan** plugin for automatic material management.

0 commit comments

Comments
 (0)