-
Notifications
You must be signed in to change notification settings - Fork 218
Add autoceiling script and documentation #1515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| -- AutoCeiling.lua | ||
| -- Purpose: flood-fill the connected dug area on the cursor z-level (z0) | ||
| -- and place constructed floors directly above (z0+1). When the buildingplan | ||
| -- plugin is enabled, planned constructions are created. Otherwise we fall back | ||
| -- to native construction designations so dwarves get immediate jobs. | ||
| -- The script skips tiles that already have a player-made construction or | ||
| -- any existing building at the target tile on z0+1. | ||
|
|
||
| ------------------------- | ||
| -- Configuration defaults | ||
| ------------------------- | ||
| local CONFIG = { | ||
| MAX_FILL_TILES = 2000, -- positive integer; safety limit | ||
| ALLOW_DIAGONALS = false, -- set true to allow 8-way fill | ||
| MAX_LIMIT_HARD = 4000 -- hard clamp to avoid runaway fills | ||
| } | ||
|
|
||
| ------------------------- | ||
| -- Utilities and guards | ||
| ------------------------- | ||
| local function err(msg) qerror('AutoCeiling: ' .. tostring(msg)) end | ||
|
|
||
| local function xyz2pos(x, y, z) | ||
| return { x = x, y = y, z = z } | ||
| end | ||
|
|
||
| -- Cache frequently used modules/tables for readability | ||
| local maps = dfhack.maps | ||
| local constructions = dfhack.constructions | ||
| local buildings = dfhack.buildings | ||
| local tattrs = df.tiletype.attrs | ||
|
|
||
| ------------------------- | ||
| -- World and map helpers | ||
| ------------------------- | ||
| local function in_bounds(x, y, z) | ||
| return maps.isValidTilePos(x, y, z) | ||
| end | ||
|
|
||
| local function get_tiletype(x, y, z) | ||
unboundlopez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return maps.getTileType(x, y, z) | ||
| end | ||
|
|
||
| local function tile_shape(tt) | ||
| if not tt then return nil end | ||
| local a = tattrs[tt] | ||
| return (a and a.shape ~= df.tiletype_shape.NONE) and a.shape or nil | ||
| end | ||
|
|
||
| ------------------------- | ||
| -- Predicates | ||
| ------------------------- | ||
| local function is_walkable_dug(tt) | ||
| local s = tile_shape(tt) | ||
| if not s then return false end | ||
| return s == df.tiletype_shape.FLOOR | ||
| or s == df.tiletype_shape.RAMP | ||
| or s == df.tiletype_shape.STAIR_UP | ||
| or s == df.tiletype_shape.STAIR_DOWN | ||
| or s == df.tiletype_shape.STAIR_UPDOWN | ||
| or s == df.tiletype_shape.EMPTY | ||
| end | ||
unboundlopez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| local function is_constructed_tile(x, y, z) | ||
unboundlopez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return constructions.findAtTile(x, y, z) ~= nil | ||
| end | ||
|
|
||
| local function has_any_building(x, y, z) | ||
| return buildings.findAtTile(xyz2pos(x, y, z)) ~= nil | ||
| end | ||
|
|
||
| ------------------------- | ||
| -- Flood fill | ||
| ------------------------- | ||
| local function flood_fill_footprint(seed_x, seed_y, z0) | ||
| local footprint = {} | ||
| local visited = {} | ||
| local queue = { { seed_x, seed_y } } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you're special-casing the very first I'm not going to game out whether that would work with your current code. Just something to consider. Maybe something like this would work": local queue = {}
local queue_pos = 1
local function push_if_ok()
...
push_if_ok(seed_x, seed_y)Again, I didn't test this. |
||
| visited[seed_x .. ',' .. seed_y] = true | ||
| local queue_pos = 1 | ||
|
|
||
| local function push_if_ok(x, y) | ||
| if not in_bounds(x, y, z0) then return end | ||
| local key = x .. ',' .. y | ||
| if visited[key] then return end | ||
| local tt = get_tiletype(x, y, z0) | ||
| if is_walkable_dug(tt) then | ||
| visited[key] = true | ||
| table.insert(queue, { x, y }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if your queue could directly store Then you could pass the current queue element directly to your functions or to API calls. You could also just copy a queue element directly into I mean, your code clearly works, but doing it that way seems like it would be more elegant. Let's see, you still couldn't use a (There is an API call (Although... if it's the exact same table instead of a copy or a newly-constructed table, an existence probe would work. I think.) |
||
| end | ||
| end | ||
|
|
||
| while queue_pos <= #queue and #footprint < CONFIG.MAX_FILL_TILES do | ||
| local x, y = table.unpack(queue[queue_pos]) | ||
| queue_pos = queue_pos + 1 | ||
| table.insert(footprint, { x = x, y = y }) | ||
| push_if_ok(x + 1, y) | ||
| push_if_ok(x - 1, y) | ||
| push_if_ok(x, y + 1) | ||
| push_if_ok(x, y - 1) | ||
| if CONFIG.ALLOW_DIAGONALS then | ||
| push_if_ok(x + 1, y + 1) | ||
| push_if_ok(x + 1, y - 1) | ||
| push_if_ok(x - 1, y + 1) | ||
| push_if_ok(x - 1, y - 1) | ||
| end | ||
| end | ||
|
|
||
| if #queue > CONFIG.MAX_FILL_TILES then | ||
| dfhack.printerr(('AutoCeiling: flood fill truncated at %d tiles'):format(CONFIG.MAX_FILL_TILES)) | ||
| end | ||
| return footprint | ||
| end | ||
|
|
||
| ------------------------- | ||
| -- Placement strategies | ||
| ------------------------- | ||
| local function place_planned(bp, pos) | ||
| local ok, bld = pcall(function() | ||
| return dfhack.buildings.constructBuilding{ | ||
| type = df.building_type.Construction, | ||
| subtype = df.construction_type.Floor, | ||
| pos = pos | ||
| } | ||
| end) | ||
| if not ok or not bld then return false, 'construct-error' end | ||
| pcall(function() bp.addPlannedBuilding(bld) end) | ||
| return true | ||
| end | ||
|
|
||
| local function place_native(cons, pos) | ||
| if not cons or not cons.designateNew then return false, 'no-constructions-api' end | ||
|
|
||
| local ok, res = pcall(function() | ||
| return cons.designateNew(pos, df.construction_type.Floor, -1, -1) | ||
| end) | ||
| if ok and res then return true end | ||
|
|
||
| local ok2, res2 = pcall(function() | ||
| return cons.designateNew(pos, df.construction_type.Floor, df.item_type.BOULDER, -1) | ||
| end) | ||
| if ok2 and res2 then return true end | ||
|
|
||
| return false, 'designate-error' | ||
| end | ||
|
|
||
| ------------------------- | ||
| -- Main | ||
| ------------------------- | ||
| local utils = require('utils') | ||
|
|
||
| local function main(...) | ||
| local args = {...} | ||
|
|
||
| for _, raw in ipairs(args) do | ||
| local s = tostring(raw):lower() | ||
| local num = tonumber(s) | ||
| if num then | ||
| if num < 1 then err('MAX_FILL_TILES must be >= 1') end | ||
| if num > CONFIG.MAX_LIMIT_HARD then | ||
| dfhack.printerr(('clamping MAX_FILL_TILES from %d to %d'):format(num, CONFIG.MAX_LIMIT_HARD)) | ||
| num = CONFIG.MAX_LIMIT_HARD | ||
| end | ||
| CONFIG.MAX_FILL_TILES = math.floor(num) | ||
| elseif s == 't' or s == 'true' then | ||
| CONFIG.ALLOW_DIAGONALS = true | ||
| elseif s == 'h' or s == 'help' then | ||
| print('Usage: autoceiling [t] [<max_fill_tiles>]') | ||
| print(' t: enable diagonal flood fill') | ||
| print(' <max_fill_tiles>: positive integer, default ' .. CONFIG.MAX_FILL_TILES) | ||
| return | ||
| elseif s ~= '' then | ||
| err('unknown argument: ' .. tostring(raw)) | ||
| end | ||
| end | ||
|
|
||
| local cur = utils.clone(df.global.cursor) | ||
| if cur.x == -30000 then err('cursor not set. Move to a dug tile and run again.') end | ||
| local z0 = cur.z | ||
| local seed_tt = get_tiletype(cur.x, cur.y, z0) | ||
| if not is_walkable_dug(seed_tt) then err('cursor tile is not dug/open interior') end | ||
|
|
||
| local footprint = flood_fill_footprint(cur.x, cur.y, z0) | ||
| if #footprint == 0 then | ||
| print('AutoCeiling: nothing to do — no connected dug tiles found at cursor') | ||
| return | ||
| end | ||
| local z_surface = z0 + 1 | ||
|
|
||
| -- Require buildingplan directly; let it error if missing | ||
| local bp = require('plugins.buildingplan') | ||
| if bp and (not bp.isEnabled or not bp.isEnabled()) then bp = nil end | ||
| local cons = dfhack.constructions | ||
|
|
||
| local placed, skipped = 0, 0 | ||
| local reasons = {} | ||
| local function skip(reason) | ||
| skipped = skipped + 1 | ||
| reasons[reason] = (reasons[reason] or 0) + 1 | ||
| end | ||
|
|
||
| for i, foot in ipairs(footprint) do | ||
| local x, y = foot.x, foot.y | ||
| local pos = xyz2pos(x, y, z_surface) | ||
| if not in_bounds(x, y, z_surface) then | ||
| skip('oob') | ||
| elseif is_constructed_tile(x, y, z_surface) then | ||
| skip('constructed') | ||
| elseif has_any_building(x, y, z_surface) then | ||
| skip('building') | ||
| else | ||
| local ok, why | ||
| if bp then | ||
| ok, why = place_planned(bp, pos) | ||
| else | ||
| ok, why = place_native(cons, pos) | ||
| end | ||
| if ok then placed = placed + 1 else skip(why or 'unknown') end | ||
| end | ||
| end | ||
|
|
||
| if bp and bp.doCycle then pcall(function() bp.doCycle() end) end | ||
|
|
||
| print(('AutoCeiling: placed %d floor construction(s); skipped %d'):format(placed, skipped)) | ||
| if bp then | ||
| print('buildingplan active: created planned floors that will auto-assign materials') | ||
| elseif cons and cons.designateNew then | ||
| print('used native construction designations') | ||
| else | ||
| print('no buildingplan and no constructions API available') | ||
| end | ||
| for k, v in pairs(reasons) do | ||
| print((' skipped %-18s %d'):format(k, v)) | ||
| end | ||
| end | ||
|
|
||
| main(...) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| autoceiling | ||
| =========== | ||
|
|
||
| .. dfhack-tool:: | ||
| :summary: Place floors above dug areas to seal surface openings. | ||
| :tags: construction automation utility | ||
|
|
||
| **AutoCeiling** is a DFHack Lua script 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 prevents surface collapse and stops | ||
| creatures from entering your fortress through unexpected openings. It’s | ||
| especially useful when building farms directly below the surface, since those | ||
| areas are prone to collapsing without warning and can leave open spaces that | ||
| allow surface creatures to breach your fort. | ||
|
|
||
| Usage | ||
| ----- | ||
|
|
||
| :: | ||
|
|
||
| autoceiling [t] [<max>] | ||
|
|
||
| Examples | ||
| -------- | ||
|
|
||
| ``autoceiling`` | ||
| Run with default settings (4,000 tile flood-fill limit, no diagonal fill). | ||
|
|
||
| ``autoceiling t`` | ||
| Enable diagonal flood-fill connections (8-way fill). | ||
|
|
||
| ``autoceiling 500`` | ||
| Raise or lower flood-fill limits. | ||
unboundlopez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ``autoceiling t 6000`` or ``autoceiling 6000 t`` | ||
| Allow diagonals and increase fill limit to 6,000 tiles. | ||
|
|
||
| Options | ||
| ------- | ||
|
|
||
| ``t`` | ||
| Enables 8-directional (diagonal) flood fill mode. | ||
|
|
||
| ``<max>`` | ||
| Sets the maximum number of tiles the flood fill can cover (default: 4000). | ||
|
|
||
| These are the only two options available for this command. Use ``t`` to toggle | ||
| diagonal fill and ``<max>`` to control the tile limit for flood fill. | ||
Uh oh!
There was an error while loading. Please reload this page.