v50 Steam/Premium information for editors
  • v50 information can now be added to pages in the main namespace. v0.47 information can still be found in the DF2014 namespace. See here for more details on the new versioning policy.
  • Use this page to report any issues related to the migration.
This notice may be cached—the current version can be found here.

User:Fleeting Frames/constructmultiz

From Dwarf Fortress Wiki
< User:Fleeting Frames
Revision as of 12:21, 30 March 2020 by Fleeting Frames (talk | contribs) (Ver 1.01)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

See Forum thread for usage.

--lets one construct simultaneously on z-axis
local helptext = [===[
constructonmultiplezlevels
==========================
    version 1.01 (beta)
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

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

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
    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
  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 gui = require 'gui'
  local dlg = require 'gui.dialogs'
  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)
    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 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(object, flag)
    -- Utility function for safely requesting info from userdata
    -- Returns nil if the object doesn't have flag attribute, else returns it's value
    -- Because well, ordinarily, {}[flag] returns nil.
    -- However, if object is unit - or some other type, it may instead throw an error
    local a = {}
    if not object or not flag then return nil end
        --Crash is still possible for attempting to pairs a nil
    for index, value in pairs(object) do
        a[index] = value
    end
    local returnvalue = a[flag]
    a = nil --lua automatically garbage cleans tables without variable that links to them.
    return returnvalue
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])


if zpositive > 0 or znegative > 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, zoffset)
    bd = {}
    bd.x = lbd.x1
    bd.width = 1+lbd.x2-lbd.x1
    bd.height = 1+lbd.y2-lbd.y1
    bd.y = lbd.y1
    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 getFlag(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
        createdbuilding.y1 = lbd.y1
        createdbuilding.x2 = lbd.x2
        createdbuilding.y2 = lbd.y2
    end
    createdbuilding.centerx = lbd.centerx
    createdbuilding.centery = lbd.centery
    --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
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
  for i = 1, w*h do
   if zpositive > 0 then
    for zoff = 1, zpositive do
      copy1building(df.global.world.buildings.all[lenb-i],zoff)
    end
   end
   if znegative > 0 then
    for zoff = -1, -znegative, -1 do
      copy1building(df.global.world.buildings.all[lenb-i],zoff)
    end
   end
  end

end
end