-
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 1 commit
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 |
|---|---|---|
|
|
@@ -10,51 +10,41 @@ | |
| -- Configuration defaults | ||
| ------------------------- | ||
| local CONFIG = { | ||
| MAX_FILL_TILES = 4000, -- safety limit | ||
| ALLOW_DIAGONALS = false -- can be overridden by parameter | ||
| 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 try_require(modname) | ||
| local ok, mod = pcall(require, modname) | ||
| if ok and mod then return mod end | ||
| return nil | ||
| 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 W = df.global.world | ||
| local XMAX, YMAX, ZMAX = W.map.x_count, W.map.y_count, W.map.z_count | ||
|
|
||
| local function in_bounds(x, y, z) | ||
unboundlopez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return x >= 0 and y >= 0 and z >= 0 and x < XMAX and y < YMAX and z < ZMAX | ||
| end | ||
|
|
||
| local function get_block(x, y, z) | ||
| return dfhack.maps.getTileBlock(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
|
||
| local b = get_block(x, y, z) | ||
| if not b then return nil end | ||
| return b.tiletype[x % 16][y % 16] | ||
| return maps.getTileType(x, y, z) | ||
| end | ||
|
|
||
| local function tile_shape(tt) | ||
| if not tt then return nil end | ||
| local a = df.tiletype.attrs[tt] | ||
| return a and a.shape or nil | ||
| end | ||
|
|
||
| local function tile_material(tt) | ||
| if not tt then return nil end | ||
| local a = df.tiletype.attrs[tt] | ||
| return a and a.material or nil | ||
| local a = tattrs[tt] | ||
| return (a and a.shape ~= df.tiletype_shape.NONE) and a.shape or nil | ||
| end | ||
|
|
||
| ------------------------- | ||
|
|
@@ -72,57 +62,51 @@ local function is_walkable_dug(tt) | |
| end | ||
|
|
||
| local function is_constructed_tile(x, y, z) | ||
unboundlopez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| local tt = get_tiletype(x, y, z) | ||
| local mat = tile_material(tt) | ||
| return mat == df.tiletype_material.CONSTRUCTION | ||
| return constructions.findAtTile(x, y, z) ~= nil | ||
| end | ||
|
|
||
| local function has_any_building(x, y, z) | ||
| -- Also detects in-progress constructions as buildings | ||
| return dfhack.buildings.findAtTile({ x = x, y = y, z = z }) ~= nil | ||
| return buildings.findAtTile(xyz2pos(x, y, z)) ~= nil | ||
| end | ||
|
|
||
| ------------------------- | ||
| -- Flood fill | ||
| ------------------------- | ||
| local function push_if_ok(q, visited, x, y, z) | ||
| if not in_bounds(x, y, z) then return end | ||
| local key = x .. ',' .. y | ||
| if visited[key] then return end | ||
| local tt = get_tiletype(x, y, z) | ||
| if is_walkable_dug(tt) then | ||
| visited[key] = true | ||
| q[#q + 1] = { x, y } | ||
| end | ||
| end | ||
|
|
||
| local function flood_fill_footprint(seed_x, seed_y, z0) | ||
| local footprint = {} | ||
| local visited = {} | ||
| local q = { { seed_x, seed_y } } | ||
| 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 head = 1 | ||
| while head <= #q and #footprint < CONFIG.MAX_FILL_TILES do | ||
| local x, y = table.unpack(q[head]); head = head + 1 | ||
| footprint[#footprint + 1] = { x = x, y = y } | ||
| 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(q, visited, x + 1, y, z0) | ||
| push_if_ok(q, visited, x - 1, y, z0) | ||
| push_if_ok(q, visited, x, y + 1, z0) | ||
| push_if_ok(q, visited, x, y - 1, z0) | ||
| push_if_ok(q, visited, x + 1, y + 1, z0) | ||
| push_if_ok(q, visited, x + 1, y - 1, z0) | ||
| push_if_ok(q, visited, x - 1, y + 1, z0) | ||
| push_if_ok(q, visited, x - 1, y - 1, z0) | ||
| else | ||
| push_if_ok(q, visited, x + 1, y, z0) | ||
| push_if_ok(q, visited, x - 1, y, z0) | ||
| push_if_ok(q, visited, x, y + 1, z0) | ||
| push_if_ok(q, visited, x, y - 1, z0) | ||
| 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 #q > CONFIG.MAX_FILL_TILES then | ||
| if #queue > CONFIG.MAX_FILL_TILES then | ||
| dfhack.printerr(('AutoCeiling: flood fill truncated at %d tiles'):format(CONFIG.MAX_FILL_TILES)) | ||
| end | ||
| return footprint | ||
|
|
@@ -131,53 +115,82 @@ end | |
| ------------------------- | ||
| -- Placement strategies | ||
| ------------------------- | ||
| local function place_planned(bp, x, y, z) | ||
| 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 = { x = x, y = y, z = z } | ||
| 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, x, y, z) | ||
| if not cons or not cons.designate then return false, 'no-constructions-api' end | ||
| local ok, derr = pcall(function() | ||
| cons.designate{ pos = { x = x, y = y, z = z }, type = df.construction_type.Floor } | ||
| 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 not ok then return false, 'designate-error' end | ||
| return true | ||
| 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 = {...} | ||
| -- Allow user to set diagonals with parameter 't' or 'true' | ||
| if #args > 0 and (args[1] == 't' or args[1] == 'true') then | ||
| CONFIG.ALLOW_DIAGONALS = true | ||
|
|
||
| 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 | ||
|
|
||
| -- Validate cursor and tile | ||
| local cur = df.global.cursor | ||
| 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 | ||
|
|
||
| -- Discover footprint and target surface level | ||
| 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 | ||
|
|
||
| -- Load optional DFHack helpers | ||
| local bp = try_require('plugins.buildingplan') | ||
| -- 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 = try_require('dfhack.constructions') | ||
| local cons = dfhack.constructions | ||
|
|
||
| local placed, skipped = 0, 0 | ||
| local reasons = {} | ||
|
|
@@ -186,9 +199,9 @@ local function main(...) | |
| reasons[reason] = (reasons[reason] or 0) + 1 | ||
| end | ||
|
|
||
| -- Process each tile | ||
| for i = 1, #footprint do | ||
| local x, y = footprint[i].x, footprint[i].y | ||
| 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 | ||
|
|
@@ -198,9 +211,9 @@ local function main(...) | |
| else | ||
| local ok, why | ||
| if bp then | ||
| ok, why = place_planned(bp, x, y, z_surface) | ||
| ok, why = place_planned(bp, pos) | ||
| else | ||
| ok, why = place_native(cons, x, y, z_surface) | ||
| ok, why = place_native(cons, pos) | ||
| end | ||
| if ok then placed = placed + 1 else skip(why or 'unknown') end | ||
| end | ||
|
|
@@ -211,7 +224,7 @@ local function main(...) | |
| 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.designate then | ||
| elseif cons and cons.designateNew then | ||
| print('used native construction designations') | ||
| else | ||
| print('no buildingplan and no constructions API available') | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the reason I mentioned
xyz2pos()is that it's a standard API call.You've basically reimplemented it, which is fine. Just saying you didn't need to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duplicating code that already exists in the code library is a no-no and will result in a review rejection, so you might as well fix this nw before i get around to finding it myself when i do the review. Don't Repeat Yourself also covers repeating someone else