mirror of
https://github.com/kepler155c/opus
synced 2024-11-18 14:44:50 +00:00
1198 lines
30 KiB
Lua
1198 lines
30 KiB
Lua
local class = require('class')
|
|
local DEFLATE = require('deflatelua')
|
|
local UI = require('ui')
|
|
local Point = require('point')
|
|
|
|
--[[
|
|
Loading and manipulating a schematic
|
|
--]]
|
|
|
|
local schematicMagic = 0x0a00
|
|
local gzipMagic = 0x1f8b
|
|
|
|
local Schematic = class()
|
|
function Schematic:init(args)
|
|
self.blocks = { }
|
|
self.damages = { }
|
|
self.originalBlocks = { }
|
|
self.x, self.y, self.z = 0, 0, 0
|
|
self.height = 0
|
|
self.index = 1
|
|
end
|
|
|
|
--[[
|
|
Credit to Orwell for the schematic file reader code
|
|
http://www.computercraft.info/forums2/index.php?/topic/1949-turtle-schematic-file-builder/
|
|
|
|
Some parts of the file reader code was modified from the original
|
|
--]]
|
|
|
|
function Schematic:discardBytes(h, n, spinner)
|
|
for i = 1,n do
|
|
h:readbyte()
|
|
if (i % 1000) == 0 then
|
|
spinner:spin()
|
|
end
|
|
end
|
|
end
|
|
|
|
function Schematic:readname(h)
|
|
local n1 = h:readbyte(h)
|
|
local n2 = h:readbyte(h)
|
|
|
|
if(n1 == nil or n2 == nil) then
|
|
return ""
|
|
end
|
|
|
|
local n = n1*256 + n2
|
|
|
|
local str = ""
|
|
for i=1,n do
|
|
local c = h:readbyte(h)
|
|
if c == nil then
|
|
return
|
|
end
|
|
str = str .. string.char(c)
|
|
end
|
|
return str
|
|
end
|
|
|
|
function Schematic:parse(a, h, containsName, spinner)
|
|
|
|
if a==0 then
|
|
return
|
|
end
|
|
|
|
local name
|
|
if containsName then
|
|
name = self:readname(h)
|
|
end
|
|
|
|
if a==1 then
|
|
self:discardBytes(h, 1, spinner)
|
|
elseif a==2 then
|
|
local i1 = h:readbyte(h)
|
|
local i2 = h:readbyte(h)
|
|
local i = i1*256 + i2
|
|
if(name=="Height") then
|
|
--self.height = i
|
|
elseif (name=="Length") then
|
|
self.length = i
|
|
elseif (name=="Width") then
|
|
self.width = i
|
|
end
|
|
return 2
|
|
elseif a==3 then
|
|
self:discardBytes(h, 4, spinner)
|
|
return 4
|
|
elseif a==4 then
|
|
self:discardBytes(h,8, spinner)
|
|
return 8
|
|
elseif a==5 then
|
|
self:discardBytes(h,4, spinner)
|
|
return 4
|
|
elseif a==6 then
|
|
self:discardBytes(h,8, spinner)
|
|
elseif a==7 then
|
|
local i1 = h:readbyte(h)
|
|
local i2 = h:readbyte(h)
|
|
local i3 = h:readbyte(h)
|
|
local i4 = h:readbyte(h)
|
|
local i = bit.blshift(i1, 24) + bit.blshift(i2, 16) + bit.blshift(i3, 8) + i4
|
|
|
|
if not self.length or not self.width then
|
|
|
|
self:discardBytes(h,i, spinner)
|
|
self.twopass = true
|
|
|
|
elseif name == "Blocks" then
|
|
for i = 1, i do
|
|
local id = h:readbyte(h)
|
|
self:assignCoord(i, id)
|
|
if (i % 1000) == 0 then
|
|
spinner:spin()
|
|
end
|
|
end
|
|
elseif name == "Data" then
|
|
for i = 1, i do
|
|
local dmg = h:readbyte(h)
|
|
if dmg > 0 then
|
|
self.damages[i] = dmg
|
|
end
|
|
if (i % 1000) == 0 then
|
|
spinner:spin()
|
|
end
|
|
end
|
|
else
|
|
self:discardBytes(h,i, spinner)
|
|
end
|
|
elseif a==8 then
|
|
local i1 = h:readbyte(h)
|
|
local i2 = h:readbyte(h)
|
|
local i = i1*256 + i2
|
|
self:discardBytes(h,i, spinner)
|
|
elseif a==9 then
|
|
local type = h:readbyte(h)
|
|
local i1 = h:readbyte(h)
|
|
local i2 = h:readbyte(h)
|
|
local i3 = h:readbyte(h)
|
|
local i4 = h:readbyte(h)
|
|
local i = bit.blshift(i1, 24) + bit.blshift(i2, 16) + bit.blshift(i3, 8) + i4
|
|
|
|
for j=1,i do
|
|
self:parse(type, h, false, spinner)
|
|
end
|
|
elseif a > 11 then
|
|
error('invalid tag')
|
|
end
|
|
end
|
|
-- end http://www.computercraft.info/forums2/index.php?/topic/1949-turtle-schematic-file-builder/
|
|
|
|
function Schematic:copyBlocks(iblocks, oblocks, throttle)
|
|
for k,b in ipairs(iblocks) do
|
|
oblocks[k] = Util.shallowCopy(b)
|
|
if (k % 1000) == 0 then
|
|
throttle()
|
|
end
|
|
end
|
|
end
|
|
|
|
function Schematic:reload(throttle)
|
|
self.blocks = { }
|
|
self:copyBlocks(self.originalBlocks, self.blocks, throttle)
|
|
|
|
for _,ri in pairs(self.rowIndex) do
|
|
ri.loaded = false
|
|
end
|
|
end
|
|
|
|
function Schematic:getMagic(fh)
|
|
fh:open()
|
|
|
|
local magic = fh:readbyte() * 256 + fh:readbyte()
|
|
|
|
fh:close()
|
|
|
|
return magic
|
|
end
|
|
|
|
function Schematic:isCompressed(filename)
|
|
local h = fs.open(filename, "rb")
|
|
|
|
if not h then
|
|
error('unable to open: ' .. filename)
|
|
end
|
|
|
|
local magic = h.read() * 256 + h.read()
|
|
|
|
h.close()
|
|
|
|
return magic == gzipMagic
|
|
end
|
|
|
|
function Schematic:checkFileType(fh)
|
|
|
|
local magic = self:getMagic(fh)
|
|
if magic ~= schematicMagic then
|
|
error('Unknown file type')
|
|
end
|
|
end
|
|
|
|
local DiskFile = class()
|
|
function DiskFile:init(args)
|
|
Util.merge(self, args)
|
|
end
|
|
|
|
function DiskFile:open()
|
|
self.h = fs.open(self.filename, "rb")
|
|
if not self.h then
|
|
error('unable to open: ' .. self.filename)
|
|
end
|
|
end
|
|
function DiskFile:readbyte()
|
|
return self.h.read()
|
|
end
|
|
function DiskFile:close()
|
|
self.h.close()
|
|
end
|
|
|
|
local MemoryFile = class()
|
|
function MemoryFile:init(args)
|
|
self.s = { }
|
|
self.i = 1
|
|
end
|
|
function MemoryFile:open(filename)
|
|
self.i = 1
|
|
end
|
|
function MemoryFile:close() end
|
|
|
|
function MemoryFile:readbyte()
|
|
local b = self.s[self.i]
|
|
self.i = self.i + 1
|
|
return b
|
|
end
|
|
|
|
function MemoryFile:write(b)
|
|
self.s[#self.s + 1] = b
|
|
end
|
|
|
|
function Schematic:decompress(ifname, spinner)
|
|
|
|
local ifh = fs.open(ifname, "rb")
|
|
if not ifh then
|
|
error('Unable to open ' .. ifname)
|
|
end
|
|
|
|
local mh = MemoryFile()
|
|
|
|
DEFLATE.gunzip({
|
|
input=function(...) spinner:spin() return ifh.read() end,
|
|
output=function(b) mh:write(b) end,
|
|
disable_crc=true
|
|
})
|
|
|
|
ifh.close()
|
|
|
|
spinner:stop()
|
|
|
|
return mh
|
|
end
|
|
|
|
function Schematic:loadpass(fh, spinner)
|
|
|
|
fh:open()
|
|
|
|
while true do
|
|
local a = fh:readbyte()
|
|
|
|
if not a then
|
|
break
|
|
end
|
|
self:parse(a, fh, true, spinner)
|
|
if self.twopass and self.width and self.length then
|
|
break
|
|
end
|
|
|
|
spinner:spin()
|
|
end
|
|
|
|
fh:close()
|
|
|
|
self:assignDamages(spinner)
|
|
self.damages = nil
|
|
|
|
self:copyBlocks(self.blocks, self.originalBlocks, function() spinner:spin() end)
|
|
|
|
spinner:stop()
|
|
end
|
|
|
|
function Schematic:load(filename)
|
|
|
|
local cursorX, cursorY = term.getCursorPos()
|
|
local spinner = UI.Spinner({
|
|
x = UI.term.width,
|
|
y = cursorY - 1
|
|
})
|
|
local f
|
|
|
|
if self:isCompressed(filename) then
|
|
local originalFile = filename
|
|
filename = originalFile .. '.uncompressed'
|
|
|
|
if not fs.exists(filename) then
|
|
print('Decompressing')
|
|
f = self:decompress(originalFile, spinner)
|
|
end
|
|
end
|
|
|
|
self.filename = string.match(filename, '([^/]+)$')
|
|
|
|
if not f then
|
|
f = DiskFile({ filename = filename })
|
|
end
|
|
|
|
self:checkFileType(f)
|
|
|
|
print('Initial pass ')
|
|
self:loadpass(f, spinner)
|
|
|
|
if self.twopass then
|
|
self.twopass = nil
|
|
self.blocks = { }
|
|
self.damages = { }
|
|
self.originalBlocks = { }
|
|
self.x, self.y, self.z = 0, 0, 0
|
|
self.height = 0
|
|
self.index = 1
|
|
|
|
print('Second pass ')
|
|
self:loadpass(f, spinner)
|
|
end
|
|
|
|
self.rowIndex = { }
|
|
for k,b in ipairs(self.blocks) do
|
|
local ri = self.rowIndex[b.y]
|
|
if not ri then
|
|
self.rowIndex[b.y] = { s = k, e = k }
|
|
else
|
|
ri.e = k
|
|
end
|
|
end
|
|
|
|
self.cache = Util.readTable('usr/builder/' .. self.filename .. '.cache') or { }
|
|
end
|
|
|
|
function Schematic:assignCoord(i, id)
|
|
|
|
if id > 0 then
|
|
table.insert(self.blocks, {
|
|
id = id,
|
|
index = i,
|
|
x = self.x,
|
|
z = self.z,
|
|
y = self.y,
|
|
})
|
|
end
|
|
|
|
self.x = self.x + 1
|
|
if self.x >= self.width then
|
|
self.x = 0
|
|
self.z = self.z + 1
|
|
end
|
|
if self.z >= self.length then
|
|
self.z = 0
|
|
self.y = self.y + 1
|
|
end
|
|
if self.y >= self.height then
|
|
self.height = self.y + 1
|
|
end
|
|
end
|
|
|
|
function Schematic:assignDamages(spinner)
|
|
print('Assigning damages')
|
|
|
|
local i = 0
|
|
for _,b in pairs(self.blocks) do
|
|
b.dmg = self.damages[b.index] or 0
|
|
i = i + 1
|
|
if (i % 1000) == 0 then
|
|
spinner:spin()
|
|
end
|
|
end
|
|
end
|
|
|
|
function Schematic:findIndexAt(x, z, y)
|
|
if y < 0 then
|
|
return
|
|
end
|
|
|
|
local ri = self.rowIndex[y]
|
|
if ri then
|
|
for i = ri.s, ri.e do
|
|
local b = self.blocks[i]
|
|
if b.x == x and b.z == z and b.y == y then
|
|
if b.id == 'minecraft:air' then
|
|
-- this will definitely screw up placement order if a substition is made with air after starting
|
|
-- as blocks will be placed differently and could have a different heading
|
|
break
|
|
end
|
|
return i, b
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Schematic:findBlockAtSide(b, side)
|
|
local hi = turtle.getHeadingInfo(side)
|
|
local index = self:findIndexAt(b.x + hi.xd, b.z + hi.zd, b.y + hi.yd)
|
|
if index then
|
|
return self.blocks[index] -- could be better
|
|
end
|
|
end
|
|
|
|
function Schematic:addPlacementChain(chains, chain)
|
|
local t = { }
|
|
for _,v in ipairs(chain) do
|
|
local k = self:findIndexAt(v.x, v.z, v.y)
|
|
if k then
|
|
local b = self.blocks[k] -- could be better
|
|
b.index = v.y * self.width * self.length + v.z * self.width + v.x + 1
|
|
table.insert(t, b)
|
|
end
|
|
end
|
|
if #t > 1 then
|
|
local keys = { }
|
|
for _,b in pairs(t) do
|
|
keys[b.index] = true
|
|
end
|
|
table.insert(chains, {
|
|
blocks = t,
|
|
keys = keys
|
|
})
|
|
end
|
|
end
|
|
|
|
function Schematic:bestSide(b, chains, ...)
|
|
local directions = { ... }
|
|
local blocks = { }
|
|
|
|
for k,d in pairs(directions) do
|
|
local hi = turtle.getHeadingInfo(d)
|
|
local sb = self:findIndexAt(b.x - hi.xd, b.z - hi.zd, b.y)
|
|
if not sb then
|
|
b.heading = turtle.getHeadingInfo(d).heading
|
|
b.direction = d .. '-block'
|
|
return
|
|
end
|
|
blocks[k] = {
|
|
b = self.blocks[sb],
|
|
hi = hi,
|
|
d = d
|
|
}
|
|
end
|
|
|
|
local bestBlock
|
|
for _,sb in ipairs(blocks) do
|
|
if not sb.b.direction then -- could be better
|
|
bestBlock = sb
|
|
break
|
|
end
|
|
end
|
|
|
|
if not bestBlock then
|
|
local sideDirections = {
|
|
[ 'east-block' ] = 'east',
|
|
[ 'south-block' ] = 'south',
|
|
[ 'west-block' ] = 'west',
|
|
[ 'north-block' ] = 'north'
|
|
}
|
|
for _,sb in ipairs(blocks) do
|
|
if not bestBlock then
|
|
bestBlock = sb
|
|
end
|
|
if not sideDirections[sb.b.direction] then
|
|
bestBlock = sb
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local hi = bestBlock.hi
|
|
b.heading = hi.heading -- ?????????????????????????????????
|
|
b.direction = bestBlock.d .. '-block'
|
|
self:addPlacementChain(chains, {
|
|
{ x = b.x, z = b.z, y = b.y },
|
|
{ x = b.x - hi.xd, z = b.z - hi.zd, y = b.y }
|
|
})
|
|
end
|
|
|
|
function Schematic:bestFlipSide(b, chains)
|
|
-- If there is a block to place this one against
|
|
|
|
local directions = {
|
|
[ 'east-block-flip' ] = 'east',
|
|
[ 'west-block-flip' ] = 'west',
|
|
[ 'north-block-flip' ] = 'north',
|
|
[ 'south-block-flip' ] = 'south',
|
|
}
|
|
|
|
local d = directions[b.direction]
|
|
local hi = turtle.getHeadingInfo(d)
|
|
local _, fb = self:findIndexAt(b.x + hi.xd, b.z + hi.zd, b.y)
|
|
|
|
if fb then
|
|
self:addPlacementChain(chains, {
|
|
{ x = b.x + hi.xd, z = b.z + hi.zd, y = b.y }, -- block we are placing against
|
|
{ x = b.x, z = b.z, y = b.y }, -- the block (or torch, etc)
|
|
{ x = b.x - hi.xd, z = b.z - hi.zd, y = b.y }, -- room for the turtle
|
|
})
|
|
b.direction = d .. '-block'
|
|
else
|
|
self:addPlacementChain(chains, {
|
|
{ x = b.x, z = b.z, y = b.y }, -- the block (or torch, etc)
|
|
{ x = b.x - hi.xd, z = b.z - hi.zd, y = b.y }, -- room for the turtle
|
|
{ x = b.x + hi.xd, z = b.z + hi.zd, y = b.y }, -- block we are placing against
|
|
})
|
|
b.direction = turtle.getHeadingInfo((hi.heading + 2) % 4).direction .. '-block'
|
|
end
|
|
end
|
|
|
|
function Schematic:bestOfTwoSides(b, chains, side1, side2) -- could be better
|
|
|
|
local sb
|
|
local fb = b -- first block
|
|
local lb = b -- last block
|
|
local od = b.direction -- original direction
|
|
|
|
-- find the last block in the row with the same two-sided direction
|
|
while true do
|
|
sb = self:findBlockAtSide(lb, side2)
|
|
if not sb or sb.direction ~= b.direction then
|
|
break
|
|
end
|
|
lb = sb
|
|
end
|
|
|
|
-- find the first block
|
|
while true do
|
|
sb = self:findBlockAtSide(fb, side1)
|
|
if not sb or sb.direction ~= b.direction then
|
|
break
|
|
end
|
|
fb = sb
|
|
end
|
|
|
|
-- set the placement order to side1 -> side2
|
|
if fb ~= lb then -- only 1 block
|
|
|
|
local pc = { } -- placementChain
|
|
b = fb
|
|
|
|
while true do
|
|
|
|
table.insert(pc, { x = b.x, z = b.z, y = b.y })
|
|
|
|
b.direction = side1 .. '-block'
|
|
b.heading = turtle.getHeadingInfo(side1).heading
|
|
|
|
if b == lb then
|
|
break
|
|
end
|
|
|
|
b = self:findBlockAtSide(b, side2)
|
|
end
|
|
|
|
self:addPlacementChain(chains, pc)
|
|
end
|
|
|
|
-- can we place the first block from the side (instead of using piston) ?
|
|
sb = self:findBlockAtSide(fb, side1)
|
|
if not sb then
|
|
local ub = self:findBlockAtSide(fb, 'down')
|
|
if not ub then
|
|
fb.direction = side1 .. '-block'
|
|
fb.heading = turtle.getHeadingInfo(side1).heading
|
|
else
|
|
fb.direction = od
|
|
end
|
|
else -- really should use placement chain
|
|
fb.direction = od
|
|
end
|
|
|
|
-- can we place the last block from the side (instead of using piston) ?
|
|
sb = self:findBlockAtSide(lb, side2)
|
|
if not sb then
|
|
local ub = self:findBlockAtSide(lb, 'down')
|
|
if not ub then
|
|
lb.direction = side1 .. '-block'
|
|
lb.heading = turtle.getHeadingInfo(side1).heading
|
|
else
|
|
fb.direction = od
|
|
end
|
|
else
|
|
lb.direction = od
|
|
end
|
|
|
|
end
|
|
|
|
-- Determine the best way to place each block
|
|
function Schematic:determineBlockPlacement(y)
|
|
|
|
-- NOTE: blocks are evaluated top to bottom
|
|
|
|
print('Processing level ' .. y)
|
|
|
|
local spinner = UI.Spinner({
|
|
x = 1,
|
|
spinSymbols = { 'o.....', '.o....', '..o...', '...o..', '....o.', '.....o' }
|
|
})
|
|
local stairDownDirections = {
|
|
[ 'north-down' ] = 'north',
|
|
[ 'south-down' ] = 'south',
|
|
[ 'east-down' ] = 'east',
|
|
[ 'west-down' ] = 'west'
|
|
}
|
|
local stairUpDirections = {
|
|
[ 'east-up' ] = { 'east', 'east-block', 1, 0, 'west-block' },
|
|
[ 'west-up' ] = { 'west', 'west-block', -1, 0, 'east-block' },
|
|
[ 'north-up' ] = { 'north', 'north-block', 0, -1, 'south-block' },
|
|
[ 'south-up' ] = { 'south', 'south-block', 0, 1, 'north-block' }
|
|
}
|
|
local twoSideDirections = {
|
|
[ 'east-west-block' ] = true,
|
|
[ 'north-south-block' ] = true,
|
|
}
|
|
local directions = {
|
|
[ 'north' ] = 'north',
|
|
[ 'south' ] = 'south',
|
|
[ 'east' ] = 'east',
|
|
[ 'west' ] = 'west',
|
|
}
|
|
local blockDirections = {
|
|
[ 'east-block' ] = 'east',
|
|
[ 'south-block' ] = 'south',
|
|
[ 'west-block' ] = 'west',
|
|
[ 'north-block' ] = 'north',
|
|
}
|
|
local doorDirections = {
|
|
[ 'east-door' ] = 'east',
|
|
[ 'south-door' ] = 'south',
|
|
[ 'west-door' ] = 'west',
|
|
[ 'north-door' ] = 'north',
|
|
}
|
|
local vineDirections = {
|
|
[ 'east-block-vine' ] = 'east-block',
|
|
[ 'south-block-vine' ] = 'south-block',
|
|
[ 'west-block-vine' ] = 'west-block',
|
|
[ 'north-block-vine' ] = 'north-block'
|
|
}
|
|
local flipDirections = {
|
|
[ 'east-block-flip' ] = 'east-block',
|
|
[ 'south-block-flip' ] = 'south-block',
|
|
[ 'west-block-flip' ] = 'west-block',
|
|
[ 'north-block-flip' ] = 'north-block'
|
|
}
|
|
|
|
local dirtyBlocks = {}
|
|
local dirtyBlocks2 = {}
|
|
local chains = {}
|
|
|
|
local ri = self.rowIndex[y]
|
|
if not ri then
|
|
ri = { s = -1, e = -2 }
|
|
self.rowIndex[y] = ri
|
|
end
|
|
|
|
for k = ri.s, ri.e do
|
|
local b = self.blocks[k]
|
|
local d = b.direction
|
|
|
|
if d then
|
|
if vineDirections[d] then
|
|
local _, aboveBlock = self:findIndexAt(b.x, b.z, b.y+1)
|
|
|
|
if aboveBlock and aboveBlock.id == b.id and aboveBlock.dmg == b.dmg and aboveBlock.direction == d then
|
|
-- only need to place top vine
|
|
b.id = 'minecraft:air'
|
|
b.dmg = 0
|
|
b.direction = nil
|
|
else
|
|
b.direction = vineDirections[d]
|
|
table.insert(dirtyBlocks, b)
|
|
end
|
|
elseif twoSideDirections[d] then
|
|
table.insert(dirtyBlocks2, b)
|
|
else
|
|
table.insert(dirtyBlocks, b)
|
|
end
|
|
spinner:spin(#dirtyBlocks + #dirtyBlocks2 .. ' blocks remaining ')
|
|
end
|
|
end
|
|
|
|
-- Util.filterInplace(dirtyBlocks, function(b) return b.id ~= 'minecraft:air' end)
|
|
|
|
-- remove directional info from slabs where possible
|
|
-- iterate backwards to process top planes first
|
|
for k = #dirtyBlocks, 1, -1 do
|
|
local b = dirtyBlocks[k]
|
|
local d = b.direction
|
|
|
|
if d == 'top' then
|
|
-- slab occupying top of voxel
|
|
-- can be placed from the top if there is no block below
|
|
local belowBlock = self:findIndexAt(b.x, b.z, b.y-1)
|
|
if not belowBlock then
|
|
b.direction = nil
|
|
table.remove(dirtyBlocks, k)
|
|
end
|
|
elseif d == 'bottom' then
|
|
b.bottom = true -- flag this as a bottom block
|
|
local _,db = self:findIndexAt(b.x, b.z, b.y-1)
|
|
if db then
|
|
if not db.direction or db.direction ~= 'bottom' then
|
|
-- not a slab below, ok to place from above
|
|
if not db.bottom then
|
|
b.direction = nil
|
|
end
|
|
end
|
|
-- it is a slab below - must be pistoned
|
|
table.remove(dirtyBlocks, k)
|
|
end
|
|
end
|
|
spinner:spin(#dirtyBlocks + #dirtyBlocks2 .. ' blocks remaining ')
|
|
end
|
|
|
|
-- iterate through the directional blocks setting the placement strategy
|
|
while #dirtyBlocks > 0 do
|
|
local b = table.remove(dirtyBlocks)
|
|
local d = b.direction or ''
|
|
|
|
spinner:spin(#dirtyBlocks + #dirtyBlocks2 .. ' blocks remaining ')
|
|
|
|
if directions[d] then
|
|
b.heading = turtle.getHeadingInfo(directions[d]).heading
|
|
end
|
|
|
|
if doorDirections[d] then
|
|
|
|
local hi = turtle.getHeadingInfo(doorDirections[d])
|
|
b.heading = hi.heading
|
|
b.twoHigh = true
|
|
|
|
self:addPlacementChain(chains, {
|
|
{ x = b.x, z = b.z, y = b.y },
|
|
{ x = b.x - hi.xd, z = b.z - hi.zd, y = b.y },
|
|
})
|
|
end
|
|
|
|
if stairDownDirections[d] then
|
|
if not self:findIndexAt(b.x, b.z, b.y-1) then
|
|
b.direction = stairDownDirections[b.direction]
|
|
b.heading = turtle.getHeadingInfo(b.direction).heading
|
|
else
|
|
b.heading = turtle.getHeadingInfo(stairDownDirections[b.direction]).heading
|
|
end
|
|
end
|
|
|
|
if d == 'bottom' then
|
|
-- slab occupying bottom of voxel
|
|
-- can be placed from top if a block is below
|
|
-- otherwise, needs to be placed from side
|
|
|
|
-- except... if the block below is a slab :(
|
|
--local _,db = self:findIndexAt(b.x, b.z, b.y-1)
|
|
--if not db then
|
|
-- no block below, place from side
|
|
|
|
-- took care of all other cases above
|
|
self:bestSide(b, chains, 'east', 'south', 'west', 'north')
|
|
|
|
-- elseif not db.direction or db.direction ~= 'bottom' then
|
|
-- not a slab below, ok to place from above
|
|
-- b.direction = nil
|
|
--end
|
|
-- otherwise, builder will piston it in from above
|
|
|
|
elseif stairUpDirections[d] then
|
|
-- a directional stair
|
|
-- turtle can place correctly from above if there is a block below
|
|
-- otherwise, the turtle must place the block from the same plane
|
|
-- against another block
|
|
-- if no block to place against (from side) then the turtle must place from
|
|
-- the other side
|
|
--
|
|
-- Stair bug in 1.7 - placing a stair southward doesn't respect the turtle's direction
|
|
-- all other directions are fine
|
|
-- any stair southwards that can't be placed against another block must be pistoned
|
|
local sd = stairUpDirections[d]
|
|
|
|
if self:findIndexAt(b.x, b.z, b.y-1) then
|
|
-- there's a block below
|
|
b.direction = sd[1]
|
|
b.heading = turtle.getHeadingInfo(b.direction).heading
|
|
else
|
|
local _,pb = self:findIndexAt(b.x + sd[3], b.z + sd[4], b.y)
|
|
if pb and pb.direction ~= sd[5] then
|
|
-- place stair against another block (that's not relying on this block to be down first)
|
|
d = sd[2] -- fall through to the blockDirections code below
|
|
b.direction = sd[2]
|
|
else
|
|
b.heading = (turtle.getHeadingInfo(sd[1]).heading + 2) % 4
|
|
end
|
|
end
|
|
elseif flipDirections[d] then
|
|
self:bestFlipSide(b, chains)
|
|
end
|
|
|
|
if blockDirections[d] then
|
|
-- placing a block from the side
|
|
local hi = turtle.getHeadingInfo(blockDirections[d])
|
|
b.heading = hi.heading
|
|
self:addPlacementChain(chains, {
|
|
{ x = b.x + hi.xd, z = b.z + hi.zd, y = b.y }, -- block we are placing against
|
|
{ x = b.x, z = b.z, y = b.y }, -- the block (or torch, etc)
|
|
{ x = b.x - hi.xd, z = b.z - hi.zd, y = b.y } -- room for the turtle
|
|
})
|
|
end
|
|
end
|
|
|
|
-- pass 3
|
|
while #dirtyBlocks2 > 0 do
|
|
local b = table.remove(dirtyBlocks2)
|
|
local d = b.direction
|
|
|
|
spinner:spin(#dirtyBlocks2 .. ' blocks remaining ')
|
|
|
|
if d == 'east-west-block' then
|
|
self:bestOfTwoSides(b, chains, 'east', 'west')
|
|
elseif d == 'north-south-block' then
|
|
self:bestOfTwoSides(b, chains, 'north', 'south')
|
|
end
|
|
end
|
|
|
|
term.clearLine()
|
|
|
|
self:setPlacementOrder(spinner, chains)
|
|
local plane = self:optimizeRoute(spinner, y)
|
|
|
|
term.clearLine()
|
|
spinner:stop()
|
|
|
|
for k,b in ipairs(plane) do
|
|
self.blocks[ri.s + k - 1] = b
|
|
end
|
|
end
|
|
|
|
function Schematic:getComputedBlock(i)
|
|
local b = self.blocks[i]
|
|
|
|
-- has this level been computed ?
|
|
if not self.rowIndex[b.y].loaded then
|
|
-- compute each level up til this one (unless saved in cache)
|
|
for y = 0, b.y - 1 do
|
|
if not self.cache[y] then
|
|
self:determineBlockPlacement(y)
|
|
end
|
|
end
|
|
self:determineBlockPlacement(b.y)
|
|
-- get the block now at the computed location
|
|
b = self.blocks[i]
|
|
end
|
|
|
|
return b
|
|
end
|
|
|
|
-- set the order for block dependencies
|
|
function Schematic:setPlacementOrder(spinner, placementChains)
|
|
|
|
local cursorX, cursorY = term.getCursorPos()
|
|
|
|
-- optimize for overlapping check
|
|
for _,chain in pairs(placementChains) do
|
|
for index,_ in pairs(chain.keys) do
|
|
if not chain.startRow or (index < chain.startRow) then
|
|
chain.startRow = index
|
|
end
|
|
if not chain.endRow or (index > chain.endRow) then
|
|
chain.endRow = index
|
|
end
|
|
end
|
|
end
|
|
|
|
local function groupOverlappingChains(t, groupedChain, chain, spinner)
|
|
local found = true
|
|
|
|
local function overlaps(chain1, chain2)
|
|
if chain1.startRow > chain2.endRow or
|
|
chain2.startRow > chain1.endRow then
|
|
return false
|
|
end
|
|
for k,_ in pairs(chain1.keys) do
|
|
if chain2.keys[k] then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
while found do
|
|
found = false
|
|
for k, v in pairs(t) do
|
|
local o = overlaps(chain, v)
|
|
if o then
|
|
table.remove(t, k)
|
|
table.insert(groupedChain, v)
|
|
groupOverlappingChains(t, groupedChain, v, spinner)
|
|
spinner:spin()
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- group together any placement chains with overlapping blocks
|
|
local groupedChains = {}
|
|
while #placementChains > 0 do
|
|
local groupedChain = {}
|
|
local chain = table.remove(placementChains)
|
|
table.insert(groupedChain, chain)
|
|
table.insert(groupedChains, groupedChain)
|
|
groupOverlappingChains(placementChains, groupedChain, chain, spinner)
|
|
spinner:spin('chains: ' .. #groupedChains .. ' ' .. #placementChains .. ' ')
|
|
end
|
|
|
|
--Logger.log('schematic', 'groups: ' .. #groupedChains)
|
|
--Logger.setFileLogging('chains')
|
|
|
|
local function mergeChains(chains)
|
|
|
|
--[[
|
|
Logger.debug('---------------')
|
|
Logger.log('schematic', 'mergeChains: ' .. #chains)
|
|
for _,chain in ipairs(chains) do
|
|
Logger.log('schematic', chain)
|
|
for _,e in ipairs(chain) do
|
|
Logger.log('schematic', string.format('%d:%d:%d %s %d:%d',
|
|
e.block.x, e.block.z, e.block.y, tostring(e.block.direction), e.block.id, e.block.dmg))
|
|
end
|
|
end
|
|
]]--
|
|
|
|
local masterChain = table.remove(chains)
|
|
|
|
--[[ it's something like this:
|
|
|
|
A chain B chain result
|
|
1 1
|
|
2 -------- 2 2
|
|
3 3
|
|
4 4
|
|
5 5
|
|
6 -------- 6 6
|
|
7 7
|
|
--]]
|
|
local function splice(chain1, chain2)
|
|
for k,v in ipairs(chain1) do
|
|
for k2,v2 in ipairs(chain2) do
|
|
if v == v2 then
|
|
local index = k
|
|
local dupe
|
|
for i = k2-1, 1, -1 do
|
|
dupe = false
|
|
-- traverse back through the first chain aligning on matches
|
|
for j = index-1, 1, -1 do
|
|
if chain1[j] == chain2[i] then
|
|
index = j
|
|
dupe = true
|
|
break
|
|
end
|
|
end
|
|
if not dupe then
|
|
table.insert(chain1, index, chain2[i])
|
|
end
|
|
end
|
|
index = k+1
|
|
for i = k2+1, #chain2, 1 do
|
|
dupe = false
|
|
for j = index, #chain1, 1 do
|
|
if chain1[j] == chain2[i] then
|
|
index = j
|
|
dupe = true
|
|
break
|
|
end
|
|
end
|
|
if not dupe then
|
|
table.insert(chain1, index, chain2[i])
|
|
end
|
|
index = index + 1
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
while #chains > 0 do
|
|
for k,chain in pairs(chains) do
|
|
if splice(masterChain.blocks, chain.blocks) then
|
|
table.remove(chains, k)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[
|
|
Logger.log('schematic', 'master chain: ')
|
|
Logger.log('schematic', masterChain)
|
|
Logger.log('schematic', '---------------')
|
|
for _,e in ipairs(masterChain.blocks) do
|
|
Logger.log('schematic', string.format('%d:%d:%d %s %s:%d',
|
|
e.x, e.z, e.y, tostring(e.direction), e.id, e.dmg))
|
|
end
|
|
--]]
|
|
|
|
return masterChain
|
|
end
|
|
|
|
-- combine the individual overlapping placement chains into 1 long master chain
|
|
local masterChains = {}
|
|
for _,group in pairs(groupedChains) do
|
|
spinner:spin('chains: ' .. #masterChains)
|
|
table.insert(masterChains, mergeChains(group))
|
|
end
|
|
|
|
local function removeDuplicates(chain)
|
|
for k,v in ipairs(chain) do
|
|
for i = #chain, k+1, -1 do
|
|
if v == chain[i] then
|
|
v.info = 'Unplaceable'
|
|
table.remove(chain, i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- any chains with duplicates cannot be placed correctly
|
|
-- there are some cases where a turtle cannot place blocks the same as a player
|
|
for _,chain in pairs(masterChains) do
|
|
removeDuplicates(chain.blocks)
|
|
spinner:spin('chains: ' .. #masterChains)
|
|
|
|
--[[
|
|
Logger.log('schematic', "MASTER CHAIN")
|
|
for _,e in ipairs(chain) do
|
|
Logger.log('schematic', string.format('%d:%d:%d %s %d:%d',
|
|
e.block.x, e.block.z, e.block.y, tostring(e.block.direction), e.block.id, e.block.dmg))
|
|
end
|
|
--]]
|
|
|
|
end
|
|
term.clearLine()
|
|
|
|
-- set dependent blocks for optimize routine
|
|
for k,chain in pairs(masterChains) do
|
|
spinner:spin('chains: ' .. #masterChains - k)
|
|
|
|
local prev
|
|
for k,b in ipairs(chain.blocks) do
|
|
b.prev = prev
|
|
b.next = chain.blocks[k + 1]
|
|
prev = b
|
|
end
|
|
end
|
|
|
|
term.clearLine()
|
|
|
|
return t
|
|
end
|
|
|
|
function Schematic:optimizeRoute(spinner, y)
|
|
|
|
local function getNearestNeighbor(p, pt, maxDistance)
|
|
local key, block, heading
|
|
local moves = maxDistance
|
|
|
|
local function getMoves(b, k)
|
|
local distance = math.abs(pt.x - b.x) + math.abs(pt.z - b.z)
|
|
|
|
if distance < moves then
|
|
-- this operation is expensive - only run if distance is close
|
|
local c, h = Point.calculateMoves(pt, b, distance)
|
|
if c < moves then
|
|
block = b
|
|
key = k
|
|
moves = c
|
|
heading = h
|
|
end
|
|
end
|
|
end
|
|
|
|
local function blockReady(b)
|
|
if b.u then
|
|
return false
|
|
end
|
|
if b.prev and not b.prev.u then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local mid = pt.index
|
|
local forward = mid + 1
|
|
local backward = mid - 1
|
|
while forward <= #p or backward > 0 do
|
|
if forward <= #p then
|
|
local b = p[forward]
|
|
if blockReady(b) then
|
|
getMoves(b, forward)
|
|
if moves <= 1 then
|
|
break
|
|
end
|
|
if moves < maxDistance and math.abs(b.z - pt.z) > moves and pt.index > 0 then
|
|
forward = #p
|
|
end
|
|
end
|
|
forward = forward + 1
|
|
end
|
|
if backward > 0 then
|
|
local b = p[backward]
|
|
if blockReady(b) then
|
|
getMoves(b, backward)
|
|
if moves <= 1 then
|
|
break
|
|
end
|
|
if moves < maxDistance and math.abs(pt.z - b.z) > moves then
|
|
backward = 0
|
|
end
|
|
end
|
|
backward = backward - 1
|
|
end
|
|
end
|
|
pt.x = block.x
|
|
pt.z = block.z
|
|
pt.y = block.y
|
|
pt.heading = heading
|
|
pt.index = key
|
|
block.u = true
|
|
return block
|
|
end
|
|
|
|
local pt = Util.shallowCopy(self.cache[y - 1] or turtle.point)
|
|
local t = {}
|
|
local ri = self.rowIndex[y]
|
|
local blockCount = ri.e - ri.s + 1
|
|
|
|
local function extractPlane()
|
|
local t = {}
|
|
local dt = {}
|
|
for i = ri.s, ri.e do
|
|
local b = self.blocks[i]
|
|
if b.twoHigh then
|
|
b.last = true
|
|
while b.next do
|
|
b = b.next
|
|
b.last = true
|
|
end
|
|
end
|
|
end
|
|
for i = ri.s, ri.e do
|
|
local b = self.blocks[i]
|
|
if b.last then
|
|
table.insert(dt, b)
|
|
else
|
|
table.insert(t, b)
|
|
end
|
|
end
|
|
return t, dt
|
|
end
|
|
|
|
local maxDistance = self.width*self.length
|
|
local plane, doors = extractPlane(y)
|
|
spinner:spin(percent)
|
|
pt.index = 0
|
|
for i = 1, #plane do
|
|
local b = getNearestNeighbor(plane, pt, maxDistance)
|
|
table.insert(t, b)
|
|
local percent = math.floor(#t * 100 / blockCount) .. '%'
|
|
spinner:spin(percent .. ' ' .. blockCount - i .. ' ')
|
|
end
|
|
-- all two high blocks are placed last on each plane
|
|
pt.index = 0
|
|
for i = 1, #doors do
|
|
local b = getNearestNeighbor(doors, pt, maxDistance)
|
|
table.insert(t, b)
|
|
local percent = math.floor(#t * 100 / blockCount) .. '%'
|
|
spinner:spin(percent .. ' ' .. blockCount - #plane - i .. ' ')
|
|
end
|
|
|
|
self.rowIndex[y].loaded = true
|
|
if not self.cache[y] then
|
|
self.cache[y] = Util.shallowCopy(pt)
|
|
Util.writeTable('usr/builder/' .. self.filename .. '.cache', self.cache)
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
return Schematic
|