User:Fleeting Frames/constructmultiz

From Dwarf Fortress Wiki
Jump to navigation Jump to search

See Forum thread for usage.

--lets one construct simultaneously on z-axis
--[[TODO list:
    Add checking for minimap being present when rendering

    Construct # previous buildings (optionally #a to #b previous buildings or selected building(s))
    Allow constructing constructmultiz buildings by simply copying job_filter.

    Construct in x, y or fii° direction. Allow cursor position to determine fii° (useful when combined with previous one)
    This means way to place cursor. mouse_x,_y will work - on same z-level. Burrow named 'cursor' would work on different z-level.
    With lineiterator already present, could theoretically use paintstring to paint the expected line.

    Construct on designations of given type, priority and marked/not /construct on adjacent matching designations.  

    Construct all buildings in a burrow.

    Given a set of buildings and set of locations, loop through the buildings when copying.

    High priority corners

    trigger designation seek when placing on a designation

    Better automaterial support: Keeping constructmultiz settings and automaterial's both visible by masking with display and listening to relevant keys


]]
local helptext = [===[
constructonmultiplezlevels
==========================
    version 1.34
Script to construct multiple z-levels of buildings at once.
Currently uses general seeking for matching material and type,
expect for bucket, barrel, chain, mechanism, screw, pipe, anvil.
General: Dwarf will use the matching item closest to them.

Usage:
  constructmultiz x y
    orders x above and y below same buildings as the last one.
  constructmultiz display
    Displays an indicator_screen in bottom-right for bound x and y
    Also enables Ctrl+A and Ctrl+B to place bindings
  constructmultiz bind
    Adds keybinds to call this when building and exits
  constructmultiz unbind
    Removes keybinds to call this when building and exits
  constructmultiz bindings
    displays list of what bindings it would use and exits]===]

local args = {...}
local argsline = ""
if args then argsline = table.concat(args, " ") end

if not args or argsline:find("help") or argsline:find("?") then
print(helptext)
qerror("")
end

local getBlock = dfhack.maps.getTileBlock
local getFlags = dfhack.maps.getTileFlags
local tin = table.insert
local gui = require 'gui'
local utils = require 'utils'


function getCurHeightAndDepth()
local keybinding_text = dfhack.run_command_silent("keybinding list Enter")
                      --get all enter keybinds
                      --I'd need to see D and shift-enter ones too if this was setting them
if not keybinding_text:find("No bindings") then --No bindings, ergo nothing to do
    for single_bind in keybinding_text:gmatch("%s%s[^%s].-\n") do
        --iterate through all Enter binds
        if single_bind:find('constructmultiz') then
            --found one constructmultiz under enter,
            local twonumbers = single_bind:match(":.*")
            --isolating it and returning first and last number in the command
            return tonumber(twonumbers:match("%d+")), tonumber(twonumbers:match(" %d+%s$"))
        end
    end
end
return 0, 0 --if found nothing, reset to nothing
end


local function getLen(data)
  -- Can't # a hashed table, must use pairs.
  local len = 0
  for i, val in pairs(data) do
    len = len +1
  end
  return len
end

designationMapTable = designationMapTable or false
function getDesignationMapTable()
    --doesn't currently support digs and plant gathering/ramp removal
    --tree cutting? Irrelevant I guess.
    if not designationMapTable then
    designationMapTable = {}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_DIG")] = {dig = df.tile_dig_designation.Default}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_STAIR_UPDOWN")] = {dig = df.tile_dig_designation.UpDownStair}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_CHANNEL")] = {dig = df.tile_dig_designation.Channel}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_RAMP")] = {dig = df.tile_dig_designation.Ramp}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_STAIR_DOWN")] = {dig = df.tile_dig_designation.DownStair}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_STAIR_UP")] = {dig = df.tile_dig_designation.UpStair}
        --smoothing, plant gathering, probably other designations that don't set dig to 2+ have issue of losing themselves over time (with save and reload?)
    designationMapTable[gui.getKeyDisplay("DESIGNATE_SMOOTH")] = {smooth = 1} 
    designationMapTable[gui.getKeyDisplay("DESIGNATE_ENGRAVE")] = {smooth = 2}
    --problematic designations: plant gathering, ramp removing, deconstructing, tree cutting, fortifying, carving tracks

    designationMapTable[gui.getKeyDisplay("DESIGNATE_TRACK")] = {carve_track_north =false, carve_track_south =false, carve_track_east=false, carve_track_west=false}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_DIG_REMOVE_STAIRS_RAMPS")] = {dig = df.tile_dig_designation.Default, targettype = "^[^C].*Ramp"}
    --matching non-constructed ramps
    designationMapTable[gui.getKeyDisplay("DESIGNATE_PLANTS")] = {dig = df.tile_dig_designation.Default, targettype = "Shrub"}

        --Following two need to check they're on construction
    designationMapTable[gui.getKeyDisplay("DESIGNATE_REMOVE_CONSTRUCTION")] = {dig = df.tile_dig_designation.Default, targettype = "Constructed"}
    designationMapTable[gui.getKeyDisplay("DESIGNATE_FORTIFY")] = {smooth = 1, targettype = "Constructed"}

    end
    return designationMapTable
end

local function getPriorities(map_block)
    for i,v in pairs(map_block.block_events) do
        if v._type == df.block_square_event_designation_priorityst then return v.priority end
    end
end

local function decodeDesignation(designationstring)
    --designationstring is something like d7mO
    --marked is in tileoccupancy[x][y].
    --designationtype is in designation[x][y].dig or .smooth
    --priority is in blockevents[#ofblock_square_event_designation_priorityst].priority[x][y]
    local designationtype = getDesignationMapTable()[designationstring:match('.')]
    if getLen(designationtype) == 4 then
        designationtype = {
            carve_track_north =designationstring:match('n') and true or false, 
            carve_track_south =designationstring:match('s') and true or false, 
            carve_track_east=designationstring:match('e') and true or false, 
            carve_track_west=designationstring:match('w') and true or false}
    end
    local targettype = designationtype.targettype
    designationtype.targettype = nil
    local designationpriority = tonumber(designationstring:match('[%d|%,]+'))
    local ismarked = designationstring:match('m') and true or false
    local checkold = designationstring:match('O') and true or false
    return designationtype, designationpriority, ismarked, targettype, checkold
end

function isbetween(b,a,c)
    if a<=b and b<=c then return true end
    if a>=b and b>=c then return true end
end

local once = true

function isTileInArea(pos,area)
    if true then once = false 
        --print(area, type(area),type(area)=="number",pos.x,pos.y,pos.z,df.global.window_z,(df.global.window_z+area)) --stops script cold for some reason
    end--]]
    if pos.x < 0 then return false end
    if not area then return true end
    --print("isbetween",(type(area)=="number") and isbetween(pos.z,df.global.window_z,df.global.window_z+area))
    --if type(area)=="number" and (pos.z==27) then print("pos is 27") else print ("pos is "..tostring(pos.z)) end
    if type(area)=="number" and isbetween(pos.z,df.global.window_z,(df.global.window_z+area)) then return true 
    end
    if type(area) =="userdata" and area._type == df.burrow and dfhack.burrows.isAssignedTile(area,pos) then return true end
end
local area
    --Iterator for iterating through designations of a type in an area or everywhere.
function targetDesignations(designationstring, sarea)
    --returns designationIterator, state, key
    if sarea and type(sarea)=="string" then
        area = dfhack.burrows.findByName(sarea) or
                (sarea:find("Burrow %i") and df.burrow.find(tonumber(sarea:match("%i")))) or
                tonumber(sarea)
    end
    local dtype, dpriority, ismarked, targettype, checkold = decodeDesignation(designationstring)
    local function iterateBlocks(area)
        if not area or type(area)=="number" then
        for iB = 0, #df.global.world.map.map_blocks -1 do
            if(checkold or df.global.world.map.map_blocks[iB].flags.designated) 
                and (not area or isbetween(df.global.world.map.map_blocks[iB].map_pos.z,df.global.window_z,df.global.window_z+area)) then
                coroutine.yield(df.global.world.map.map_blocks[iB])
            end
        end
        else
            for i,v in pairs(dfhack.burrows.listBlocks(area)) do
                coroutine.yield(v)
            end
        end
    end
    local blockroutine = coroutine.create(iterateBlocks)

    local function getNextBlock(area)
        local errorfree, value = coroutine.resume(blockroutine,area)
        if not errorfree then return nil end
        return value
    end

    local jobroutine,getNextJob
    local checkJobs = getJobbedDesignation()[designationstring:sub(0,1)]
    print(checkJobs, df.job_type[checkJobs])
    if checkJobs then
        local function iterateJobs(area)
            local job
            local IsOld, JobListItem = pcall(function() return df.global.world.job_list end)--TODO replace slow pcall
            if not IsOld then JobListItem=df.global.world.jobs.list end
            while(JobListItem.next) and ((os.clock()-starttime)<5) do 
                job = JobListItem.item
                rcount = rcount +1
                if job and isTileInArea(job.pos,area) then
                    coroutine.yield(job)
                end
                JobListItem=JobListItem.next 
            end
        end

        jobroutine = coroutine.create(iterateJobs)

        getNextJob = function(area)
            local errorfree, value = coroutine.resume(jobroutine, area)
            if not errorfree then return nil else return value end
        end
    end

    function blockChanged(tx,ty,tz)--TODO do I use dfhack.maps??
        local blockx,blocky,blockz
        if (tx%16) ~=blockx or (ty%16) ~=blocky or tz ~=blockz then
            blockx =tx%16
            blocky =ty%16
            blockz=tz
            return true
        end
    end

    local dtypel = getLen(dtype)
    local x,y,z,cblock, priorities
    local cy,cx,ct = 0,-1,0 --start for cx -1 so I can add before returning.
    cblock, curJob = getNextBlock(area), true
        --return #(.th designation) and it's x, y and z coordinates
    local function designationIterator(_,nthdesignation)
        nthdesignation = nthdesignation +1
        --TODO If area was burrow check only burrow tiles not burrow block tiles
        while cblock do
        priorities = getPriorities(cblock)
            while(cy<16) do
            while(cx<15) do
            cx = cx+1
                if not area or area and type(area)=="number" or (type(area) == "userdata" and area._type == df.burrow and dfhack.burrows.isAssignedBlockTile(area,cblock,cx,cy)) then
                if(cblock.occupancy[cx][cy].dig_marked == ismarked) then
                if((not dpriority) or priorities and priorities[cx][cy] == (dpriority*1000)) then
                ct = 0
                for key, value in pairs(dtype) do
                    if(dtypel == 1 and (cblock.designation[cx][cy][key] == value or checkOld and priorities and targettype and cblock.designation[cx][cy][key] == 0)) then
                        --in case of offloaded designations, designation is zero but priority/marked is kept.
                        --of course, neither of those are necessarily checked so this might need a TODO fix
                        --also what's the point of checkOld+targettype here if I check jobs anyway??
                        --checkold should be more like checkoldmarked, and priorities shouldn't matter
                        if not targettype or df.tiletype[cblock.tiletype[cx][cy]]:match(targettype) then
                            return nthdesignation, (cblock.map_pos.x+cx), (cblock.map_pos.y+cy), cblock.map_pos.z
                        end
                    elseif(dtypel == 4) and (cblock.occupancy[cx][cy][key] == value) then
                        ct = ct + 1
                        if(ct == 4) then return nthdesignation, (cblock.map_pos.x+cx), (cblock.map_pos.y+cy), cblock.map_pos.z end
                    end
                end
                end
                end
                end
            end
            cy = cy +1
            cx = -1
            end

        if cy == 16 then cy, cx = 0, -1 end
        cblock = getNextBlock(area)
        end
        --tempBlocks = nil
        --Things don't end here; less simple designations can get offloaded into joblist!
        if checkJobs then
            while curJob and ((os.clock()-starttime)<5) do
            curJob = getNextJob(area)-- must be before return, or it doesn't get the next one.
            if(curJob) then
            if dpriority and blockChanged(curJob.pos.x, curJob.pos.y, curJob.pos.z) then 
                priorities = getPriorities(dfhack.maps.getTileBlock(curJob.pos.x, curJob.pos.y, curJob.pos.z)) 
            end
            jcount= jcount +1

            if checkJobs == "Smooth" and ( curJob.job_type == df.job_type.DetailFloor or curJob.job_type == df.job_type.DetailWall) or curJob.job_type == checkJobs then
                if ((not dpriority) or priorities and priorities[curJob.pos.x%16][curJob.pos.y%16] == (dpriority*1000)) then
                    return nthdesignation, curJob.pos.x, curJob.pos.y, curJob.pos.z
                end
            end
            end

            end
        end
    end
    return designationIterator, nil, 0
end

    --Iterator for for-loop for getting nr,x,y,z of next element in line
function amathline(desiredlength,width,length,height)
--returns lineiterator, state, curlength
    local width = width --masking with locals since going to modify them
    local length = length
    local height = height
    if not width or width==1 then width = 0 end --boxes of a,b and a,b,0 and a,b,1 are equivalent
    if not length or length==1 then length = 0 end
    if not height or height==1 then height = 0 end
    local xm = width<0 and -1 or 1 --used only for returning
    local ym = length<0 and -1 or 1
    local zm = height<0 and -1 or 1
    width = width<0 and -width or width
    length = length<0 and -length or length
    height = height<0 and -height or height
    local cx, cy, cz = 1,1,1 --first building is on 1,1,1
    --ok, so, for boxes to be coordinate-agnostic, each coordinate must be projected off a plane, which means taking into account both coordinates off that plane
    local wlmax = width>length and width or length
    local whmax = width>height and width or height
    local lhmax = length>height and length or height
    local cxy, cxz, cyz=1,1,1
    local function lineiterator(desiredlength, curlength)
        if curlength<desiredlength then
            curlength = curlength+1
            cxy = cy > cx and cy or cx
            cxz = cz > cx and cz or cx
            cyz = cy > cz and cy or cz
            --simultaneous assignment avoids off-by-1 errors, projecting off a plane avoids coordinate dependence
            cx, cy, cz = cx+ (width>=lhmax and 1 or  ((cx/cyz <= width/lhmax) and 1 or 0)),
                         cy+ (length>=whmax and 1 or ((cy/cxz <= length/whmax) and 1 or 0)), 
                         cz+ (height>=wlmax and 1 or ((cz/cxy <= height/wlmax) and 1 or 0))
            return curlength, cx*xm, cy*ym, cz*zm
        end
    end
return lineiterator, desiredlength, 1 --first building is already in place
end

jobbedDesignations = jobbedDesignations or false
function getJobbedDesignation()--designation_key)
    if not jobbedDesignations then 
        --accessible designations get offloaded into joblist, have to look for them there.
    jobbedDesignations = {}
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_DIG")] = df.job_type.Dig
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_STAIR_UPDOWN")] = df.job_type.CarveUpDownStaircase
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_CHANNEL")] = df.job_type.DigChannel
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_RAMP")] = df.job_type.CarveRamp
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_STAIR_DOWN")] = df.job_type.CarveDownwardStaircase
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_STAIR_UP")] = df.job_type.CarveUpwardStaircase
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_SMOOTH")] = "Smooth" --smooth and engrave are both either DetailFloor or DetailWall jobs
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_ENGRAVE")] = "Smooth" --"Engrave" can't be currently checked
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_DIG_REMOVE_STAIRS_RAMPS")] = df.job_type.RemoveStairs
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_PLANTS")] = df.job_type.GatherPlants

    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_REMOVE_CONSTRUCTION")] = df.job_type.RemoveConstruction
    jobbedDesignations[gui.getKeyDisplay("DESIGNATE_FORTIFY")] = df.job_type.CarveFortification

    --jobbedDesignations[gui.getKeyDisplay("DESIGNATE_TRACK")] = df.job_type.CarveTrack --TODO check job.item_category 18-21 NSWE is saved
    end
    return jobbedDesignations
end

jobbedDesignationsInv = jobbedDesignationsInv or false
if not jobbedDesignationsInv then
    local dummy = copyall(getJobbedDesignation())
    dummy[gui.getKeyDisplay("DESIGNATE_SMOOTH")] = nil
    dummy[gui.getKeyDisplay("DESIGNATE_ENGRAVE")] = nil
    jobbedDesignationsInv = utils.invert(dummy)
    jobbedDesignationsInv[df.job_type.DetailFloor] = gui.getKeyDisplay("DESIGNATE_SMOOTH")
    jobbedDesignationsInv[df.job_type.DetailWall] = gui.getKeyDisplay("DESIGNATE_SMOOTH")
end

    --Iterator for iterating through designations of a type in an area or everywhere.
z = z or -999

function normalDesignations()
    --returns designationIterator, state, key
    local function iterateBlocks()
        for areax = 1, df.global.world.map.x_count, 16 do
            for areay = 1, df.global.world.map.y_count, 16 do
                coroutine.yield(getBlock(areax,areay,z))
            end
        end
    end
    local blockroutine = coroutine.create(iterateBlocks)

    local function getNextBlock()
        local errorfree, value = coroutine.resume(blockroutine)
        if not errorfree then return nil end
        return value
    end

    function blockChanged(tx,ty,tz)
        local blockx,blocky,blockz
        if (tx%16) ~=blockx or (ty%16) ~=blocky or tz ~=blockz then
            blockx =tx%16
            blocky =ty%16
            blockz=tz
            return true
        end
    end

    local x,y,z,cblock, priorities
    local cy,cx,ct = 0,-1,0 --start for cx -1 so I can add before returning.

    cblock, curJob = getNextBlock(), true
        --return #(.th designation) and it's x, y and z coordinates
    local ddig, dsmooth, dmarked, dpriority, dn, ds, de, dw
    local function designationIterator(_,nthdesignation)
        nthdesignation = nthdesignation +1
        while cblock do
        priorities = getPriorities(cblock)
            while(cy<16) do
            while(cx<15) do
            cx = cx+1
            if cblock.occupancy[cx][cy].building == 0 then --no point watching tiles with buildings as those can't have buildings placed on top anyway
                ddig=cblock.designation[cx][cy].dig
                dsmooth=cblock.designation[cx][cy].smooth
                dmarked = cblock.occupancy[cx][cy].dig_marked
                dpriority = priorities and priorities[cx][cy] or false
                dn=cblock.occupancy[cx][cy].carve_track_north
                ds=cblock.occupancy[cx][cy].carve_track_south
                de=cblock.occupancy[cx][cy].carve_track_east
                dw=cblock.occupancy[cx][cy].carve_track_west
                dtt = (ddig==df.tile_dig_designation.Default or dsmooth == 1) and cblock.tiletype[cx][cy] or 0

                if not (ddig == 0 and dsmooth == 0 --[=[ and dpriority == 0 --]=] and (not dn) and (not ds) and (not de) and (not dw)) then
                    return nthdesignation, {x=cblock.map_pos.x+cx, y=cblock.map_pos.y+cy, dig=ddig, smooth=dsmooth, marked=dmarked, priority=dpriority, n=dn, s=ds, e=de, w=dw, tt=dtt}
                end
            end

            end
            cy = cy +1
            cx = -1
            end

        if cy == 16 then cy, cx = 0, -1 end
        cblock = getNextBlock()
        end
    end
    return designationIterator, nil, 0
end

function jobDesignations()
    --returns designationIterator, state, key
    local curJob = true
        --return #(.th designation) and it's x, y and z coordinates
    local dn, ds, de, dw
    local job
    local IsOld, JobListItem = pcall(function() return df.global.world.job_list end)--TODO replace slow pcall
    if not IsOld then JobListItem=df.global.world.jobs.list end
    starttime=os.clock()
    local function designationIterator(_,nthdesignation)
        while JobListItem.next do --and ((os.clock()-starttime)<1) do
            curJob = JobListItem.item
            JobListItem = JobListItem.next
            if (curJob and curJob.pos.z == z) then
                if jobbedDesignationsInv[curJob.job_type] and select(2,getFlags(curJob.pos.x, curJob.pos.y, z)).building==0 then 
                -- designation job on a tile without building
                --    priority can be still checked after building is placed
                    dn= curJob.job_type == df.job_type.CarveTrack and job.item_category[18]
                    ds= curJob.job_type == df.job_type.CarveTrack and job.item_category[19]
                    dw= curJob.job_type == df.job_type.CarveTrack and job.item_category[20]
                    de= curJob.job_type == df.job_type.CarveTrack and job.item_category[21]
                    --tiletype is necessary to tell apart smooth and carve - but that check can be done when there's a difference.
                    nthdesignation = nthdesignation +1
                    return nthdesignation, {x=curJob.pos.x, y=curJob.pos.y, letter = jobbedDesignationsInv[curJob.job_type], n=dn, s=ds, e=de, w=dw}
                end
            end
        end
    end
    return designationIterator, nil, 0
end

function makeDesignationTable(updatez)
    DesignationTable = updatez and DesignationTable or {}
    DesignationTable[z]={normal = {}, job = {} }
    for dnr, dtab in normalDesignations() do
        tin(DesignationTable[z].normal, dtab)
    end

    for dnr, jtab in jobDesignations() do
        tin(DesignationTable[z].job, jtab)
    end
end


function findSameZDiff()
    local diffTable = {}
    local dflag, oflag
    local letter, ddig, dsmooth, dmarked, dpriority, dn, ds, de, dw
    local designationString, digString, digSmooth
    for i, dtab in pairs(DesignationTable[z].normal) do
        dflag, oflag = getFlags(dtab.x, dtab.y, z)
        if oflag.building > 0 then    --building was placed where previously it wasn't.
            --how do I handle simultaneous smooth and dig? As two separate?
            if dtab.dig == df.tile_dig_designation.Default then
                for dletter, identity in pairs(getDesignationMapTable()) do
                    if identity.dig == dtab.dig and identity.targettype and df.tiletype[dtab.tt]:match(identity.targettype) then 
                        digString=dletter
                    end
                end
                if not digString then 
                    digString = gui.getKeyDisplay("DESIGNATE_DIG")
                end
            else
                for dletter, identity in pairs(getDesignationMapTable()) do
                    if dtab.dig > 0 and identity.dig == dtab.dig then 
                        digString=dletter
                    end
                end
            end

            if dtab.smooth == 1 then
                for dletter, identity in pairs(getDesignationMapTable()) do
                    if identity.smooth == dtab.smooth and identity.targettype and df.tiletype[dtab.tt]:match(identity.targettype) then 
                        digSmooth=dletter
                    end
                end
                if not digSmooth then 
                    digSmooth = gui.getKeyDisplay("DESIGNATE_SMOOTH")
                end
            else
                for dletter, identity in pairs(getDesignationMapTable()) do
                    if dtab.smooth > 0 and identity.smooth == dtab.smooth then 
                        digSmooth=dletter
                    end
                end
            end
            designationString = (dtab.marked and 'm' or '') .. (dtab.priority and dtab.priority/1000 or '') .. 'O'
            if digString then
                digString = digString .. designationString
                diffTable[digString] = true
                digString = nil
            end
            if digSmooth then
                digSmooth = digSmooth .. designationString
                diffTable[digSmooth] = true
                digSmooth = nil
            end

            DesignationTable[z].normal[i] = nil
        end
                --ddig=cblock.designation[cx][cy].dig --only digs are zeroed when floor is placed on top
                --dsmooth=cblock.designation[cx][cy].smooth
                --dmarked = cblock.occupancy[cx][cy].dig_marked
                --dpriority = priorities and priorities[cx][cy] or 0
                --dn=cblock.occupancy[cx][cy].carve_track_north
                --ds=cblock.occupancy[cx][cy].carve_track_south
                --de=cblock.occupancy[cx][cy].carve_track_east
                --dw=cblock.occupancy[cx][cy].carve_track_west
                --dtt = (ddig==df.tile_dig_designation.Default or dsmooth == 1) and cblock.tiletype[cx][cy] or 0

    end

    for i, jtab in pairs(DesignationTable[z].job) do
        dflag, oflag = getFlags(jtab.x, jtab.y, z)
        if oflag.building > 0 then    --building was placed where previously it wasn't.
            --designationString='j2mO'
            dpriority = getPriorities(getBlock(jtab.x, jtab.y, z))
            dpriority = dpriority and dpriority[jtab.x%16][jtab.y%16] or false
            designationString = jtab.letter .. (dpriority and dpriority/1000 or '') .. (oflag.dig_marked and 'm' or '' ) ..'O' -- always Old/job tiles
            --Also still doesn't match tracks properly
            diffTable[designationString] = true
            DesignationTable[z].job[i] = nil
        end
    end

    return diffTable
end

function initBindings()
  local keybinding_list = {} --hides higher-lever list via same name
  table.insert(keybinding_list, 'B@dwarfmode/Default "constructmultiz display"')
  table.insert(keybinding_list, 'D@dwarfmode/Build/Material/Groups "constructmultiz ' .. curheight .. ' ' .. curdepth .. '"')
  table.insert(keybinding_list, 'Enter@dwarfmode/Build/Position/FarmPlot "constructmultiz ' .. curheight .. ' ' .. curdepth .. '"')
  table.insert(keybinding_list, 'Enter@dwarfmode/Build/Position/RoadDirt "constructmultiz ' .. curheight .. ' ' .. curdepth .. '"')
  table.insert(keybinding_list, 'Enter@dwarfmode/Build/Material/Groups "constructmultiz ' .. curheight .. ' ' .. curdepth .. '"')
  table.insert(keybinding_list, 'Shift-Enter@dwarfmode/Build/Material/Groups "constructmultiz ' .. curheight .. ' ' .. curdepth .. '"')
  return keybinding_list
end

local function constructMultiZPowerState(enable)
    for bindingi=1, #keybinding_list do
        dfhack.run_command_silent("keybinding " .. (enable and "add " or "clear ")  .. keybinding_list[bindingi])
    end
end

curheight, curdepth = getCurHeightAndDepth()
keybinding_list = initBindings()

if argsline:find("unbind") then
    constructMultiZPowerState(false)
    qerror("")
elseif argsline:find("bindings") then
   for bindingi=1, #keybinding_list do
       dfhack.println(keybinding_list[bindingi])
   end
   dfhack.println('(Internal with gui) Ctrl-A@dwarfmode/Build/Type "constructmultiz display adjust height"')
   dfhack.println('(Internal with gui) Ctrl-B@dwarfmode/Build/Type "constructmultiz display adjust depth"')
   qerror("")
elseif argsline:find("bind") then
    constructMultiZPowerState(true)
    qerror("")
end

if argsline:find("display") then
  local function adjustbinds(height, depth)
    constructMultiZPowerState(false)
    --first, remove old binds
    print(curheight .. " " .. curdepth)
    curheight = height+curheight
    curdepth = depth+curdepth
    --figure out new heights
    if curheight < 0 then curheight = 0 end
    if curdepth < 0 then curdepth = 0 end
    --if they're negative, limit them to 0
    keybinding_list = initBindings()
    --reconstruct binding table
    constructMultiZPowerState(true)
    --then add new bindings
    print("p " .. curheight .. " " .. curdepth)
  end
  local indipresent, indi = pcall(function() return dfhack.script_environment('gui/indicator_screen') end)
  --attempt to get indicator_screen - not necessarily present.

  local changeHeight --defined below
  local dlg = require 'gui.dialogs'
  z = df.global.window_z
  makeDesignationTable()
  function numberprompt (above, initialvalue)
        local retvalue = tostring(initialvalue)
        dlg.showInputPrompt(
            'Change number for ' .. (above and "above" or "below"),
            ('Enter a new number for the building ' .. (above and 'height:' or 'depth:')), COLOR_GREEN,
            retvalue,
            function(changedvalue)
              changeHeight (above, (tonumber(changedvalue))- (above and curheight or curdepth))
            end
        )
  end
 if indipresent then
  local texts
  changeHeight = function(above, newvalue)
    if above then
      adjustbinds(newvalue, 0)
      texts[3].text = " " .. curheight .. " "
      texts[3].color = (curheight ~= 0 and 10 or 8)
    else
      adjustbinds(0, newvalue)
      texts[5].text = " " .. curdepth .. " "
      texts[5].color = (curdepth ~= 0 and 10 or 8)
    end
  end
  texts = {
  --Building # above and # below was initial text plan
  --Build # up & # low(Ctrl+A/B) was new, to fit binds >_>
  {text = "" }, --empty table to adjust signature location 1 up.
  {text = "Build", notEndOfLine = true},
  {text = " " .. curheight .. " ", color = (curheight ~= 0 and 10 or 8), notEndOfLine = true,
   onclick = function() changeHeight(true,1) end, onrclick = function() changeHeight(true,-1) end},
  {text = "up &", notEndOfLine = true},
  {text = " " .. curdepth .. " ", color = (curdepth ~= 0 and 10 or 8), notEndOfLine = true,
   onclick = function() changeHeight(false,1) end, onrclick = function() changeHeight(false,-1) end},
  {text = "low(", notEndOfLine = true},
  {text = "Ctrl+A", color = 12, notEndOfLine = true, onclick = function() numberprompt(true, curheight) end},
  {text = "/", color = 4, notEndOfLine = true, onclick = function() numberprompt(false, curheight) end},
  {text = "B", color = 12, notEndOfLine = true, onclick = function() numberprompt(false, curheight) end},
  {text = ")"},
  color = 7
  }
  if indi.indicator_screen_version >= 1.1 then
    texts.onhoverfunction = function()
      dfhack.screen.paintString(2, df.global.gps.dimx-30,df.global.gps.dimy -7,"(r-)click to nudge numbers")
    end
  end
  local newscreen = indi.getScreen(texts, {x = -30, y = -7})
  function newscreen:onResize() self:adjustDims(true, -30, -7) end --ordinary resize maintains topleft position
  --newscreen.signature = false --would otherwise cover date indicator
  local oldInput = newscreen.onInput
  local function repeatfunction(rfunc, stopconditionfunc, N)
    --takes two functions and number N, evalutes second every N frames until it is true, then calls first.
    local myrfunc
    myrfunc = function ()
      dfhack.timeout(N, "frames", function()
         if stopconditionfunc() then rfunc() else myrfunc() end
       end)
    end
    myrfunc()
  end
  
  local function isInBuildType() 
    return dfhack.gui.getFocusString(df.global.gview.view.child):find("dwarfmode/Build/Type")
  end
  local notincountdown = true
  local function multizonInput(self, keys)
    oldInput(self, keys)--TODO adapt for new indi in 4303
    if z ~= df.global.window_z then 
      z = df.global.window_z 
      makeDesignationTable(true)
    end
    if (dfhack.gui.getFocusString(df.global.gview.view.child):find("dwarfmode/Build/Position/Construction")
    or (keys.SELECT and
        dfhack.gui.getFocusString(df.global.gview.view.child):find("dwarfmode/Build/Material/Groups") ) )
        and notincountdown then
        notincountdown = false
        texts[1].text = "Hiding this indicator in 2"
        texts[1].color = 12
        dfhack.timeout(math.floor(df.global.enabler.fps), "frames", function()
         texts[1].text = "Hiding this indicator in 1" end)
        dfhack.timeout(math.floor(2*df.global.enabler.fps), "frames", function()
         texts[1].text = ""
         if indi.indicator_screen_version >= 1.1 and self._native then
           self._native.parent.child = nil
           repeatfunction(function()
             indi.placeOnTop(self._native)
             notincountdown = true 
             end, isInBuildType, 10)
         else self:dismiss() end
        end)
    end
    if keys.CUSTOM_CTRL_A then
    numberprompt(true, curheight) --adusting height
    elseif keys.CUSTOM_CTRL_B then
    numberprompt(false, curdepth) --adjusting depth
    end
  end
    newscreen.onInput = multizonInput
    newscreen:show()
 elseif argsline:find("display adjust") then
  changeHeight =
  function (above, newvalue)
    if above then
      adjustbinds(newvalue, 0)
    else
      adjustbinds(0, newvalue)
    end
  end
  if argsline:find("display adjust height") then
    numberprompt(true, curheight)
  elseif argsline:find("display adjust depth") then
    numberprompt(false, curdepth)
  end
 else
   dfhack.run_command('keybinding add Ctrl-A@dwarfmode/Build/Type "constructmultiz display adjust height"')
   dfhack.run_command('keybinding add Ctrl-B@dwarfmode/Build/Type "constructmultiz display adjust depth"')
   dfhack.run_command_silent('keybinding clear B@dwarfmode/Default "constructmultiz display"')
 end
end


function getFlag(dfvalue, key)
    -- Utility function for safely requesting info from df data
    if not dfvalue or not key then return nil end --pairs crash prevention
    flagtypetable = flagtypetable or {} --memoization so it doesn't iterate through the dfvalue if we've already checked that type of value for given flag
    if flagtypetable[dfvalue._type] and flagtypetable[dfvalue._type][key] then return dfvalue[key] end
    if flagtypetable[dfvalue._type] and flagtypetable[dfvalue._type][key]==false then return nil end
    if not flagtypetable[dfvalue._type] then flagtypetable[dfvalue._type] = {} end
    for akey, avalue in pairs(dfvalue) do
        if akey == key then
            flagtypetable[dfvalue._type][key] = true
            return dfvalue[akey] 
        end
    end
    flagtypetable[dfvalue._type][key] = false
end

local context = dfhack.gui.getCurFocus()
if #args == 2 and (
   not (context:find("dwarfmode/Build/Position/FarmPlot")
    or context:find("dwarfmode/Build/Position/RoadDirt")
    or context:find("dwarfmode/Build/Material/Groups") )) then
    --Because one can choose multiple materials with enter, the script should only launch after last material
function getBuildingTypeIndex(building_type_name)
  local simplename = building_type_name
   :gsub("_.", string.upper)
     :gsub("_", "")
       :gsub("<building","")
         :gsub("st:.*","")
  if df.building_type[simplename] then return df.building_type[simplename] end
--above fails for <building_farmplotst: 0xetcetera> resulting in Farmplot instead of FarmPlot, so...
  for i = df.building_type._first_item, df.building_type._last_item do
    if df.building_type[i]:lower() == simplename:lower() then
      return i
    end
  end
end

local zpositive = tonumber(args[1]) -- 0 0 should equal nothing being done
local znegative = tonumber(args[2])
local zDiff = findSameZDiff()
local difflen = getLen(zDiff)

if zpositive > 0 or znegative > 0 or difflen > 0 then

local buildings = require('dfhack.buildings')
--never used that local, so idk how useful it is *shrug*

    --copy1building takes a preexisting building and orders a really similar thing built x zlevels above or below
    --its' called in a for loop, so better take variable declarations out of it when reasonable
local flippedPumps = 0 -- for multiz pumpstacks
local bd, argumenttable
local usemyfilterbuildings = {} -- Small list that gets special handling.
usemyfilterbuildings[df.building_type.RoadPaved] = true --Paved Road
usemyfilterbuildings[df.building_type.Bridge] = true --Bridge
usemyfilterbuildings[df.building_type.WindowGem] = true --Gem Window
usemyfilterbuildings[df.building_type.Weapon] = true --Upright weapon
usemyfilterbuildings[df.building_type.Bookcase] = true --Bookcase
local tostring2 = tostring
local tadd = table.insert    --tadd is less missleading than tin or tins
function copy1building(lbd, xoffset, yoffset, zoffset)
    bd = {}
    bd.x = lbd.x1+xoffset
    bd.width = 1+lbd.x2-lbd.x1
    bd.height = 1+lbd.y2-lbd.y1
    bd.y = lbd.y1+yoffset
    bd.z = lbd.z+zoffset

    bd.type = getBuildingTypeIndex(tostring2(lbd))
    if getFlag(lbd, "type") then bd.subtype = lbd.type
    elseif getFlag(lbd, "trap_type") then bd.subtype = lbd.trap_type
    elseif getFlag(lbd, "bait_type") then bd.subtype = lbd.bait_type end
    if getFlag(lbd, "custom_type") then bd.custom = lbd.custom_type end
    --bd.items = {df.item.find(22603)}    --how to build with items instead
    bd.filters = {}

    if not (usemyfilterbuildings[bd.type]
        or (bd.type == df.building_type.Trap and bd.subtype == 4 )) then -- All traps other than weapon trap
        argumenttable = {material = {mat_type = lbd.mat_type, mat_index = lbd.mat_index,}}
        if #lbd.jobs[0].items>0 then
          argumenttable.material.item_type =
            df.item_type[tostring2(lbd.jobs[0].items[0].item):gsub("st:.*",""):gsub("<item_",""):upper()]
        end
        local getfilters = dfhack.buildings.getFiltersByType(argumenttable, bd.type, bd.subtype, bd.custom)
            --TODO: Figure out if fixing ash with item_type breaks anything due first item being not as intended
            -- i.e. mat_index and mat_type legal mismatch.
            --roads, maybe?
            --if it is in order of age, maybe constructing ashery with ash bar and newer/older bucket?
            --ditto for weapon traps and such.
            --farm plot and dirt road doesn't have any items.

    --[=====[ Machinery:
            --lever, gear assembly, roller report unknown material?
            -- Specificaly, for unknown mat gear assembly item_type and vector_id get passed, mat_type and mat_index not
            -- The worker picks the mechanism closest to them, not closest to building site or newest or oldest.
            -- Weapons:
            -- upright spears only request 1 spike, weapon traps 1 any mechanism and 1 any weapon.
            -- Multi-mat buildings:
            -- Gem windows only request quantity 3 any small gem of first? newest? base material? Works fine, though
            -- roads have same trouble AND soap+boulder road doesn't get design.flags.rough = true
            -- bridges probably too
            -- The lack of item type (being set to -1) might also be a problem. Can specify it via job items tho.
            -- Also at least the road doesn't use willy-nilly highwood non-logs, so there's that.

            --for bucket/weapon/mechanism/etc. it only specifies appropriate item type, vector id and flags, not material
            --q: do I want to restrict these when mass-producing?
            --a: weapon and mechanism maybe. anvil def not. screw, pipe, chain, bucket, barrel eh.

            --instrument is untested

            --]=====]
        if getfilters then
        --some cases, it can't get filters.
         for i=1, #getfilters do
          tadd(bd.filters,
          getfilters[i])
         end
            else
             qerror("Can't find appropriate building materials for building " .. tostring2(lbd))
        end
    elseif bd.type == df.building_type.Bookcase then --bookcase
        tadd(bd.filters, {
        item_type = df.item_type.TOOL,
        item_subtype = lbd.jobs[0].items[0].item.subtype.subtype,
        has_tool_use = lbd.jobs[0].items[0].item.subtype.tool_use[0], --both of these are unnecessary tbh.
        mat_index = lbd.mat_index,
        mat_type = lbd.mat_type,
        new = true
        })
    elseif bd.type == df.building_type.RoadPaved or
           bd.type == df.building_type.Bridge or
           bd.type == df.building_type.WindowGem then
      for itemindex = 0, #lbd.jobs[0].items-1 do
        tadd(bd.filters, {
        item_type = df.item_type[tostring2(lbd.jobs[0].items[itemindex].item)
                                  :gsub("st:.*",""):gsub("<item_",""):upper()],
        mat_index = lbd.jobs[0].items[itemindex].item.mat_index,
        mat_type = lbd.jobs[0].items[itemindex].item.mat_type,
        quantity = 1,
        new = true,
        flags2 = (bd.type ~= df.building_type.WindowGem and {building_material = true, non_economic = false} or nil)
        })
      end
    elseif bd.type == df.building_type.Trap and bd.subtype == 4
        or bd.type == df.building_type.Weapon then
        --Weapon traps or upright weapons
        --How specific do I want to be with those two?
        --First is the matter of component weapon.
        --Second is the matter of mechanism.
        --When the hell does one even buid multiple zs of weapon traps?
        --When building snaking dodge-me setups, perhaps.
        --In that case, would want same quality mechanism (impossible)
        --And either random weapons made from goblinite or all serrated glass discs and such.
        --The latter is more important to not screw up than the former.
        local itemfilter
      for itemindex = 0, #lbd.jobs[0].items-1 do
       itemfilter = {
        item_type = df.item_type[tostring2(lbd.jobs[0].items[itemindex].item)
                                  :gsub("st:.*",""):gsub("<item_",""):upper()],
        quantity = 1,
        new = true
       }
       if itemfilter.item_type == df.item_type.TRAPPARTS then
            --Keep grabbing random closest mechanism.
        itemfilter.vector_id = df.job_item_vector_id.TRAPPARTS --probs unnecessary
       else
            --Utilize weapon or trapcomp with exact same material and type.
        itemfilter.mat_index = lbd.jobs[0].items[itemindex].item.mat_index
        itemfilter.mat_type = lbd.jobs[0].items[itemindex].item.mat_type
        itemfilter.item_subtype = lbd.jobs[0].items[itemindex].item.subtype.subtype
       end
        tadd(bd.filters, itemfilter)
      end
    end
    local createdbuilding = dfhack.buildings.constructBuilding(bd)
    --Here is where I would have to reorient pumps, rollers, bridges, horizontal axles, water wheels
    -- Also, I'm getting inconsistencent failures now.
      if createdbuilding then
    if getFlag(createdbuilding,"direction") then --pump, roller, bridge
        createdbuilding.direction = lbd.direction
        if bd.type == df.building_type.ScrewPump then
            --flip screw pump around
            createdbuilding.direction = (createdbuilding.direction +2*(1+flippedPumps) ) % 4
            flippedPumps = 1+flippedPumps
        end
        if bd.type == df.building_type.Rollers and lbd.speed ~= 50000 then
            --roller speed adjusment, if necessary
            createdbuilding.speed = lbd.speed
        end
    elseif getFlag(createdbuilding,"is_vertical") ~= nil then --horizontal axle, water wheel
        --need to fix both orientation and dimensions
        createdbuilding.is_vertical = lbd.is_vertical
    end

    if getFlag(createdbuilding, "friction") then
        --probably just track stops, but gets given to all traps at least
        createdbuilding.friction = lbd.friction
        if getFlag(createdbuilding, "dump_x_shift") then --track stop dump direction
            createdbuilding.dump_x_shift = lbd.dump_x_shift
            createdbuilding.dump_y_shift = lbd.dump_y_shift
            createdbuilding.use_dump = lbd.use_dump
        end
    end

    if bd.type == 23 and bd.subtype == 1 then --pressure plate
        for index, v in pairs(createdbuilding.plate_info) do
            if index ~= "flags" then
                --set all plate setting to prototype's
                createdbuilding.plate_info[index] = lbd.plate_info[index]
            else
                for flagname, flagstate in pairs(v) do
                    --set all plate enabled states to prototype's
                    createdbuilding.plate_info[index][flagname] = lbd.plate_info[index][flagname]
                end
            end
        end
    end

    if bd.type == df.building_type.RoadPaved or --Paved road
       bd.type == df.building_type.Bridge then  --Bridge
    -- Buildings for which using boulders changes appearance.
    -- Could also pick and iterate through the design of any architecture,
    -- but that shouldn't be necessary.
        createdbuilding.design.flags.rough = lbd.design.flags.rough
    end

    if (getFlag(createdbuilding,"is_vertical") or getFlag(createdbuilding,"direction") )
    and bd.type ~= df.building_type.Bridge then --bridges are already perfect in x and y
        createdbuilding.x1 = lbd.x1+xoffset
        createdbuilding.y1 = lbd.y1+yoffset
        createdbuilding.x2 = lbd.x2+xoffset
        createdbuilding.y2 = lbd.y2+yoffset
    end
    createdbuilding.centerx = lbd.centerx+xoffset
    createdbuilding.centery = lbd.centery+yoffset
    --constructbuilding tends to round center up where df rounds down.
    if flippedPumps % 2 ~= 0 and bd.type == df.building_type.ScrewPump then
      --screw pump direction was swapped, so it'd center tile has to be swapped as well
      --otherwise dwarf tries to build it on impassable tile. 
     if createdbuilding.direction % 2 == 0 then --northsouth
       createdbuilding.centery = createdbuilding.centery == createdbuilding.y1 and createdbuilding.y2 or createdbuilding.y1
     else --eastwest
       createdbuilding.centerx = createdbuilding.centerx == createdbuilding.x1 and createdbuilding.x2 or createdbuilding.x1
     end
    end
      end
end

local lenb = #df.global.world.buildings.all --changes during usage so must save beforehand
local w,h
--[[
for curlength, x, y,z in a_line(17,1,-17,1) do
    copy1building(df.global.world.buildings.all[lenb-1], x-1, y, z-1)
end
--]]

if getBuildingTypeIndex(tostring(df.global.world.buildings.all[lenb-1])) ~= 34 then
  --not construction
  w, h = 1,1
else
  w = df.global.world.building_width
  h = df.global.world.building_height
end
  local xoff = 0
  local yoff = 0
 
  local nr = 0
 if difflen > 0 then  
   local hdif = zpositive-znegative
   if zpositive > 0 and znegative > 0 then
     hdif = zpositive+znegative
     df.global.window_z = df.global.window_z - znegative
   end
   
   for dstring, _ in pairs(zDiff) do --typically 1 member
     for curlength, x, y,z in targetDesignations(dstring, tostring(hdif)) do --only current z for now, could use zpositive or znegative to do on other zs
      local tbld = df.global.world.buildings.all[lenb-1-curlength%w*h]
      copy1building(tbld, x-tbld.x1, y-tbld.y1, z-tbld.z)
      nr = nr + 1
     end
   end
   
   if zpositive > 0 and znegative > 0 then
     df.global.window_z = df.global.window_z + znegative
   end
   
 elseif zpositive ~= 0 or znegative ~= 0 then 
  for i = 1, w*h do
   if zpositive > 0 then
    for zoff = 1, zpositive do
      copy1building(df.global.world.buildings.all[lenb-i], xoff, yoff, zoff)
      nr = nr +1
    end
   end
   if znegative > 0 then
    for zoff = -1, -znegative, -1 do
      copy1building(df.global.world.buildings.all[lenb-i], xoff, yoff, zoff)
      nr = nr +1
    end
   end
  end
 end
 if (nr > 0) then print("Placed " .. nr .. " buildings.") end
 
end
end