1010-- Configuration defaults
1111---- ---------------------
1212local CONFIG = {
13- MAX_FILL_TILES = 4000 , -- safety limit
14- ALLOW_DIAGONALS = false -- can be overridden by parameter
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
1516}
1617
1718---- ---------------------
1819-- Utilities and guards
1920---- ---------------------
2021local function err (msg ) qerror (' AutoCeiling: ' .. tostring (msg )) end
2122
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
23+ local function xyz2pos (x , y , z )
24+ return { x = x , y = y , z = z }
2625end
2726
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+
2833---- ---------------------
2934-- World and map helpers
3035---- ---------------------
31- local W = df .global .world
32- local XMAX , YMAX , ZMAX = W .map .x_count , W .map .y_count , W .map .z_count
33-
3436local 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 )
37+ return maps .isValidTilePos (x , y , z )
4038end
4139
4240local 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 ]
41+ return maps .getTileType (x , y , z )
4642end
4743
4844local function tile_shape (tt )
4945 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
46+ local a = tattrs [tt ]
47+ return (a and a .shape ~= df .tiletype_shape .NONE ) and a .shape or nil
5848end
5949
6050---- ---------------------
@@ -72,57 +62,51 @@ local function is_walkable_dug(tt)
7262end
7363
7464local 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
65+ return constructions .findAtTile (x , y , z ) ~= nil
7866end
7967
8068local 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
69+ return buildings .findAtTile (xyz2pos (x , y , z )) ~= nil
8370end
8471
8572---- ---------------------
8673-- Flood fill
8774---- ---------------------
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-
9975local function flood_fill_footprint (seed_x , seed_y , z0 )
10076 local footprint = {}
10177 local visited = {}
102- local q = { { seed_x , seed_y } }
78+ local queue = { { seed_x , seed_y } }
10379 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 }
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 )
108101 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 )
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 )
122106 end
123107 end
124108
125- if # q > CONFIG .MAX_FILL_TILES then
109+ if # queue > CONFIG .MAX_FILL_TILES then
126110 dfhack .printerr ((' AutoCeiling: flood fill truncated at %d tiles' ):format (CONFIG .MAX_FILL_TILES ))
127111 end
128112 return footprint
@@ -131,53 +115,82 @@ end
131115---- ---------------------
132116-- Placement strategies
133117---- ---------------------
134- local function place_planned (bp , x , y , z )
118+ local function place_planned (bp , pos )
135119 local ok , bld = pcall (function ()
136120 return dfhack .buildings .constructBuilding {
137121 type = df .building_type .Construction ,
138122 subtype = df .construction_type .Floor ,
139- pos = { x = x , y = y , z = z }
123+ pos = pos
140124 }
141125 end )
142126 if not ok or not bld then return false , ' construct-error' end
143127 pcall (function () bp .addPlannedBuilding (bld ) end )
144128 return true
145129end
146130
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 }
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 )
151136 end )
152- if not ok then return false , ' designate-error' end
153- return true
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'
154145end
155146
156147---- ---------------------
157148-- Main
158149---- ---------------------
150+ local utils = require (' utils' )
151+
159152local function main (...)
160153 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
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 ' .. CONFIG .MAX_FILL_TILES )
171+ return
172+ elseif s ~= ' ' then
173+ err (' unknown argument: ' .. tostring (raw ))
174+ end
164175 end
165176
166- -- Validate cursor and tile
167- local cur = df .global .cursor
177+ local cur = utils .clone (df .global .cursor )
168178 if cur .x == - 30000 then err (' cursor not set. Move to a dug tile and run again.' ) end
169179 local z0 = cur .z
170180 local seed_tt = get_tiletype (cur .x , cur .y , z0 )
171181 if not is_walkable_dug (seed_tt ) then err (' cursor tile is not dug/open interior' ) end
172182
173- -- Discover footprint and target surface level
174183 local footprint = flood_fill_footprint (cur .x , cur .y , z0 )
184+ if # footprint == 0 then
185+ print (' AutoCeiling: nothing to do — no connected dug tiles found at cursor' )
186+ return
187+ end
175188 local z_surface = z0 + 1
176189
177- -- Load optional DFHack helpers
178- local bp = try_require (' plugins.buildingplan' )
190+ -- Require buildingplan directly; let it error if missing
191+ local bp = require (' plugins.buildingplan' )
179192 if bp and (not bp .isEnabled or not bp .isEnabled ()) then bp = nil end
180- local cons = try_require ( ' dfhack.constructions' )
193+ local cons = dfhack .constructions
181194
182195 local placed , skipped = 0 , 0
183196 local reasons = {}
@@ -186,9 +199,9 @@ local function main(...)
186199 reasons [reason ] = (reasons [reason ] or 0 ) + 1
187200 end
188201
189- -- Process each tile
190- for i = 1 , # footprint do
191- local x , y = footprint [ i ]. x , footprint [ i ]. y
202+ for i , foot in ipairs ( footprint ) do
203+ local x , y = foot . x , foot . y
204+ local pos = xyz2pos ( x , y , z_surface )
192205 if not in_bounds (x , y , z_surface ) then
193206 skip (' oob' )
194207 elseif is_constructed_tile (x , y , z_surface ) then
@@ -198,9 +211,9 @@ local function main(...)
198211 else
199212 local ok , why
200213 if bp then
201- ok , why = place_planned (bp , x , y , z_surface )
214+ ok , why = place_planned (bp , pos )
202215 else
203- ok , why = place_native (cons , x , y , z_surface )
216+ ok , why = place_native (cons , pos )
204217 end
205218 if ok then placed = placed + 1 else skip (why or ' unknown' ) end
206219 end
@@ -211,7 +224,7 @@ local function main(...)
211224 print ((' AutoCeiling: placed %d floor construction(s); skipped %d' ):format (placed , skipped ))
212225 if bp then
213226 print (' buildingplan active: created planned floors that will auto-assign materials' )
214- elseif cons and cons .designate then
227+ elseif cons and cons .designateNew then
215228 print (' used native construction designations' )
216229 else
217230 print (' no buildingplan and no constructions API available' )
0 commit comments