diff --git a/apis/base64.lua b/apis/base64.lua new file mode 100644 index 0000000..767b86d --- /dev/null +++ b/apis/base64.lua @@ -0,0 +1,135 @@ +-- Base64 Encoder / Decoder +-- By KillaVanilla +-- see: http://www.computercraft.info/forums2/index.php?/topic/12450-killavanillas-various-apis/ + +Base64 = { } + +local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +local function sixBitToBase64(input) + return string.sub(alphabet, input+1, input+1) +end + +local function base64ToSixBit(input) + for i=1, 64 do + if input == string.sub(alphabet, i, i) then + return i-1 + end + end +end + +local function octetToBase64(o1, o2, o3) + local i1 = sixBitToBase64(bit.brshift(bit.band(o1, 0xFC), 2)) + local i2 = "A" + local i3 = "=" + local i4 = "=" + if o2 then + i2 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o1, 3), 4), bit.brshift(bit.band(o2, 0xF0), 4) )) + if not o3 then + i3 = sixBitToBase64(bit.blshift(bit.band(o2, 0x0F), 2)) + else + i3 = sixBitToBase64(bit.bor( bit.blshift(bit.band(o2, 0x0F), 2), bit.brshift(bit.band(o3, 0xC0), 6) )) + end + else + i2 = sixBitToBase64(bit.blshift(bit.band(o1, 3), 4)) + end + if o3 then + i4 = sixBitToBase64(bit.band(o3, 0x3F)) + end + + return i1..i2..i3..i4 +end + +-- octet 1 needs characters 1/2 +-- octet 2 needs characters 2/3 +-- octet 3 needs characters 3/4 + +local function base64ToThreeOctet(s1) + local c1 = base64ToSixBit(string.sub(s1, 1, 1)) + local c2 = base64ToSixBit(string.sub(s1, 2, 2)) + local c3 = 0 + local c4 = 0 + local o1 = 0 + local o2 = 0 + local o3 = 0 + if string.sub(s1, 3, 3) == "=" then + c3 = nil + c4 = nil + elseif string.sub(s1, 4, 4) == "=" then + c3 = base64ToSixBit(string.sub(s1, 3, 3)) + c4 = nil + else + c3 = base64ToSixBit(string.sub(s1, 3, 3)) + c4 = base64ToSixBit(string.sub(s1, 4, 4)) + end + o1 = bit.bor( bit.blshift(c1, 2), bit.brshift(bit.band( c2, 0x30 ), 4) ) + if c3 then + o2 = bit.bor( bit.blshift(bit.band(c2, 0x0F), 4), bit.brshift(bit.band( c3, 0x3C ), 2) ) + else + o2 = nil + end + if c4 then + o3 = bit.bor( bit.blshift(bit.band(c3, 3), 6), c4 ) + else + o3 = nil + end + return o1, o2, o3 +end + +local function splitIntoBlocks(bytes) + local blockNum = 1 + local blocks = {} + for i=1, #bytes, 3 do + blocks[blockNum] = {bytes[i], bytes[i+1], bytes[i+2]} + --[[ + if #blocks[blockNum] < 3 then + for j=#blocks[blockNum]+1, 3 do + table.insert(blocks[blockNum], 0) + end + end + ]] + blockNum = blockNum+1 + end + return blocks +end + +function Base64.encode(bytes) + local blocks = splitIntoBlocks(bytes) + local output = "" + for i=1, #blocks do + output = output..octetToBase64( unpack(blocks[i]) ) + end + return output +end + +function Base64.decode(str) + local bytes = {} + local blocks = {} + local blockNum = 1 + for i=1, #str, 4 do + blocks[blockNum] = string.sub(str, i, i+3) + blockNum = blockNum+1 + end + for i=1, #blocks do + local o1, o2, o3 = base64ToThreeOctet(blocks[i]) + table.insert(bytes, o1) + table.insert(bytes, o2) + table.insert(bytes, o3) +if (i % 1000) == 0 then + os.sleep(0) +end + end + -- Remove padding: + --[[ + for i=#bytes, 1, -1 do + if bytes[i] ~= 0 then + break + else + bytes[i] = nil + end + end + ]] + return bytes +end + +return Base64 diff --git a/apis/blocks.lua b/apis/blocks.lua new file mode 100644 index 0000000..ce1d91d --- /dev/null +++ b/apis/blocks.lua @@ -0,0 +1,613 @@ +local class = require('class') +local Util = require('util') +local TableDB = require('tableDB') +local JSON = require('json') + +-- see https://github.com/Khroki/MCEdit-Unified/blob/master/pymclevel/minecraft.yaml +-- see https://github.com/Khroki/MCEdit-Unified/blob/master/Items/minecraft/blocks.json + +--[[-- nameDB --]]-- +local nameDB = TableDB({ + fileName = 'blocknames.db' +}) +function nameDB:load(dir, blockDB) + self.fileName = fs.combine(dir, self.fileName) + if fs.exists(self.fileName) then + TableDB.load(self) + end + self.blockDB = blockDB +end + +function nameDB:getName(id, dmg) + return self:lookupName(id, dmg) or id .. ':' .. dmg +end + +function nameDB:lookupName(id, dmg) + -- is it in the name db ? + local name = self:get({ id, dmg }) + if name then + return name + end + + -- is it in the block db ? + for _,v in pairs(self.blockDB.data) do + if v.strId == id and v.dmg == dmg then + return v.name + end + end +end + +--[[-- blockDB --]]-- +local blockDB = TableDB() + +function blockDB:load() + + local dir = fs.getDir(shell.getRunningProgram()) + local blocks = JSON.decodeFromFile(fs.combine(dir, 'etc/blocks.json')) + + if not blocks then + error('Unable to read blocks.json') + end + + for strId, block in pairs(blocks) do + strId = 'minecraft:' .. strId + if type(block.name) == 'string' then + self:add(block.id, 0, block.name, strId, block.place) + else + for nid,name in pairs(block.name) do + self:add(block.id, nid - 1, name, strId, block.place) + end + end + end +end + +function blockDB:lookup(id, dmg) + if not id then + return + end + + return self.data[id .. ':' .. dmg] +end + +function blockDB:add(id, dmg, name, strId, place) + local key = id .. ':' .. dmg + + TableDB.add(self, key, { + id = id, + dmg = dmg, + name = name, + strId = strId, + place = place, + }) +end + +--[[-- placementDB --]]-- +-- in memory table that expands the standardBlock and blockType tables for each item/dmg/placement combination +local placementDB = TableDB() + +function placementDB:load(sbDB, btDB) + + for k,blockType in pairs(sbDB.data) do + local bt = btDB.data[blockType] + if not bt then + error('missing block type: ' .. blockType) + end + local id, dmg = string.match(k, '(%d+):*(%d+)') + self:addSubsForBlockType(tonumber(id), tonumber(dmg), bt) + end +end + +function placementDB:load2(sbDB, btDB) + + for k,v in pairs(sbDB.data) do + if v.place then + local bt = btDB.data[v.place] + if not bt then + error('missing block type: ' .. v.place) + end + local id, dmg = string.match(k, '(%d+):*(%d+)') + self:addSubsForBlockType(tonumber(id), tonumber(dmg), bt) + end + end + + -- special case for quartz pillars + self:addSubsForBlockType(155, 2, btDB.data['quartz-pillar']) +end + + +function placementDB:addSubsForBlockType(id, dmg, bt) + for _,sub in pairs(bt) do + local odmg = sub.odmg + if type(sub.odmg) == 'string' then + odmg = dmg + tonumber(string.match(odmg, '+(%d+)')) + end + + local b = blockDB:lookup(id, dmg) + local strId = tostring(id) + if b then + strId = b.strId + end + + self:add( + id, + odmg, + sub.sid or strId, + sub.sdmg or dmg, + sub.dir, + sub.extra) + end +end + +function placementDB:add(id, dmg, sid, sdmg, direction, extra) + if direction and #direction == 0 then + direction = nil + end + + local entry = { + oid = id, -- numeric ID + odmg = dmg, -- dmg with placement info + id = sid, -- string ID + dmg = sdmg, -- dmg without placement info + direction = direction, + } + if extra then + Util.merge(entry, extra) + end + + self.data[id .. ':' .. dmg] = entry +end + +--[[-- BlockTypeDB --]]-- +local blockTypeDB = TableDB() + +function blockTypeDB:addTemp(blockType, subs) + local bt = self.data[blockType] + if not bt then + bt = { } + self.data[blockType] = bt + end + for _,sub in pairs(subs) do + table.insert(bt, { + odmg = sub[1], + sid = sub[2], + sdmg = sub[3], + dir = sub[4], + extra = sub[5] + }) + end + self.dirty = true +end + +function blockTypeDB:load() + + blockTypeDB:addTemp('stairs', { + { 0, nil, 0, 'east-up' }, + { 1, nil, 0, 'west-up' }, + { 2, nil, 0, 'south-up' }, + { 3, nil, 0, 'north-up' }, + { 4, nil, 0, 'east-down' }, + { 5, nil, 0, 'west-down' }, + { 6, nil, 0, 'south-down' }, + { 7, nil, 0, 'north-down' }, + }) + blockTypeDB:addTemp('gate', { + { 0, nil, 0, 'north' }, + { 1, nil, 0, 'east' }, + { 2, nil, 0, 'south' }, + { 3, nil, 0, 'west' }, + { 4, nil, 0, 'north' }, + { 5, nil, 0, 'east' }, + { 6, nil, 0, 'south' }, + { 7, nil, 0, 'west' }, + }) + blockTypeDB:addTemp('pumpkin', { + { 0, nil, 0, 'north-block' }, + { 1, nil, 0, 'east-block' }, + { 2, nil, 0, 'south-block' }, + { 3, nil, 0, 'west-block' }, + { 4, nil, 0, 'north-block' }, + { 5, nil, 0, 'east-block' }, + { 6, nil, 0, 'south-block' }, + { 7, nil, 0, 'west-block' }, + }) + blockTypeDB:addTemp('anvil', { + { 0, nil, 0, 'south' }, + { 1, nil, 0, 'east' }, + { 2, nil, 0, 'south'}, + { 3, nil, 0, 'east' }, + { 4, nil, 0, 'south' }, + { 5, nil, 0, 'east' }, + { 6, nil, 0, 'east' }, + { 7, nil, 0, 'south' }, + { 8, nil, 0, 'south' }, + { 9, nil, 0, 'east' }, + { 10, nil, 0, 'east' }, + { 11, nil, 0, 'south' }, + { 12, nil, 0 }, + { 13, nil, 0 }, + { 14, nil, 0 }, + { 15, nil, 0 }, + }) + blockTypeDB:addTemp('bed', { + { 0, nil, 0, 'south' }, + { 1, nil, 0, 'west' }, + { 2, nil, 0, 'north' }, + { 3, nil, 0, 'east' }, + { 4, nil, 0, 'south' }, + { 5, nil, 0, 'west' }, + { 6, nil, 0, 'north' }, + { 7, nil, 0, 'east' }, + { 8, 'minecraft:air', 0 }, + { 9, 'minecraft:air', 0 }, + { 10, 'minecraft:air', 0 }, + { 11, 'minecraft:air', 0 }, + { 12, 'minecraft:air', 0 }, + { 13, 'minecraft:air', 0 }, + { 14, 'minecraft:air', 0 }, + { 15, 'minecraft:air', 0 }, + }) + blockTypeDB:addTemp('comparator', { + { 0, nil, 0, 'south' }, + { 1, nil, 0, 'west' }, + { 2, nil, 0, 'north' }, + { 3, nil, 0, 'east' }, + { 4, nil, 0, 'south' }, + { 5, nil, 0, 'west' }, + { 6, nil, 0, 'north' }, + { 7, nil, 0, 'east' }, + { 8, nil, 0, 'south' }, + { 9, nil, 0, 'west' }, + { 10, nil, 0, 'north' }, + { 11, nil, 0, 'east' }, + { 12, nil, 0, 'south' }, + { 13, nil, 0, 'west' }, + { 14, nil, 0, 'north' }, + { 15, nil, 0, 'east' }, + }) + blockTypeDB:addTemp('quartz-pillar', { + { 2, nil, 2 }, + { 3, nil, 2, 'north-south-block' }, + { 4, nil, 2, 'east-west-block' }, -- should be east-west-block + }) + blockTypeDB:addTemp('hay-bale', { + { 0, nil, 0 }, + { 4, nil, 0, 'east-west-block' }, -- should be east-west-block + { 8, nil, 0, 'north-south-block' }, + }) + blockTypeDB:addTemp('button', { + { 1, nil, 0, 'west-block' }, + { 2, nil, 0, 'east-block' }, + { 3, nil, 0, 'north-block' }, + { 4, nil, 0, 'south-block' }, + { 5, nil, 0 }, -- block top + }) + blockTypeDB:addTemp('cauldron', { + { 0, nil, 0 }, + { 1, nil, 0 }, + { 2, nil, 0 }, + { 3, nil, 0 }, + }) + blockTypeDB:addTemp('dispenser', { + { 0, nil, 0, 'wrench-down' }, + { 1, nil, 0, 'wrench-up' }, + { 2, nil, 0, 'south' }, + { 3, nil, 0, 'north' }, + { 4, nil, 0, 'east' }, + { 5, nil, 0, 'west' }, + { 9, nil, 0 }, + }) + blockTypeDB:addTemp('end_rod', { + { 0, nil, 0, 'wrench-down' }, + { 1, nil, 0, 'wrench-up' }, + { 2, nil, 0, 'south-block-flip' }, + { 3, nil, 0, 'north-block-flip' }, + { 4, nil, 0, 'east-block-flip' }, + { 5, nil, 0, 'west-block-flip' }, + { 9, nil, 0 }, + }) + blockTypeDB:addTemp('hopper', { + { 0, nil, 0 }, + { 1, nil, 0 }, + { 2, nil, 0, 'south-block' }, + { 3, nil, 0, 'north-block' }, + { 4, nil, 0, 'east-block' }, + { 5, nil, 0, 'west-block' }, + { 8, nil, 0 }, + { 9, nil, 0 }, + { 10, nil, 0 }, + { 11, nil, 0, 'south-block' }, + { 12, nil, 0, 'north-block' }, + { 13, nil, 0, 'east-block' }, + { 14, nil, 0, 'west-block' }, + }) + blockTypeDB:addTemp('mobhead', { + { 0, nil, 0 }, + { 1, nil, 0 }, + { 2, nil, 0, 'south-block' }, + { 3, nil, 0, 'north-block' }, + { 4, nil, 0, 'west-block' }, + { 5, nil, 0, 'east-block' }, + }) + blockTypeDB:addTemp('rail', { + { 0, nil, 0, 'south' }, + { 1, nil, 0, 'east' }, + { 2, nil, 0, 'east' }, + { 3, nil, 0, 'east' }, + { 4, nil, 0, 'south' }, + { 5, nil, 0, 'south' }, + { 6, nil, 0, 'east' }, + { 7, nil, 0, 'south' }, + { 8, nil, 0, 'east' }, + { 9, nil, 0, 'south' }, + }) + blockTypeDB:addTemp('adp-rail', { + { 0, nil, 0, 'south' }, + { 1, nil, 0, 'east' }, + { 2, nil, 0, 'east' }, + { 3, nil, 0, 'east' }, + { 4, nil, 0, 'south' }, + { 5, nil, 0, 'south' }, + { 8, nil, 0, 'south' }, + { 9, nil, 0, 'east' }, + { 10, nil, 0, 'east' }, + { 11, nil, 0, 'east' }, + { 12, nil, 0, 'south' }, + { 13, nil, 0, 'south' }, + }) + blockTypeDB:addTemp('signpost', { + { 0, nil, 0, 'north' }, + { 1, nil, 0, 'north', { facing = 1 } }, + { 2, nil, 0, 'north', { facing = 2 } }, + { 3, nil, 0, 'north', { facing = 3 } }, + { 4, nil, 0, 'east' }, + { 5, nil, 0, 'east', { facing = 1 } }, + { 6, nil, 0, 'east', { facing = 2 } }, + { 7, nil, 0, 'east', { facing = 3 } }, + { 8, nil, 0, 'south' }, + { 9, nil, 0, 'south', { facing = 1 } }, + { 10, nil, 0, 'south', { facing = 2 } }, + { 11, nil, 0, 'south', { facing = 3 } }, + { 12, nil, 0, 'west' }, + { 13, nil, 0, 'west', { facing = 1 } }, + { 14, nil, 0, 'west', { facing = 2 } }, + { 15, nil, 0, 'west', { facing = 3 } }, + }) + blockTypeDB:addTemp('vine', { + { 0, nil, 0 }, + { 1, nil, 0, 'south-block-vine' }, + { 2, nil, 0, 'west-block-vine' }, + { 3, nil, 0, 'south-block-vine' }, + { 4, nil, 0, 'north-block-vine' }, + { 5, nil, 0, 'south-block-vine' }, + { 6, nil, 0, 'north-block-vine' }, + { 7, nil, 0, 'south-block-vine' }, + { 8, nil, 0, 'east-block-vine' }, + { 9, nil, 0, 'south-block-vine' }, + { 10, nil, 0, 'east-block-vine' }, + { 11, nil, 0, 'east-block-vine' }, + { 12, nil, 0, 'east-block-vine' }, + { 13, nil, 0, 'east-block-vine' }, + { 14, nil, 0, 'east-block-vine' }, + { 15, nil, 0, 'east-block-vine' }, + }) + blockTypeDB:addTemp('torch', { + { 0, nil, 0 }, + { 1, nil, 0, 'west-block' }, + { 2, nil, 0, 'east-block' }, + { 3, nil, 0, 'north-block' }, + { 4, nil, 0, 'south-block' }, + { 5, nil, 0 }, + }) + blockTypeDB:addTemp('tripwire', { + { 0, nil, 0, 'north-block' }, + { 1, nil, 0, 'east-block' }, + { 2, nil, 0, 'south-block' }, + { 3, nil, 0, 'west-block' }, + }) + blockTypeDB:addTemp('trapdoor', { + { 0, nil, 0, 'south-block' }, + { 1, nil, 0, 'north-block' }, + { 2, nil, 0, 'east-block' }, + { 3, nil, 0, 'west-block' }, + { 4, nil, 0, 'south-block' }, + { 5, nil, 0, 'north-block' }, + { 6, nil, 0, 'east-block' }, + { 7, nil, 0, 'west-block' }, + { 8, nil, 0, 'south-block' }, + { 9, nil, 0, 'north-block' }, + { 10, nil, 0, 'east-block' }, + { 11, nil, 0, 'west-block' }, + { 12, nil, 0, 'south-block' }, + { 13, nil, 0, 'north-block' }, + { 14, nil, 0, 'east-block' }, + { 15, nil, 0, 'west-block' }, + }) + blockTypeDB:addTemp('piston', { -- piston placement is broken in 1.7 -- need to add work around + { 0, nil, 0, 'piston-down' }, + { 1, nil, 0, 'piston-up' }, + { 2, nil, 0, 'piston-north' }, + { 3, nil, 0, 'piston-south' }, + { 4, nil, 0, 'piston-west' }, + { 5, nil, 0, 'piston-east' }, + { 8, nil, 0, 'piston-down' }, + { 9, nil, 0, 'piston-up' }, + { 10, nil, 0, 'piston-north' }, + { 11, nil, 0, 'piston-south' }, + { 12, nil, 0, 'piston-west' }, + { 13, nil, 0, 'piston-east' }, + }) + blockTypeDB:addTemp('lever', { + { 0, nil, 0, 'up' }, + { 1, nil, 0, 'west-block' }, + { 2, nil, 0, 'east-block' }, + { 3, nil, 0, 'north-block' }, + { 4, nil, 0, 'south-block' }, + { 5, nil, 0, 'north' }, + { 6, nil, 0, 'west' }, + { 7, nil, 0, 'up' }, + { 8, nil, 0, 'up' }, + { 9, nil, 0, 'west-block' }, + { 10, nil, 0, 'east-block' }, + { 11, nil, 0, 'north-block' }, + { 12, nil, 0, 'south-block' }, + { 13, nil, 0, 'north' }, + { 14, nil, 0, 'west' }, + { 15, nil, 0, 'up' }, + }) + blockTypeDB:addTemp('wallsign-ladder', { + { 0, nil, 0 }, + { 1, nil, 0 }, + { 2, nil, 0, 'south-block' }, + { 3, nil, 0, 'north-block' }, + { 4, nil, 0, 'east-block' }, + { 5, nil, 0, 'west-block' }, + }) + blockTypeDB:addTemp('chest-furnace', { + { 0, nil, 0 }, + { 2, nil, 0, 'south' }, + { 3, nil, 0, 'north' }, + { 4, nil, 0, 'east' }, + { 5, nil, 0, 'west' }, + }) + blockTypeDB:addTemp('repeater', { + { 0, nil, 0, 'north' }, + { 1, nil, 0, 'east' }, + { 2, nil, 0, 'south' }, + { 3, nil, 0, 'west' }, + { 4, nil, 0, 'north' }, + { 5, nil, 0, 'east' }, + { 6, nil, 0, 'south' }, + { 7, nil, 0, 'west' }, + { 8, nil, 0, 'north' }, + { 9, nil, 0, 'east' }, + { 10, nil, 0, 'south' }, + { 11, nil, 0, 'west' }, + { 12, nil, 0, 'north' }, + { 13, nil, 0, 'east' }, + { 14, nil, 0, 'south' }, + { 15, nil, 0, 'west' }, + }) + blockTypeDB:addTemp('flatten', { + { 0, nil, 0 }, + { 1, nil, 0 }, + { 2, nil, 0 }, + { 3, nil, 0 }, + { 4, nil, 0 }, + { 5, nil, 0 }, + { 6, nil, 0 }, + { 7, nil, 0 }, + { 8, nil, 0 }, + { 9, nil, 0 }, + { 10, nil, 0 }, + { 11, nil, 0 }, + { 12, nil, 0 }, + { 13, nil, 0 }, + { 14, nil, 0 }, + { 15, nil, 0 }, + }) + blockTypeDB:addTemp('sapling', { + { '+0', nil, nil }, + { '+8', nil, nil }, + }) + blockTypeDB:addTemp('leaves', { + { '+0', nil, nil }, + { '+4', nil, nil }, + { '+8', nil, nil }, + { '+12', nil, nil }, + }) + blockTypeDB:addTemp('slab', { + { '+0', nil, nil, 'bottom' }, + { '+8', nil, nil, 'top' }, + }) + blockTypeDB:addTemp('largeplant', { + { '+0', nil, nil, 'east-door', { twoHigh = true } }, -- should use a generic double tall keyword + { '+8', 'minecraft:air', 0 }, + }) + blockTypeDB:addTemp('wood', { + { '+0', nil, nil }, + { '+4', nil, nil, 'east-west-block' }, + { '+8', nil, nil, 'north-south-block' }, + { '+12', nil, nil }, + }) + blockTypeDB:addTemp('door', { + { 0, nil, 0, 'east-door', { twoHigh = true } }, + { 1, nil, 0, 'south-door', { twoHigh = true } }, + { 2, nil, 0, 'west-door', { twoHigh = true } }, + { 3, nil, 0, 'north-door', { twoHigh = true } }, + { 4, nil, 0, 'east-door', { twoHigh = true } }, + { 5, nil, 0, 'south-door', { twoHigh = true } }, + { 6, nil, 0, 'west-door', { twoHigh = true } }, + { 7, nil, 0, 'north-door', { twoHigh = true } }, + { 8,'minecraft:air', 0 }, + { 9,'minecraft:air', 0 }, + { 10,'minecraft:air', 0 }, + { 11,'minecraft:air', 0 }, + { 12,'minecraft:air', 0 }, + { 13,'minecraft:air', 0 }, + { 14,'minecraft:air', 0 }, + { 15,'minecraft:air', 0 }, + }) + blockTypeDB:addTemp('cocoa', { + { 0, nil, 0, 'south-block' }, + { 1, nil, 0, 'west-block' }, + { 2, nil, 0, 'north-block' }, + { 3, nil, 0, 'east-block' }, + { 4, nil, 0, 'south-block' }, + { 5, nil, 0, 'west-block' }, + { 6, nil, 0, 'north-block' }, + { 7, nil, 0, 'east-block' }, + { 8, nil, 0, 'south-block' }, + { 9, nil, 0, 'west-block' }, + { 10, nil, 0, 'north-block' }, + { 11, nil, 0, 'east-block' }, + }) +end + +local Blocks = class() +function Blocks:init(args) + + Util.merge(self, args) + self.blockDB = blockDB + self.nameDB = nameDB + + blockDB:load() +-- standardBlockDB:load() + blockTypeDB:load() + nameDB:load(self.dir, blockDB) +-- placementDB:load(standardBlockDB, blockTypeDB) + placementDB:load2(blockDB, blockTypeDB) + +-- _G._b = blockDB +-- _G._s = standardBlockDB +-- _G._bt = blockTypeDB +-- _G._p = placementDB + +-- Util.writeTable('pb1.lua', placementDB.data) + +-- placementDB.data = { } + +-- Util.writeTable('pb2.lua', placementDB.data) +end + +-- for an ID / dmg (with placement info) - return the correct block (without the placment info embedded in the dmg) +function Blocks:getPlaceableBlock(id, dmg) + + local p = placementDB:get({id, dmg}) + if p then + return Util.shallowCopy(p) + end + + local b = blockDB:get({id, dmg}) + if b then + return { id = b.strId, dmg = b.dmg } + end + + b = blockDB:get({id, 0}) + if b then + return { id = b.strId, dmg = b.dmg } + end + + return { id = id, dmg = dmg } +end + +return Blocks diff --git a/apis/chestAdapter.lua b/apis/chestAdapter.lua new file mode 100644 index 0000000..3688bd1 --- /dev/null +++ b/apis/chestAdapter.lua @@ -0,0 +1,111 @@ +local class = require('class') +local Logger = require('logger') + +local ChestProvider = class() + +function ChestProvider:init(args) + + args = args or { } + + self.stacks = {} + self.name = 'chest' + self.direction = args.direction or 'up' + self.wrapSide = args.wrapSide or 'bottom' + self.p = peripheral.wrap(self.wrapSide) +end + +function ChestProvider:isValid() + return self.p and self.p.getAllStacks +end + +function ChestProvider:refresh() + if self.p then + self.p.condenseItems() + self.stacks = self.p.getAllStacks(false) + local t = { } + for _,s in ipairs(self.stacks) do + local key = s.id .. ':' .. s.dmg + if t[key] and t[key].qty < 64 then + t[key].max_size = t[key].qty + else + t[key] = { + qty = s.qty + } + end + end + for _,s in ipairs(self.stacks) do + local key = s.id .. ':' .. s.dmg + if t[key].max_size then + s.max_size = t[key].qty + else + s.max_size = 64 + end + end + end + return self.stacks +end + +function ChestProvider:getItemInfo(id, dmg) + local item = { id = id, dmg = dmg, qty = 0, max_size = 64 } + for _,stack in pairs(self.stacks) do + if stack.id == id and stack.dmg == dmg then + item.name = stack.display_name + item.qty = item.qty + stack.qty + item.max_size = stack.max_size + end + end + if item.name then + return item + end +end + +function ChestProvider:craft(id, dmg, qty) + return false +end + +function ChestProvider:craftItems(items) +end + +function ChestProvider:provide(item, qty, slot) + if self.p then + self.stacks = self.p.getAllStacks(false) + for key,stack in pairs(self.stacks) do + if stack.id == item.id and stack.dmg == item.dmg then + local amount = math.min(qty, stack.qty) + self.p.pushItemIntoSlot(self.direction, key, amount, slot) + qty = qty - amount + if qty <= 0 then + break + end + end + end + end +end + +function ChestProvider:extract(slot, qty) + if self.p then + self.p.pushItem(self.direction, slot, qty) + end +end + +function ChestProvider:insert(slot, qty) + if self.p then + local s, m = pcall(function() self.p.pullItem(self.direction, slot, qty) end) + if not s and m then + print('chestProvider:pullItem') + print(m) + Logger.log('chestProvider', 'Insert failed, trying again') + sleep(1) + s, m = pcall(function() self.p.pullItem(self.direction, slot, qty) end) + if not s and m then + print('chestProvider:pullItem') + print(m) + Logger.log('chestProvider', 'Insert failed again') + else + Logger.log('chestProvider', 'Insert successful') + end + end + end +end + +return ChestProvider diff --git a/apis/chestAdapter18.lua b/apis/chestAdapter18.lua new file mode 100644 index 0000000..4cc2d6b --- /dev/null +++ b/apis/chestAdapter18.lua @@ -0,0 +1,140 @@ +local class = require('class') +local Util = require('util') +local itemDB = require('itemDB') +local Peripheral = require('peripheral') + +local ChestAdapter = class() + +local keys = Util.transpose({ + 'damage', + 'displayName', + 'maxCount', + 'maxDamage', + 'name', + 'nbtHash', +}) + +function ChestAdapter:init(args) + local defaults = { + items = { }, + name = 'chest', + direction = 'up', + wrapSide = 'bottom', + } + Util.merge(self, defaults) + Util.merge(self, args) + + local chest = Peripheral.getBySide(self.wrapSide) + if not chest then + chest = Peripheral.getByMethod('list') + end + if chest then + Util.merge(self, chest) + end +end + +function ChestAdapter:isValid() + return not not self.list +end + +function ChestAdapter:getCachedItemDetails(item, k) + local key = { item.name, item.damage, item.nbtHash } + + local detail = itemDB:get(key) + if not detail then + pcall(function() detail = self.getItemMeta(k) end) + if not detail then + return + end +-- NOT SUFFICIENT + if detail.name ~= item.name then + return + end + + for _,k in ipairs(Util.keys(detail)) do + if not keys[k] then + detail[k] = nil + end + end + + itemDB:add(key, detail) + end + if detail then + return Util.shallowCopy(detail) + end +end + +function ChestAdapter:refresh(throttle) + return self:listItems(throttle) +end + +-- provide a consolidated list of items +function ChestAdapter:listItems(throttle) + self.cache = { } + local items = { } + + throttle = throttle or Util.throttle() + + for k,v in pairs(self.list()) do + local key = table.concat({ v.name, v.damage, v.nbtHash }, ':') + + local entry = self.cache[key] + if not entry then + entry = self:getCachedItemDetails(v, k) + if entry then + entry.count = 0 + self.cache[key] = entry + table.insert(items, entry) + end + end + + if entry then + entry.count = entry.count + v.count + end + throttle() + end + + itemDB:flush() + + return items +end + +function ChestAdapter:getItemInfo(name, damage, nbtHash) + if not self.cache then + self:listItems() + end + local key = table.concat({ name, damage, nbtHash }, ':') + return self.cache[key] +end + +function ChestAdapter:craft(name, damage, qty) +end + +function ChestAdapter:craftItems(items) +end + +function ChestAdapter:provide(item, qty, slot, direction) + local stacks = self.list() + for key,stack in pairs(stacks) do + if stack.name == item.name and stack.damage == item.damage then + local amount = math.min(qty, stack.count) + if amount > 0 then + self.pushItems(direction or self.direction, key, amount, slot) + end + qty = qty - amount + if qty <= 0 then + break + end + end + end +end + +function ChestAdapter:extract(slot, qty, toSlot) + self.pushItems(self.direction, slot, qty, toSlot) +end + +function ChestAdapter:insert(slot, qty) + self.pullItems(self.direction, slot, qty) +end + +return ChestAdapter diff --git a/apis/deflatelua.lua b/apis/deflatelua.lua new file mode 100644 index 0000000..55bb7e9 --- /dev/null +++ b/apis/deflatelua.lua @@ -0,0 +1,870 @@ +--[[ + +LUA MODULE + + compress.deflatelua - deflate (and gunzip/zlib) implemented in Lua. + +SYNOPSIS + + local DEFLATE = require 'compress.deflatelua' + -- uncompress gzip file + local fh = assert(io.open'foo.txt.gz', 'rb') + local ofh = assert(io.open'foo.txt', 'wb') + DEFLATE.gunzip {input=fh, output=ofh} + fh:close(); ofh:close() + -- can also uncompress from string including zlib and raw DEFLATE formats. + +DESCRIPTION + + This is a pure Lua implementation of decompressing the DEFLATE format, + including the related zlib and gzip formats. + + Note: This library only supports decompression. + Compression is not currently implemented. + +API + + Note: in the following functions, input stream `fh` may be + a file handle, string, or an iterator function that returns strings. + Output stream `ofh` may be a file handle or a function that + consumes one byte (number 0..255) per call. + + DEFLATE.inflate {input=fh, output=ofh} + + Decompresses input stream `fh` in the DEFLATE format + while writing to output stream `ofh`. + DEFLATE is detailed in http://tools.ietf.org/html/rfc1951 . + + DEFLATE.gunzip {input=fh, output=ofh, disable_crc=disable_crc} + + Decompresses input stream `fh` with the gzip format + while writing to output stream `ofh`. + `disable_crc` (defaults to `false`) will disable CRC-32 checking + to increase speed. + gzip is detailed in http://tools.ietf.org/html/rfc1952 . + + DEFLATE.inflate_zlib {input=fh, output=ofh, disable_crc=disable_crc} + + Decompresses input stream `fh` with the zlib format + while writing to output stream `ofh`. + `disable_crc` (defaults to `false`) will disable CRC-32 checking + to increase speed. + zlib is detailed in http://tools.ietf.org/html/rfc1950 . + + DEFLATE.adler32(byte, crc) --> rcrc + + Returns adler32 checksum of byte `byte` (number 0..255) appended + to string with adler32 checksum `crc`. This is internally used by + `inflate_zlib`. + ADLER32 in detailed in http://tools.ietf.org/html/rfc1950 . + +COMMAND LINE UTILITY + + A `gunziplua` command line utility (in folder `bin`) is also provided. + This mimicks the *nix `gunzip` utility but is a pure Lua implementation + that invokes this library. For help do + + gunziplua -h + +DEPENDENCIES + + Requires 'digest.crc32lua' (used for optional CRC-32 checksum checks). + https://github.com/davidm/lua-digest-crc32lua + + Will use a bit library ('bit', 'bit32', 'bit.numberlua') if available. This + is not that critical for this library but is required by digest.crc32lua. + + 'pythonic.optparse' is only required by the optional `gunziplua` + command-line utilty for command line parsing. + https://github.com/davidm/lua-pythonic-optparse + +INSTALLATION + + Copy the `compress` directory into your LUA_PATH. + +REFERENCES + + [1] DEFLATE Compressed Data Format Specification version 1.3 + http://tools.ietf.org/html/rfc1951 + [2] GZIP file format specification version 4.3 + http://tools.ietf.org/html/rfc1952 + [3] http://en.wikipedia.org/wiki/DEFLATE + [4] pyflate, by Paul Sladen + http://www.paul.sladen.org/projects/pyflate/ + [5] Compress::Zlib::Perl - partial pure Perl implementation of + Compress::Zlib + http://search.cpan.org/~nwclark/Compress-Zlib-Perl/Perl.pm + +LICENSE + + (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + (end license) +--]] + +local M = {_TYPE='module', _NAME='compress.deflatelua', _VERSION='0.3.20111128'} + +local assert = assert +local error = error +local ipairs = ipairs +local pairs = pairs +local print = print +local require = require +local tostring = tostring +local type = type +local setmetatable = setmetatable +local io = io +local math = math +local table_sort = table.sort +local math_max = math.max +local string_char = string.char + +--[[ + Requires the first module listed that exists, else raises like `require`. + If a non-string is encountered, it is returned. + Second return value is module name loaded (or ''). + --]] +local function requireany(...) + local errs = {} + for i = 1, select('#', ...) do local name = select(i, ...) + if type(name) ~= 'string' then return name, '' end + local ok, mod = pcall(require, name) + if ok then return mod, name end + errs[#errs+1] = mod + end + error(table.concat(errs, '\n'), 2) +end + + +--local crc32 = require "digest.crc32lua" . crc32_byte +--local bit, name_ = requireany('bit', 'bit32', 'bit.numberlua', nil) +local bit +local crc32 + +local DEBUG = false + +-- Whether to use `bit` library functions in current module. +-- Unlike the crc32 library, it doesn't make much difference in this module. +local NATIVE_BITOPS = (bit ~= nil) + +local band, lshift, rshift +if NATIVE_BITOPS then + band = bit.band + lshift = bit.lshift + rshift = bit.rshift +end + + +local function warn(s) + io.stderr:write(s, '\n') +end + + +local function debug(...) + print('DEBUG', ...) +end + + +local function runtime_error(s, level) + level = level or 1 + error({s}, level+1) +end + + +local function make_outstate(outbs) + local outstate = {} + outstate.outbs = outbs + outstate.window = {} + outstate.window_pos = 1 + return outstate +end + + +local function output(outstate, byte) + -- debug('OUTPUT:', s) + local window_pos = outstate.window_pos + outstate.outbs(byte) + outstate.window[window_pos] = byte + outstate.window_pos = window_pos % 32768 + 1 -- 32K +end + + +local function noeof(val) + return assert(val, 'unexpected end of file') +end + + +local function hasbit(bits, bit) + return bits % (bit + bit) >= bit +end + + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k) + t[k] = v + return v + end + return t +end + + +-- small optimization (lookup table for powers of 2) +local pow2 = memoize(function(n) return 2^n end) + +--local tbits = memoize( +-- function(bits) +-- return memoize( function(bit) return getbit(bits, bit) end ) +-- end ) + + +-- weak metatable marking objects as bitstream type +local is_bitstream = setmetatable({}, {__mode='k'}) + + +-- DEBUG +-- prints LSB first +--[[ +local function bits_tostring(bits, nbits) + local s = '' + local tmp = bits + local function f() + local b = tmp % 2 == 1 and 1 or 0 + s = s .. b + tmp = (tmp - b) / 2 + end + if nbits then + for i=1,nbits do f() end + else + while tmp ~= 0 do f() end + end + + return s +end +--]] + +local function bytestream_from_file(fh) + local o = {} + function o:read() + local sb = fh:read(1) + if sb then return sb:byte() end + end + return o +end + + +local function bytestream_from_string(s) + local i = 1 + local o = {} + function o:read() + local by + if i <= #s then + by = s:byte(i) + i = i + 1 + end + return by + end + return o +end + + +local function bytestream_from_function(f) + local i = 0 + local buffer = '' + local o = {} + function o:read() + return f() +-- i = i + 1 +-- if i > #buffer then +-- buffer = f() + -- if not buffer then return end + -- i = 1 +-- end +-- return buffer:byte(i,i) + end + return o +end + + +local function bitstream_from_bytestream(bys) + local buf_byte = 0 + local buf_nbit = 0 + local o = {} + + function o:nbits_left_in_byte() + return buf_nbit + end + + if NATIVE_BITOPS then + function o:read(nbits) + nbits = nbits or 1 + while buf_nbit < nbits do + local byte = bys:read() + if not byte then return end -- note: more calls also return nil + buf_byte = buf_byte + lshift(byte, buf_nbit) + buf_nbit = buf_nbit + 8 + end + local bits + if nbits == 0 then + bits = 0 + elseif nbits == 32 then + bits = buf_byte + buf_byte = 0 + else + bits = band(buf_byte, rshift(0xffffffff, 32 - nbits)) + buf_byte = rshift(buf_byte, nbits) + end + buf_nbit = buf_nbit - nbits + return bits + end + else + function o:read(nbits) + nbits = nbits or 1 + while buf_nbit < nbits do + local byte = bys:read() + if not byte then return end -- note: more calls also return nil + buf_byte = buf_byte + pow2[buf_nbit] * byte + buf_nbit = buf_nbit + 8 + end + local m = pow2[nbits] + local bits = buf_byte % m + buf_byte = (buf_byte - bits) / m + buf_nbit = buf_nbit - nbits + return bits + end + end + + is_bitstream[o] = true + + return o +end + + +local function get_bitstream(o) + local bs + if is_bitstream[o] then + return o + elseif io.type(o) == 'file' then + bs = bitstream_from_bytestream(bytestream_from_file(o)) + elseif type(o) == 'string' then + bs = bitstream_from_bytestream(bytestream_from_string(o)) + elseif type(o) == 'function' then + bs = bitstream_from_bytestream(bytestream_from_function(o)) + else + runtime_error 'unrecognized type' + end + return bs +end + + +local function get_obytestream(o) + local bs + if io.type(o) == 'file' then + bs = function(sbyte) o:write(string_char(sbyte)) end + elseif type(o) == 'function' then + bs = o + else + runtime_error('unrecognized type: ' .. tostring(o)) + end + return bs +end + + +local function HuffmanTable(init, is_full) + local t = {} + if is_full then + for val,nbits in pairs(init) do + if nbits ~= 0 then + t[#t+1] = {val=val, nbits=nbits} + --debug('*',val,nbits) + end + end + else + for i=1,#init-2,2 do + local firstval, nbits, nextval = init[i], init[i+1], init[i+2] + --debug(val, nextval, nbits) + if nbits ~= 0 then + for val=firstval,nextval-1 do + t[#t+1] = {val=val, nbits=nbits} + end + end + end + end + table_sort(t, function(a,b) + return a.nbits == b.nbits and a.val < b.val or a.nbits < b.nbits + end) + + -- assign codes + local code = 1 -- leading 1 marker + local nbits = 0 + for i,s in ipairs(t) do + if s.nbits ~= nbits then + code = code * pow2[s.nbits - nbits] + nbits = s.nbits + end + s.code = code + --debug('huffman code:', i, s.nbits, s.val, code, bits_tostring(code)) + code = code + 1 + end + + local minbits = math.huge + local look = {} + for i,s in ipairs(t) do + minbits = math.min(minbits, s.nbits) + look[s.code] = s.val + end + + --for _,o in ipairs(t) do + -- debug(':', o.nbits, o.val) + --end + + -- function t:lookup(bits) return look[bits] end + + local msb = NATIVE_BITOPS and function(bits, nbits) + local res = 0 + for i=1,nbits do + res = lshift(res, 1) + band(bits, 1) + bits = rshift(bits, 1) + end + return res + end or function(bits, nbits) + local res = 0 + for i=1,nbits do + local b = bits % 2 + bits = (bits - b) / 2 + res = res * 2 + b + end + return res + end + + local tfirstcode = memoize( + function(bits) return pow2[minbits] + msb(bits, minbits) end) + + function t:read(bs) + local code = 1 -- leading 1 marker + local nbits = 0 + while 1 do + if nbits == 0 then -- small optimization (optional) + code = tfirstcode[noeof(bs:read(minbits))] + nbits = nbits + minbits + else + local b = noeof(bs:read()) + nbits = nbits + 1 + code = code * 2 + b -- MSB first + --[[NATIVE_BITOPS + code = lshift(code, 1) + b -- MSB first + --]] + end + --debug('code?', code, bits_tostring(code)) + local val = look[code] + if val then + --debug('FOUND', val) + return val + end + end + end + + return t +end + + +local function parse_gzip_header(bs) + -- local FLG_FTEXT = 2^0 + local FLG_FHCRC = 2^1 + local FLG_FEXTRA = 2^2 + local FLG_FNAME = 2^3 + local FLG_FCOMMENT = 2^4 + + local id1 = bs:read(8) + local id2 = bs:read(8) + if id1 ~= 31 or id2 ~= 139 then + runtime_error 'not in gzip format' + end + local cm = bs:read(8) -- compression method + local flg = bs:read(8) -- FLaGs + local mtime = bs:read(32) -- Modification TIME + local xfl = bs:read(8) -- eXtra FLags + local os = bs:read(8) -- Operating System + + if DEBUG then + debug("CM=", cm) + debug("FLG=", flg) + debug("MTIME=", mtime) + -- debug("MTIME_str=",os.date("%Y-%m-%d %H:%M:%S",mtime)) -- non-portable + debug("XFL=", xfl) + debug("OS=", os) + end + + if not os then runtime_error 'invalid header' end + + if hasbit(flg, FLG_FEXTRA) then + local xlen = bs:read(16) + local extra = 0 + for i=1,xlen do + extra = bs:read(8) + end + if not extra then runtime_error 'invalid header' end + end + + local function parse_zstring(bs) + repeat + local by = bs:read(8) + if not by then runtime_error 'invalid header' end + until by == 0 + end + + if hasbit(flg, FLG_FNAME) then + parse_zstring(bs) + end + + if hasbit(flg, FLG_FCOMMENT) then + parse_zstring(bs) + end + + if hasbit(flg, FLG_FHCRC) then + local crc16 = bs:read(16) + if not crc16 then runtime_error 'invalid header' end + -- IMPROVE: check CRC. where is an example .gz file that + -- has this set? + if DEBUG then + debug("CRC16=", crc16) + end + end +end + +local function parse_zlib_header(bs) + local cm = bs:read(4) -- Compression Method + local cinfo = bs:read(4) -- Compression info + local fcheck = bs:read(5) -- FLaGs: FCHECK (check bits for CMF and FLG) + local fdict = bs:read(1) -- FLaGs: FDICT (present dictionary) + local flevel = bs:read(2) -- FLaGs: FLEVEL (compression level) + local cmf = cinfo * 16 + cm -- CMF (Compresion Method and flags) + local flg = fcheck + fdict * 32 + flevel * 64 -- FLaGs + + if cm ~= 8 then -- not "deflate" + runtime_error("unrecognized zlib compression method: " + cm) + end + if cinfo > 7 then + runtime_error("invalid zlib window size: cinfo=" + cinfo) + end + local window_size = 2^(cinfo + 8) + + if (cmf*256 + flg) % 31 ~= 0 then + runtime_error("invalid zlib header (bad fcheck sum)") + end + + if fdict == 1 then + runtime_error("FIX:TODO - FDICT not currently implemented") + local dictid_ = bs:read(32) + end + + return window_size +end + +local function parse_huffmantables(bs) + local hlit = bs:read(5) -- # of literal/length codes - 257 + local hdist = bs:read(5) -- # of distance codes - 1 + local hclen = noeof(bs:read(4)) -- # of code length codes - 4 + + local ncodelen_codes = hclen + 4 + local codelen_init = {} + local codelen_vals = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15} + for i=1,ncodelen_codes do + local nbits = bs:read(3) + local val = codelen_vals[i] + codelen_init[val] = nbits + end + local codelentable = HuffmanTable(codelen_init, true) + + local function decode(ncodes) + local init = {} + local nbits + local val = 0 + while val < ncodes do + local codelen = codelentable:read(bs) + --FIX:check nil? + local nrepeat + if codelen <= 15 then + nrepeat = 1 + nbits = codelen + --debug('w', nbits) + elseif codelen == 16 then + nrepeat = 3 + noeof(bs:read(2)) + -- nbits unchanged + elseif codelen == 17 then + nrepeat = 3 + noeof(bs:read(3)) + nbits = 0 + elseif codelen == 18 then + nrepeat = 11 + noeof(bs:read(7)) + nbits = 0 + else + error 'ASSERT' + end + for i=1,nrepeat do + init[val] = nbits + val = val + 1 + end + end + local huffmantable = HuffmanTable(init, true) + return huffmantable + end + + local nlit_codes = hlit + 257 + local ndist_codes = hdist + 1 + + local littable = decode(nlit_codes) + local disttable = decode(ndist_codes) + + return littable, disttable +end + + +local tdecode_len_base +local tdecode_len_nextrabits +local tdecode_dist_base +local tdecode_dist_nextrabits +local function parse_compressed_item(bs, outstate, littable, disttable) + local val = littable:read(bs) + --debug(val, val < 256 and string_char(val)) + if val < 256 then -- literal + output(outstate, val) + elseif val == 256 then -- end of block + return true + else + if not tdecode_len_base then + local t = {[257]=3} + local skip = 1 + for i=258,285,4 do + for j=i,i+3 do t[j] = t[j-1] + skip end + if i ~= 258 then skip = skip * 2 end + end + t[285] = 258 + tdecode_len_base = t + --for i=257,285 do debug('T1',i,t[i]) end + end + if not tdecode_len_nextrabits then + local t = {} + if NATIVE_BITOPS then + for i=257,285 do + local j = math_max(i - 261, 0) + t[i] = rshift(j, 2) + end + else + for i=257,285 do + local j = math_max(i - 261, 0) + t[i] = (j - (j % 4)) / 4 + end + end + t[285] = 0 + tdecode_len_nextrabits = t + --for i=257,285 do debug('T2',i,t[i]) end + end + local len_base = tdecode_len_base[val] + local nextrabits = tdecode_len_nextrabits[val] + local extrabits = bs:read(nextrabits) + local len = len_base + extrabits + + if not tdecode_dist_base then + local t = {[0]=1} + local skip = 1 + for i=1,29,2 do + for j=i,i+1 do t[j] = t[j-1] + skip end + if i ~= 1 then skip = skip * 2 end + end + tdecode_dist_base = t + --for i=0,29 do debug('T3',i,t[i]) end + end + if not tdecode_dist_nextrabits then + local t = {} + if NATIVE_BITOPS then + for i=0,29 do + local j = math_max(i - 2, 0) + t[i] = rshift(j, 1) + end + else + for i=0,29 do + local j = math_max(i - 2, 0) + t[i] = (j - (j % 2)) / 2 + end + end + tdecode_dist_nextrabits = t + --for i=0,29 do debug('T4',i,t[i]) end + end + local dist_val = disttable:read(bs) + local dist_base = tdecode_dist_base[dist_val] + local dist_nextrabits = tdecode_dist_nextrabits[dist_val] + local dist_extrabits = bs:read(dist_nextrabits) + local dist = dist_base + dist_extrabits + + --debug('BACK', len, dist) + for i=1,len do + local pos = (outstate.window_pos - 1 - dist) % 32768 + 1 -- 32K + output(outstate, assert(outstate.window[pos], 'invalid distance')) + end + end + return false +end + + +local function parse_block(bs, outstate) + local bfinal = bs:read(1) + local btype = bs:read(2) + + local BTYPE_NO_COMPRESSION = 0 + local BTYPE_FIXED_HUFFMAN = 1 + local BTYPE_DYNAMIC_HUFFMAN = 2 + local BTYPE_RESERVED_ = 3 + + if DEBUG then + debug('bfinal=', bfinal) + debug('btype=', btype) + end + + if btype == BTYPE_NO_COMPRESSION then + bs:read(bs:nbits_left_in_byte()) + local len = bs:read(16) + local nlen_ = noeof(bs:read(16)) + + for i=1,len do + local by = noeof(bs:read(8)) + output(outstate, by) + end + elseif btype == BTYPE_FIXED_HUFFMAN or btype == BTYPE_DYNAMIC_HUFFMAN then + local littable, disttable + if btype == BTYPE_DYNAMIC_HUFFMAN then + littable, disttable = parse_huffmantables(bs) + else + littable = HuffmanTable {0,8, 144,9, 256,7, 280,8, 288,nil} + disttable = HuffmanTable {0,5, 32,nil} + end + + repeat + local is_done = parse_compressed_item( + bs, outstate, littable, disttable) + until is_done + else + runtime_error 'unrecognized compression type' + end + + return bfinal ~= 0 +end + + +function M.inflate(t) + local bs = get_bitstream(t.input) + local outbs = get_obytestream(t.output) + local outstate = make_outstate(outbs) + + repeat + local is_final = parse_block(bs, outstate) + until is_final +end +local inflate = M.inflate + + +function M.gunzip(t) + local bs = get_bitstream(t.input) + local outbs = get_obytestream(t.output) + local disable_crc = t.disable_crc + if disable_crc == nil then disable_crc = false end + + parse_gzip_header(bs) + + local data_crc32 = 0 + + inflate{input=bs, output= + disable_crc and outbs or + function(byte) + data_crc32 = crc32(byte, data_crc32) + outbs(byte) + end + } + + bs:read(bs:nbits_left_in_byte()) + + local expected_crc32 = bs:read(32) + local isize = bs:read(32) -- ignored + if DEBUG then + debug('crc32=', expected_crc32) + debug('isize=', isize) + end + if not disable_crc and data_crc32 then + if data_crc32 ~= expected_crc32 then + runtime_error('invalid compressed data--crc error') + end + end + if bs:read() then + warn 'trailing garbage ignored' + end +end + + +function M.adler32(byte, crc) + local s1 = crc % 65536 + local s2 = (crc - s1) / 65536 + s1 = (s1 + byte) % 65521 + s2 = (s2 + s1) % 65521 + return s2*65536 + s1 +end -- 65521 is the largest prime smaller than 2^16 + + +function M.inflate_zlib(t) + local bs = get_bitstream(t.input) + local outbs = get_obytestream(t.output) + local disable_crc = t.disable_crc + if disable_crc == nil then disable_crc = false end + + local window_size_ = parse_zlib_header(bs) + + local data_adler32 = 1 + + inflate{input=bs, output= + disable_crc and outbs or + function(byte) + data_adler32 = M.adler32(byte, data_adler32) + outbs(byte) + end + } + + bs:read(bs:nbits_left_in_byte()) + + local b3 = bs:read(8) + local b2 = bs:read(8) + local b1 = bs:read(8) + local b0 = bs:read(8) + local expected_adler32 = ((b3*256 + b2)*256 + b1)*256 + b0 + if DEBUG then + debug('alder32=', expected_adler32) + end + if not disable_crc then + if data_adler32 ~= expected_adler32 then + runtime_error('invalid compressed data--crc error') + end + end + if bs:read() then + warn 'trailing garbage ignored' + end +end + + +return M \ No newline at end of file diff --git a/apis/itemDB.lua b/apis/itemDB.lua new file mode 100644 index 0000000..5545cf0 --- /dev/null +++ b/apis/itemDB.lua @@ -0,0 +1,39 @@ +local Util = require('util') +local TableDB = require('tableDB') + +local itemDB = TableDB({ fileName = 'usr/etc/items.db' }) + +function itemDB:get(key) + + local item = TableDB.get(self, key) + + if item then + return item + end + + if key[2] ~= 0 then + item = TableDB.get(self, { key[1], 0, key[3] }) + if item and item.maxDamage > 0 then + item = Util.shallowCopy(item) + item.damage = key[2] + item.displayName = string.format('%s (damage: %d)', item.displayName, item.damage) + return item + end + end +end + +function itemDB:add(key, item) + + if item.maxDamage > 0 then + key = { key[1], 0, key[3] } + end + TableDB.add(self, key, item) +end + +function itemDB:makeKey(item) + return { item.name, item.damage, item.nbtHash } +end + +itemDB:load() + +return itemDB diff --git a/apis/me.lua b/apis/me.lua new file mode 100644 index 0000000..88d573f --- /dev/null +++ b/apis/me.lua @@ -0,0 +1,167 @@ +local Util = require('util') + +local ME = { + jobList = { } +} + +function ME.setDevice(device) + ME.p = device + --Util.merge(ME, ME.p) + + if not device then + error('ME device not attached') + end + + for k,v in pairs(ME.p) do + if not ME[k] then + ME[k] = v + end + end +end + +function ME.isAvailable() + return not Util.empty(ME.getAvailableItems()) +end + +-- Strip off color prefix +local function safeString(text) + + local val = text:byte(1) + + if val < 32 or val > 128 then + + local newText = {} + for i = 4, #text do + local val = text:byte(i) + newText[i - 3] = (val > 31 and val < 127) and val or 63 + end + return string.char(unpack(newText)) + end + + return text +end + +function ME.getAvailableItems() + local items + pcall(function() + items = ME.p.getAvailableItems('all') + for k,v in pairs(items) do + v.id = v.item.id + v.name = safeString(v.item.display_name) + v.qty = v.item.qty + v.dmg = v.item.dmg + v.max_dmg = v.item.max_dmg + v.nbt_hash = v.item.nbt_hash + end + end) + + return items or { } +end + +function ME.getItemCount(id, dmg, nbt_hash, ignore_dmg) + + local fingerprint = { + id = id, + nbt_hash = nbt_hash, + } + + if not ignore_dmg or ignore_dmg ~= 'yes' then + fingerprint.dmg = dmg or 0 + end + + local item = ME.getItemDetail(fingerprint, false) + + if item then + return item.qty + end + + return 0 +end + +function ME.extract(id, dmg, nbt_hash, qty, direction, slot) + dmg = dmg or 0 + qty = qty or 1 + direction = direction or 'up' + return pcall(function() + local fingerprint = { + dmg = dmg, + id = id, + nbt_hash = nbt_hash + } + return ME.exportItem(fingerprint, direction, qty, slot) + end) +end + +function ME.insert(slot, qty, direction) + direction = direction or 'up' + return ME.pullItem(direction, slot, qty) +end + +function ME.isCrafting() + local cpus = ME.p.getCraftingCPUs() or { } + for k,v in pairs(cpus) do + if v.busy then + return true + end + end +end + +function ME.isCPUAvailable() + local cpus = ME.p.getCraftingCPUs() or { } + local available = false + + for cpu,v in pairs(cpus) do + if not v.busy then + available = true + elseif not ME.jobList[cpu] then -- something else is crafting something (don't know what) + return false -- return false since we are in an unknown state + end + end + return available +end + +function ME.getJobList() + + local cpus = ME.p.getCraftingCPUs() or { } + for cpu,v in pairs(cpus) do + if not v.busy then + ME.jobList[cpu] = nil + end + end + + return ME.jobList +end + +function ME.craft(id, dmg, nbt_hash, qty) + local cpus = ME.p.getCraftingCPUs() or { } + for cpu,v in pairs(cpus) do + if not v.busy then + ME.p.requestCrafting({ + id = id, + dmg = dmg or 0, + nbt_hash = nbt_hash, + }, + qty or 1, + cpu + ) + + os.sleep(0) -- tell it to craft, yet it doesn't show busy - try waiting a cycle... + cpus = ME.p.getCraftingCPUs() or { } + if not cpus[cpu].busy then + -- print('sleeping again') + os.sleep(.1) -- sigh + cpus = ME.p.getCraftingCPUs() or { } + end + + -- not working :( + if cpus[cpu].busy then + ME.jobList[cpu] = { id = id, dmg = dmg, qty = qty, nbt_hash = nbt_hash } + return true + end + break -- only need to try the first available cpu + end + end + return false +end + +return ME \ No newline at end of file diff --git a/apis/meAdapter.lua b/apis/meAdapter.lua new file mode 100644 index 0000000..e146196 --- /dev/null +++ b/apis/meAdapter.lua @@ -0,0 +1,157 @@ +local class = require('class') +local Util = require('util') +local Peripheral = require('peripheral') + +local MEProvider = class() + +function MEProvider:init(args) + local defaults = { + items = { }, + name = 'ME', + } + Util.merge(self, defaults) + Util.merge(self, args) + + if self.side then + local mep = peripheral.wrap('bottom') + if mep then + Util.merge(self, mep) + end + else + local mep = Peripheral.getByMethod('getAvailableItems') + if mep then + Util.merge(self, mep) + end + end + + local sides = { + top = 'down', + bottom = 'up', + east = 'west', + west = 'east', + north = 'south', + south = 'north', + } + self.oside = sides[self.direction or self.side] +end + +function MEProvider:isValid() + return self.getAvailableItems and self.getAvailableItems() +end + +-- Strip off color prefix +local function safeString(text) + + local val = text:byte(1) + + if val < 32 or val > 128 then + + local newText = {} + for i = 4, #text do + local val = text:byte(i) + newText[i - 3] = (val > 31 and val < 127) and val or 63 + end + return string.char(unpack(newText)) + end + + return text +end + +local convertNames = { + name = 'id', + damage = 'dmg', + maxCount = 'max_size', + count = 'qty', + displayName = 'display_name', + maxDamage = 'max_dmg', +} + +local function convertItem(item) + for k,v in pairs(convertNames) do + item[k] = item[v] + item[v] = nil + end + item.displayName = safeString(item.displayName) +end + +function MEProvider:refresh() + self.items = self.getAvailableItems('all') + for _,v in pairs(self.items) do + Util.merge(v, v.item) + convertItem(v) + end + return self.items +end + +function MEProvider:listItems() + self:refresh() + return self.items +end + +function MEProvider:getItemInfo(name, damage) + + for key,item in pairs(self.items) do + if item.name == name and item.damage == damage then + return item + end + end +end + +function MEProvider:craft(name, damage, count) + + self:refresh() + + local item = self:getItemInfo(name, damage) + + if item and item.is_craftable then + + self.requestCrafting({ id = name, dmg = damage }, count) + return true + end +end + +function MEProvider:craftItems(items) + local cpus = self.getCraftingCPUs() or { } + local count = 0 + + for _,cpu in pairs(cpus) do + if cpu.busy then + return + end + end + + for _,item in pairs(items) do + if count >= #cpus then + break + end + if self:craft(item.name, item.damage, item.count) then + count = count + 1 + end + end +end + +function MEProvider:provide(item, count, slot) + return pcall(function() + self.exportItem({ + id = item.name, + dmg = item.damage + }, self.oside, count, slot) + end) +end + +function MEProvider:insert(slot, count) + local s, m = pcall(function() self.pullItem(self.oside, slot, count) end) + if not s and m then + print('MEProvider:pullItem') + print(m) + sleep(1) + s, m = pcall(function() self.pullItem(self.oside, slot, count) end) + if not s and m then + print('MEProvider:pullItem') + print(m) + read() + end + end +end + +return MEProvider diff --git a/apis/message.lua b/apis/message.lua new file mode 100644 index 0000000..fd38ed1 --- /dev/null +++ b/apis/message.lua @@ -0,0 +1,106 @@ +local Event = require('event') +local Logger = require('logger') + +local Message = { } + +local messageHandlers = {} + +function Message.enable() + if not device.wireless_modem.isOpen(os.getComputerID()) then + device.wireless_modem.open(os.getComputerID()) + end + if not device.wireless_modem.isOpen(60000) then + device.wireless_modem.open(60000) + end +end + +if device and device.wireless_modem then + Message.enable() +end + +Event.on('device_attach', function(event, deviceName) + if deviceName == 'wireless_modem' then + Message.enable() + end +end) + +function Message.addHandler(type, f) + table.insert(messageHandlers, { + type = type, + f = f, + enabled = true + }) +end + +function Message.removeHandler(h) + for k,v in pairs(messageHandlers) do + if v == h then + messageHandlers[k] = nil + break + end + end +end + +Event.on('modem_message', + function(event, side, sendChannel, replyChannel, msg, distance) + if msg and msg.type then -- filter out messages from other systems + local id = replyChannel + Logger.log('modem_receive', { id, msg.type }) + --Logger.log('modem_receive', msg.contents) + for k,h in pairs(messageHandlers) do + if h.type == msg.type then +-- should provide msg.contents instead of message - type is already known + h.f(h, id, msg, distance) + end + end + end + end +) + +function Message.send(id, msgType, contents) + if not device.wireless_modem then + error('No modem attached', 2) + end + + if id then + Logger.log('modem_send', { tostring(id), msgType }) + device.wireless_modem.transmit(id, os.getComputerID(), { + type = msgType, contents = contents + }) + else + Logger.log('modem_send', { 'broadcast', msgType }) + device.wireless_modem.transmit(60000, os.getComputerID(), { + type = msgType, contents = contents + }) + end +end + +function Message.broadcast(t, contents) + if not device.wireless_modem then + error('No modem attached', 2) + end + + Message.send(nil, t, contents) +-- Logger.log('rednet_send', { 'broadcast', t }) +-- rednet.broadcast({ type = t, contents = contents }) +end + +function Message.waitForMessage(msgType, timeout, fromId) + local timerId = os.startTimer(timeout) + repeat + local e, side, _id, id, msg, distance = os.pullEvent() + if e == 'modem_message' then + if msg and msg.type and msg.type == msgType then + if not fromId or id == fromId then + return e, id, msg, distance + end + end + end + until e == 'timer' and side == timerId +end + +function Message.enableWirelessLogging() + Logger.setWirelessLogging() +end + +return Message \ No newline at end of file diff --git a/apis/refinedAdapter.lua b/apis/refinedAdapter.lua new file mode 100644 index 0000000..f404fc5 --- /dev/null +++ b/apis/refinedAdapter.lua @@ -0,0 +1,143 @@ +local class = require('class') +local Util = require('util') +local Peripheral = require('peripheral') +local itemDB = require('itemDB') + +local RefinedAdapter = class() + +local keys = { + 'damage', + 'displayName', + 'maxCount', + 'maxDamage', + 'name', + 'nbtHash', +} + +function RefinedAdapter:init(args) + local defaults = { + items = { }, + name = 'refinedStorage', + } + Util.merge(self, defaults) + Util.merge(self, args) + + local controller = Peripheral.getByType('refinedstorage:controller') + if controller then + Util.merge(self, controller) + end +end + +function RefinedAdapter:isValid() + return not not self.listAvailableItems +end + +function RefinedAdapter:isOnline() + return self.getNetworkEnergyStored() > 0 +end + +function RefinedAdapter:getCachedItemDetails(item) + local key = { item.name, item.damage, item.nbtHash } + + local detail = itemDB:get(key) + if not detail then + detail = self.findItem(item) + if detail then + local meta + pcall(function() meta = detail.getMetadata() end) + if not meta then + return + end + Util.merge(detail, meta) + + local t = { } + for _,k in pairs(keys) do + t[k] = detail[k] + end + + detail = t + itemDB:add(key, detail) + end + end + if detail then + return Util.shallowCopy(detail) + end +end + +function RefinedAdapter:listItems() + local items = { } + local list + + pcall(function() + list = self.listAvailableItems() + end) + + if list then + + local throttle = Util.throttle() + + for _,v in pairs(list) do + local item = self:getCachedItemDetails(v) + if item then + item.count = v.count + table.insert(items, item) + end + throttle() + end + itemDB:flush() + end + + return items +end + +function RefinedAdapter:getItemInfo(fingerprint) + + local key = { fingerprint.name, fingerprint.damage, fingerprint.nbtHash } + + local item = itemDB:get(key) + if not item then + return self:getCachedItemDetails(fingerprint) + end + + local detail = self.findItem(item) + if detail then + item.count = detail.count + return item + end +end + +function RefinedAdapter:isCrafting(item) + for _,task in pairs(self.getCraftingTasks()) do + local output = task.getPattern().outputs[1] + if output.name == item.name and + output.damage == item.damage and + output.nbtHash == item.nbtHash then + return true + end + end + return false +end + +function RefinedAdapter:craft(item, qty) + local detail = self.findItem(item) + if detail then + return detail.craft(qty) + end +end + +function RefinedAdapter:craftItems(items) + return false +end + +function RefinedAdapter:provide(item, qty, slot) +end + +function RefinedAdapter:extract(slot, qty) +-- self.pushItems(self.direction, slot, qty) +end + +function RefinedAdapter:insert(slot, qty) +-- self.pullItems(self.direction, slot, qty) +end + +return RefinedAdapter diff --git a/apis/schematic.lua b/apis/schematic.lua new file mode 100644 index 0000000..f7d08d9 --- /dev/null +++ b/apis/schematic.lua @@ -0,0 +1,1175 @@ +local class = require('class') +local Util = require('util') +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 name == "Blocks" then + for i = 1, i do + local id = h:readbyte(h) + if id > 0 then + table.insert(self.blocks, { + id = id, + index = i, + }) + end + 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) + + spinner:spin() + end + + fh:close() + + print('Assigning coords ') + local index = 1 + for _, b in ipairs(self.blocks) do + while index < b.index do + 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 + index = index + 1 + end + b.x = self.x + b.y = self.y + b.z = self.z + spinner:spin() + end + + 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('Loading blocks ') + self:loadpass(f, spinner) + + 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:assignDamages(spinner) + print('Assigning damages') + + for _,b in pairs(self.blocks) do + b.dmg = self.damages[b.index] or 0 + spinner:spin() + end +end + +function Schematic:findIndexAt(x, z, y, allBlocks) + 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' and not allBlocks then + -- this will possibly 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 + + 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 diff --git a/apis/tableDB.lua b/apis/tableDB.lua new file mode 100644 index 0000000..6299088 --- /dev/null +++ b/apis/tableDB.lua @@ -0,0 +1,54 @@ +local class = require('class') +local Util = require('util') + +local TableDB = class() +function TableDB:init(args) + local defaults = { + fileName = '', + dirty = false, + data = { }, + tabledef = { }, + } + Util.merge(defaults, args) + Util.merge(self, defaults) +end + +function TableDB:load() + local table = Util.readTable(self.fileName) + if table then + self.data = table.data + self.tabledef = table.tabledef + end +end + +function TableDB:add(key, entry) + if type(key) == 'table' then + key = table.concat(key, ':') + end + self.data[key] = entry + self.dirty = true +end + +function TableDB:get(key) + if type(key) == 'table' then + key = table.concat(key, ':') + end + return self.data[key] +end + +function TableDB:remove(key) + self.data[key] = nil + self.dirty = true +end + +function TableDB:flush() + if self.dirty then + Util.writeTable(self.fileName, { + -- tabledef = self.tabledef, + data = self.data, + }) + self.dirty = false + end +end + +return TableDB diff --git a/apps/base64dl.lua b/apps/base64dl.lua new file mode 100644 index 0000000..203b4a1 --- /dev/null +++ b/apps/base64dl.lua @@ -0,0 +1,36 @@ +requireInjector(getfenv(1)) + +Base64 = require('base64') + +local args = { ... } + +if not args[2] then + error('Syntax: base64dl ') +end + +local c = http.get(args[2]) + +if not c then + error('unable to open url') +end + +local data = c.readAll() +c.close() + +print('size: ' .. #data) +local decoded = Base64.decode(data) +print('decoded: ' .. #decoded) + +local file = io.open(shell.resolve(args[1]), "wb") +if not file then + error('Unable to open ' .. args[1], 2) +end +for k,b in ipairs(decoded) do + if (k % 1000) == 0 then + os.sleep(0) + end + file:write(b) +end + +file:close() +print('done') diff --git a/apps/builder.lua b/apps/builder.lua new file mode 100644 index 0000000..020fb1b --- /dev/null +++ b/apps/builder.lua @@ -0,0 +1,2172 @@ +if not turtle and not commands then + error('Must be run on a turtle or a command computer') +end + +requireInjector(getfenv(1)) +package.path = package.path .. ':/' .. fs.getDir(shell.getRunningProgram()) .. '/apis' + +local Blocks = require('blocks') +local class = require('class') +local Event = require('event') +local MEAdapter = require('meAdapter') +local Message = require('message') +local Point = require('point') +local Schematic = require('schematic') +local TableDB = require('tableDB') +local UI = require('ui') +local Util = require('util') + +local ChestAdapter = require('chestAdapter') +if Util.getVersion() == 1.8 then + ChestAdapter = require('chestAdapter18') +end + +if not _G.device then + local Opus = require('opus') + Opus.loadExtensions() +end + +local BUILDER_DIR = 'usr/builder' + +local schematic = Schematic() +local blocks = Blocks({ dir = BUILDER_DIR }) + +local SUPPLIES_PT = { x = -1, z = -1, y = 0 } + +local Builder = { + version = '1.71', + isCommandComputer = not turtle, + slots = { }, + index = 1, + mode = 'build', + fuelItem = { id = 'minecraft:coal', dmg = 0 }, + resourceSlots = 14, + facing = 'south', + confirmFacing = false, + wrenchSucks = false, +} + +local pistonFacings + +-- Temp functions until conversion to new adapters is complete +local function convertSingleForward(item) + item.name = item.id + item.damage = item.dmg + item.count = item.count + item.maxCount = item.max_size + return item +end + +local function convertForward(t) + for _,v in pairs(t) do + convertSingleForward(v) + end + return t +end + +local function convertSingleBack(item) + if item then + item.id = item.name + item.dmg = item.damage + item.qty = item.count + item.max_size = item.maxCount + item.display_name = item.displayName + end + return item +end + +local function convertBack(t) + for _,v in pairs(t) do + convertSingleBack(v) + end + return t +end + +--[[-- SubDB --]]-- +subDB = TableDB({ + fileName = fs.combine(BUILDER_DIR, 'sub.db'), +}) + +function subDB:load() + if fs.exists(self.fileName) then + TableDB.load(self) + elseif not Builder.isCommandComputer then + self:seedDB() + end +end + +function subDB:seedDB() + self.data = { + [ "minecraft:redstone_wire:0" ] = "minecraft:redstone:0", + [ "minecraft:wall_sign:0" ] = "minecraft:sign:0", + [ "minecraft:standing_sign:0" ] = "minecraft:sign:0", + [ "minecraft:potatoes:0" ] = "minecraft:potato:0", + [ "minecraft:unlit_redstone_torch:0" ] = "minecraft:redstone_torch:0", + [ "minecraft:powered_repeater:0" ] = "minecraft:repeater:0", + [ "minecraft:unpowered_repeater:0" ] = "minecraft:repeater:0", + [ "minecraft:carrots:0" ] = "minecraft:carrot:0", + [ "minecraft:cocoa:0" ] = "minecraft:dye:3", + [ "minecraft:unpowered_comparator:0" ] = "minecraft:comparator:0", + [ "minecraft:powered_comparator:0" ] = "minecraft:comparator:0", + [ "minecraft:piston_head:0" ] = "minecraft:air:0", + [ "minecraft:piston_extension:0" ] = "minecraft:air:0", + [ "minecraft:minecraft:portal:0" ] = "minecraft:air:0", + [ "minecraft:double_wooden_slab:0" ] = "minecraft:planks:0", + [ "minecraft:double_wooden_slab:1" ] = "minecraft:planks:1", + [ "minecraft:double_wooden_slab:2" ] = "minecraft:planks:2", + [ "minecraft:double_wooden_slab:3" ] = "minecraft:planks:3", + [ "minecraft:double_wooden_slab:4" ] = "minecraft:planks:4", + [ "minecraft:double_wooden_slab:5" ] = "minecraft:planks:5", + [ "minecraft:lit_redstone_lamp:0" ] = "minecraft:redstone_lamp:0", + [ "minecraft:double_stone_slab:1" ] = "minecraft:sandstone:0", + [ "minecraft:double_stone_slab:2" ] = "minecraft:planks:0", + [ "minecraft:double_stone_slab:3" ] = "minecraft:cobblestone:0", + [ "minecraft:double_stone_slab:4" ] = "minecraft:brick_block:0", + [ "minecraft:double_stone_slab:5" ] = "minecraft:stonebrick:0", + [ "minecraft:double_stone_slab:6" ] = "minecraft:nether_brick:0", + [ "minecraft:double_stone_slab:7" ] = "minecraft:quartz_block:0", + [ "minecraft:double_stone_slab:9" ] = "minecraft:sandstone:2", + [ "minecraft:double_stone_slab2:0" ] = "minecraft:sandstone:0", + [ "minecraft:stone_slab:2" ] = "minecraft:wooden_slab:0", + [ "minecraft:wheat:0" ] = "minecraft:wheat_seeds:0", + [ "minecraft:flowing_water:0" ] = "minecraft:air:0", + [ "minecraft:lit_furnace:0" ] = "minecraft:furnace:0", + [ "minecraft:wall_banner:0" ] = "minecraft:banner:0", + [ "minecraft:standing_banner:0" ] = "minecraft:banner:0", + [ "minecraft:tripwire:0" ] = "minecraft:string:0", + [ "minecraft:pumpkin_stem:0" ] = "minecraft:pumpkin_seeds:0", + } + self.dirty = true + self:flush() +end + +function subDB:add(s) + TableDB.add(self, { s.id, s.dmg }, table.concat({ s.sid, s.sdmg }, ':')) + self:flush() +end + +function subDB:remove(s) + -- TODO: tableDB.remove should take table key + TableDB.remove(self, s.id .. ':' .. s.dmg) + self:flush() +end + +function subDB:extract(s) + local id, dmg = s:match('(.+):(%d+)') + return id, tonumber(dmg) +end + +function subDB:getSubstitutedItem(id, dmg) + local sub = TableDB.get(self, { id, dmg }) + if sub then + id, dmg = self:extract(sub) + end + return { id = id, dmg = dmg } +end + +function subDB:lookupBlocksForSub(sid, sdmg) + local t = { } + for k,v in pairs(self.data) do + local id, dmg = self:extract(v) + if id == sid and dmg == sdmg then + id, dmg = self:extract(k) + t[k] = { id = id, dmg = dmg, sid = sid, sdmg = sdmg } + end + end + return t +end + +--[[-- maxStackDB --]]-- +maxStackDB = TableDB({ + fileName = fs.combine(BUILDER_DIR, 'maxstack.db'), +}) + +function maxStackDB:get(id, dmg) + return self.data[id .. ':' .. dmg] or 64 +end + +--[[-- Spinner --]]-- +UI.Spinner = class() +function UI.Spinner:init(args) + local defaults = { + UIElement = 'Spinner', + timeout = .095, + x = 1, + y = 1, + c = os.clock(), + spinIndex = 0, + spinSymbols = { '-', '/', '|', '\\' } + } + defaults.x, defaults.y = term.getCursorPos() + defaults.startX = defaults.x + defaults.startY = defaults.y + + UI.setProperties(self, defaults) + UI.setProperties(self, args) +end + +function UI.Spinner:spin(text) + local cc = os.clock() + if cc > self.c + self.timeout then + term.setCursorPos(self.x, self.y) + local str = self.spinSymbols[self.spinIndex % #self.spinSymbols + 1] + if text then + str = str .. ' ' .. text + end + term.write(str) + self.spinIndex = self.spinIndex + 1 + os.sleep(0) + self.c = os.clock() + end +end + +function UI.Spinner:stop(text) + term.setCursorPos(self.x, self.y) + local str = string.rep(' ', #self.spinSymbols) + if text then + str = str .. ' ' .. text + end + term.write(str) + term.setCursorPos(self.startX, self.startY) +end + +--[[-- Builder --]]-- +function Builder:getBlockCounts() + local blocks = { } + + -- add a couple essential items to the supply list to allow replacements + if not self.isCommandComputer then + local wrench = subDB:getSubstitutedItem('SubstituteAWrench', 0) + wrench.qty = 0 + wrench.need = 1 + blocks[wrench.id .. ':' .. wrench.dmg] = wrench + + local fuel = subDB:getSubstitutedItem(Builder.fuelItem.id, Builder.fuelItem.dmg) + fuel.qty = 0 + fuel.need = 1 + blocks[fuel.id .. ':' .. fuel.dmg] = fuel + + blocks['minecraft:piston:0'] = { + id = 'minecraft:piston', + dmg = 0, + qty = 0, + need = 1, + } + end + + for k,b in ipairs(schematic.blocks) do + if k >= self.index then + local key = tostring(b.id) .. ':' .. b.dmg + local block = blocks[key] + if not block then + block = Util.shallowCopy(b) + block.qty = 0 + block.need = 0 + blocks[key] = block + end + block.need = block.need + 1 + end + end + + return blocks +end + +function Builder:selectItem(id, dmg) + + for k,s in ipairs(self.slots) do + if s.qty > 0 and s.id == id and s.dmg == dmg then + -- check to see if someone pulled items from inventory + -- or we passed over a hopper + if turtle.getItemCount(s.index) > 0 then + if k > 1 and s.qty > 1 then + table.remove(self.slots, k) + table.insert(self.slots, 1, s) + end + turtle.select(s.index) + return s + end + end + end +end + +function Builder:getAirResupplyList(blockIndex) + + local slots = { } + + if self.mode == 'destroy' then + for i = 1, self.resourceSlots do + slots[i] = { + qty = 0, + need = 0, + index = i + } + end + else + slots, _ = self:getGenericSupplyList(blockIndex) + end + + local fuel = subDB:getSubstitutedItem(Builder.fuelItem.id, Builder.fuelItem.dmg) + + slots[15] = { + id = 'minecraft:chest', --'ironchest:BlockIronChest', -- + dmg = 0, + qty = 0, + need = 1, + index = 15, + } + + slots[16] = { + id = fuel.id, + dmg = fuel.dmg, + wrench = true, + qty = 0, + need = 64, + index = 16, + } + + return slots +end + +function Builder:getSupplyList(blockIndex) + + local slots, lastBlock = self:getGenericSupplyList(blockIndex) + + slots[15] = { + id = 'minecraft:piston', + dmg = 0, + qty = 0, + need = 1, + index = 15, + } + + local wrench = subDB:getSubstitutedItem('SubstituteAWrench', 0) + slots[16] = { + id = wrench.id, + dmg = wrench.dmg, + wrench = true, + qty = 0, + need = 1, + index = 16, + } + + self.slots = slots + + return lastBlock +end + +function Builder:getGenericSupplyList(blockIndex) + + local slots = { } + + for i = 1, self.resourceSlots do + slots[i] = { + qty = 0, + need = 0, + index = i + } + end + + local function getSlot(id, dmg) + -- find matching slot + local maxStack = maxStackDB:get(id, dmg) + for _, s in ipairs(slots) do + if s.id == id and s.dmg == dmg and s.need < maxStack then + return s + end + end + -- return first available slot + for _, s in ipairs(slots) do + if not s.id then + s.key = id .. ':' .. dmg + s.id = id + s.dmg = dmg + return s + end + end + end + + local lastBlock = blockIndex + for k = blockIndex, #schematic.blocks do + lastBlock = k + local b = schematic:getComputedBlock(k) + + if b.id ~= 'minecraft:air' then + local slot = getSlot(b.id, b.dmg) + if not slot then + break + end + slot.need = slot.need + 1 + end + end + + for _,s in pairs(slots) do + if s.id then + s.name = blocks.nameDB:getName(s.id, s.dmg) + end + end + + return slots, lastBlock +end + +function Builder:substituteBlocks(throttle) + + for _,b in pairs(schematic.blocks) do + + -- replace schematic block type with substitution + local pb = blocks:getPlaceableBlock(b.id, b.dmg) + + Util.merge(b, pb) + + b.odmg = pb.odmg or pb.dmg + + local sub = subDB:get({ b.id, b.dmg }) + if sub then + b.id, b.dmg = subDB:extract(sub) + end + throttle() + end +end + +function Builder:dumpInventory() + + local success = true + + for i = 1, 16 do + local qty = turtle.getItemCount(i) + if qty > 0 then + self.itemAdapter:insert(i, qty) + end + if turtle.getItemCount(i) ~= 0 then + success = false + end + end + turtle.select(1) + + return success +end + +function Builder:dumpInventoryWithCheck() + + while not self:dumpInventory() do + print('Storage is full or missing - make space or replace') + print('Press enter to continue') + turtle.setHeading(0) + read() + end +end + +function Builder:autocraft(supplies) + local t = { } + + for i,s in pairs(supplies) do + local key = s.id .. ':' .. s.dmg + local item = t[key] + if not item then + item = { + id = s.id, + dmg = s.dmg, + qty = 0, + } + t[key] = item + end + item.qty = item.qty + (s.need - s.qty) + end + + Builder.itemAdapter:craftItems(convertForward(t)) +end + +function Builder:getSupplies() + + self.itemAdapter:refresh() + + local t = { } + for _,s in ipairs(self.slots) do + if s.need > 0 then + local item = convertSingleBack(self.itemAdapter:getItemInfo(s.id, s.dmg)) + if item then + s.name = item.display_name + + local qty = math.min(s.need - s.qty, item.qty) + + if qty + s.qty > item.max_size then + maxStackDB:add({ s.id, s.dmg }, item.max_size) + maxStackDB.dirty = true + maxStackDB:flush() + qty = item.max_size + s.need = qty + end + if qty > 0 then + self.itemAdapter:provide(convertSingleForward(item), qty, s.index) + s.qty = turtle.getItemCount(s.index) + end + else + s.name = blocks.nameDB:getName(s.id, s.dmg) + end + end + if s.qty < s.need then + table.insert(t, s) + end + end + + return t +end + +Event.on('build', function() + Builder:build() +end) + +function Builder:refuel() + while turtle.getFuelLevel() < 4000 and self.fuelItem do + print('Refueling') + turtle.select(1) + + local fuel = subDB:getSubstitutedItem(self.fuelItem.id, self.fuelItem.dmg) + + self.itemAdapter:provide(convertSingleForward(fuel), 64, 1) + if turtle.getItemCount(1) == 0 then + print('Out of fuel, add fuel to chest/ME system') + turtle.setHeading(0) + turtle.status = 'waiting' + os.sleep(5) + else + turtle.refuel(64) + end + end +end + +function Builder:inAirDropoff() + if not device.wireless_modem then + return false + end + + self:log('Requesting air supply drop for supply #: ' .. 1) + while true do + Message.broadcast('needSupplies', { point = turtle.getPoint(), uid = 1 }) + local _, id, msg, _ = Message.waitForMessage('gotSupplies', 1) + + if not msg or not msg.contents then + Message.broadcast('supplyList', { uid = 1, slots = self:getAirResupplyList() }) + return false + end + + turtle.status = 'waiting' + + if msg.contents.point then + local pt = msg.contents.point + + self:log('Received supply location') + os.sleep(0) + + turtle.goto(pt.x, pt.z, pt.y) + os.sleep(.1) -- random computer is not connected error + + local chestAdapter = ChestAdapter({ direction = 'down', wrapSide = 'top' }) + + if not chestAdapter:isValid() then + self:log('Chests above is not valid') + return false + end + + local oldAdapter = self.itemAdapter + self.itemAdapter = chestAdapter + + if not self:dumpInventory() then + self:log('Unable to dump inventory') + self.itemAdapter = oldAdapter + return false + end + + self.itemAdapter = oldAdapter + + Message.broadcast('thanks', { }) + + for i = 1,12 do -- wait til supplier is idle before sending next request + if turtle.detectUp() then + os.sleep(.25) + end + end + os.sleep(.1) + + Message.broadcast('supplyList', { uid = 1, slots = self:getAirResupplyList() }) + + return true + end + end +end + +function Builder:inAirResupply() + + if not device.wireless_modem then + return false + end + + local oldAdapter = self.itemAdapter + + self:log('Requesting air supply drop for supply #: ' .. self.slotUid) + while true do + Message.broadcast('needSupplies', { point = turtle.getPoint(), uid = Builder.slotUid }) + local _, id, msg, _ = Message.waitForMessage('gotSupplies', 1) + + if not msg or not msg.contents then + self.itemAdapter = oldAdapter + return false + end + + turtle.status = 'waiting' + + if msg.contents.point then + local pt = msg.contents.point + + self:log('Received supply location') + os.sleep(0) + + turtle.goto(pt.x, pt.z, pt.y) + os.sleep(.1) -- random computer is not connected error + + local chestAdapter = ChestAdapter({ direction = 'down', wrapSide = 'top' }) + + if not chestAdapter:isValid() then + Util.print('not valid') + read() + end + + self.itemAdapter = chestAdapter + + if not self:dumpInventory() then + self.itemAdapter = oldAdapter + return false + end + self:refuel() + + local lastBlock = self:getSupplyList(self.index) + local supplies = self:getSupplies() + + Message.broadcast('thanks', { }) + + self.itemAdapter = oldAdapter + + if #supplies == 0 then + + for i = 1,12 do -- wait til supplier is idle before sending next request + if turtle.detectUp() then + os.sleep(.25) + end + end + os.sleep(.1) + if lastBlock < #schematic.blocks then + self:sendSupplyRequest(lastBlock) + else + Message.broadcast('finished') + end + + return true + end + self:log('Missing supplies - manually resupplying') + return false + end + end +end + +function Builder:sendSupplyRequest(lastBlock) + + if device.wireless_modem then + local slots = self:getAirResupplyList(lastBlock) + self.slotUid = os.clock() + + Message.broadcast('supplyList', { uid = self.slotUid, slots = slots }) + end +end + +function Builder:resupply() + + if self.slotUid and self:inAirResupply() then + os.queueEvent('build') + return + end + + turtle.status = 'resupplying' + + self:log('Resupplying') + turtle.gotoYlast(SUPPLIES_PT) + os.sleep(.1) -- random 'Computer is not connected' error... + self:dumpInventoryWithCheck() + self:refuel() + local lastBlock = self:getSupplyList(self.index) + if lastBlock < #schematic.blocks then + self:sendSupplyRequest(lastBlock) + elseif device.wireless_modem then + Message.broadcast('finished') + end + os.sleep(1) + local supplies = self:getSupplies() + + if #supplies == 0 then + os.queueEvent('build') + else + turtle.setHeading(0) + self:autocraft(supplies) + supplyPage:setSupplies(supplies) + UI:setPage('supply') + end +end + +function Builder:placeDown(slot) + return turtle.placeDown(slot.index) +end + +function Builder:placeUp(slot) + return turtle.placeUp(slot.index) +end + +function Builder:place(slot) + return turtle.place(slot.index) +end + +function Builder:getWrenchSlot() + + local wrench = subDB:getSubstitutedItem('SubstituteAWrench', 0) + return Builder:selectItem(wrench.id, wrench.dmg) +end + +-- figure out our orientation in the world +function Builder:getTurtleFacing() + + if Util.getVersion() == 1.8 then + + local directions = { -- reversed directions + [5] = 'west', + [3] = 'north', + [4] = 'east', + [2] = 'south', + } + + if self:selectItem('minecraft:piston', 0) then + turtle.placeUp() + local _, bi = turtle.inspectUp() + turtle.digUp() + return directions[bi.metadata] + end + return + end + return Builder.facing +end + +function Builder:wrenchBlock(side, facing, cache) + + local s = Builder:getWrenchSlot() + + if not s then + b.needResupply = true + return false + end + + local key = turtle.point.heading .. '-' .. facing + if cache then + local count = cache[side][key] + + if count then + turtle.select(s.index) + for i = 1,count do + turtle.getAction(side).place() + end + return true + end + end + + local directions = { + [5] = 'east', + [3] = 'south', + [4] = 'west', + [2] = 'north', + [0] = 'down', + [1] = 'up', + } + + if turtle.getHeadingInfo(facing).heading < 4 then + local offsetDirection = (turtle.getHeadingInfo(Builder.facing).heading + + turtle.getHeadingInfo(facing).heading) % 4 + facing = turtle.getHeadingInfo(offsetDirection).direction + end + + count = 0 + print('determining wrench count') + for i = 1, 6 do + local _, bi = turtle.getAction(side).inspect() + + if facing == directions[bi.metadata] then + if cache then + cache[side][key] = count + end + return true + end + count = count + 1 + turtle.getAction(side).place() + end + + return false +end + +function Builder:rotateBlock(side, facing) + + local s = Builder:getWrenchSlot() + + if not s then + b.needResupply = true + return false + end + + for i = 1, facing do + turtle.getAction(side).place() + end + + return true + + --[[ + local origFacing + while true do + local _, bi = turtle.getAction(side).inspect() + + -- spin until it repeats + if not origFacing then + origFacing = bi.metadata + elseif bi.metadata == origFacing then + return false + end + + if facing == bi.metadata then + return true + end + turtle.getAction(side).place() + end + + return false + ]]-- +end + +-- place piston, wrench piston to face downward, extend, remove piston +function Builder:placePiston(b) + + local ps = Builder:selectItem('minecraft:piston', 0) + local ws = Builder:getWrenchSlot() + + if not ps or not ws then + b.needResupply = true + -- a hopper may have eaten the piston + return + end + + if not turtle.place(ps.index) then + return + end + + if self.wrenchSucks then + turtle.turnRight() + turtle.forward() + turtle.turnLeft() + turtle.forward() + turtle.turnLeft() + end + + local success = self:wrenchBlock('forward', 'down', pistonFacings) --wrench piston to point downwards + + rs.setOutput('front', true) + os.sleep(.25) + rs.setOutput('front', false) + os.sleep(.25) + turtle.select(ps.index) + turtle.dig() + + if not success and not self.wrenchSucks then + self.wrenchSucks = true + success = self:placePiston(b) + end + + return success +end + +function Builder:goto(x, z, y, heading) + if not turtle.goto(x, z, y, heading) then + print('stuck') + print('Press enter to continue') + os.sleep(1) + turtle.status = 'stuck' + read() + end +end + +-- goto used when turtle could be below travel plane +-- if the distance is no more than 1 block, there's no need to pop back to the travel plane +function Builder:gotoEx(x, z, y, h, travelPlane) + local distance = math.abs(turtle.getPoint().x - x) + math.abs(turtle.getPoint().z - z) + + -- following code could be better + if distance == 0 then + turtle.gotoY(y) + elseif distance == 1 then + if turtle.point.y < y then + turtle.gotoY(y) + end + elseif distance > 1 then + self:gotoTravelPlane(travelPlane) + end + self:goto(x, z, y, h) +end + +function Builder:placeDirectionalBlock(b, slot, travelPlane) + local d = b.direction + + local function getAdjacentPoint(pt, direction) + local hi = turtle.getHeadingInfo(direction) + return { x = pt.x + hi.xd, z = pt.z + hi.zd, y = pt.y + hi.yd, heading = (hi.heading + 2) % 4 } + end + + local directions = { + [ 'north' ] = 'north', + [ 'south' ] = 'south', + [ 'east' ] = 'east', + [ 'west' ] = 'west', + } + if directions[d] then + self:gotoEx(b.x, b.z, b.y, turtle.getHeadingInfo(directions[d]).heading, travelPlane) + b.placed = self:placeDown(slot) + end + + if d == 'top' then + self:gotoEx(b.x, b.z, b.y+1, nil, travelPlane) + if self:placeDown(slot) then + turtle.goback() + b.placed = self:placePiston(b) + end + end + + if d == 'bottom' then + local t = { + [1] = getAdjacentPoint(b, 'east'), + [2] = getAdjacentPoint(b, 'south'), + [3] = getAdjacentPoint(b, 'west'), + [4] = getAdjacentPoint(b, 'north'), + } + + local c = Point.closest(turtle.getPoint(), t) + self:gotoEx(c.x, c.z, c.y, c.heading, travelPlane) + + if self:place(slot) then + turtle.up() + b.placed = self:placePiston(b) + end + end + + local stairDownDirections = { + [ 'north-down' ] = 'north', + [ 'south-down' ] = 'south', + [ 'east-down' ] = 'east', + [ 'west-down' ] = 'west' + } + if stairDownDirections[d] then + self:gotoEx(b.x, b.z, b.y+1, turtle.getHeadingInfo(stairDownDirections[d]).heading, travelPlane) + if self:placeDown(slot) then + turtle.goback() + b.placed = self:placePiston(b) + end + end + + local stairUpDirections = { + [ 'north-up' ] = 'south', + [ 'south-up' ] = 'north', + [ 'east-up' ] = 'west', + [ 'west-up' ] = 'east' + } + if stairUpDirections[d] then + + local isSouth = (turtle.getHeadingInfo(Builder.facing).heading + + turtle.getHeadingInfo(stairUpDirections[d]).heading) % 4 == 1 + + if Util.getVersion() == 1.8 then + isSouth = false -- no stair bug in this version + end + + if isSouth then + + -- for some reason, the south facing stair doesn't place correctly + -- jump through some hoops to place it + self:gotoEx(b.x, b.z, b.y, (turtle.getHeadingInfo(stairUpDirections[d]).heading + 2) % 4, travelPlane) + if self:placeUp(slot) then + turtle.goback() + turtle.gotoY(turtle.point.y + 2) + b.placed = self:placePiston(b) + turtle.down() + b.placed = self:placePiston(b) + + b.heading = turtle.point.heading -- stop debug message below since we are pointing in wrong direction + end + else + local hi = turtle.getHeadingInfo(stairUpDirections[d]) + self:gotoEx(b.x - hi.xd, b.z - hi.zd, b.y, hi.heading, travelPlane) + if self:place(slot) then + turtle.up() + b.placed = self:placePiston(b) + end + end + end + + local horizontalDirections = { + [ 'east-west-block' ] = { 'east', 'west' }, + [ 'north-south-block' ] = { 'north', 'south' }, + } + if horizontalDirections[d] then + + local t = { + [1] = getAdjacentPoint(b, horizontalDirections[d][1]), + [2] = getAdjacentPoint(b, horizontalDirections[d][2]), + } + + local c = Point.closest(turtle.getPoint(), t) + self:gotoEx(c.x, c.z, c.y, c.heading, travelPlane) + + if self:place(slot) then + turtle.up() + b.placed = self:placePiston(b) + end + end + + local pistonDirections = { + [ 'piston-north' ] = 'north', + [ 'piston-south' ] = 'south', + [ 'piston-west' ] = 'west', + [ 'piston-east' ] = 'east', + [ 'piston-down' ] = 'down', + [ 'piston-up' ] = 'up', + } + + if pistonDirections[d] then + -- why are pistons so broke in cc 1.7 ?????????????????????? + + local ws = Builder:getWrenchSlot() + + if not ws then + b.needResupply = true + -- a hopper may have eaten the piston + return false + end + + -- piston turns relative to turtle position :) + local rotatedPistonDirections = { + [ 'piston-east' ] = 0, + [ 'piston-south' ] = 1, + [ 'piston-west' ] = 2, + [ 'piston-north' ] = 3, + } + + self:gotoEx(b.x, b.z, b.y, nil, travelPlane) + + local heading = rotatedPistonDirections[d] + if heading and turtle.point.heading % 2 ~= heading % 2 then + turtle.setHeading(heading) + end + + if self:placeDown(slot) then + b.placed = self:wrenchBlock('down', pistonDirections[d], pistonFacings) + end + end + + local wrenchDirections = { + [ 'wrench-down' ] = 'down', + [ 'wrench-up' ] = 'up', + } + + if wrenchDirections[d] then + + local ws = Builder:getWrenchSlot() + + if not ws then + b.needResupply = true + -- a hopper may have eaten the piston + return false + end + + self:gotoEx(b.x, b.z, b.y, nil, travelPlane) + + if self:placeDown(slot) then + b.placed = self:wrenchBlock('down', wrenchDirections[d]) + end + end + + local doorDirections = { + [ 'east-door' ] = 'east', + [ 'south-door' ] = 'south', + [ 'west-door' ] = 'west', + [ 'north-door' ] = 'north', + } + if doorDirections[d] then + local hi = turtle.getHeadingInfo(doorDirections[d]) + self:gotoEx(b.x - hi.xd, b.z - hi.zd, b.y - 1, hi.heading, travelPlane) + b.placed = self:place(slot) + end + + local blockDirections = { + [ 'north-block' ] = 'north', + [ 'south-block' ] = 'south', + [ 'east-block' ] = 'east', + [ 'west-block' ] = 'west', + } + if blockDirections[d] then + local hi = turtle.getHeadingInfo(blockDirections[d]) + self:gotoEx(b.x - hi.xd, b.z - hi.zd, b.y-1, hi.heading, travelPlane) + b.placed = self:place(slot) + end + + if b.facing then + self:rotateBlock('down', b.facing) + end + +-- debug +if d ~= 'top' and d ~= 'bottom' and not horizontalDirections[d] and not pistonDirections[d] then + if not b.heading or turtle.getHeading() ~= b.heading then + self:log(d .. ' - ' .. turtle.getHeading() .. ' - ' .. (b.heading or 'nil')) + --read() + end +end + + return b.placed +end + +function Builder:reloadSchematic(throttle) + schematic:reload(throttle) + self:substituteBlocks(throttle) +end + +function Builder:log(...) + Util.print(...) +end + +function Builder:logBlock(index, b) + local bdir = b.direction or '' + local logText = string.format('%d %s:%d (x:%d,z:%d:y:%d) %s', + index, b.id, b.dmg, b.x, b.z, b.y, bdir) + self:log(logText) + -- self:log(b.index) -- unique identifier of block + + if device.wireless_modem then + Message.broadcast('builder', { x = b.x, y = b.y, z = b.z, heading = b.heading }) + end +end + +function Builder:saveProgress(index) + Util.writeTable( + fs.combine(BUILDER_DIR, schematic.filename .. '.progress'), + { index = index, facing = Builder.facing } + ) +end + +function Builder:loadProgress(filename) + local progress = Util.readTable(fs.combine(BUILDER_DIR, filename)) + if progress then + Builder.index = progress.index + if Builder.index > #schematic.blocks then + Builder.index = 1 + end + Builder.facing = progress.facing or 'south' + end +end + +-- find the highest y in the last 2 planes +function Builder:findTravelPlane(index) + + local travelPlane + + for i = index, 1, -1 do + local b = schematic.blocks[i] + + local y = b.y + if b.twoHigh then + y = y + 1 + end + if not travelPlane or y > travelPlane then + travelPlane = y + elseif travelPlane and travelPlane - y > 2 then + break + end + end + + return travelPlane or 0 +end + +function Builder:gotoTravelPlane(travelPlane) + if travelPlane > turtle.point.y then + turtle.gotoY(travelPlane) + end +end + +function Builder:build() + + local direction = 1 + local last = #schematic.blocks + local travelPlane = 0 + local minFuel = schematic.height + schematic.width + schematic.length + 100 + local throttle = Util.throttle() + + if self.mode == 'destroy' then + direction = -1 + last = 1 + turtle.status = 'destroying' + elseif not self.isCommandComputer then + travelPlane = self:findTravelPlane(self.index) + turtle.status = 'building' + if not self.confirmFacing then + local facing = self:getTurtleFacing() + if facing then + self.confirmFacing = true + self.facing = facing + end + end + end + + UI:setPage('blank') + + for i = self.index, last, direction do + self.index = i + + local b = schematic:getComputedBlock(i) + + if b.id ~= 'minecraft:air' then + + if self.isCommandComputer then + self:logBlock(self.index, b) + + local id = b.id + if self.mode == 'destroy' then + id = 'minecraft:air' + end + + local function placeBlock(id, dmg, x, y, z) + + local cx, cy, cz = commands.getBlockPosition() + + local command = table.concat({ + "setblock", + cx + x + 1, + "~" .. y, + cz + z + 1, + id, + dmg, + }, ' ') + + commands.execAsync(command) + + local result = { os.pullEvent("task_complete") } + if not result[4] then + Util.print(result[5]) + if self.mode ~= 'destroy' then + read() + end + end + end + + placeBlock(id, b.odmg, b.x, b.y, b.z) + + if b.twoHigh then + local _, topBlock = schematic:findIndexAt(b.x, b.z, b.y + 1, true) + if topBlock then + placeBlock(id, topBlock.odmg, b.x, b.y + 1, b.z) + end + end + + elseif self.mode == 'destroy' then + + b.heading = nil -- don't make the supplier follow the block heading + self:logBlock(self.index, b) + if b.y ~= turtle.getPoint().y then + turtle.gotoY(b.y) + end + self:goto(b.x, b.z, b.y) + turtle.digDown() + + -- if no supplier, then should fill all slots + + if turtle.getItemCount(self.resourceSlots) > 0 or turtle.getFuelLevel() < minFuel then + if turtle.getFuelLevel() < minFuel or not self:inAirDropoff() then + turtle.gotoPoint(SUPPLIES_PT) + os.sleep(.1) -- random 'Computer is not connected' error... + self:dumpInventoryWithCheck() + self:refuel() + end + turtle.status = 'destroying' + end + + else -- Build mode + + local slot = Builder:selectItem(b.id, b.dmg) + if not slot or turtle.getFuelLevel() < minFuel then + + if turtle.getPoint().x > -1 or turtle.getPoint().z > -1 then + self:gotoTravelPlane(travelPlane) + end + self:resupply() + return + end + local y = b.y + if b.twoHigh then + y = b.y + 1 + end + if y > travelPlane then + travelPlane = y + end + + self:logBlock(self.index, b) + + if b.direction then + b.needResupply = false + self:placeDirectionalBlock(b, slot, travelPlane) + if b.needResupply then -- lost our piston in a hopper probably + self:gotoTravelPlane(travelPlane) + self:resupply() + return + end + else + self:gotoTravelPlane(travelPlane) + self:goto(b.x, b.z, b.y) + b.placed = self:placeDown(slot) + end + + if b.placed then + slot.qty = slot.qty - 1 + else + print('failed to place block') + end + end + if self.mode == 'destroy' then + self:saveProgress(math.max(self.index, 1)) + else + self:saveProgress(self.index + 1) + end + else + throttle() -- sleep in case there are a large # of skipped blocks + end + + if turtle.abort then + turtle.status = 'aborting' + self:gotoTravelPlane(travelPlane) + turtle.gotoPoint(SUPPLIES_PT) + turtle.setHeading(0) + Builder:dumpInventory() + Event.exitPullEvents() + UI.term:reset() + print('Aborted') + return + end + end + + if device.wireless_modem then + Message.broadcast('finished') + end + if not self.isCommandComputer then + self:gotoTravelPlane(travelPlane) + turtle.gotoPoint(SUPPLIES_PT) + turtle.setHeading(0) + Builder:dumpInventory() + + for i = 1, 4 do + turtle.turnRight() + end + end + +--self.index = 1 +--os.queueEvent('build') + --UI.term:reset() + fs.delete(schematic.filename .. '.progress') + print('Finished') + Event.exitPullEvents() +end + +--[[-- blankPage --]]-- +blankPage = UI.Page() +function blankPage:draw() + self:clear() + self:setCursorPos(1, 1) +end + +function blankPage:enable() + self:sync() + UI.Page.enable(self) +end + +--[[-- selectSubstitutionPage --]]-- +selectSubstitutionPage = UI.Page({ + titleBar = UI.TitleBar({ + title = 'Select a substitution', + previousPage = 'listing' + }), + grid = UI.ScrollingGrid({ + columns = { + { heading = 'id', key = 'id' }, + { heading = 'dmg', key = 'dmg' }, + }, + sortColumn = 'id', + height = UI.term.height-1, + autospace = true, + y = 2, + }), +}) + +function selectSubstitutionPage:enable() + self.grid:adjustWidth() + self.grid:setIndex(1) + UI.Page.enable(self) +end + +function selectSubstitutionPage:eventHandler(event) + + if event.type == 'grid_select' then + substitutionPage.sub = event.selected + UI:setPage(substitutionPage) + elseif event.type == 'key' and event.key == 'q' then + UI:setPreviousPage() + else + return UI.Page.eventHandler(self, event) + end + return true +end + +--[[-- substitutionPage --]]-- +substitutionPage = UI.Page { + backgroundColor = colors.gray, + titleBar = UI.TitleBar { + previousPage = true, + title = 'Substitute a block' + }, + menuBar = UI.MenuBar { + y = 2, + buttons = { + { text = 'Accept', event = 'accept', help = 'Accept' }, + { text = 'Revert', event = 'revert', help = 'Restore to original' }, + { text = 'Air', event = 'air', help = 'Air' }, + }, + }, + info = UI.Window { y = 4, width = UI.term.width, height = 3 }, + grid = UI.ScrollingGrid { + columns = { + { heading = 'Name', key = 'display_name', width = UI.term.width-9 }, + { heading = 'Qty', key = 'fQty', width = 5 }, + }, + sortColumn = 'name', + height = UI.term.height-7, + y = 7, + }, + throttle = UI.Throttle { }, + statusBar = UI.StatusBar { } +} + +substitutionPage.menuBar:add({ + filterLabel = UI.Text({ + value = 'Search', + x = UI.term.width-14, + textColor = colors.black, + }), + filter = UI.TextEntry({ + x = UI.term.width-7, + width = 7, + }) +}) + +function substitutionPage.info:draw() + + local sub = self.parent.sub + local inName = blocks.nameDB:getName(sub.id, sub.dmg) + local outName = '' + if sub.sid then + outName = blocks.nameDB:getName(sub.sid, sub.sdmg) + end + + self:clear() + self:setCursorPos(1, 1) + self:print(' Replace ' .. inName .. '\n') + self:print(' ' .. sub.id .. ':' .. sub.dmg .. '\n', nil, colors.yellow) + self:print(' With ' .. outName) +end + +function substitutionPage:enable() + + self.allItems = convertBack(Builder.itemAdapter:refresh()) + self.grid.values = self.allItems + for _,item in pairs(self.grid.values) do + item.key = item.id .. ':' .. item.dmg + item.lname = string.lower(item.display_name) + item.fQty = Util.toBytes(item.qty) + end + self.grid:update() + + self.menuBar.filter.value = '' + self.menuBar.filter.pos = 1 + self:setFocus(self.menuBar.filter) + UI.Page.enable(self) +end + +--function substitutionPage:focusFirst() +-- self.menuBar.filter:focus() +--end + +function substitutionPage:applySubstitute(id, dmg) + self.sub.sid = id + self.sub.sdmg = dmg +end + +function substitutionPage:eventHandler(event) + + if event.type == 'grid_focus_row' then + local s = string.format('%s:%d', + event.selected.id, + event.selected.dmg) + + self.statusBar:setStatus(s) + self.statusBar:draw() + + elseif event.type == 'grid_select' then + if not blocks.nameDB:lookupName(event.selected.id, event.selected.dmg) then + blocks.nameDB:add({event.selected.id, event.selected.dmg}, event.selected.name) + blocks.nameDB:flush() + end + + self:applySubstitute(event.selected.id, event.selected.dmg) + self.info:draw() + + elseif event.type == 'text_change' then + local text = event.text + if #text == 0 then + self.grid.values = self.allItems + else + self.grid.values = { } + for _,item in pairs(self.allItems) do + if string.find(item.lname, text) then + table.insert(self.grid.values, item) + end + end + end + --self.grid:adjustWidth() + self.grid:update() + self.grid:setIndex(1) + self.grid:draw() + + elseif event.type == 'accept' or event.type == 'air' or event.type == 'revert' then + self.statusBar:setStatus('Saving changes...') + self.statusBar:draw() + self:sync() + + if event.type == 'air' then + self:applySubstitute('minecraft:air', 0) + end + + if event.type == 'revert' then + subDB:remove(self.sub) + elseif not self.sub.sid then + self.statusBar:setStatus('Select a substition') + self.statusBar:draw() + return UI.Page.eventHandler(self, event) + else + subDB:add(self.sub) + end + + self.throttle:enable() + Builder:reloadSchematic(function() self.throttle:update() end) + self.throttle:disable() + UI:setPage('listing') + + elseif event.type == 'cancel' then + UI:setPreviousPage() + end + + return UI.Page.eventHandler(self, event) +end + +--[[-- SupplyPage --]]-- +supplyPage = UI.Page { + titleBar = UI.TitleBar { + title = 'Waiting for supplies', + previousPage = 'start' + }, + menuBar = UI.MenuBar { + y = 2, + buttons = { + --{ text = 'Refresh', event = 'refresh', help = 'Refresh inventory' }, + { text = 'Continue', event = 'build', help = 'Continue building' }, + { text = 'Menu', event = 'menu', help = 'Return to main menu' }, + { text = 'Force Craft', event = 'craft', help = 'Request crafting (again)' }, + } + }, + grid = UI.Grid { + columns = { + { heading = 'Name', key = 'name', width = UI.term.width - 7 }, + { heading = 'Need', key = 'need', width = 4 }, + }, + sortColumn = 'name', + y = 3, + width = UI.term.width, + height = UI.term.height - 3 + }, + statusBar = UI.StatusBar { + columns = { + { 'Help', 'help', UI.term.width - 13 }, + { 'Fuel', 'fuel', 11 } + } + }, + accelerators = { + c = 'craft', + r = 'refresh', + b = 'build', + m = 'menu', + }, +} + +function supplyPage:eventHandler(event) + + if event.type == 'craft' then + local s = self.grid:getSelected() + if Builder.itemAdapter:craft(s.id, s.dmg, s.need-s.qty) then + local name = s.name or '' + self.statusBar:timedStatus('Requested ' .. s.need-s.qty .. ' ' .. name, 3) + else + self.statusBar:timedStatus('Unable to craft') + end + + elseif event.type == 'refresh' then + self:refresh() + + elseif event.type == 'build' then + Builder:build() + + elseif event.type == 'menu' then + Builder:dumpInventory() + --Builder.status = 'idle' + UI:setPage('start') + turtle.status = 'idle' + + elseif event.type == 'grid_focus_row' then + self.statusBar:setValue('help', event.selected.id .. ':' .. event.selected.dmg) + self.statusBar:draw() + + elseif event.type == 'focus_change' then + self.statusBar:timedStatus(event.focused.help, 3) + end + + return UI.Page.eventHandler(self, event) +end + +function supplyPage:enable() + self.grid:setIndex(1) + self.statusBar:setValue('fuel', + string.format('Fuel: %dk', math.floor(turtle.getFuelLevel() / 1024))) +-- self.statusBar:setValue('block', +-- string.format('Block: %d', Builder.index)) + + Event.addNamedTimer('supplyRefresh', 6, true, function() + if self.enabled then + Builder:autocraft(Builder:getSupplies()) + self:refresh() + self.statusBar:timedStatus('Refreshed ', 2) + self:sync() + end + end) + UI.Page.enable(self) +end + +function supplyPage:disable() + Event.cancelNamedTimer('supplyRefresh') +end + +function supplyPage:setSupplies(supplies) + local t = { } + for _,s in pairs(supplies) do + local key = s.id .. ':' .. s.dmg + local entry = t[key] + if not entry then + entry = Util.shallowCopy(s) + t[key] = entry + else + entry.need = entry.need + s.need + end + entry.need = entry.need - turtle.getItemCount(s.index) + end + + self.grid:setValues(t) +end + +function supplyPage:refresh() + self.statusBar:timedStatus('Refreshed ', 3) + local supplies = Builder:getSupplies() + if #supplies == 0 then + Builder:build() + else + self:setSupplies(supplies) + self.grid:draw() + end +end + +--[[-- ListingPage --]]-- +listingPage = UI.Page({ + titleBar = UI.TitleBar({ + title = 'Supply List', + previousPage = 'start' + }), + menuBar = UI.MenuBar({ + y = 2, + buttons = { + { text = 'Craft', event = 'craft', help = 'Request crafting' }, + { text = 'Refresh', event = 'refresh', help = 'Refresh inventory' }, + { text = 'Toggle', event = 'toggle', help = 'Toggles needed blocks' }, + { text = 'Substitute', event = 'edit', help = 'Substitute a block' }, + } + }), + grid = UI.ScrollingGrid({ + columns = { + { heading = 'Name', key = 'name', width = UI.term.width - 14 }, + { heading = 'Need', key = 'need', width = 5 }, + { heading = 'Have', key = 'qty', width = 5 }, + }, + sortColumn = 'name', + y = 3, + height = UI.term.height-3, + help = 'Set a block type or pick a substitute block' + }), + accelerators = { + q = 'menu', + c = 'craft', + r = 'refresh', + t = 'toggle', + }, + statusBar = UI.StatusBar(), + fullList = true +}) + +function listingPage:enable(throttle) + listingPage:refresh(throttle) + UI.Page.enable(self) +end + +function listingPage:eventHandler(event) + + if event.type == 'craft' then + local s = self.grid:getSelected() + local item = convertSingleBack(Builder.itemAdapter:getItemInfo(s.id, s.dmg)) + if item and item.is_craftable then + local qty = math.max(0, s.need - item.qty) + + if item then + Builder.itemAdapter:craft(s.id, s.dmg, qty) + local name = s.name or s.key + self.statusBar:timedStatus('Requested ' .. qty .. ' ' .. name, 3) + end + else + self.statusBar:timedStatus('Unable to craft') + end + + elseif event.type == 'grid_focus_row' then + self.statusBar:setStatus(event.selected.id .. ':' .. event.selected.dmg) + self.statusBar:draw() + + elseif event.type == 'refresh' then + self:refresh() + self:draw() + self.statusBar:timedStatus('Refreshed ', 3) + + elseif event.type == 'toggle' then + self.fullList = not self.fullList + self:refresh() + self:draw() + + elseif event.type == 'menu' then + UI:setPage('start') + + elseif event.type == 'edit' or event.type == 'grid_select' then + self:manageBlock(self.grid:getSelected()) + + elseif event.type == 'focus_change' then + if event.focused.help then + self.statusBar:timedStatus(event.focused.help, 3) + end + end + + return UI.Page.eventHandler(self, event) +end + +function listingPage.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.need = Util.toBytes(row.need) + row.qty = Util.toBytes(row.qty) + return row +end + +function listingPage.grid:getRowTextColor(row, selected) + if row.is_craftable then + return colors.yellow + end + return UI.Grid:getRowTextColor(row, selected) +end + +function listingPage:refresh(throttle) + + local supplyList = Builder:getBlockCounts() + + Builder.itemAdapter:refresh(throttle) + + for _,b in pairs(supplyList) do + if b.need > 0 then + local item = convertSingleBack(Builder.itemAdapter:getItemInfo(b.id, b.dmg)) + + if item then + local block = blocks.blockDB:lookup(b.id, b.dmg) + if not block then + blocks.nameDB:add({b.id, b.dmg}, item.display_name) + elseif not block.name and item.display_name then + blocks.nameDB:add({b.id, b.dmg}, item.display_name) + end + b.name = item.display_name + b.qty = item.qty + b.is_craftable = item.is_craftable + else + b.name = blocks.nameDB:getName(b.id, b.dmg) + end + end + if throttle then + throttle() + end + end + blocks.nameDB:flush() + + if self.fullList then + self.grid:setValues(supplyList) + else + local t = {} + for _,b in pairs(supplyList) do + if self.fullList or b.qty < b.need then + table.insert(t, b) + end + end + self.grid:setValues(t) + end + self.grid:setIndex(1) +end + +function listingPage:manageBlock(selected) + + local substitutes = subDB:lookupBlocksForSub(selected.id, selected.dmg) + + if Util.empty(substitutes) then + substitutionPage.sub = { id = selected.id, dmg = selected.dmg } + UI:setPage(substitutionPage) + elseif Util.size(substitutes) == 1 then + local _,sub = next(substitutes) + substitutionPage.sub = sub + UI:setPage(substitutionPage) + else + selectSubstitutionPage.selected = selected + selectSubstitutionPage.grid:setValues(substitutes) + UI:setPage(selectSubstitutionPage) + end +end + +--[[-- startPage --]]-- + +local wy = 2 +local my = 4 + +if UI.term.width < 30 then + wy = 9 + my = 2 +end + +local startPage = UI.Page { + -- titleBar = UI.TitleBar({ title = 'Builder v' .. Builder.version }), + window = UI.Window { + x = UI.term.width-16, + y = wy, + width = 16, + height = 9, + backgroundColor = colors.gray, + grid = UI.Grid { + columns = { + { heading = 'Name', key = 'name', width = 6 }, + { heading = 'Value', key = 'value', width = 7 }, + }, + disableHeader = true, + --y = UI.term.height-1, + x = 1, + y = 2, + width = 16, + height = 9, + --autospace = true, + selectable = false, + backgroundColor = colors.gray + }, + }, + menu = UI.Menu { + x = 2, + y = my, + height = 7, + menuItems = { + { prompt = 'Set starting level', event = 'startLevel' }, + { prompt = 'Set starting block', event = 'startBlock' }, + { prompt = 'Supply list', event = 'assignBlocks' }, + { prompt = 'Toggle mode', event = 'toggleMode' }, + { prompt = 'Toggle facing', event = 'toggleFacing' }, + { prompt = 'Begin', event = 'begin' }, + { prompt = 'Quit', event = 'quit' } + } + }, + throttle = UI.Throttle { }, + accelerators = { + x = 'test', + q = 'quit' + } +} + +function startPage:draw() + local fuel = turtle.getFuelLevel() + if fuel > 9999 then + fuel = string.format('%dk', math.floor(fuel/1024)) + end + local t = { + { name = 'mode', value = Builder.mode }, + { name = 'start', value = Builder.index }, + { name = 'blocks', value = #schematic.blocks }, + --{ name = 'fuel', value = fuel }, + { name = 'facing', value = Builder.facing }, + { name = 'length', value = schematic.length }, + { name = 'width', value = schematic.width }, + { name = 'height', value = schematic.height }, + } + + self.window.grid:setValues(t) + UI.Page.draw(self) +end + +function startPage:enable() + self:setFocus(self.menu) + UI.Page.enable(self) +end + +function startPage:eventHandler(event) + + if event.type == 'startLevel' then + local dialog = UI.Dialog({ + title = 'Enter Starting Level', + height = 7, + form = UI.Form { + y = 3, x = 2, height = 4, + text = UI.Text({ x = 5, y = 1, textColor = colors.gray, value = '0 - ' .. schematic.height }), + textEntry = UI.TextEntry({ x = 15, y = 1, '0 - 11', width = 7 }), + }, + statusBar = UI.StatusBar(), + }) + + dialog.eventHandler = function(self, event) + if event.type == 'form_complete' then + local l = tonumber(self.form.textEntry.value) + if l and l < schematic.height and l >= 0 then + for k,v in pairs(schematic.blocks) do + if v.y >= l then + Builder.index = k + Builder:saveProgress(Builder.index) + UI:setPreviousPage() + break + end + end + else + self.statusBar:timedStatus('Invalid Level', 3) + end + elseif event.type == 'form_cancel' or event.type == 'cancel' then + UI:setPreviousPage() + else + return UI.Dialog.eventHandler(self, event) + end + return true + end + + dialog:setFocus(dialog.form.textEntry) + UI:setPage(dialog) + + elseif event.type == 'startBlock' then + local dialog = UI.Dialog { + title = 'Enter Block Number', + height = 7, + form = UI.Form { + y = 3, x = 2, height = 4, + text = UI.Text { x = 2, y = 1, value = '1 - ' .. #schematic.blocks, textColor = colors.gray }, + textEntry = UI.TextEntry { x = 16, y = 1, value = tostring(Builder.index), width = 10, limit = 8 } + }, + statusBar = UI.StatusBar(), + } + + dialog.eventHandler = function(self, event) + if event.type == 'form_complete' then + local bn = tonumber(self.form.textEntry.value) + if bn and bn < #schematic.blocks and bn >= 0 then + Builder.index = bn + Builder:saveProgress(Builder.index) + UI:setPreviousPage() + else + self.statusBar:timedStatus('Invalid Block', 3) + end + elseif event.type == 'form_cancel' or event.type == 'cancel' then + UI:setPreviousPage() + else + return UI.Dialog.eventHandler(self, event) + end + return true + end + + dialog:setFocus(dialog.form.textEntry) + UI:setPage(dialog) + + elseif event.type == 'assignBlocks' then + -- this might be an approximation of the blocks needed + -- as the current level's route may or may not have been + -- computed + Builder:dumpInventory() + UI:setPage('listing', function() self.throttle:update() end) + self.throttle:disable() + + elseif event.type == 'toggleMode' then + if Builder.mode == 'build' then + if Builder.index == 1 then + Builder.index = #schematic.blocks + end + Builder.mode = 'destroy' + else + if Builder.index == #schematic.blocks then + Builder.index = 1 + end + Builder.mode = 'build' + end + self:draw() + + elseif event.type == 'toggleFacing' then + local directions = { + [ 'north' ] = 'east', + [ 'east' ] = 'south', + [ 'south' ] = 'west', + [ 'west' ] = 'north', + } + + Builder.facing = directions[Builder.facing] + Builder:saveProgress(Builder.index) + self:draw() + + elseif event.type == 'begin' then + UI:setPage('blank') + --Builder.status = 'building' + + turtle.status = 'thinking' + print('Reloading schematic') + Builder:reloadSchematic(Util.throttle()) + Builder:dumpInventory() + Builder:refuel() + + if Builder.mode == 'destroy' then + if device.wireless_modem then + Message.broadcast('supplyList', { uid = 1, slots = Builder:getAirResupplyList() }) + end + print('Beginning destruction') + else + print('Starting build') + end + + -- reset piston cache in case wrench was substituted + pistonFacings = { + down = { }, + forward = { }, + } + + Builder:build() + + elseif event.type == 'quit' then + UI.term:reset() + Event.exitPullEvents() + end + + return UI.Page.eventHandler(self, event) +end + +--[[-- startup logic --]]-- +local args = {...} +if #args < 1 then + error('supply file name') +end + +Builder.itemAdapter = MEAdapter() +if not Builder.itemAdapter:isValid() then + Builder.itemAdapter = ChestAdapter() + if not Builder.itemAdapter:isValid() then + error('A chest or ME interface must be below turtle') + end +end + +if commands then + turtle = { + policies = { }, + point = { x = -1, y = 0, z = -1, heading = 0 }, + getFuelLevel = function() return 20000 end, + select = function() end, + getItemCount = function() return 0 end, + getHeadingInfo = function(heading) + local headings = { + [ 0 ] = { xd = 1, zd = 0, yd = 0, heading = 0, direction = 'east' }, + [ 1 ] = { xd = 0, zd = 1, yd = 0, heading = 1, direction = 'south' }, + [ 2 ] = { xd = -1, zd = 0, yd = 0, heading = 2, direction = 'west' }, + [ 3 ] = { xd = 0, zd = -1, yd = 0, heading = 3, direction = 'north' }, + [ 4 ] = { xd = 0, zd = 0, yd = 1, heading = 4, direction = 'up' }, + [ 5 ] = { xd = 0, zd = 0, yd = -1, heading = 5, direction = 'down' } + } + local namedHeadings = { + east = headings[0], + south = headings[1], + west = headings[2], + north = headings[3], + up = headings[4], + down = headings[5] + } + if heading and type(heading) == 'string' then + return namedHeadings[heading] + end + heading = heading or 0 + return headings[heading] + end, + } +end + +multishell.setTitle(multishell.getCurrent(), 'Builder v' .. Builder.version) + +maxStackDB:load() +subDB:load() + +UI.term:reset() +turtle.status = 'reading' +print('Loading schematic') +schematic:load(args[1]) +print('Substituting blocks') + +Builder:substituteBlocks(Util.throttle()) + +if not fs.exists(BUILDER_DIR) then + fs.makeDir(BUILDER_DIR) +end + +Builder:loadProgress(schematic.filename .. '.progress') + +UI:setPages({ + listing = listingPage, + start = startPage, + supply = supplyPage, + blank = blankPage +}) + +UI:setPage('start') + +if Builder.isCommandComputer then + Event.pullEvents() +else + turtle.run(function() + turtle.setPolicy(turtle.policies.digAttack) + turtle.setPoint(SUPPLIES_PT) + turtle.point.heading = 0 + Event.pullEvents() + end) +end diff --git a/apps/chestManager.lua b/apps/chestManager.lua new file mode 100644 index 0000000..03f2a38 --- /dev/null +++ b/apps/chestManager.lua @@ -0,0 +1,939 @@ +requireInjector(getfenv(1)) + +local ChestAdapter = require('chestAdapter18') +local Config = require('config') +local Craft = require('turtle.craft') +local Event = require('event') +local itemDB = require('itemDB') +local Peripheral = require('peripheral') +local RefinedAdapter = require('refinedAdapter') +local Terminal = require('terminal') +local UI = require('ui') +local Util = require('util') + +multishell.setTitle(multishell.getCurrent(), 'Resource Manager') + +-- 3 wide monitor (any side of turtle) + +-- Config location is /sys/config/resourceManager +-- adjust directions in that file if needed + +local config = { + trashDirection = 'up', -- trash /chest in relation to chest + turtleDirection = 'down', -- turtle in relation to chest +} + +Config.load('resourceManager', config) + +local controller = RefinedAdapter() +if not controller:isValid() then +-- error('Refined storage controller not found') + controller = nil +end + +local chestAdapter = ChestAdapter({ direction = 'west', wrapSide = 'back' }) +local turtleChestAdapter = ChestAdapter({ direction = 'up', wrapSide = 'bottom' }) + +local RESOURCE_FILE = 'usr/etc/resources.db' +local RECIPES_FILE = 'sys/etc/recipes.db' + +local jobListGrid +local craftingPaused = false +local recipes = Util.readTable(RECIPES_FILE) or { } +local resources = Util.readTable(RESOURCE_FILE) or { } + +Craft.setRecipes(recipes) + +for _,r in pairs(resources) do + r.maxDamage = nil + r.displayName = nil + r.count = nil + r.lname = nil + r.has_recipe = nil + + if not r.ignoreDamage then + r.ignoreDamage = nil + end + + if not r.auto then + r.auto = nil + end +end +Util.writeTable(RESOURCE_FILE, resources) + +local function getItem(items, inItem, ignoreDamage) + for _,item in pairs(items) do + if item.name == inItem.name then + if ignoreDamage then + return item + elseif item.damage == inItem.damage and item.nbtHash == inItem.nbtHash then + return item + end + end + end +end + +local function splitKey(key) + local t = Util.split(key, '(.-):') + local item = { } + if #t[#t] > 2 then + item.nbtHash = table.remove(t) + end + item.damage = tonumber(table.remove(t)) + item.name = table.concat(t, ':') + return item +end + +local function getItemQuantity(items, item) + item = getItem(items, item) + if item then + return item.count + end + return 0 +end + +local function getItemDetails(items, item) + local cItem = getItem(items, item) + if cItem then + return cItem + end + cItem = itemDB:get(itemDB:makeKey(item)) + if cItem then + return { count = 0, maxCount = cItem.maxCount } + end + return { count = 0, maxCount = 64 } +end + +local function uniqueKey(item) + return table.concat({ item.name, item.damage, item.nbtHash }, ':') +end + +local function getName(item) + local detail = itemDB:get(itemDB:makeKey(item)) + if detail then + return detail.displayName + end + return item.name .. ':' .. item.damage +end + +local function mergeResources(t) + for _,v in pairs(resources) do + local item = getItem(t, v) + if item then + Util.merge(item, v) + else + item = Util.shallowCopy(v) + item.count = 0 + table.insert(t, item) + end + end + + for k in pairs(recipes) do + local v = splitKey(k) + local item = getItem(t, v) + if not item then + item = Util.shallowCopy(v) + item.count = 0 + table.insert(t, item) + end + item.has_recipe = true + end + + for _,v in pairs(t) do + if not v.displayName then + v.displayName = getName(v) + end + v.lname = v.displayName:lower() + end +end + +local function filterItems(t, filter) + if filter then + local r = {} + filter = filter:lower() + for k,v in pairs(t) do + if string.find(v.lname, filter) then + table.insert(r, v) + end + end + return r + end + return t +end + +local function sumItems3(ingredients, items, summedItems, count) + + local canCraft = 0 + for _,key in pairs(ingredients) do + local item = splitKey(key) + local summedItem = summedItems[key] + if not summedItem then + summedItem = Util.shallowCopy(item) + summedItem.recipe = recipes[key] + summedItem.count = getItemQuantity(items, summedItem) + summedItems[key] = summedItem + end + summedItem.count = summedItem.count - count + if summedItem.recipe and summedItem.count < 0 then + local need = math.ceil(-summedItem.count / summedItem.recipe.count) + summedItem.count = 0 + sumItems3(summedItem.recipe.ingredients, items, summedItems, need) + end + end +end + +local function isGridClear() + for i = 1, 16 do + if turtle.getItemCount(i) ~= 0 then + return false + end + end + return true +end + +local function clearGrid() + for i = 1, 16 do + local count = turtle.getItemCount(i) + if count > 0 then + chestAdapter:insert(i, count) + if turtle.getItemCount(i) ~= 0 then + return false + end + end + end + return true +end + +local function addCraftingRequest(item, craftList, count) + local key = uniqueKey(item) + local request = craftList[key] + if not craftList[key] then + request = { name = item.name, damage = item.damage, nbtHash = nbtHash, count = 0 } + request.displayName = getName(request) + craftList[key] = request + end + request.count = request.count + count +end + +local function craftItem(recipe, items, originalItem, craftList, count) + + if craftingPaused or not device.workbench or not isGridClear() then + return + end + + local toCraft = Craft.getCraftableAmount(recipe, count, items) + + if toCraft > 0 then + Craft.craftRecipe(recipe, toCraft, chestAdapter) + clearGrid() + items = chestAdapter:listItems() + end + + count = count - toCraft + + if count > 0 then + local summedItems = { } + sumItems3(recipe.ingredients, items, summedItems, math.ceil(count / recipe.count)) + + for key,ingredient in pairs(summedItems) do + if not ingredient.recipe and ingredient.count < 0 then + addCraftingRequest(ingredient, craftList, -ingredient.count) + end + end + end +end + +local function craftItems(craftList, allItems) + + for _,key in pairs(Util.keys(craftList)) do + local item = craftList[key] + local recipe = recipes[key] + if recipe then + craftItem(recipe, allItems, item, craftList, item.count) + allItems = chestAdapter:listItems() -- refresh counts + elseif item.rsControl then + item.status = 'Activated' + end + end + + for key,item in pairs(craftList) do + + if not recipes[key] then + if not controller then + item.status = '(no recipe)' + else + if controller:isCrafting(item) then + item.status = '(crafting)' + else + + local count = item.count + while count >= 1 do -- try to request smaller quantities until successful + local s, m = pcall(function() + item.status = '(no recipe)' + if not controller:craft(item, count) then + item.status = '(missing ingredients)' + error('failed') + end + item.status = '(crafting)' + end) + if s then + break -- successfully requested crafting + end + count = math.floor(count / 2) + end + end + end + end + end +end + +local function jobMonitor(jobList) + + local mon = Peripheral.getByType('monitor') + + if mon then + mon = UI.Device({ + device = mon, + textScale = .5, + }) + else + mon = UI.Device({ + device = Terminal.getNullTerm(term.current()) + }) + end + + jobListGrid = UI.Grid({ + parent = mon, + sortColumn = 'displayName', + columns = { + { heading = 'Qty', key = 'count', width = 6 }, + { heading = 'Crafting', key = 'displayName', width = mon.width / 2 - 10 }, + { heading = 'Status', key = 'status', width = mon.width - 10 }, + }, + }) +end + +local function getAutocraftItems() + local craftList = { } + + for _,res in pairs(resources) do + + if res.auto then + res.count = 4 -- this could be higher to increase autocrafting speed + local key = uniqueKey(res) + craftList[key] = res + end + end + return craftList +end + +local function getItemWithQty(items, res, ignoreDamage) + + local item = getItem(items, res, ignoreDamage) + + if item then + + if ignoreDamage then + local count = 0 + + for _,v in pairs(items) do + if item.name == v.name and item.nbtHash == v.nbtHash then + if item.maxDamage > 0 or item.damage == v.damage then + count = count + v.count + end + end + end + + item.count = count + end + end + + return item +end + +local function watchResources(items) + + local craftList = { } + + for k, res in pairs(resources) do + local item = getItemWithQty(items, res, res.ignoreDamage) + if not item then + item = { + damage = res.damage, + nbtHash = res.nbtHash, + name = res.name, + displayName = getName(res), + count = 0 + } + end + + if res.limit and item.count > res.limit then + chestAdapter:provide(res, item.count - res.limit, nil, config.trashDirection) + + elseif res.low and item.count < res.low then + if res.ignoreDamage then + item.damage = 0 + end + local key = uniqueKey(res) + craftList[key] = { + damage = item.damage, + nbtHash = item.nbtHash, + count = res.low - item.count, + name = item.name, + displayName = item.displayName, + status = '', + rsControl = res.rsControl, + } + end + + if res.rsControl and res.rsDevice and res.rsSide then + pcall(function() + device[res.rsDevice].setOutput(res.rsSide, item.count < res.low) + end) + end + end + + return craftList +end + +local itemPage = UI.Page { + backgroundColor = colors.lightGray, + titleBar = UI.TitleBar { + title = 'Limit Resource', + previousPage = true, + event = 'form_cancel', + backgroundColor = colors.green + }, + displayName = UI.Window { + x = 2, y = 2, width = UI.term.width - 4, height = 3, + }, + form = UI.Form { + x = 4, y = 5, height = 8, rex = -4, + [1] = UI.TextEntry { + width = 7, + backgroundColor = colors.gray, + backgroundFocusColor = colors.gray, + formLabel = 'Min', formKey = 'low', help = 'Craft if below min' + }, + [2] = UI.TextEntry { + width = 7, + backgroundColor = colors.gray, + backgroundFocusColor = colors.gray, + formLabel = 'Max', formKey = 'limit', help = 'Eject if above max' + }, + [3] = UI.Chooser { + width = 7, + formLabel = 'Autocraft', formKey = 'auto', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Craft until out of ingredients' + }, + [4] = UI.Chooser { + width = 7, + formLabel = 'Ignore Dmg', formKey = 'ignore_dmg', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Ignore damage of item' + }, + [5] = UI.Chooser { + width = 7, + formLabel = 'RS Control', formKey = 'rsControl', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Control via redstone' + }, + [6] = UI.Chooser { + width = 25, + formLabel = 'RS Device', formKey = 'rsDevice', + --choices = devices, + help = 'Redstone Device' + }, + [7] = UI.Chooser { + width = 10, + formLabel = 'RS Side', formKey = 'rsSide', + --nochoice = 'No', + choices = { + { name = 'up', value = 'up' }, + { name = 'down', value = 'down' }, + { name = 'east', value = 'east' }, + { name = 'north', value = 'north' }, + { name = 'west', value = 'west' }, + { name = 'south', value = 'south' }, + }, + help = 'Output side' + }, + }, + statusBar = UI.StatusBar { } +} + +function itemPage.displayName:draw() + local item = self.parent.item + local str = string.format('Name: %s\nDamage: %d', item.displayName, item.damage) + if item.nbtHash then + str = str .. string.format('\n%s', item.nbtHash) + end + self:setCursorPos(1, 1) + self:print(str) +end + +function itemPage:enable(item) + self.item = item + + self.form:setValues(item) + self.titleBar.title = item.name + + local devices = self.form[6].choices + Util.clear(devices) + for _,device in pairs(device) do + if device.setOutput then + table.insert(devices, { name = device.name, value = device.name }) + end + end + + if Util.size(devices) == 0 then + table.insert(devices, { name = 'None found', values = '' }) + end + + UI.Page.enable(self) + self:focusFirst() +end + +function itemPage:eventHandler(event) + if event.type == 'form_cancel' then + UI:setPreviousPage() + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + self.statusBar:draw() + + elseif event.type == 'form_complete' then + local values = self.form.values + local keys = { 'name', 'auto', 'low', 'limit', 'damage', + 'nbtHash', 'ignoreDamage', + 'rsControl', 'rsDevice', 'rsSide', } + + local filtered = { } + for _,key in pairs(keys) do + filtered[key] = values[key] + end + filtered.low = tonumber(filtered.low) + filtered.limit = tonumber(filtered.limit) + + --filtered.ignoreDamage = filtered.ignoreDamage == true + --filtered.auto = filtered.auto == true + --filtered.rsControl = filtered.rsControl == true + + if filtered.auto ~= true then + filtered.auto = nil + end + + if filtered.rsControl ~= true then + filtered.rsControl = nil + filtered.rsSide = nil + filtered.rsDevice = nil + end + + if values.ignoreDamage == true then + filtered.damage = 0 + end + + resources[uniqueKey(filtered)] = filtered + Util.writeTable(RESOURCE_FILE, resources) + + UI:setPreviousPage() + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +local listingPage = UI.Page { + menuBar = UI.MenuBar { + buttons = { + { text = 'Learn', event = 'learn' }, + { text = 'Forget', event = 'forget' }, + { text = 'Craft', event = 'craft' }, + }, + }, + grid = UI.Grid { + y = 2, height = UI.term.height - 2, + columns = { + { heading = 'Name', key = 'displayName' , width = 22 }, + { heading = 'Qty', key = 'count' , width = 5 }, + { heading = 'Min', key = 'low' , width = 4 }, + { heading = 'Max', key = 'limit' , width = 4 }, + }, + sortColumn = 'displayName', + }, + statusBar = UI.StatusBar { + backgroundColor = colors.gray, + width = UI.term.width, + filterText = UI.Text { + x = 2, width = 6, + value = 'Filter', + }, + filter = UI.TextEntry { + x = 9, width = 19, + limit = 50, + }, + refresh = UI.Button { + x = 31, width = 8, + text = 'Refresh', + event = 'refresh', + }, + }, + accelerators = { + r = 'refresh', + q = 'quit', + } +} + +function listingPage.grid:getRowTextColor(row, selected) + if row.is_craftable then + return colors.yellow + end + if row.has_recipe then + if selected then + return colors.blue + end + return colors.lightBlue + end + return UI.Grid:getRowTextColor(row, selected) +end + +function listingPage.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.count = Util.toBytes(row.count) + if row.low then + row.low = Util.toBytes(row.low) + end + if row.limit then + row.limit = Util.toBytes(row.limit) + end + return row +end + +function listingPage.statusBar:draw() + return UI.Window.draw(self) +end + +function listingPage.statusBar.filter:eventHandler(event) + if event.type == 'mouse_rightclick' then + self.value = '' + self:draw() + local page = UI:getCurrentPage() + page.filter = nil + page:applyFilter() + page.grid:draw() + page:setFocus(self) + end + return UI.TextEntry.eventHandler(self, event) +end + +function listingPage:eventHandler(event) + if event.type == 'quit' then + UI:exitPullEvents() + + elseif event.type == 'grid_select' then + local selected = event.selected + UI:setPage('item', selected) + + elseif event.type == 'refresh' then + self:refresh() + self.grid:draw() + self.statusBar.filter:focus() + + elseif event.type == 'learn' then + UI:setPage('learn') + + elseif event.type == 'craft' then + UI:setPage('craft') + + elseif event.type == 'forget' then + local item = self.grid:getSelected() + if item then + local key = uniqueKey(item) + + if recipes[key] then + recipes[key] = nil + Util.writeTable(RECIPES_FILE, recipes) + end + + if resources[key] then + resources[key] = nil + Util.writeTable(RESOURCE_FILE, resources) + end + + self.statusBar:timedStatus('Forgot: ' .. item.name, 3) + self:refresh() + self.grid:draw() + end + + elseif event.type == 'text_change' then + self.filter = event.text + if #self.filter == 0 then + self.filter = nil + end + self:applyFilter() + self.grid:draw() + self.statusBar.filter:focus() + + else + UI.Page.eventHandler(self, event) + end + return true +end + +function listingPage:enable() + self:refresh() + self:setFocus(self.statusBar.filter) + UI.Page.enable(self) +end + +function listingPage:refresh() + self.allItems = chestAdapter:listItems() + mergeResources(self.allItems) + self:applyFilter() +end + +function listingPage:applyFilter() + local t = filterItems(self.allItems, self.filter) + self.grid:setValues(t) +end + +-- without duck antenna +local function getTurtleInventoryOld() + local inventory = { } + for i = 1,16 do + if turtle.getItemCount(i) > 0 then + turtle.select(i) + local item = turtle.getItemDetail() + inventory[i] = { + name = item.name, + damage = item.damage, + count = item.count, + } + end + end + return inventory +end + +local function getTurtleInventory() + local inventory = { } + for i = 1,16 do + local qty = turtle.getItemCount(i) + if qty > 0 then + turtleChestAdapter:insert(i, qty) + local items = turtleChestAdapter:listItems() + _, inventory[i] = next(items) + turtleChestAdapter:extract(1, qty, i) + end + end + return inventory +end + +local function filter(t, filter) + local keys = Util.keys(t) + for _,key in pairs(keys) do + if not Util.key(filter, key) then + t[key] = nil + end + end +end + +local function learnRecipe(page) + local recipe = { } + local ingredients = getTurtleInventory() + if ingredients then + turtle.select(1) + if device.workbench and turtle.craft() then + recipe = getTurtleInventory() + if recipe and recipe[1] then + clearGrid() + + local key = uniqueKey(recipe[1]) + local newRecipe = { + count = recipe[1].count, + ingredients = ingredients, + } + if recipe[1].maxCount ~= 64 then + newRecipe.maxCount = recipe[1].maxCount + end + + for k,ingredient in pairs(ingredients) do + ingredients[k] = uniqueKey(ingredient) + end + + recipes[key] = newRecipe + + Util.writeTable(RECIPES_FILE, recipes) + + local displayName = getName(recipe[1]) + + listingPage.statusBar.filter:setValue(displayName) + listingPage.statusBar:timedStatus('Learned: ' .. displayName, 3) + listingPage.filter = displayName + listingPage:refresh() + listingPage.grid:draw() + + return true + end + else + page.statusBar:timedStatus('Failed to craft', 3) + end + else + page.statusBar:timedStatus('No recipe defined', 3) + end +end + +local learnPage = UI.Dialog { + height = 7, width = UI.term.width - 6, + backgroundColor = colors.lightGray, + title = 'Learn Recipe', + idField = UI.Text { + x = 5, + y = 3, + width = UI.term.width - 10, + value = 'Place recipe in turtle' + }, + accept = UI.Button { + rx = -13, ry = -2, + text = 'Ok', event = 'accept', + }, + cancel = UI.Button { + rx = -8, ry = -2, + text = 'Cancel', event = 'cancel' + }, + statusBar = UI.StatusBar { + status = 'Crafting paused' + } +} + +function learnPage:enable() + craftingPaused = true + self:focusFirst() + UI.Dialog.enable(self) +end + +function learnPage:disable() + craftingPaused = false + UI.Dialog.disable(self) +end + +function learnPage:eventHandler(event) + if event.type == 'cancel' then + UI:setPreviousPage() + elseif event.type == 'accept' then + if learnRecipe(self) then + UI:setPreviousPage() + end + else + return UI.Dialog.eventHandler(self, event) + end + return true +end + +local craftPage = UI.Dialog { + height = 6, width = UI.term.width - 10, + backgroundColor = colors.lightGray, + title = 'Enter amount to craft', + idField = UI.TextEntry { + x = 15, + y = 3, + width = 10, + limit = 6, + value = '1', + backgroundColor = colors.black, + backgroundFocusColor = colors.black, + }, + accept = UI.Button { + rx = -7, ry = -1, + backgroundColor = colors.green, + text = '+', event = 'accept', + }, + cancel = UI.Button { + rx = -3, ry = -1, + backgroundColor = colors.red, + backgroundFocusColor = colors.red, + text = '\215', event = 'cancel' + }, +} + +function craftPage:draw() + UI.Dialog.draw(self) + self:write(6, 3, 'Quantity') +end + +function craftPage:enable() + craftingPaused = true + self:focusFirst() + UI.Dialog.enable(self) +end + +function craftPage:disable() + craftingPaused = false + UI.Dialog.disable(self) +end + +function craftPage:eventHandler(event) + if event.type == 'cancel' then + UI:setPreviousPage() + elseif event.type == 'accept' then + + else + return UI.Dialog.eventHandler(self, event) + end + return true +end + +UI:setPages({ + listing = listingPage, + item = itemPage, + learn = learnPage, + craft = craftPage, +}) + +UI:setPage(listingPage) +listingPage:setFocus(listingPage.statusBar.filter) + +clearGrid() +jobMonitor() +jobListGrid:draw() +jobListGrid:sync() + +Event.onInterval(5, function() + + if not craftingPaused then + local items = chestAdapter:listItems() + if Util.size(items) == 0 then + jobListGrid.parent:clear() + jobListGrid.parent:centeredWrite(math.ceil(jobListGrid.parent.height/2), 'No items in system') + jobListGrid:sync() + + else + local craftList = watchResources(items) + jobListGrid:setValues(craftList) + --jobListGrid:draw() + --jobListGrid:sync() + craftItems(craftList, items) + jobListGrid:update() + jobListGrid:draw() + jobListGrid:sync() + craftList = getAutocraftItems(items) -- autocrafted items don't show on job monitor + craftItems(craftList, items) + end + end +end) + +UI:pullEvents() +jobListGrid.parent:reset() diff --git a/apps/logMonitor.lua b/apps/logMonitor.lua new file mode 100644 index 0000000..d54c2b1 --- /dev/null +++ b/apps/logMonitor.lua @@ -0,0 +1,101 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local Message = require('message') +local UI = require('ui') +local Util = require('util') + +multishell.setTitle(multishell.getCurrent(), 'Log Monitor') + +if not device.wireless_modem then + error('Wireless modem is required') +end +device.wireless_modem.open(59998) + +local ids = { } +local messages = { } +local terminal = UI.term + +if device.openperipheral_bridge then + + UI.Glasses = require('glasses') + + terminal = UI.Glasses({ + x = 4, + y = 175, + height = 40, + width = 64, + textScale = .5, + backgroundOpacity = .65, + + }) +elseif device.monitor then + terminal = UI.Device({ + deviceType = 'monitor', + textScale = .5 + }) +end + +terminal:clear() + +function getClient(id) + if not ids[id] then + ids[id] = { + titleBar = UI.TitleBar({ title = 'ID: ' .. id, parent = terminal }), + scrollingText = UI.ScrollingText({ parent = terminal }) + } + local clientCount = Util.size(ids) + local clientHeight = math.floor((terminal.height - clientCount) / clientCount) + terminal:clear() + local y = 1 + for k,v in pairs(ids) do + v.titleBar.y = y + y = y + 1 + v.scrollingText.height = clientHeight + v.scrollingText.y = y + y = y + clientHeight + v.scrollingText:clear() + + v.titleBar:draw() + v.scrollingText:draw() + end + end + return ids[id] +end + +Event.on('logMessage', function() + local t = { } + while #messages > 0 do + local msg = messages[1] + table.remove(messages, 1) + local client = getClient(msg.id) + client.scrollingText:appendLine(string.format('%d %s', math.floor(os.clock()), msg.text)) + t[msg.id] = client + end + for _,client in pairs(t) do + client.scrollingText:draw() + end + terminal:sync() +end) + +Message.addHandler('log', function(h, id, msg) + table.insert(messages, { id = id, text = msg.contents }) + os.queueEvent('logMessage') +end) + +Event.on('monitor_touch', function() + terminal:reset() + ids = { } +end) + +Event.on('mouse_click', function() + terminal:reset() + ids = { } +end) + +Event.on('char', function() + Event.exitPullEvents() +end) + +Event.pullEvents(logWriter) +terminal:reset() diff --git a/apps/mirror.lua b/apps/mirror.lua new file mode 100644 index 0000000..ab3c8c1 --- /dev/null +++ b/apps/mirror.lua @@ -0,0 +1,25 @@ +requireInjector(getfenv(1)) + +local Terminal = require('terminal') + +local args = { ... } +local mon = device[table.remove(args, 1) or 'monitor'] +if not mon then + error('mirror: Invalid device') +end + +mon.clear() +mon.setTextScale(.5) +mon.setCursorPos(1, 1) + +local oterm = Terminal.copy(term.current()) +Terminal.mirror(term.current(), mon) + +term.current().getSize = mon.getSize + +if #args > 0 then + shell.run(unpack(args)) + Terminal.copy(oterm, term.current()) + + mon.setCursorBlink(false) +end diff --git a/apps/mirrorClient.lua b/apps/mirrorClient.lua new file mode 100644 index 0000000..7152ee6 --- /dev/null +++ b/apps/mirrorClient.lua @@ -0,0 +1,86 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local Logger = require('logger') +local Socket = require('socket') +local Terminal = require('terminal') +local Util = require('util') + +Logger.setScreenLogging() + +local remoteId +local args = { ... } +if #args == 1 then + remoteId = tonumber(args[1]) +else + print('Enter host ID') + remoteId = tonumber(read()) +end + +if not remoteId then + error('Syntax: mirrorClient ') +end + +local function wrapTerm(socket) + local methods = { 'blit', 'clear', 'clearLine', 'setCursorPos', 'write', + 'setTextColor', 'setTextColour', 'setBackgroundColor', + 'setBackgroundColour', 'scroll', 'setCursorBlink', } + + socket.term = multishell.term + socket.oldTerm = Util.shallowCopy(socket.term) + + for _,k in pairs(methods) do + socket.term[k] = function(...) + if not socket.queue then + socket.queue = { } + Event.onTimeout(0, function() + if socket.queue then + socket:write(socket.queue) + socket.queue = nil + end + end) + end + table.insert(socket.queue, { + f = k, + args = { ... }, + }) + socket.oldTerm[k](...) + end + end +end + +while true do + print('connecting...') + local socket + + while true do + socket = Socket.connect(remoteId, 5901) + if socket then + break + end + os.sleep(3) + end + + print('connected') + + wrapTerm(socket) + + os.queueEvent('term_resize') + + while true do + local e = Event.pullEvent() + if e[1] == 'terminate' then + break + end + if not socket.connected then + break + end + end + + for k,v in pairs(socket.oldTerm) do + socket.term[k] = v + end + + socket:close() + +end diff --git a/apps/mirrorHost.lua b/apps/mirrorHost.lua new file mode 100644 index 0000000..4ece283 --- /dev/null +++ b/apps/mirrorHost.lua @@ -0,0 +1,53 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local Logger = require('logger') +local Socket = require('socket') + +Logger.setScreenLogging() + +local args = { ... } +local mon = device[args[1] or 'monitor'] + +if not mon then + error('Monitor not attached') +end + +mon.setBackgroundColor(colors.black) +mon.clear() + +while true do + local socket = Socket.server(5901) + + print('mirror: connection from ' .. socket.dhost) + + Event.addRoutine(function() + while true do + local data = socket:read() + if not data then + break + end + for _,v in ipairs(data) do + mon[v.f](unpack(v.args)) + end + end + end) + + -- ensure socket is connected + Event.onInterval(3, function(h) + if not socket:ping() then + Event.off(h) + end + end) + + while true do + Event.pullEvent() + if not socket.connected then + break + end + end + + print('connection lost') + + socket:close() +end diff --git a/apps/pickup.lua b/apps/pickup.lua new file mode 100644 index 0000000..3e9b08b --- /dev/null +++ b/apps/pickup.lua @@ -0,0 +1,337 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local GPS = require('gps') +local Logger = require('logger') +local MEProvider = require('meProvider') +local Point = require('point') +local Socket = require('socket') +local Util = require('util') + +if not device.wireless_modem then + error('Modem is required') +end + +Logger.setWirelessLogging() + +if not turtle then + error('Can only be run on a turtle') +end + +local blocks = { } +local meProvider = MEProvider() +local items = { } + +local pickups = Util.readTable('pickup.tbl') or { } +local cells = Util.readTable('cells.tbl') or { } +local refills = Util.readTable('refills.tbl') or { } +local fluids = Util.readTable('fluids.tbl') or { } +local chestPt = turtle.loadLocation('chest') +local chargePt = turtle.loadLocation('charge') + +local fuel = { + item = { + id = 'minecraft:coal', + dmg = 0, + }, + qty = 64 +} + +local slots + +turtle.setMoveCallback(function(action, pt) + if slots then + for _,slot in pairs(slots) do + if turtle.getItemCount(slot.index) ~= slot.qty then + printError('Slots changed') + Event.exitPullEvents() + end + end + end +end) + +function refuel() + if turtle.getFuelLevel() < 5000 then + print('refueling') + turtle.status = 'refueling' + gotoPoint(chestPt, true) + dropOff(chestPt) + while turtle.getFuelLevel() < 5000 do + turtle.select(1) + meProvider:provide(fuel.item, fuel.qty, 1) + turtle.refuel(64) + print(turtle.getFuelLevel()) + os.sleep(1) + end + end +end + +function pickUp(pt) + turtle.status = 'picking up' + gotoPoint(pt, true) + while true do + if not turtle.selectOpenSlot() then + dropOff(chestPt) + gotoPoint(pt, true) + end + turtle.select(1) + if not turtle.suckDown(64) then + return + end + end +end + +function dropOff(pt) + if turtle.selectSlotWithItems() then + gotoPoint(pt, true) + turtle.emptyInventory(turtle.dropDown) + if pt == chestPt then + print('refreshing items') + items = meProvider:refresh() + end + end +end + +function gotoPoint(pt, doDetect) + slots = turtle.getInventory() + while not turtle.pathfind(pt, blocks) do + if turtle.abort then + error('aborted') + end + turtle.status = 'blocked' + os.sleep(5) + end + + if doDetect and not turtle.detectDown() then + printError('Missing target') + Event.exitPullEvents() + end +end + +function checkCell(pt) + if not turtle.selectOpenSlot() then + dropOff(chestPt) + end + + print('checking cell') + turtle.status = 'recharging' + gotoPoint(pt, true) + local c = peripheral.wrap('bottom') + local energy = c.getMaxEnergyStored() - + c.getEnergyStored() + if energy > 20000 then + print('charging cell') + turtle.selectOpenSlot() + turtle.digDown() + gotoPoint(chargePt, true) + turtle.dropDown() + os.sleep(energy / 20000) + turtle.suckDown() + print('replacing cell') + gotoPoint(pt) + if not turtle.placeDown() then + error('could not place down cell') + end + end +end + +function fluid(points) + print('checking fluid') + turtle.status = 'fluiding' + gotoPoint(points.source, true) + turtle.select(1) + turtle.digDown() + gotoPoint(points.target) + if not turtle.placeDown() then + error('could not place fluid container') + end + os.sleep(5) + turtle.digDown() + gotoPoint(points.source) + turtle.placeDown() +end + +function refill(entry) + dropOff(chestPt) + + turtle.status = 'refilling' + gotoPoint(chestPt) + for _,item in pairs(entry.items) do + meProvider:provide(item, tonumber(item.qty), turtle.selectOpenSlot()) + end + + if turtle.selectSlotWithItems() then + if entry.point then + dropOff(entry.point) + end + end +end + +function oldRefill(points) + gotoPoint(points.source) + repeat until not turtle.suckDown(64) + if points.target then + dropOff(points.target) + end + if points.targets then + for k,target in pairs(points.targets) do + dropOff(target) + end + end + dropOff(points.source) + dropOff(chestPt) +end + +local function makeKey(pt) + return string.format('%d:%d:%d', pt.x, pt.y, pt.z) +end + +local function pickupHost(socket) + + while true do + local data = socket:read() + if not data then + print('pickup: closing connection to ' .. socket.dhost) + return + end + + print('command: ' .. data.type) + + if data.type == 'pickup' then + local key = makeKey(data.point) + pickups[key] = data.point + Util.writeTable('pickup.tbl', pickups) + socket:write( { type = "response", response = 'added' }) + + elseif data.type == 'items' then + socket:write( { type = "response", response = items }) + + elseif data.type == 'refill' then + local key = makeKey(data.entry.point) + refills[key] = data.entry + Util.writeTable('refills.tbl', refills) + socket:write( { type = "response", response = 'added' }) + + elseif data.type == 'setPickup' then + chestPt = data.point +-- fix + turtle.storeLocation('chest', chestPt) + socket:write( { type = "response", response = 'Location set' }) + + elseif data.type == 'setRecharge' then + chargePt = data.point +-- fix + turtle.storeLocation('charge', chargePt) + socket:write( { type = "response", response = 'Location set' }) + + elseif data.type == 'charge' then + local key = makeKey(data.point) + cells[key] = data.point + Util.writeTable('cells.tbl', cells) + socket:write( { type = "response", response = 'added' }) + + elseif data.type == 'fluid' then + + elseif data.type == 'clear' then + local key = makeKey(data.point) + refills[key] = nil + cells[key] = nil + fluids[key] = nil + pickups[key] = nil + + Util.writeTable('refills.tbl', refills) + Util.writeTable('cells.tbl', cells) + Util.writeTable('fluids.tbl', fluids) + Util.writeTable('pickup.tbl', pickups) + + socket:write( { type = "response", response = 'cleared' }) + else + print('unknown command') + end + end +end + +Event.addRoutine(function() + while true do + print('waiting for connection on port 5222') + local socket = Socket.server(5222) + + print('pickup: connection from ' .. socket.dhost) + + Event.addRoutine(function() pickupHost(socket) end) + end +end) + +local function eachEntry(t, fn) + + local keys = Util.keys(t) + for _,key in pairs(keys) do + if t[key] then + if turtle.abort then + return + end + fn(t[key]) + end + end +end + +local function eachClosestEntry(t, fn) + + local points = { } + for k,v in pairs(t) do + v = Util.shallowCopy(v) + v.key = k + table.insert(points, v) + end + + while not Util.empty(points) do + local closest = Point.closest(turtle.point, points) + if turtle.abort then + return + end + if t[closest.key] then + fn(closest) + end + for k,v in pairs(points) do + if v.key == closest.key then + table.remove(points, k) + break + end + end + end +end + +Event.addRoutine(function() + + while true do + if chestPt then + eachClosestEntry(pickups, pickUp) + eachEntry(refills, refill) + refuel() + end + eachEntry(fluids, fluid) + if chargePt then + eachEntry(cells, checkCell) + end + print('sleeping') + turtle.status = 'sleeping' + if turtle.abort then + printError('aborted') + break + end + os.sleep(60) + end + + Event.exitPullEvents() +end) + +turtle.run(function() + + if not turtle.enableGPS() then + error('turtle: No GPS found') + end + + refuel() + Event.pullEvents() + +end) diff --git a/apps/pickupRemote.lua b/apps/pickupRemote.lua new file mode 100644 index 0000000..a42d9e4 --- /dev/null +++ b/apps/pickupRemote.lua @@ -0,0 +1,231 @@ +if not device.wireless_modem then + error('Wireless modem is required') +end + +requireInjector(getfenv(1)) + +local Event = require('event') +local GPS = require('gps') +local Socket = require('socket') +local UI = require('ui') +local Util = require('util') + +multishell.setTitle(multishell.getCurrent(), 'Pickup Remote') + +local id + +local mainPage = UI.Page({ + menu = UI.Menu({ + centered = true, + y = 2, + menuItems = { + { prompt = 'Pickup', event = 'pickup', help = 'Pickup items from this location' }, + { prompt = 'Charge cell', event = 'charge', help = 'Recharge this cell' }, + { prompt = 'Refill', event = 'refill', help = 'Recharge this cell' }, + { prompt = 'Set pickup location', event = 'setPickup', help = 'Recharge this cell' }, + { prompt = 'Set recharge location', event = 'setRecharge', help = 'Recharge this cell' }, + { prompt = 'Clear', event = 'clear', help = 'Remove this location' }, + }, + }), + statusBar = UI.StatusBar(), + accelerators = { + q = 'quit', + }, +}) + +local refillPage = UI.Page({ + menuBar = UI.MenuBar({ + y = 1, + buttons = { + { text = 'Done', event = 'done', help = 'Pickup items from this location' }, + { text = 'Back', event = 'back', help = 'Recharge this cell' }, + }, + }), + grid1 = UI.ScrollingGrid({ + columns = { + { heading = 'Name', key = 'name', width = UI.term.width-9 }, + { heading = 'Qty', key = 'fQty', width = 5 }, + }, + sortColumn = 'name', + height = 8, + y = 3, + }), + grid2 = UI.ScrollingGrid({ + columns = { + { heading = 'Name', key = 'name', width = UI.term.width-9 }, + { heading = 'Qty', key = 'qty', width = 5 }, + }, + sortColumn = 'name', + height = 4, + y = 12, + }), + statusBar = UI.StatusBar(), + accelerators = { + q = 'quit', + }, +}) + +refillPage.menuBar:add({ + filter = UI.TextEntry({ + x = UI.term.width-10, + width = 10, + }) +}) + +local function sendCommand(cmd) + local socket = Socket.connect(id, 5222) + if not socket then + mainPage.statusBar:timedStatus('Unable to connect', 3) + return + end + + socket:write(cmd) + local m = socket:read(3) + socket:close() + if m then + return m.response + end + mainPage.statusBar:timedStatus('No response', 3) +end + +local function getPoint() + local gpt = GPS.getPoint() + if not gpt then + mainPage.statusBar:timedStatus('Unable to get location', 3) + end + return gpt +end + +function refillPage:eventHandler(event) + + if event.type == 'grid_select' then + + local item = { + name = event.selected.name, + id = event.selected.id, + dmg = event.selected.dmg, + qty = 0, + } + + local dialog = UI.Dialog({ + x = 1, + width = UI.term.width, + text = UI.Text({ x = 3, y = 3, value = 'Quantity' }), + textEntry = UI.TextEntry({ x = 14, y = 3 }) + }) + + dialog.eventHandler = function(self, event) + if event.type == 'accept' then + local l = tonumber(self.textEntry.value) + if l and l <= 1024 and l > 0 then + item.qty = self.textEntry.value + table.insert(refillPage.grid2.values, item) + refillPage.grid2:update() + UI:setPreviousPage() + else + self.statusBar:timedStatus('Invalid Quantity', 3) + end + return true + end + + return UI.Dialog.eventHandler(self, event) + end + + dialog.titleBar.title = item.name + dialog:setFocus(dialog.textEntry) + UI:setPage(dialog) + + elseif event.type == 'text_change' then + local text = event.text + if #text == 0 then + self.grid1.values = self.allItems + else + self.grid1.values = { } + for _,item in pairs(self.allItems) do + if string.find(item.lname, text) then + table.insert(self.grid1.values, item) + end + end + end + --self.grid:adjustWidth() + self.grid1:update() + self.grid1:setIndex(1) + self.grid1:draw() + + elseif event.type == 'back' then + UI:setPreviousPage() + + elseif event.type == 'done' then + UI:setPage(mainPage) + local pt = getPoint() + if pt then + local response = sendCommand({ type = 'refill', entry = { point = pt, items = self.grid2.values } }) + if response then + mainPage.statusBar:timedStatus(response, 3) + end + end + + elseif event.type == 'grid_focus_row' then + self.statusBar:setStatus(event.selected.id .. ':' .. event.selected.dmg) + self.statusBar:draw() + end + + return UI.Page.eventHandler(self, event) +end + +function refillPage:enable() + for _,item in pairs(self.allItems) do + item.lname = string.lower(item.name) + item.fQty = Util.toBytes(item.qty) + end + + self.grid1:setValues(self.allItems) + + self.menuBar.filter.value = '' + self.menuBar.filter.pos = 1 + self:setFocus(self.menuBar.filter) + UI.Page.enable(self) +end + +function mainPage:eventHandler(event) + + if event.type == 'quit' then + Event.exitPullEvents() + + elseif event.type == 'refill' then + local response = sendCommand({ type = 'items' }) + if response then + refillPage.allItems = response + refillPage.grid2:setValues({ }) + UI:setPage(refillPage) + end + + elseif event.type == 'pickup' or event.type == 'setPickup' or + event.type == 'setRecharge' or event.type == 'charge' or + event.type == 'clear' then + local pt = getPoint() + if pt then + local response = sendCommand({ type = event.type, point = pt }) + if response then + self.statusBar:timedStatus(response, 3) + end + end + + end + + return UI.Page.eventHandler(self, event) +end + +local args = { ... } +if #args == 1 then + id = tonumber(args[1]) +end + +if not id then + error('Syntax: pickupRemote ') +end + +UI:setPage(mainPage) + +Event.pullEvents() +UI.term:reset() diff --git a/apps/recorder.lua b/apps/recorder.lua new file mode 100644 index 0000000..b889475 --- /dev/null +++ b/apps/recorder.lua @@ -0,0 +1,538 @@ +-- +---------------------+------------+---------------------+ +-- | | | | +-- | | RecGif | | +-- | | | | +-- +---------------------+------------+---------------------+ + +local version = "Version 1.1.6" + +-- Records your terminal and saves the result as an animating GIF. +-- http://www.computercraft.info/forums2/index.php?/topic/24840-recgif/ + +-- ---------------------------------------------------------- + +-- Original code by Bomb Bloke +-- Modified to integrate with opus os + +requireInjector(getfenv(1)) + +local Util = require('util') + +local recTerm, oldTerm, arg, showInput, skipLast, lastDelay, curInput = {}, Util.shallowCopy(multishell.term), {...}, false, false, 2, "" +local curBlink, oldBlink, tTerm, buffer, colourNum, xPos, yPos, oldXPos, oldYPos, tCol, bCol, xSize, ySize = false, false, {}, {}, {}, 1, 1, 1, 1, colours.white, colours.black, oldTerm.getSize() +local greys, buttons = {["0"] = true, ["7"] = true, ["8"] = true, ["f"] = true}, {"l", "r", "m"} +local charW, charH, chars, resp +local filename + +local calls = { } +local curCalls = { delay = 0 } +local callListCount = 0 +local callCount = 0 + +local function showSyntax() + print('Gif Recorder by Bomb Bloke\n') + print('Syntax: recGif [-i] [-s] [-ld:] filename') + print(' -i : show input') + print(' -s : skip last') + print(' -ld : last delay') +end + +for i = #arg, 1, -1 do + local curArg = arg[i]:lower() + + if curArg == "-i" then + showInput, ySize = true, ySize + 1 + table.remove(arg, i) + elseif curArg == "-s" then + skipLast = true + table.remove(arg, i) + elseif curArg:sub(1, 4) == "-ld:" then + curArg = tonumber(curArg:sub(5)) + if curArg then lastDelay = curArg end + table.remove(arg, i) + elseif curArg == '-?' then + showSyntax() + return + elseif i ~= #arg then + showSyntax() + printError('\nInvalid argument') + return + end +end + +print('Press control-p to stop recording') + +local filename = arg[#arg] +if not filename then + print('Enter file name:') + filename = read() +end + +if #filename == 0 then + showSyntax() + print() + error('Invalid file name') +end + +print('Initializing...') + +-- don't pollute global env +-- convert these to require style apis +local function loadAPI(url, env) + local apiEnv = Util.shallowCopy(env) + apiEnv.shell = nil + apiEnv.multishell = nil + setmetatable(apiEnv, { __index = _G }) + local fn = Util.loadUrl(url, apiEnv) + fn() + return apiEnv +end + +bbpack = loadAPI('http://pastebin.com/raw/PdrJjb5S', getfenv(1)) +GIF = loadAPI('http://pastebin.com/raw/5uk9uRjC', getfenv(1)) + +Util.runUrl(getfenv(1), 'http://pastebin.com/raw/cUYTGbpb', 'get', 'Y0eLUPtr') + +local function snooze() + local myEvent = tostring({}) + os.queueEvent(myEvent) + os.pullEvent(myEvent) +end + +local function safeString(text) + local newText = {} + + for i = 1, #text do + local val = text:byte(i) + newText[i] = (val > 31 and val < 127) and val or 63 + end + + return string.char(unpack(newText)) +end + +local function safeCol(text, subst) + local newText = {} + + for i = 1, #text do + local val = text:sub(i, i) + newText[i] = greys[val] and val or subst + end + + return table.concat(newText) +end + +-- Build a terminal that records stuff: + +recTerm = multishell.term + +for key, func in pairs(oldTerm) do + recTerm[key] = function(...) + local result = { func(...) } + + if callCount == 0 then + os.queueEvent('capture_frame') + end + callCount = callCount + 1 + curCalls[callCount] = { key, ... } + return unpack(result) + end +end + +local tabId = multishell.getCurrent() + +multishell.addHotkey(25, function() + os.queueEvent('recorder_stop') +end) + +local tabs = multishell.getTabs() +for _,tab in pairs(tabs) do + if tab.isOverview then + multishell.hideTab(tabId) + multishell.setFocus(tab.tabId) + os.queueEvent('term_resize') + break + end +end + +local curTime = os.clock() - 1 + +while true do + local event = { os.pullEventRaw() } + + if event[1] == 'recorder_stop' or event[1] == 'terminate' then + break + end + + if event[1] == 'capture_frame' then + local newTime = os.clock() + + if callListCount > 0 then + calls[callListCount].delay = (newTime - curTime) + end + + curTime = newTime + callListCount = callListCount + 1 + calls[callListCount] = curCalls + + curCalls, callCount = { delay = 0 }, 0 + end +end + +multishell.removeHotkey(25) + +for k,fn in pairs(oldTerm) do + multishell.term[k] = fn +end + +multishell.unhideTab(tabId) +multishell.setFocus(tabId) + +if #calls[#calls] == 0 then calls[#calls] = nil end +if skipLast and #calls > 1 then calls[#calls] = nil end + +calls[#calls].delay = lastDelay + +print(string.format("Encoding %d frames...", #calls)) +--Util.writeTable('tmp/raw.txt', calls) + +-- Perform a quick re-parse of the recorded data (adding frames for when the cursor blinks): + +do + local callListCount, tempCalls, blink, oldBlink, curBlink, blinkDelay = 1, {}, false, false, true, 0 + + for i = 1, #calls - 1 do + curCalls = calls[i] + tempCalls[callListCount] = curCalls + for j = 1, #curCalls do if curCalls[j][1] == "setCursorBlink" then blink = curCalls[j][2] end end + + if blink then + if blinkDelay == 0 then + curCalls[#curCalls + 1] = {"toggleCur", curBlink} + blinkDelay, curBlink = 0.4, not curBlink + end + + while tempCalls[callListCount].delay > blinkDelay do + local remainder = tempCalls[callListCount].delay - blinkDelay + tempCalls[callListCount].delay = blinkDelay + callListCount = callListCount + 1 + tempCalls[callListCount] = {{"toggleCur", curBlink}, ["delay"] = remainder} + blinkDelay, curBlink = 0.4, not curBlink + end + + blinkDelay = blinkDelay - tempCalls[callListCount].delay + else + if oldBlink then curCalls[#curCalls + 1] = {"toggleCur", false} end + blinkDelay = (curCalls.delay - blinkDelay) % 0.4 + end + + callListCount, oldBlink = callListCount + 1, blink + end + + tempCalls[callListCount] = calls[#calls] + tempCalls[callListCount][#tempCalls[callListCount] + 1] = {"toggleCur", false} + + calls, curCalls = tempCalls, nil +end + +snooze() + +-- Load font data: +do + local ascii, counter = GIF.toPaintutils(GIF.flattenGIF(GIF.loadGIF("ascii.gif"))), 0 + local newFont, ybump, xbump = #ascii ~= #ascii[1], 0, 0 + charW, charH, chars = newFont and #ascii[1] / 16 or #ascii[1] * 3 / 64, #ascii / 16, {} + + for yy = 0, newFont and 15 or 7 do + for xx = 0, 15 do + local newChar, length = {}, 0 + + -- Place in 2d grid of bools: + for y = 1, charH do + local newRow = {} + + for x = 1, charW do + local set = ascii[y + ybump][x + xbump] == 1 + if set and x > length then length = x end + newRow[x] = set + end + + newChar[y] = newRow + end + + -- Center: + if not newFont then for y = 1, charH do for x = 1, math.floor((charW - length) / 2) do table.insert(newChar[y], 1, false) end end end + + chars[counter] = newChar + counter, xbump = counter + 1, xbump + (newFont and charW or charH) + end + xbump, ybump = 0, ybump + charH + end +end + +snooze() + +-- Terminal data translation: + +do + local hex, counter = "0123456789abcdef", 1 + + for i = 1, 16 do + colourNum[counter] = hex:sub(i, i) + counter = counter * 2 + end +end + +for y = 1, ySize do + buffer[y] = {} + for x = 1, xSize do buffer[y][x] = {" ", colourNum[tCol], colourNum[bCol]} end +end + +if showInput then for x = 1, xSize do buffer[ySize][x][3] = colourNum[colours.lightGrey] end end + +tTerm.blit = function(text, fgCol, bgCol) + if xPos > xSize or xPos + #text - 1 < 1 or yPos < 1 or yPos > ySize then return end + + if not _HOST then text = safeString(text) end + + if not term.isColour() then + fgCol = safeCol(fgCol, "0") + bgCol = safeCol(bgCol, "f") + end + + if xPos < 1 then + text = text:sub(2 - xPos) + fgCol = fgCol:sub(2 - xPos) + bgCol = bgCol:sub(2 - xPos) + xPos = 1 + end + + if xPos + #text - 1 > xSize then + text = text:sub(1, xSize - xPos + 1) + fgCol = fgCol:sub(1, xSize - xPos + 1) + bgCol = bgCol:sub(1, xSize - xPos + 1) + end + + for x = 1, #text do + buffer[yPos][xPos + x - 1][1] = text:sub(x, x) + buffer[yPos][xPos + x - 1][2] = fgCol:sub(x, x) + buffer[yPos][xPos + x - 1][3] = bgCol:sub(x, x) + end + + xPos = xPos + #text +end + +tTerm.write = function(text) + text = tostring(text) + tTerm.blit(text, string.rep(colourNum[tCol], #text), string.rep(colourNum[bCol], #text)) +end + +tTerm.clearLine = function() + local oldXPos = xPos + + xPos = 1 + tTerm.write(string.rep(" ", xSize)) + + xPos = oldXPos +end + +tTerm.clear = function() + local oldXPos, oldYPos = xPos, yPos + + for y = 1, ySize do + xPos, yPos = 1, y + tTerm.write(string.rep(" ", xSize)) + end + + xPos, yPos = oldXPos, oldYPos +end + +tTerm.setCursorPos = function(x, y) + xPos, yPos = math.floor(x), math.floor(y) +end + +tTerm.setTextColour = function(col) + tCol = col +end + +tTerm.setTextColor = function(col) + tCol = col +end + +tTerm.setBackgroundColour = function(col) + bCol = col +end + +tTerm.setBackgroundColor = function(col) + bCol = col +end + +tTerm.scroll = function(lines) + if math.abs(lines) < ySize then + local oldXPos, oldYPos = xPos, yPos + + for y = 1, ySize do + if y + lines > 0 and y + lines <= ySize then + for x = 1, xSize do + xPos, yPos = x, y + tTerm.blit(buffer[y + lines][x][1], buffer[y + lines][x][2], buffer[y + lines][x][3]) + end + else + yPos = y + tTerm.clearLine() + end + end + + xPos, yPos = oldXPos, oldYPos + else tTerm.clear() end +end + +tTerm.toggleCur = function(newBlink) + curBlink = newBlink +end + +tTerm.newInput = function(input) + local oldTC, oldBC, oldX, oldY = tCol, bCol, xPos, yPos + tCol, bCol, xPos, yPos, ySize, input = colours.grey, colours.lightGrey, 1, ySize + 1, ySize + 1, input .. " " + + while #curInput + #input + 1 > xSize do curInput = curInput:sub(curInput:find(" ") + 1) end + curInput = curInput .. input .. " " + tTerm.clearLine() + tTerm.write(curInput) + + tCol, bCol, xPos, yPos, ySize = oldTC, oldBC, oldX, oldY, ySize - 1 +end + +tTerm.key = function(key) + tTerm.newInput((not keys.getName(key)) and "unknownKey" or keys.getName(key)) +end + +tTerm.mouse_click = function(button, x, y) + tTerm.newInput(buttons[button] .. "C@" .. tostring(x) .. "x" .. tostring(y)) +end + +local image = {["width"] = xSize * charW, ["height"] = ySize * charH} + +for i = 1, #calls do + local xMin, yMin, xMax, yMax, oldBuffer, curCalls, changed = xSize + 1, ySize + 1, 0, 0, {}, calls[i], false + calls[i] = nil + + for y = 1, ySize do + oldBuffer[y] = {} + for x = 1, xSize do oldBuffer[y][x] = {buffer[y][x][1], buffer[y][x][2], buffer[y][x][3], buffer[y][x][4]} end + end + + snooze() + + if showInput then ySize = ySize - 1 end + for j = 1, #curCalls do if tTerm[curCalls[j][1]] then tTerm[curCalls[j][1]](unpack(curCalls[j], 2)) end end + if showInput then ySize = ySize + 1 end + + if i > 1 then + for yy = 1, ySize do for xx = 1, xSize do if buffer[yy][xx][1] ~= oldBuffer[yy][xx][1] or (buffer[yy][xx][2] ~= oldBuffer[yy][xx][2] and buffer[yy][xx][1] ~= " ") or buffer[yy][xx][3] ~= oldBuffer[yy][xx][3] then + changed = true + if xx < xMin then xMin = xx end + if xx > xMax then xMax = xx end + if yy < yMin then yMin = yy end + if yy > yMax then yMax = yy end + end end end + else xMin, yMin, xMax, yMax, changed = 1, 1, xSize, ySize, true end + + if oldBlink and (xPos ~= oldXPos or yPos ~= oldYPos or not curBlink) and oldXPos > 0 and oldYPos > 0 and oldXPos <= xSize and oldYPos <= ySize then + changed = true + if oldXPos < xMin then xMin = oldXPos end + if oldXPos > xMax then xMax = oldXPos end + if oldYPos < yMin then yMin = oldYPos end + if oldYPos > yMax then yMax = oldYPos end + buffer[oldYPos][oldXPos][4] = false + end + + if curBlink and (xPos ~= oldXPos or yPos ~= oldYPos or not oldBlink) and xPos > 0 and yPos > 0 and xPos <= xSize and yPos <= ySize then + changed = true + if xPos < xMin then xMin = xPos end + if xPos > xMax then xMax = xPos end + if yPos < yMin then yMin = yPos end + if yPos > yMax then yMax = yPos end + buffer[yPos][xPos][4] = true + end + + oldBlink, oldXPos, oldYPos = curBlink, xPos, yPos + + local thisFrame = { + ["xstart"] = (xMin - 1) * charW, + ["ystart"] = (yMin - 1) * charH, + ["xend"] = (xMax - xMin + 1) * charW, + ["yend"] = (yMax - yMin + 1) * charH, + ["delay"] = curCalls.delay, + ["disposal"] = 1 + } + + for y = 1, (yMax - yMin + 1) * charH do + local row = {} + for x = 1, (xMax - xMin + 1) * charW do row[x] = " " end + thisFrame[y] = row + end + + snooze() + + for yy = yMin, yMax do + local yBump = (yy - yMin) * charH + + for xx = xMin, xMax do if buffer[yy][xx][1] ~= oldBuffer[yy][xx][1] or (buffer[yy][xx][2] ~= oldBuffer[yy][xx][2] and buffer[yy][xx][1] ~= " ") or buffer[yy][xx][3] ~= oldBuffer[yy][xx][3] or buffer[yy][xx][4] ~= oldBuffer[yy][xx][4] or i == 1 then + local thisChar, thisT, thisB, xBump = chars[buffer[yy][xx][1]:byte()], buffer[yy][xx][2], buffer[yy][xx][3], (xx - xMin) * charW +if thisChar then + for y = 1, charH do + for x = 1, charW do + local ch = thisChar[y][x] and thisT or thisB + thisFrame[y + yBump][x + xBump] = ch + end + end +end + + if buffer[yy][xx][4] then + thisT, thisChar = colourNum[tCol], chars[95] + for y = 1, charH do for x = 1, charW do if thisChar[y][x] then thisFrame[y + yBump][x + xBump] = thisT end end end + end + end end + + for y = yBump + 1, yBump + charH do + local skip, chars, row = 0, {}, {} + + for x = 1, #thisFrame[y] do + if thisFrame[y][x] == " " then + if #chars > 0 then + row[#row + 1] = table.concat(chars) + chars = {} + end + + skip = skip + 1 + else + if skip > 0 then + row[#row + 1] = skip + skip = 0 + end + + chars[#chars + 1] = thisFrame[y][x] + end + end + + if #chars > 0 then row[#row + 1] = table.concat(chars) end + thisFrame[y] = row + end + + snooze() + end + + if changed then + image[#image + 1] = thisFrame + else + image[#image].delay = image[#image].delay + curCalls.delay + end +end + +buffer = nil + +GIF.saveGIF(image, filename) + +fs.delete('ascii.gif') + +print("Encode complete") diff --git a/apps/shapes.lua b/apps/shapes.lua new file mode 100644 index 0000000..5b694df --- /dev/null +++ b/apps/shapes.lua @@ -0,0 +1,517 @@ +requireInjector(getfenv(1)) + +local GPS = require('gps') +local Socket = require('socket') +local UI = require('ui') +local Util = require('util') + +multishell.setTitle(multishell.getCurrent(), 'Shapes') + +local args = { ... } +local turtleId = args[1] or error('Supply turtle ID') +turtleId = tonumber(turtleId) + +local script = [[ + +requireInjector(getfenv(1)) + +local GPS = require('gps') +local ChestAdapter = require('chestAdapter18') +local Point = require('point') +local Util = require('util') + +local itemAdapter + +function dumpInventory() + + for i = 1, 16 do + local qty = turtle.getItemCount(i) + if qty > 0 then + itemAdapter:insert(i, qty) + end + if turtle.getItemCount(i) ~= 0 then + print('Adapter is full or missing - make space or replace') + print('Press enter to continue') + read() + end + end + turtle.select(1) +end + +local function refuel() + while turtle.getFuelLevel() < 4000 do + print('Refueling') + turtle.select(1) + + itemAdapter:provide({ name = 'minecraft:coal', damage = 0 }, 64, 1) + if turtle.getItemCount(1) == 0 then + print('Out of fuel, add fuel to chest/ME system') + turtle.status = 'waiting' + os.sleep(5) + else + turtle.refuel(64) + end + end +end + +local function goto(pt) + while not turtle.gotoPoint(pt) do + print('stuck') + os.sleep(5) + end +end + +local function pathTo(pt) + while not turtle.pathfind(pt) do + print('stuck') + os.sleep(5) + end +end + +local function resupply() + + if data.suppliesPt then + pathTo(data.suppliesPt) + + itemAdapter = ChestAdapter({ direction = 'up', wrapSide = 'bottom' }) + dumpInventory() + refuel() + end +end + +local function makePlane(y) + local pt = { x = math.min(data.startPt.x, data.endPt.x), + ex = math.max(data.startPt.x, data.endPt.x), + z = math.min(data.startPt.z, data.endPt.z), + ez = math.max(data.startPt.z, data.endPt.z) } + + local blocks = { } + for z = pt.z, pt.ez do + for x = pt.x, pt.ex do + table.insert(blocks, { x = x, y = y, z = z }) + end + end + + return blocks +end + +local function optimizeRoute(plane, ptb) + + local maxDistance = 99999999 + + local function getNearestNeighbor(p, pt, threshold) + 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) + return not b.u + 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 <= threshold 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 <= threshold 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 throttle = Util.throttle() + local t = { } + ptb.index = 0 + local threshold = 0 + for i = 1, #plane do + local b = getNearestNeighbor(plane, ptb, threshold) + table.insert(t, b) + throttle() + threshold = 1 + end + + return t +end + +local function clear() + + local pt = Util.shallowCopy(data.startPt) + pt.y = math.min(data.startPt.y, data.endPt.y) + pt.heading = 0 + + local osy = pt.y + local sy = osy + 1 + local ey = math.max(data.startPt.y, data.endPt.y) + local firstPlane = true + + resupply() + + while true do + + if sy > ey then + sy = ey + end + + local plane = makePlane(sy) + plane = optimizeRoute(plane, pt) + + if firstPlane then + turtle.pathfind(plane[1]) + turtle.setPolicy(turtle.policies.digAttack) + firstPlane = false + end + + for _,b in ipairs(plane) do + turtle.gotoPoint(b) + if sy < ey then + turtle.digUp() + end + if sy > osy then + turtle.digDown() + end + if turtle.abort then + break + end + end + + if turtle.abort then + break + end + if sy + 1 >= ey then + break + end + + sy = sy + 3 + end + turtle.setPolicy(turtle.policies.none) + resupply() +end + +turtle.run(function() + turtle.status = 'Clearing' + + if turtle.enableGPS() then + + local pt = Util.shallowCopy(turtle.point) + local s, m = pcall(clear) + pathTo(pt) + + if not s and m then + error(m) + read() + end + end +end) +]] + +local levelScript = [[ + +requireInjector(getfenv(1)) + +local Point = require('point') +local Util = require('util') + +local checkedNodes = { } +local nodes = { } +local box = { } + +local function inBox(pt, box) + return pt.x >= box.x and + pt.y >= box.y and + pt.z >= box.z and + pt.x <= box.ex and + pt.y <= box.ey and + pt.z <= box.ez +end + +local function toKey(pt) + return table.concat({ pt.x, pt.y, pt.z }, ':') +end + +local function addNode(node) + + for i = 0, 5 do + local hi = turtle.getHeadingInfo(i) + local testNode = { x = node.x + hi.xd, y = node.y + hi.yd, z = node.z + hi.zd } + + if inBox(testNode, box) then + local key = toKey(testNode) + if not checkedNodes[key] then + nodes[key] = testNode + end + end + end +end + +local function dig(action) + + local directions = { + top = 'up', + bottom = 'down', + } + + -- convert to up, down, north, south, east, west + local direction = directions[action.side] or + turtle.getHeadingInfo(turtle.point.heading).direction + + local hi = turtle.getHeadingInfo(direction) + local node = { x = turtle.point.x + hi.xd, y = turtle.point.y + hi.yd, z = turtle.point.z + hi.zd } + if inBox(node, box) then + + local key = toKey(node) + checkedNodes[key] = true + nodes[key] = nil + + if action.dig() then + addNode(node) + repeat until not action.dig() -- sand, etc + return true + end + end +end + +local function move(action) + if action == 'turn' then + dig(turtle.getAction('forward')) + elseif action == 'up' then + dig(turtle.getAction('up')) + elseif action == 'down' then + dig(turtle.getAction('down')) + elseif action == 'back' then + dig(turtle.getAction('up')) + dig(turtle.getAction('down')) + end +end + +local function getAdjacentPoint(pt) + local t = { } + table.insert(t, pt) + for i = 0, 5 do + local hi = turtle.getHeadingInfo(i) + local heading + if i < 4 then + heading = (hi.heading + 2) % 4 + end + table.insert(t, { x = pt.x + hi.xd, z = pt.z + hi.zd, y = pt.y + hi.yd, heading = heading }) + end + + return Point.closest2(turtle.getPoint(), t) +end + +local function level() + + box.x = math.min(data.startPt.x, data.endPt.x) + box.y = math.min(data.startPt.y, data.endPt.y) + box.z = math.min(data.startPt.z, data.endPt.z) + box.ex = math.max(data.startPt.x, data.endPt.x) + box.ey = math.max(data.startPt.y, data.endPt.y) + box.ez = math.max(data.startPt.z, data.endPt.z) + + turtle.pathfind(data.firstPt) + + turtle.setPolicy("attack", { dig = dig }, "assuredMove") + turtle.setMoveCallback(move) + + repeat + local key = toKey(turtle.point) + + checkedNodes[key] = true + nodes[key] = nil + + dig(turtle.getAction('down')) + dig(turtle.getAction('up')) + dig(turtle.getAction('forward')) + + print(string.format('%d nodes remaining', Util.size(nodes))) + + if Util.size(nodes) == 0 then + break + end + + local node = Point.closest2(turtle.point, nodes) + node = getAdjacentPoint(node) + if not turtle.gotoPoint(node) then + break + end + until turtle.abort + + turtle.resetState() +end + +local s, m = turtle.run(function() + turtle.status = 'Leveling' + + if turtle.enableGPS() then + + local pt = Util.shallowCopy(turtle.point) + local s, m = pcall(level) + turtle.pathfind(pt) + + if not s and m then + error(m) + end + end +end) + +if not s then + error(m) +end +]] + + +local data = Util.readTable('/usr/config/shapes') or { } + +local page = UI.Page { + titleBar = UI.TitleBar { title = 'Shapes' }, + info = UI.Window { x = 5, y = 3, height = 1 }, + startCoord = UI.Button { x = 2, y = 6, text = 'Start ', event = 'startCoord' }, + endCoord = UI.Button { x = 2, y = 8, text = 'End ', event = 'endCoord' }, + supplies = UI.Button { x = 2, y = 10, text = 'Supplies', event = 'supplies' }, + first = UI.Button { x = 2, y = 11, text = 'First', event = 'firstCoord' }, + cancel = UI.Button { rx = 2, ry = -2, text = 'Abort', event = 'cancel' }, + begin = UI.Button { rx = -7, ry = -2, text = 'Begin', event = 'begin' }, + accelerators = { q = 'quit' }, + notification = UI.Notification(), + statusBar = UI.StatusBar(), +} + +function page.info:draw() + + local function size(a, b) + return (math.abs(a.x - b.x) + 1) * + (math.abs(a.y - b.y) + 1) * + (math.abs(a.z - b.z) + 1) + end + + self:clear() + if not data.startPt then + self:write(1, 1, 'Set starting corner') + elseif not data.endPt then + self:write(1, 1, 'Set ending corner') + else + self:write(1, 1, 'Blocks: ' .. size(data.startPt, data.endPt)) + end +end + +function page:getPoint() + local pt = GPS.getPoint() + if not pt then + self.notification:error('GPS not available') + end + return pt +end + +function page:runFunction(id, script) + +Util.writeFile('script.tmp', script) + self.notification:info('Connecting') + local fn, msg = loadstring(script, 'script') + if not fn then + self.notification:error('Error in script') + debug(msg) + return + end + + local socket = Socket.connect(id, 161) + if not socket then + self.notification:error('Unable to connect') + return + end + socket:write({ type = 'script', args = script }) + socket:close() + + self.notification:success('Sent') +end + +function page:eventHandler(event) + if event.type == 'startCoord' then + data.startPt = self:getPoint() + if data.startPt then + self.statusBar:setStatus('starting corner set') + Util.writeTable('/usr/config/shapes', data) + end + self:draw() + elseif event.type == 'endCoord' then + data.endPt = self:getPoint() + if data.endPt then + self.statusBar:setStatus('ending corner set') + Util.writeTable('/usr/config/shapes', data) + end + self:draw() + elseif event.type == 'firstCoord' then + data.firstPt = self:getPoint() + if data.firstPt then + self.statusBar:setStatus('first point set') + Util.writeTable('/usr/config/shapes', data) + end + self:draw() + elseif event.type == 'supplies' then + data.suppliesPt = self:getPoint() + if data.suppliesPt then + self.statusBar:setStatus('supplies location set') + Util.writeTable('/usr/config/shapes', data) + end + elseif event.type == 'begin' then + if data.startPt and data.endPt then + local s = 'local data = ' .. textutils.serialize(data) .. levelScript + self:runFunction(turtleId, s) + else + self.notification:error('Corners not set') + end + self.statusBar:setStatus('') + elseif event.type == 'cancel' then + self:runFunction(turtleId, 'turtle.abortAction()') + self.statusBar:setStatus('') + else + return UI.Page.eventHandler(self, event) + end + return true +end + +UI:setPage(page) + +UI:pullEvents() +UI.term:reset() diff --git a/apps/simpleMiner.lua b/apps/simpleMiner.lua new file mode 100644 index 0000000..e9519ec --- /dev/null +++ b/apps/simpleMiner.lua @@ -0,0 +1,633 @@ +requireInjector(getfenv(1)) + +local Logger = require('logger') +local Point = require('point') +local Util = require('util') + +if device and device.wireless_modem then + Logger.setWirelessLogging() +end + +local args = { ... } +local options = { + chunks = { arg = 'c', type = 'number', value = -1, + desc = 'Number of chunks to mine' }, + depth = { arg = 'd', type = 'number', value = 9000, + desc = 'Mining depth' }, +-- enderChest = { arg = 'e', type = 'flag', value = false, +-- desc = 'Use ender chest' }, + resume = { arg = 'r', type = 'flag', value = false, + desc = 'Resume mining' }, + fortunePick = { arg = 'p', type = 'string', value = nil, + desc = 'Pick to use with CCTweaks toolhost' }, + setTrash = { arg = 's', type = 'flag', value = false, + desc = 'Set trash items' }, + help = { arg = 'h', type = 'flag', value = false, + desc = 'Displays the options' }, +} + +local fortuneBlocks = { + [ 'minecraft:redstone_ore' ] = true, + [ 'minecraft:lapis_ore' ] = true, + [ 'minecraft:coal_ore' ] = true, + [ 'minecraft:diamond_ore' ] = true, + [ 'minecraft:emerald_ore' ] = true, +} + +local MIN_FUEL = 7500 +local LOW_FUEL = 1500 +local MAX_FUEL = 100000 + +if not term.isColor() then + MAX_FUEL = 20000 +end + +local mining = { + diameter = 1, + chunkIndex = 0, + chunks = -1, +} + +local trash +local boreDirection + +function getChunkCoordinates(diameter, index, x, z) + local dirs = { -- circumference of grid + { xd = 0, zd = 1, heading = 1 }, -- south + { xd = -1, zd = 0, heading = 2 }, + { xd = 0, zd = -1, heading = 3 }, + { xd = 1, zd = 0, heading = 0 } -- east + } + -- always move east when entering the next diameter + if index == 0 then + dirs[4].x = x + 16 + dirs[4].z = z + return dirs[4] + end + dir = dirs[math.floor(index / (diameter - 1)) + 1] + dir.x = x + dir.xd * 16 + dir.z = z + dir.zd * 16 + return dir +end + +function getBoreLocations(x, z) + + local locations = {} + + while true do + local a = math.abs(z) + local b = math.abs(x) + + if x > 0 and z > 0 or + x < 0 and z < 0 then + -- rotate coords + a = math.abs(x) + b = math.abs(z) + end + if (a % 5 == 0 and b % 5 == 0) or + (a % 5 == 2 and b % 5 == 1) or + (a % 5 == 4 and b % 5 == 2) or + (a % 5 == 1 and b % 5 == 3) or + (a % 5 == 3 and b % 5 == 4) then + table.insert(locations, { x = x, z = z, y = 0 }) + end + if z % 2 == 0 then -- forward dir + if (x + 1) % 16 == 0 then + z = z + 1 + else + x = x + 1 + end + else + if (x - 1) % 16 == 15 then + if (z + 1) % 16 == 0 then + break + end + z = z + 1 + else + x = x - 1 + end + end + end + return locations +end + +-- get the bore location closest to the miner +local function getClosestLocation(points, b) + local key = 1 + local leastMoves = 9000 + for k,pt in pairs(points) do + + local moves = Point.calculateMoves(turtle.point, pt) + + if moves < leastMoves then + key = k + leastMoves = moves + if leastMoves == 0 then + break + end + end + end + return table.remove(points, key) +end + +function getCornerOf(c) + return math.floor(c.x / 16) * 16, math.floor(c.z / 16) * 16 +end + +function nextChunk() + + local x, z = getCornerOf({ x = mining.x, z = mining.z }) + local points = math.pow(mining.diameter, 2) - math.pow(mining.diameter-2, 2) + mining.chunkIndex = mining.chunkIndex + 1 + + if mining.chunkIndex >= points then + mining.diameter = mining.diameter + 2 + mining.chunkIndex = 0 + end + + if mining.chunks ~= -1 then + local chunks = math.pow(mining.diameter-2, 2) + mining.chunkIndex + if chunks >= mining.chunks then + return false + end + end + + local nc = getChunkCoordinates(mining.diameter, mining.chunkIndex, x, z) + mining.locations = getBoreLocations(nc.x, nc.z) + + -- enter next chunk + mining.x = nc.x + mining.z = nc.z + + Util.writeTable('mining.progress', mining) + + return true +end + +function addTrash() + + if not trash then + trash = { } + end + + local slots = turtle.getFilledSlots() + + for k,slot in pairs(slots) do + trash[slot.iddmg] = true + end + + trash['minecraft:bucket:0'] = nil + Util.writeTable('mining.trash', trash) +end + +function log(text) + print(text) + Logger.log('mineWorker', text) +end + +function status(status) + turtle.status = status + log(status) +end + +function refuel() + if turtle.getFuelLevel() < MIN_FUEL then + local oldStatus = turtle.status + status('refueling') + + if turtle.select'minecraft:coal:0') then + local qty = turtle.getItemCount() + print('refueling ' .. qty) + turtle.refuel(qty) + end + if turtle.getFuelLevel() < MIN_FUEL then + log('desperate fueling') + + turtle.eachFilledSlot(function(slot) + if turtle.getFuelLevel() < MIN_FUEL then + turtle.select(slot.index) + turtle.refuel(64) + end + end) + end + log('Fuel: ' .. turtle.getFuelLevel()) + status(oldStatus) + end + + turtle.select(1) +end + +function enderChestUnload() + log('unloading') + turtle.select(1) + if not Util.tryTimed(5, function() + turtle.digDown() + return turtle.placeDown() + end) then + log('placedown failed') + else + turtle.reconcileInventory(slots, turtle.dropDown) + + turtle.select(1) + turtle.drop(64) + turtle.digDown() + end +end + +function safeGoto(x, z, y, h) + local oldStatus = turtle.status + while not turtle.pathfind({ x = x, z = z, y = y, heading = h }) do + --status('stuck') + if turtle.abort then + return false + end + --os.sleep(1) + end + turtle.status = oldStatus + return true +end + +function safeGotoY(y) + local oldStatus = turtle.status + while not turtle.gotoY(y) do + status('stuck') + if turtle.abort then + return false + end + os.sleep(1) + end + turtle.status = oldStatus + return true +end + +function makeWalkableTunnel(action, tpt, pt) + if action ~= 'turn' and not Point.compare(tpt, { x = 0, z = 0 }) then -- not at source + if not Point.compare(tpt, pt) then -- not at dest + local r, block = turtle.inspectUp() + if r and not turtle.isTurtleAtSide('top') then + if block.name ~= 'minecraft:cobblestone' and + block.name ~= 'minecraft:chest' then + turtle.digUp() + end + end + end + end +end + +function normalChestUnload() + local oldStatus = turtle.status + status('unloading') + local pt = Util.shallowCopy(turtle.point) + safeGotoY(0) + + turtle.setMoveCallback(function(action, tpt) + makeWalkableTunnel(action, tpt, { x = pt.x, z = pt.z }) + end) + + safeGoto(0, 0) + if not turtle.detectUp() then + error('no chest') + end + local slots = turtle.getFilledSlots() + for _,slot in pairs(slots) do + if not trash[slot.iddmg] and + slot.iddmg ~= 'minecraft:bucket:0' and + slot.id ~= 'minecraft:diamond_pickaxe' and + slot.id ~= 'cctweaks:toolHost' then + if slot.id ~= options.fortunePick.value then + turtle.select(slot.index) + turtle.dropUp(64) + end + end + end + turtle.select(1) + safeGoto(pt.x, pt.z, 0, pt.heading) + + turtle.clearMoveCallback() + + safeGotoY(pt.y) + status(oldStatus) +end + +function ejectTrash() + + local cobbleSlotCount = 0 + + turtle.eachFilledSlot(function(slot) + if slot.iddmg == 'minecraft:cobblestone:0' then + cobbleSlotCount = cobbleSlotCount + 1 + end + + if trash[slot.iddmg] then + -- retain 1 slot with cobble in order to indicate active mining + if slot.iddmg ~= 'minecraft:cobblestone:0' or cobbleSlotCount > 1 then + turtle.select(slot.index) + turtle.dropDown(64) + end + end + end) +end + +function mineable(action) + local r, block = action.inspect() + if not r then + return false + end + + if block.name == 'minecraft:chest' then + collectDrops(action.suck) + end + + if turtle.getFuelLevel() < (MAX_FUEL - 1000) then + if block.name == 'minecraft:lava' or block.name == 'minecraft:flowing_lava' then + if turtle.select('minecraft:bucket:0') then + if action.place() then + log('Lava! ' .. turtle.getFuelLevel()) + turtle.refuel() + log(turtle.getFuelLevel()) + end + turtle.select(1) + end + return false + end + end + + if action.side == 'bottom' then + return block.name + end + + if trash[block.name .. ':0'] then + return false + end + + return block.name +end + +function fortuneDig(action, blockName) + if options.fortunePick.value and fortuneBlocks[blockName] then + turtle.select('cctweaks:toolHost') + turtle.equipRight() + turtle.select(options.fortunePick.value) + repeat until not turtle.dig() + turtle.select('minecraft:diamond_pickaxe') + turtle.equipRight() + turtle.select(1) + return true + end +end + +function mine(action) + local blockName = mineable(action) + if blockName then + checkSpace() + --collectDrops(action.suck) + if not fortuneDig(action, blockName) then + action.dig() + end + end +end + +function bore() + + local loc = turtle.point + local level = loc.y + + turtle.select(1) + status('boring down') + boreDirection = 'down' + + while true do + if turtle.abort then + status('aborting') + return false + end + if loc.y <= -mining.depth then + break + end + + if turtle.point.y < -2 then +-- turtle.setDigPolicy(turtle.digPolicies.turtleSafe) + end + + mine(turtle.getAction('down')) + if not Util.tryTimed(3, turtle.down) then + break + end + + if loc.y < level - 1 then + mine(turtle.getAction('forward')) + turtle.turnRight() + mine(turtle.getAction('forward')) + end + end + + boreDirection = 'up' + status('boring up') + + turtle.turnRight() + mine(turtle.getAction('forward')) + + turtle.turnRight() + mine(turtle.getAction('forward')) + + turtle.turnLeft() + + while true do + if turtle.abort then + status('aborting') + return false + end + + if turtle.point.y > -2 then +-- turtle.setDigPolicy(turtle.digPolicies.turtleSafe) + end + + while not Util.tryTimed(3, turtle.up) do + status('stuck') + end + if turtle.status == 'stuck' then + status('boring up') + end + + if loc.y >= level - 1 then + break + end + + mine(turtle.getAction('forward')) + turtle.turnLeft() + mine(turtle.getAction('forward')) + end + + if turtle.getFuelLevel() < LOW_FUEL then + refuel() + local veryMinFuel = Point.turtleDistance(turtle.point, { x = 0, y = 0, z = 0}) + 512 + if turtle.getFuelLevel() < veryMinFuel then + log('Not enough fuel to continue') + return false + end + end + + return true +end + +function checkSpace() + if turtle.getItemCount(16) > 0 then + refuel() + local oldStatus = turtle.status + status('condensing') + ejectTrash() + turtle.condense() + local lastSlot = 16 + if boreDirection == 'down' then + lastSlot = 15 + end + if turtle.getItemCount(lastSlot) > 0 then + unload() + end + status(oldStatus) + turtle.select(1) + end +end + +function collectDrops(suckAction) + for i = 1, 50 do + if not suckAction() then + break + end + checkSpace() + end +end + +function Point.compare(pta, ptb) + if pta.x == ptb.x and pta.z == ptb.z then + if pta.y and ptb.y then + return pta.y == ptb.y + end + return true + end + return false +end + +function inspect(action, name) + local r, block = action.inspect() + if r and block.name == name then + return true + end +end + +function boreCommand() + local pt = getClosestLocation(mining.locations, turtle.point) + + turtle.setMoveCallback(function(action, tpt) + makeWalkableTunnel(action, tpt, pt) + end) + + safeGotoY(0) + safeGoto(pt.x, pt.z, 0) + + turtle.clearMoveCallback() + + -- location is either mined, currently being mined or is the + -- dropoff point for a turtle + if inspect(turtle.getAction('up'), 'minecraft:cobblestone') or + inspect(turtle.getAction('up'), 'minecraft:chest') or + inspect(turtle.getAction('down'), 'minecraft:cobblestone') then + return true + end + + turtle.digUp() + turtle.placeUp('minecraft:cobblestone:0') + + local success = bore() + + safeGotoY(0) -- may have aborted + turtle.digUp() + + if success then + turtle.placeDown('minecraft:cobblestone:0') -- cap with cobblestone to indicate this spot was mined out + end + + return success +end + +if not Util.getOptions(options, args) then + return +end + +mining.depth = options.depth.value +mining.chunks = options.chunks.value + +unload = normalChestUnload +--if options.enderChest.value then +-- unload = enderChestUnload +--end + +mining.x = 0 +mining.z = 0 +mining.locations = getBoreLocations(0, 0) +trash = Util.readTable('mining.trash') + +if options.resume.value then + mining = Util.readTable('mining.progress') +elseif fs.exists('mining.progress') then + print('use -r to resume') + read() +end + +if not trash or options.setTrash.value then + print('Add trash blocks, press enter when ready') + read() + addTrash() +end + +if not turtle.getSlot('minecraft:bucket:0') or + not turtle.getSlot('minecraft:cobblestone:0') then + print('Add bucket and cobblestone, press enter when ready') + read() +end + +if options.fortunePick.value then + local s = turtle.getSlot(options.fortunePick.value) + if not s then + error('fortunePick not found: ' .. options.fortunePick.value) + end + if not turtle.getSlot('cctweaks:toolHost:0') then + error('CCTweaks tool host not found') + end + trash[s.iddmg] = nil + trash['minecraft:diamond_pickaxe:0'] = nil + trash['cctweaks:toolHost:0'] = nil +end + +_G._p = trash + +local function main() + repeat + while #mining.locations > 0 do + status('searching') + if not boreCommand() then + return + end + Util.writeTable('mining.progress', mining) + end + until not nextChunk() +end + +turtle.run(function() + turtle.reset() + turtle.setPolicy(turtle.policies.digAttack) + turtle.setDigPolicy(turtle.digPolicies.turtleSafe) + + unload() + status('mining') + + local s, m = pcall(function() main() end) + if not s and m then + printError(m) + end + + safeGotoY(0) + safeGoto(0, 0, 0, 0) + unload() + turtle.reset() +end) diff --git a/apps/storageActivity.lua b/apps/storageActivity.lua new file mode 100644 index 0000000..135ee13 --- /dev/null +++ b/apps/storageActivity.lua @@ -0,0 +1,174 @@ +requireInjector(getfenv(1)) + +local ChestAdapter = require('chestAdapter18') +local Event = require('event') +local MEAdapter = require('meAdapter') +local RefinedAdapter = require('refinedAdapter') +local UI = require('ui') +local Util = require('util') + +local storage = RefinedAdapter() +if not storage:isValid() then + storage = MEAdapter() + if not storage:isValid() then + storage = ChestAdapter() + end +end + +if not storage:isValid() then + error('Not connected to a storage device') +end + +multishell.setTitle(multishell.getCurrent(), 'Storage Activity') +UI:configure('StorageActivity', ...) + +local changedPage = UI.Page({ + grid = UI.Grid({ + columns = { + { heading = 'Qty', key = 'count', width = 5 }, + { heading = 'Change', key = 'change', width = 6 }, + { heading = 'Name', key = 'displayName', width = UI.term.width - 15 }, + }, + sortColumn = 'displayName', + rey = -6, + }), + buttons = UI.Window({ + ry = -4, + height = 5, + backgroundColor = colors.gray, + prevButton = UI.Button({ + event = 'previous', + backgroundColor = colors.lightGray, + x = 2, + y = 2, + height = 3, + width = 5, + text = ' < ' + }), + resetButton = UI.Button({ + event = 'reset', + backgroundColor = colors.lightGray, + x = 8, + y = 2, + height = 3, + rex = -8, + text = 'Reset' + }), + nextButton = UI.Button({ + event = 'next', + backgroundColor = colors.lightGray, + rx = -5, + y = 2, + height = 3, + width = 5, + text = ' > ' + }) + }), + accelerators = { + q = 'quit', + } +}) + +function changedPage.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + + local ind = '+' + if row.change < 0 then + ind = '' + end + row.change = ind .. Util.toBytes(row.change) + row.count = Util.toBytes(row.count) + + return row +end + +function changedPage:eventHandler(event) + + if event.type == 'reset' then + self.lastItems = nil + self.grid:setValues({ }) + self.grid:clear() + self.grid:draw() + + elseif event.type == 'next' then + self.grid:nextPage() + + elseif event.type == 'previous' then + self.grid:previousPage() + + elseif event.type == 'quit' then + Event.exitPullEvents() + + else + return UI.Page.eventHandler(self, event) + end + + return true +end + +local function uniqueKey(item) + return table.concat({ item.name, item.damage, item.nbtHash }, ':') +end + +function changedPage:refresh() + local t = storage:listItems() + + if not t or Util.empty(t) then + self:clear() + self:centeredWrite(math.ceil(self.height/2), 'Communication failure') + return + end + + for k,v in pairs(t) do + t[k] = Util.shallowCopy(v) + end + + if not self.lastItems then + self.lastItems = t + self.grid:setValues({ }) + else + local changedItems = {} + for _,v in pairs(self.lastItems) do + found = false + for k2,v2 in pairs(t) do + if uniqueKey(v) == uniqueKey(v2) then + if v.count ~= v2.count then + local c = Util.shallowCopy(v2) + c.lastCount = v.count + table.insert(changedItems, c) + end + table.remove(t, k2) + found = true + break + end + end + -- New item + if not found then + local c = Util.shallowCopy(v) + c.lastCount = v.count + c.count = 0 + table.insert(changedItems, c) + end + end + -- No items left + for k,v in pairs(t) do + v.lastCount = 0 + table.insert(changedItems, v) + end + + for k,v in pairs(changedItems) do + v.change = v.count - v.lastCount + end + + self.grid:setValues(changedItems) + end + self.grid:draw() +end + +Event.onInterval(5, function() + changedPage:refresh() + changedPage:sync() +end) + +UI:setPage(changedPage) +UI:pullEvents() diff --git a/apps/storageManager.lua b/apps/storageManager.lua new file mode 100644 index 0000000..62ef357 --- /dev/null +++ b/apps/storageManager.lua @@ -0,0 +1,901 @@ +requireInjector(getfenv(1)) + +local Config = require('config') +local Event = require('event') +local Logger = require('logger') +local ME = require('me') +local UI = require('ui') +local Util = require('util') + +-- Must be a crafty turtle with duck antenna ! +-- 3 wide monitor (any side of turtle) + +-- Config location is /sys/config/storageMonitor +-- adjust directions in that file if needed + +local config = { + trashDirection = 'up', -- trash /chest in relation to interface + turtleDirection = 'down', -- turtle in relation to interface + noCraftingStorage = 'false' -- no ME crafting (or ability to tell if powered - use with caution) +} + +Config.load('storageMonitor', config) + +if not device.tileinterface then + error('ME interface not found') +end + +local duckAntenna + +if device.workbench then + + local oppositeSide = { + [ 'left' ] = 'right', + [ 'right' ] = 'left' + } + + local duckAntennaSide = oppositeSide[device.workbench.side] + duckAntenna = peripheral.wrap(duckAntennaSide) +end +--if not device.monitor then +-- error('Monitor not found') +--end + +ME.setDevice(device.tileinterface) + +local jobListGrid +local craftingPaused = false + +multishell.setTitle(multishell.getCurrent(), 'Storage Manager') + +Logger.disable() + +function getItem(items, inItem, ignore_dmg) + for _,item in pairs(items) do + if item.id == inItem.id then + if ignore_dmg and ignore_dmg == 'yes' then + return item + elseif item.dmg == inItem.dmg and item.nbt_hash == inItem.nbt_hash then + return item + end + end + end +end + +local function uniqueKey(item) + local key = item.id .. ':' .. item.dmg + if item.nbt_hash then + key = key .. ':' .. item.nbt_hash + end + return key +end + +function mergeResources(t) + local resources = Util.readTable('resource.limits') + resources = resources or { } + + for _,item in pairs(t) do + item.has_recipe = false + end + + for _,v in pairs(resources) do + local item = getItem(t, v) + if item then + item.limit = tonumber(v.limit) + item.low = tonumber(v.low) + item.auto = v.auto + item.ignore_dmg = v.ignore_dmg + else + v.qty = 0 + v.limit = tonumber(v.limit) + v.low = tonumber(v.low) + v.auto = v.auto + v.ignore_dmg = v.ignore_dmg + table.insert(t, v) + end + end + + recipes = Util.readTable('recipes') or { } + + for _,v in pairs(recipes) do + local item = getItem(t, v) + if item then + item.has_recipe = true + else + v.qty = 0 + v.limit = nil + v.low = nil + v.has_recipe = true + v.auto = 'no' + v.ignore_dmg = 'no' + v.has_recipe = 'true' + table.insert(t, v) + end + end +end + +function filterItems(t, filter) + local r = {} + if filter then + filter = filter:lower() + for k,v in pairs(t) do + if string.find(v.lname, filter) then + table.insert(r, v) + end + end + else + return t + end + return r +end + +function sumItems(items) + local t = {} + + for _,item in pairs(items) do + local key = uniqueKey(item) + local summedItem = t[key] + if summedItem then + summedItem.qty = summedItem.qty + item.qty + else + summedItem = Util.shallowCopy(item) + t[key] = summedItem + end + end + + return t +end + +function isGridClear() + for i = 1, 16 do + if turtle.getItemCount(i) ~= 0 then + return false + end + end + return true +end + +local function clearGrid() + for i = 1, 16 do + local count = turtle.getItemCount(i) + if count > 0 then + ME.insert(i, count, config.turtleDirection) + if turtle.getItemCount(i) ~= 0 then + return false + end + end + end + return true +end + +function turtleCraft(recipe, originalItem) + + for k,v in pairs(recipe.ingredients) do + + -- ugh + local dmg = v.dmg + + if v.max_dmg and v.max_dmg > 0 then + local item = ME.getItemDetail({ id = v.id, nbt_hash = v.nbt_hash }, false) + if item then + dmg = item.dmg + end + end + + if not ME.extract(v.id, dmg, v.nbt_hash, v.qty, config.turtleDirection, k) then + clearGrid() + originalItem.status = v.name .. ' (extract failed)' + return false + end + end + + if not turtle.craft() then + clearGrid() + return false + end + + clearGrid() + return true +end + +function craftItem(items, recipes, item, originalItem, itemList) + + local key = uniqueKey(item) + local recipe = recipes[key] + + if recipe then + + if not isGridClear() then + return + end + + local summedItems = sumItems(recipe.ingredients) + + for i = 1, math.ceil(item.qty / recipe.qty) do + + local failed = false -- try to craft all components (use all CPUs available) + + for _,ingredient in pairs(summedItems) do + local ignore_dmg = 'no' + if ingredient.max_dmg and ingredient.max_dmg > 0 then + ignore_dmg = 'yes' + end + local qty = ME.getItemCount(ingredient.id, ingredient.dmg, ingredient.nbt_hash, ignore_dmg) + if qty < ingredient.qty then + originalItem.status = ingredient.name .. ' (crafting)' + ingredient.qty = ingredient.qty - qty + if not craftItem(items, recipes, ingredient, originalItem, itemList) then + failed = true + end + end + end + + if failed then + return false + end + + if not failed and not turtleCraft(recipe, originalItem) then + Logger.debug('turtle failed to craft ' .. item.name) + return false + end + end + + return true + + else + + local meItem = getItem(items, item) + if not meItem or not meItem.is_craftable then + + if item.id == originalItem.id and item.dmg == originalItem.dmg then + originalItem.status = '(not craftable)' + else + originalItem.status = item.name .. ' (missing)' + end + + else + + if item.id == originalItem.id and item.dmg == originalItem.dmg then + item.meCraft = true + return false + end + + -- find it in the list of items to be crafted + for _,v in pairs(itemList) do + if v.id == item.id and v.dmg == item.dmg and v.nbt_hash == item.nbt_hash then + v.qty = item.qty + v.qty + return false + end + end + -- add to the item list + table.insert(itemList, { + id = item.id, + dmg = item.dmg, + nbt_hash = item.nbt_hash, + qty = item.qty, + name = item.name, + meCraft = true, + status = '' + }) + end + end + + return false +end + +function craftItems(itemList) + + local recipes = Util.readTable('recipes') or { } + local items = ME.getAvailableItems() + + -- turtle craft anything we can, build up list for ME items + local keys = Util.keys(itemList) + for _,key in pairs(keys) do + local item = itemList[key] + craftItem(items, recipes, item, item, itemList) + end + + -- second pass is to request crafting from ME with aggregated items + for _,item in pairs(itemList) do + if item.meCraft then + + local alreadyCrafting = false + local jobList = ME.getJobList() + + for _,v in pairs(jobList) do + if v.id == item.id and v.dmg == item.dmg and v.nbt_hash == item.nbt_hash then + alreadyCrafting = true + end + end + + if alreadyCrafting then + item.status = '(crafting)' + elseif not ME.isCPUAvailable() then + item.status = '(waiting)' + else + item.status = '(failed)' + + local qty = item.qty + while qty >= 1 do -- try to request smaller quantities until successful + if ME.craft(item.id, item.dmg, item.nbt_hash, qty) then + item.status = '(crafting)' + break -- successfully requested crafting + end + qty = math.floor(qty / 2) + end + end + end + end +end + +-- AE 1 (obsolete) +function isCrafting(jobList, id, dmg) + for _, job in pairs(jobList) do + if job.id == id and job.dmg == dmg then + return job + end + end +end + +local nullDevice = { + setCursorPos = function(...) end, + write = function(...) end, + getSize = function() return 13, 20 end, + isColor = function() return false end, + setBackgroundColor = function(...) end, + setTextColor = function(...) end, + clear = function(...) end, +} + +local function jobMonitor(jobList) + + local mon + + if device.monitor then + mon = UI.Device({ + deviceType = 'monitor', + textScale = .5, + }) + else + mon = UI.Device({ + device = nullDevice + }) + end + + jobListGrid = UI.Grid({ + parent = mon, + sortColumn = 'name', + columns = { + { heading = 'Qty', key = 'qty', width = 6 }, + { heading = 'Crafting', key = 'name', width = mon.width / 2 - 10 }, + { heading = 'Status', key = 'status', width = mon.width - 10 }, + }, + }) +end + +function getAutocraftItems(items) + local t = Util.readTable('resource.limits') or { } + local itemList = { } + + for _,res in pairs(t) do + + if res.auto and res.auto == 'yes' then + res.qty = 4 -- this could be higher to increase autocrafting speed + table.insert(itemList, res) + end + end + return itemList +end + +local function getItemWithQty(items, res, ignore_dmg) + + local item = getItem(items, res, ignore_dmg) + + if item then + + if ignore_dmg and ignore_dmg == 'yes' then + local qty = 0 + + for _,v in pairs(items) do + if item.id == v.id and item.nbt_hash == v.nbt_hash then + if item.max_dmg > 0 or item.dmg == v.dmg then + qty = qty + v.qty + end + end + end + + item.qty = qty + end + end + + return item +end + +function watchResources(items) + + local itemList = { } + + local t = Util.readTable('resource.limits') or { } + for k, res in pairs(t) do + local item = getItemWithQty(items, res, res.ignore_dmg) + res.limit = tonumber(res.limit) + res.low = tonumber(res.low) + if not item then + item = { + id = res.id, + dmg = res.dmg, + nbt_hash = res.nbt_hash, + name = res.name, + qty = 0 + } + end + + if res.limit and item.qty > res.limit then + Logger.debug("Purging " .. item.qty-res.limit .. " " .. res.name) + if not ME.extract(item.id, item.dmg, item.nbt_hash, item.qty - res.limit, config.trashDirection) then + Logger.debug('Failed to purge ' .. res.name) + end + + elseif res.low and item.qty < res.low then + if res.ignore_dmg and res.ignore_dmg == 'yes' then + item.dmg = 0 + end + table.insert(itemList, { + id = item.id, + dmg = item.dmg, + nbt_hash = item.nbt_hash, + qty = res.low - item.qty, + name = item.name, + status = '' + }) + end + end + + return itemList +end + +itemPage = UI.Page { + backgroundColor = colors.lightGray, + titleBar = UI.TitleBar { + title = 'Limit Resource', + previousPage = true, + event = 'form_cancel', + backgroundColor = colors.green + }, + idField = UI.Text { + x = 5, y = 3, width = UI.term.width - 10, + }, + form = UI.Form { + x = 4, y = 4, height = 8, rex = -4, + [1] = UI.TextEntry { + width = 7, + backgroundColor = colors.gray, + backgroundFocusColor = colors.gray, + formLabel = 'Min', formKey = 'low', help = 'Craft if below min' + }, + [2] = UI.TextEntry { + width = 7, + backgroundColor = colors.gray, + backgroundFocusColor = colors.gray, + formLabel = 'Max', formKey = 'limit', help = 'Eject if above max' + }, + [3] = UI.Chooser { + width = 7, + formLabel = 'Autocraft', formKey = 'auto', + nochoice = 'No', + choices = { + { name = 'Yes', value = 'yes' }, + { name = 'No', value = 'no' }, + }, + help = 'Craft until out of ingredients' + }, + [4] = UI.Chooser { + width = 7, + formLabel = 'Ignore Dmg', formKey = 'ignore_dmg', + nochoice = 'No', + choices = { + { name = 'Yes', value = 'yes' }, + { name = 'No', value = 'no' }, + }, + help = 'Ignore damage of item' + }, + }, + statusBar = UI.StatusBar { } +} + +function itemPage:enable() + UI.Page.enable(self) + self:focusFirst() +end + +function itemPage:eventHandler(event) + if event.type == 'form_cancel' then + UI:setPreviousPage() + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + self.statusBar:draw() + + elseif event.type == 'form_complete' then + local values = self.form.values + local t = Util.readTable('resource.limits') or { } + for k,v in pairs(t) do + if v.id == values.id and v.dmg == values.dmg then + table.remove(t, k) + break + end + end + local keys = { 'name', 'auto', 'id', 'low', 'dmg', 'max_dmg', 'nbt_hash', 'limit', 'ignore_dmg' } + local filtered = { } + for _,key in pairs(keys) do + filtered[key] = values[key] + end + + table.insert(t, filtered) + Util.writeTable('resource.limits', t) + UI:setPreviousPage() + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +listingPage = UI.Page { + menuBar = UI.MenuBar { + buttons = { + { text = 'Learn', event = 'learn' }, + { text = 'Forget', event = 'forget' }, + }, + }, + grid = UI.Grid { + y = 2, height = UI.term.height - 2, + columns = { + { heading = 'Name', key = 'name' , width = 22 }, + { heading = 'Qty', key = 'qty' , width = 5 }, + { heading = 'Min', key = 'low' , width = 4 }, + { heading = 'Max', key = 'limit', width = 4 }, + }, + sortColumn = 'name', + }, + statusBar = UI.StatusBar { + backgroundColor = colors.gray, + width = UI.term.width, + filterText = UI.Text { + x = 2, width = 6, + value = 'Filter', + }, + filter = UI.TextEntry { + x = 9, width = 19, + limit = 50, + }, + refresh = UI.Button { + x = 31, width = 8, + text = 'Refresh', + event = 'refresh', + }, + }, + accelerators = { + r = 'refresh', + q = 'quit', + } +} + +function listingPage.grid:getRowTextColor(row, selected) + if row.is_craftable then + return colors.yellow + end + if row.has_recipe then + if selected then + return colors.blue + end + return colors.lightBlue + end + return UI.Grid:getRowTextColor(row, selected) +end + +function listingPage.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.qty = Util.toBytes(row.qty) + if row.low then + row.low = Util.toBytes(row.low) + end + if row.limit then + row.limit = Util.toBytes(row.limit) + end + return row +end + +function listingPage.statusBar:draw() + return UI.Window.draw(self) +end + +function listingPage.statusBar.filter:eventHandler(event) + if event.type == 'mouse_rightclick' then + self.value = '' + self:draw() + local page = UI:getCurrentPage() + page.filter = nil + page:applyFilter() + page.grid:draw() + page:setFocus(self) + end + return UI.TextEntry.eventHandler(self, event) +end + +function listingPage:eventHandler(event) + if event.type == 'quit' then + Event.exitPullEvents() + + elseif event.type == 'grid_select' then + local selected = event.selected + itemPage.form:setValues(selected) + itemPage.titleBar.title = selected.name + itemPage.idField.value = selected.id + UI:setPage('item') + + elseif event.type == 'refresh' then + self:refresh() + self.grid:draw() + + elseif event.type == 'learn' then + if not duckAntenna then + self.statusBar:timedStatus('Missing peripherals', 3) + else + UI:setPage('craft') + end + + elseif event.type == 'forget' then + local item = self.grid:getSelected() + if item then + local recipes = Util.readTable('recipes') or { } + local key = uniqueKey(item) + local recipe = recipes[key] + + if recipe then + recipes[key] = nil + Util.writeTable('recipes', recipes) + end + + local resources = Util.readTable('resource.limits') or { } + for k,v in pairs(resources) do + if v.id == item.id and v.dmg == item.dmg then + table.remove(resources, k) + Util.writeTable('resource.limits', resources) + break + end + end + + self.statusBar:timedStatus('Forgot: ' .. item.name, 3) + self:refresh() + self.grid:draw() + end + + elseif event.type == 'text_change' then + self.filter = event.text + if #self.filter == 0 then + self.filter = nil + end + self:applyFilter() + self.grid:draw() + self.statusBar.filter:focus() + + else + UI.Page.eventHandler(self, event) + end + return true +end + +function listingPage:enable() + self:refresh() + self:setFocus(self.statusBar.filter) + UI.Page.enable(self) +end + +function listingPage:refresh() + self.allItems = ME.getAvailableItems('all') + + mergeResources(self.allItems) + + Util.each(self.allItems, function(item) + item.lname = item.name:lower() + end) + + self:applyFilter() +end + +function listingPage:applyFilter() + local t = filterItems(self.allItems, self.filter) + self.grid:setValues(t) +end + +-- without duck antenna +local function getTurtleInventory() + local inventory = { } + for i = 1,16 do + if turtle.getItemCount(i) > 0 then + turtle.select(i) + local item = turtle.getItemDetail() + inventory[i] = { + id = item.name, + dmg = item.damage, + qty = item.count, + name = item.name, + } + end + end + return inventory +end + +-- Strip off color prefix +local function safeString(text) + + local val = text:byte(1) + + if val < 32 or val > 128 then + + local newText = {} + for i = 4, #text do + local val = text:byte(i) + newText[i - 3] = (val > 31 and val < 127) and val or 63 + end + return string.char(unpack(newText)) + end + + return text +end + +local function filter(t, filter) + local keys = Util.keys(t) + for _,key in pairs(keys) do + if not Util.key(filter, key) then + t[key] = nil + end + end +end + +local function learnRecipe(page) + local t = Util.readTable('recipes') or { } + local recipe = { } + local ingredients = duckAntenna.getAllStacks(false) -- getTurtleInventory() + if ingredients then + turtle.select(1) + if turtle.craft() then + recipe = duckAntenna.getAllStacks(false) -- getTurtleInventory() + if recipe and recipe[1] then + recipe = recipe[1] + local key = uniqueKey(recipe) + + clearGrid() + + recipe.name = safeString(recipe.display_name) + filter(recipe, { 'name', 'id', 'dmg', 'nbt_hash', 'qty', 'max_size' }) + + for _,ingredient in pairs(ingredients) do + ingredient.name = safeString(ingredient.display_name) + filter(ingredient, { 'name', 'id', 'dmg', 'nbt_hash', 'qty', 'max_size', 'max_dmg' }) + + if ingredient.max_dmg > 0 then -- let's try this... + ingredient.dmg = 0 + end + end + recipe.ingredients = ingredients + recipe.ignore_dmg = 'no' + + t[key] = recipe + + Util.writeTable('recipes', t) + listingPage.statusBar.filter:setValue(recipe.name) + listingPage.statusBar:timedStatus('Learned: ' .. recipe.name, 3) + listingPage.filter = recipe.name + listingPage:refresh() + listingPage.grid:draw() + + return true + end + else + page.statusBar:timedStatus('Failed to craft', 3) + end + else + page.statusBar:timedStatus('No recipe defined', 3) + end +end + +craftPage = UI.Dialog { + height = 7, width = UI.term.width - 6, + backgroundColor = colors.lightGray, + titleBar = UI.TitleBar { + title = 'Learn Recipe', + previousPage = true, + }, + idField = UI.Text { + x = 5, + y = 3, + width = UI.term.width - 10, + value = 'Place recipe in turtle' + }, + accept = UI.Button { + rx = -13, ry = -2, + text = 'Ok', event = 'accept', + }, + cancel = UI.Button { + rx = -8, ry = -2, + text = 'Cancel', event = 'cancel' + }, + statusBar = UI.StatusBar { + status = 'Crafting paused' + } +} + +function craftPage:enable() + craftingPaused = true + self:focusFirst() + UI.Dialog.enable(self) +end + +function craftPage:disable() + craftingPaused = false + UI.Dialog.disable(self) +end + +function craftPage:eventHandler(event) + if event.type == 'cancel' then + UI:setPreviousPage() + elseif event.type == 'accept' then + if learnRecipe(self) then + UI:setPreviousPage() + end + else + return UI.Dialog.eventHandler(self, event) + end + return true +end + +UI:setPages({ + listing = listingPage, + item = itemPage, + craft = craftPage, +}) + +UI:setPage(listingPage) +listingPage:setFocus(listingPage.statusBar.filter) + +clearGrid() +jobMonitor() +jobListGrid:draw() +jobListGrid:sync() + +Event.onInterval(5, function() + + if not craftingPaused then + + local items = ME.getAvailableItems() + + if Util.size(items) == 0 then + jobListGrid.parent:clear() + jobListGrid.parent:centeredWrite(math.ceil(jobListGrid.parent.height/2), 'No items in system') + jobListGrid:sync() + + elseif config.noCraftingStorage ~= 'true' and #ME.getCraftingCPUs() <= 0 then -- only way to determine if AE is online + jobListGrid.parent:clear() + jobListGrid.parent:centeredWrite(math.ceil(jobListGrid.parent.height/2), 'Power failure') + jobListGrid:sync() + + else + local itemList = watchResources(items) + jobListGrid:setValues(itemList) + jobListGrid:draw() + jobListGrid:sync() + craftItems(itemList) + jobListGrid:update() + jobListGrid:draw() + jobListGrid:sync() + + itemList = getAutocraftItems(items) -- autocrafted items don't show on job monitor + craftItems(itemList) + end + end +end) + +UI:pullEvents() +jobListGrid.parent:reset() diff --git a/apps/supplier.lua b/apps/supplier.lua new file mode 100644 index 0000000..042de28 --- /dev/null +++ b/apps/supplier.lua @@ -0,0 +1,438 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local Logger = require('logger') +local MEProvider = require('meProvider') +local Message = require('message') +local Point = require('point') +local TableDB = require('tableDB') +local Util = require('util') + +--[[ + A supplier turtle for the builder turtle. For larger builds, use + ender modems. + + Setup: + + 1. chest or ME interface at level 0 (bottom of build area) + 2. builder turtle on top facing the build area + 3. If facing the build turtle, the supplier turtle is to the right + pointing at the chest/interface +]]-- + +local ChestProvider = require('chestProvider') +if Util.getVersion() == 1.8 then + ChestProvider = require('chestProvider18') +end + +if not device.wireless_modem then + error('No wireless modem detected') +end + +Logger.filter('modem_send', 'event', 'ui') +Logger.setWirelessLogging() + +local __BUILDER_ID = 6 +local itemInfoDB + +local Builder = { + version = '1.70', + ccVersion = nil, + slots = { }, + index = 1, + fuelItem = { id = 'minecraft:coal', dmg = 0 }, + resupplying = true, + ready = true, +} + +--[[-- maxStackDB --]]-- +local maxStackDB = TableDB({ + fileName = 'maxstack.db', + tabledef = { + autokeys = false, + type = 'simple', + columns = { + { label = 'Key', type = 'key', length = 8 }, + { label = 'Quantity', type = 'number', length = 2 } + } + } +}) + +function maxStackDB:get(id, dmg) + return self.data[id .. ':' .. dmg] or 64 +end + +function Builder:dumpInventory() + + local success = true + + for i = 1, 16 do + local qty = turtle.getItemCount(i) + if qty > 0 then + self.itemProvider:insert(i, qty) + end + if turtle.getItemCount(i) ~= 0 then + success = false + end + end + turtle.select(1) + + return success +end + +function Builder:dumpInventoryWithCheck() + while not self:dumpInventory() do + Builder:log('Unable to dump inventory') + print('Provider is full or missing - make space or replace') + print('Press enter to continue') + --turtle.setHeading(0) + self.ready = false + read() + end + self.ready = true +end + +function Builder:autocraft(supplies) + local t = { } + + for i,s in pairs(supplies) do + local key = s.id .. ':' .. s.dmg + local item = t[key] + if not item then + item = { + id = s.id, + dmg = s.dmg, + qty = 0, + } + t[key] = item + end + item.qty = item.qty + (s.need-s.qty) + end + + Builder.itemProvider:craftItems(t) +end + +function Builder:refuel() + while turtle.getFuelLevel() < 4000 and self.fuelItem do + Builder:log('Refueling') + turtle.select(1) + self.itemProvider:provide(self.fuelItem, 64, 1) + if turtle.getItemCount(1) == 0 then + Builder:log('Out of fuel, add coal to chest/ME system') + --turtle.setHeading(0) + os.sleep(5) + else + turtle.refuel(64) + end + end +end + +function Builder:log(...) + Logger.log('supplier', ...) + Util.print(...) +end + +function Builder:getSupplies() + + Builder.itemProvider:refresh() + + local t = { } + for _,s in ipairs(self.slots) do + if s.need > 0 then + local item = Builder.itemProvider:getItemInfo(s.id, s.dmg) + if item then + if item.name then + s.name = item.name + end + + local qty = math.min(s.need-s.qty, item.qty) + + if qty + s.qty > item.max_size then + maxStackDB:add({ s.id, s.dmg }, item.max_size) + maxStackDB.dirty = true + maxStackDB:flush() + qty = item.max_size + s.need = qty + end + if qty > 0 then + self.itemProvider:provide(item, qty, s.index) + s.qty = turtle.getItemCount(s.index) + end + end + end + if s.qty < s.need then + table.insert(t, s) + local name = s.name or s.id .. ':' .. s.dmg + local item = itemInfoDB:get({ s.id, s.dmg }) + if item then + name = item.displayName + end + + Builder:log('Need %d %s', s.need - s.qty, name) + end + end + + return t +end + +local function moveTowardsX(dx) + + local direction = dx - turtle.point.x + local move + + if direction == 0 then + return false + end + + if direction > 0 and turtle.point.heading == 0 or + direction < 0 and turtle.point.heading == 2 then + move = turtle.forward + else + move = turtle.back + end + + return move() +end + +local function moveTowardsZ(dz) + + local direction = dz - turtle.point.z + local move + + if direction == 0 then + return false + end + + if direction > 0 and turtle.point.heading == 1 or + direction < 0 and turtle.point.heading == 3 then + move = turtle.forward + else + move = turtle.back + end + + return move() +end + +function Builder:finish() + + Builder.resupplying = true + Builder.ready = false + if turtle.gotoLocation('supplies') then + turtle.setHeading(1) + os.sleep(.1) -- random 'Computer is not connected' error... + Builder:dumpInventory() + Event.exitPullEvents() + print('Finished') + end +end + +function Builder:gotoBuilder() + + if Builder.lastPoint then + turtle.status = 'tracking' + while true do + local pt = Point.copy(Builder.lastPoint) + pt.y = pt.y + 3 + if turtle.point.y ~= pt.y then + turtle.gotoY(pt.y) + else + local distance = Point.turtleDistance(turtle.point, pt) + if distance <= 3 then + Builder:log('Synchronized') + break + end + + if turtle.point.heading % 2 == 0 then + if turtle.point.x == pt.x then + turtle.headTowardsZ(pt.z) + moveTowardsZ(pt.z) + else + moveTowardsX(pt.x) + end + elseif turtle.point.z ~= pt.z then + moveTowardsZ(pt.z) + else + turtle.headTowardsX(pt.x) + moveTowardsX(pt.x) + end + end + end + end +end + +Message.addHandler('builder', + function(h, id, msg, distance) + if not id or id ~= __BUILDER_ID then + return + end + + if not Builder.resupplying then + local pt = msg.contents + pt.y = pt.y + 3 + + turtle.status = 'supervising' + turtle.gotoYfirst(pt) + end + end) + +Message.addHandler('supplyList', + function(h, id, msg, distance) + if not id or id ~= __BUILDER_ID then + return + end + + turtle.status = 'resupplying' + Builder.resupplying = true + Builder.slots = msg.contents.slots + Builder.slotUid = msg.contents.uid + + Builder:log('Received supply list ' .. Builder.slotUid) + + os.sleep(0) + if not turtle.gotoLocation('supplies') then + Builder:log('Failed to go to supply location') + self.ready = false + Event.exitPullEvents() + end + turtle.setHeading(1) + os.sleep(.2) -- random 'Computer is not connected' error... + Builder:dumpInventoryWithCheck() + Builder:refuel() + + while true do + local supplies = Builder:getSupplies() + if #supplies == 0 then + break + end + Builder:autocraft(supplies) + turtle.status = 'waiting' + os.sleep(5) + end + Builder:log('Got all supplies') + os.sleep(0) + Builder:gotoBuilder() + Builder.resupplying = false + end) + +Message.addHandler('needSupplies', + function(h, id, msg, distance) + if not id or id ~= __BUILDER_ID then + return + end + + if Builder.resupplying or msg.contents.uid ~= Builder.slotUid then + + Builder:log('No supplies ready') + + Message.send(__BUILDER_ID, 'gotSupplies') + else + turtle.status = 'supplying' + Builder:log('Supplying') + os.sleep(0) + + local pt = msg.contents.point + pt.y = turtle.getPoint().y + pt.heading = nil + if not turtle.gotoYfirst(pt) then -- location of builder + Builder.resupplying = true + Message.send(__BUILDER_ID, 'gotSupplies') + os.sleep(0) + if not turtle.gotoLocation('supplies') then + Builder:log('failed to go to supply location') + --self.ready = false + Event.exitPullEvents() + end + turtle.setHeading(1) + return + end + pt.y = pt.y - 2 -- location where builder should go for the chest to be above + + turtle.select(15) + turtle.placeDown() + os.sleep(.1) -- random computer not connected error + local p = ChestProvider({ direction = 'up', wrapSide = 'bottom' }) + for i = 1, 16 do + p:insert(i, 64) + end + + Message.send(__BUILDER_ID, 'gotSupplies', { supplies = true, point = pt }) + + Message.waitForMessage('thanks', 5, __BUILDER_ID) + --os.sleep(0) + + --p.condenseItems() + for i = 1, 16 do + p:extract(i, 64) + end + turtle.digDown() + turtle.status = 'waiting' + end + end) + +Message.addHandler('finished', + function(h, id) + if not id or id ~= __BUILDER_ID then + return + end + Builder:finish() + end) + +Event.on('turtle_abort', + function() + turtle.abort = false + turtle.status = 'aborting' + Builder:finish() + end) + +local function onTheWay() -- parallel routine + while true do + local e, side, _id, id, msg, distance = os.pullEvent('modem_message') + if Builder.ready then + if id == __BUILDER_ID and msg and msg.type then + if msg.type == 'needSupplies' then + Message.send(__BUILDER_ID, 'gotSupplies', { supplies = true }) + elseif msg.type == 'builder' then + Builder.lastPoint = msg.contents + end + end + end + end +end + +local args = {...} +if #args < 2 then + error('syntax: ') +end + +__BUILDER_ID = tonumber(args[1]) + +maxStackDB:load() + +itemInfoDB = TableDB({ + fileName = 'items.db' +}) + +itemInfoDB:load() + +Builder.itemProvider = MEProvider({ direction = args[2] }) +if not Builder.itemProvider:isValid() then + local sides = { + east = 'west', + west = 'east', + north = 'south', + south = 'north', + } + + Builder.itemProvider = ChestProvider({ direction = sides[args[2]], wrapSide = 'front' }) + if not Builder.itemProvider:isValid() then + error('A chest or ME interface must be in front of turtle') + end +end + +turtle.run(function() + turtle.setPoint({ x = -1, z = -2, y = -1, heading = 1 }) + + turtle.saveLocation('supplies') + + Event.pullEvents(onTheWay) +end) diff --git a/apps/t.lua b/apps/t.lua new file mode 100644 index 0000000..49f2aa1 --- /dev/null +++ b/apps/t.lua @@ -0,0 +1,89 @@ +function doCommand(command, moves) + + local function format(value) + if type(value) == 'boolean' then + if value then return 'true' end + return 'false' + end + if type(value) ~= 'table' then + return value + end + local str + for k,v in pairs(value) do + if not str then + str = '{ ' + else + str = str .. ', ' + end + str = str .. k .. '=' .. tostring(v) + end + if str then + str = str .. ' }' + else + str = '{ }' + end + + return str + end + + local function runCommand(fn, arg) + local r = { fn(arg) } + if r[2] then + print(format(r[1]) .. ': ' .. format(r[2])) + elseif r[1] then + print(format(r[1])) + end + return r[1] + end + + local cmds = { + [ 's' ] = turtle.select, + [ 'rf' ] = turtle.refuel, + [ 'gh' ] = function() turtle.pathfind({ x = 0, y = 0, z = 0, heading = 0}) end, + } + + local repCmds = { + [ 'u' ] = turtle.up, + [ 'd' ] = turtle.down, + [ 'f' ] = turtle.forward, + [ 'r' ] = turtle.turnRight, + [ 'l' ] = turtle.turnLeft, + [ 'ta' ] = turtle.turnAround, + [ 'DD' ] = turtle.digDown, + [ 'DU' ] = turtle.digUp, + [ 'D' ] = turtle.dig, + [ 'p' ] = turtle.place, + [ 'pu' ] = turtle.placeUp, + [ 'pd' ] = turtle.placeDown, + [ 'b' ] = turtle.back, + [ 'gfl' ] = turtle.getFuelLevel, + [ 'gp' ] = turtle.getPoint, + [ 'R' ] = function() turtle.setPoint({x = 0, y = 0, z = 0, heading = 0}) return turtle.point end + } + + if cmds[command] then + runCommand(cmds[command], moves) + elseif repCmds[command] then + for i = 1, moves do + if not runCommand(repCmds[command]) then + break + end + end + end +end + +local args = {...} + +if #args > 0 then + doCommand(args[1], args[2] or 1) +else + print('Enter command (q to quit):') + while true do + local cmd = read() + if cmd == 'q' then break + end + args = { } + cmd:gsub('%w+', function(w) table.insert(args, w) end) + doCommand(args[1], args[2] or 1) + end +end diff --git a/apps/treefarm.lua b/apps/treefarm.lua new file mode 100644 index 0000000..e593991 --- /dev/null +++ b/apps/treefarm.lua @@ -0,0 +1,743 @@ +requireInjector(getfenv(1)) + +--[[ + Requirements: + Place turtle against an oak tree or oak sapling + Area around turtle must be flat and can only be dirt or grass + (10 blocks in each direction from turtle) + Turtle must have: crafting table, chest + Turtle must have a pick equipped on the left side + + Optional: + Add additional sapling types that can grow with a single sapling + + Notes: + If the turtle does not get any saplings from the initial tree, place + down another sapling in front of the turtle. + + The program will be able to survive server restarts as long as it has + created the cobblestone line. If the program is stopped before that time, + place the turtle in the original position before restarting the program. +]]-- + +local ChestAdapter = require('chestAdapter18') +local Craft = require('turtle.craft') +local Level = require('turtle.level') +local Pathing = require('turtle.pathfind') +local Point = require('point') +local Util = require('util') + +local FUEL_BASE = 0 +local FUEL_DIRE = FUEL_BASE + 10 +local FUEL_GOOD = FUEL_BASE + 2000 + +local MIN_CHARCOAL = 24 +local MAX_SAPLINGS = 32 + +local GRID_WIDTH = 8 +local GRID_LENGTH = 10 +local GRID = { + TL = { x = 8, y = 0, z = -8 }, + TR = { x = 8, y = 0, z = 8 }, + BL = { x = -10, y = 0, z = -8 }, + BR = { x = -10, y = 0, z = 8 }, +} + +local HOME_PT = { x = 0, y = 0, z = 0, heading = 0 } + +local DIG_BLACKLIST = { + [ 'minecraft:furnace' ] = true, + [ 'minecraft:lit_furnace' ] = true, + [ 'minecraft:chest' ] = true, +} + +local COBBLESTONE = 'minecraft:cobblestone:0' +local CHARCOAL = 'minecraft:coal:1' +local OAK_LOG = 'minecraft:log:0' +local OAK_PLANK = 'minecraft:planks:0' +local CHEST = 'minecraft:chest:0' +local FURNACE = 'minecraft:furnace:0' +local SAPLING = 'minecraft:sapling:0' +local STONE = 'minecraft:stone:0' +local TORCH = 'minecraft:torch:0' +local DIRT = 'minecraft:dirt:0' +local APPLE = 'minecraft:apple:0' +local STICK = 'minecraft:stick:0' +local CRAFTING_TABLE = 'minecraft:crafting_table:0' + +local ALL_SAPLINGS = { + SAPLING +} + +local state = Util.readTable('usr/config/treefarm') or { + trees = { + { x = 1, y = 0, z = 0 } + } +} + +local clock = os.clock() +local recipes = Util.readTable('sys/etc/recipes.db') or { } + +Craft.setRecipes(recipes) + +local function inspect(fn) + local s, item = fn() + if s and item then + return item.name .. ':' .. item.metadata + end + return 'minecraft:air:0' +end + +local function setState(key, value) + state[key] = value + Util.writeTable('usr/config/treefarm', state) +end + +local function refuel() + if turtle.getFuelLevel() < FUEL_GOOD then + local charcoal = turtle.getItemCount(CHARCOAL) + if charcoal > 1 then + turtle.refuel(CHARCOAL, math.min(charcoal - 1, MIN_CHARCOAL / 2)) + print('fuel: ' .. turtle.getFuelLevel()) + end + end + return true +end + +local function safePlaceBlock(item) + + if turtle.placeUp(item) then + return true + end + + local s, m = turtle.inspectUp() + if s and not DIG_BLACKLIST[m.name] then + turtle.digUp() + return turtle.placeUp(item) + end + + turtle.forward() + return turtle.placeUp(item) +end + +local function craftItem(item, qty) + + local success + + if safePlaceBlock(CHEST) then + + if turtle.equip('left', 'minecraft:crafting_table') then + + local chestAdapter = ChestAdapter({ + wrapSide = 'top', + direction = 'down', + }) + if not chestAdapter:isValid() then + print('invalid chestAdapter') + read() + end + + Util.print('Crafting %d %s', (qty or 1), item) + success = Craft.craftRecipe(recipes[item], qty or 1, chestAdapter) + + repeat until not turtle.suckUp() + end + turtle.equip('left', 'minecraft:diamond_pickaxe') + turtle.digUp() + end + + return success +end + +local function cook(item, count, result, fuel, fuelCount) + + setState('cooking', true) + + fuel = fuel or CHARCOAL + fuelCount = fuelCount or math.ceil(count / 8) + Util.print('Making %d %s', count, result) + + turtle.dropForwardAt(state.furnace, fuel, fuelCount) + turtle.dropDownAt(state.furnace, item, count) + + count = count + turtle.getItemCount(result) + turtle.select(1) + turtle.pathfind(Point.below(state.furnace)) + repeat + os.sleep(1) + turtle.suckUp() + until turtle.getItemCount(result) >= count + + setState('cooking') +end + +local function makeSingleCharcoal() + + local slots = turtle.getSummedInventory() + + if not state.furnace or + slots[CHARCOAL] or + not slots[OAK_LOG] or + slots[OAK_LOG].count < 2 then + return true + end + + turtle.faceAgainst(state.furnace) + if craftItem(OAK_PLANK) then + cook(OAK_LOG, 1, CHARCOAL, OAK_PLANK, 1) + turtle.refuel(OAK_PLANK) + end + + return true +end + +local function makeCharcoal() + + local slots = turtle.getSummedInventory() + + if not state.furnace or + not slots[CHARCOAL] or + slots[CHARCOAL].count >= MIN_CHARCOAL then + return true + end + + local function getLogSlot(slots) + local maxslot = { count = 0 } + for k,slot in pairs(slots) do + if string.match(k, 'minecraft:log') then + if slot.count > maxslot.count then + maxslot = slot + end + end + end + return maxslot + end + + repeat + local slots = turtle.getSummedInventory() + local charcoal = slots[CHARCOAL].count + local slot = getLogSlot(slots) + + if slot.count < 8 then + break + end + + local toCook = math.min(charcoal, math.floor(slot.count / 8)) + toCook = math.min(toCook, math.floor((MIN_CHARCOAL + 8 - charcoal) / 8)) + toCook = toCook * 8 + + cook(slot.key, toCook, CHARCOAL) + + until charcoal + toCook >= MIN_CHARCOAL + + return true +end + +local function emptyFurnace() + if state.cooking then + + print('Emptying furnace') + + turtle.suckDownAt(state.furnace) + turtle.suckForwardAt(state.furnace) + turtle.suckUpAt(state.furnace) + setState('cooking') + end +end + +local function getCobblestone(count) + + local slots = turtle.getSummedInventory() + + if not slots[COBBLESTONE] or slots[COBBLESTONE].count < count then + + print('Collecting cobblestone') + + slots[COBBLESTONE] = true + slots[DIRT] = true + + local pt = Point.copy(GRID.BR) + pt.x = GRID.BR.x + 2 + pt.z = GRID.BR.z - 2 + + turtle.pathfind(pt) + + repeat + turtle.select(1) + turtle.digDown() + turtle.down() + for i = 1, 4 do + if inspect(turtle.inspect) == STONE then + turtle.dig() + end + turtle.turnRight() + end + + for item in pairs(turtle.getSummedInventory()) do + if not slots[item] then + turtle.drop(item) + end + end + + until turtle.getItemCount(COBBLESTONE) >= count + + turtle.gotoPoint(pt) + turtle.placeDown(DIRT) + + turtle.drop(DIRT) + end +end + +local function createFurnace() + + if not state.furnace then + if turtle.getFuelLevel() < FUEL_BASE + 100 then + return true -- try again later + end + print('Adding a furnace') + getCobblestone(8) + + if craftItem(FURNACE) then + turtle.drop(COBBLESTONE) + local furnacePt = { x = GRID.BL.x + 2, y = 1, z = GRID.BL.z + 2 } + turtle.placeAt(furnacePt, FURNACE) + setState('furnace', furnacePt) + end + end +end + +local function createPerimeter() + + if not state.perimeter then + if not state.furnace or + turtle.getFuelLevel() < FUEL_BASE + 500 or + turtle.getItemCount(OAK_LOG) == 0 or + not craftItem(OAK_PLANK, 2) then + return true + end + + print('Creating a perimeter') + + getCobblestone(GRID_WIDTH * 2 + 1) + cook(COBBLESTONE, 2, STONE, OAK_PLANK, 2) + turtle.refuel(OAK_PLANK) + + turtle.pathfind(GRID.BL) + turtle.digDown() + turtle.placeDown(STONE) + + turtle.setMoveCallback(function() + local target = COBBLESTONE + if math.abs(turtle.point.x) == GRID_LENGTH and + math.abs(turtle.point.z) == GRID_WIDTH then + target = STONE + end + + if inspect(turtle.inspectDown) ~= target then + turtle.digDown() + turtle.placeDown(target) + end + end) + + turtle.pathfind(GRID.BR) + + turtle.clearMoveCallback() + turtle.drop(COBBLESTONE) + turtle.drop(DIRT) + + setState('perimeter', true) + end +end + +local function createChests() + if state.chest_1 then + return + end + if state.perimeter and + turtle.getFuelLevel() > FUEL_GOOD and + Craft.canCraft(CHEST, 4, turtle.getSummedInventory()) then + + print('Adding storage') + if craftItem(CHEST, 4) then + + local pt = Point.copy(GRID.BL) + pt.x = pt.x + 1 + pt.y = pt.y - 1 + + for i = 1, 2 do + pt.z = pt.z + 1 + + turtle.digDownAt(pt) + turtle.placeDown(CHEST) + + pt.z = pt.z + 1 + + turtle.digDownAt(pt) + turtle.placeDown(CHEST) + + setState('chest_' .. i, Util.shallowCopy(pt)) + + pt.z = pt.z + 1 + end + turtle.drop(DIRT) + turtle.refuel(OAK_PLANK) + end + end + return true +end + +local function dropOffItems() + + if state.chest_1 then + local slots = turtle.getSummedInventory() + + if state.chest_1 and + slots[CHARCOAL] and + slots[CHARCOAL].count >= MIN_CHARCOAL and + (turtle.getItemCount('minecraft:log') > 0 or + turtle.getItemCount('minecraft:log2') > 0) then + + print('Storing logs') + turtle.pathfind(Point.above(state.chest_1)) + turtle.dropDown('minecraft:log') + turtle.dropDown('minecraft:log2') + end + + if slots[APPLE] then + print('Storing apples') + turtle.dropDownAt(state.chest_2, APPLE) + end + end + + return true +end + +local function eatSaplings() + + local slots = turtle.getSummedInventory() + + for _, sapling in pairs(ALL_SAPLINGS) do + if slots[sapling] and slots[sapling].count > MAX_SAPLINGS then + turtle.refuel(sapling, slots[sapling].count - MAX_SAPLINGS) + end + end + return true +end + +local function placeTorches() + if state.torches then + return + end + + if turtle.getFuelLevel() > 100 and + Craft.canCraft(TORCH, 4, turtle.getSummedInventory()) then + + print('Placing torches') + + if craftItem(TORCH, 4) then + local pts = { } + for x = -4, 4, 8 do + for z = -4, 4, 8 do + table.insert(pts, { x = x, y = 0, z = z }) + end + end + Point.eachClosest(turtle.point, pts, function(pt) + turtle.placeAt(pt, TORCH) + end) + turtle.refuel(STICK) + turtle.refuel(OAK_PLANK) + setState('torches', true) + end + end + + return true +end + +local function randomSapling() + + local sapling = SAPLING + + if #state.trees > 1 then + ALL_SAPLINGS = { } + + local slots = turtle.getFilledSlots() + for _, slot in pairs(slots) do + if slot.name == 'minecraft:sapling' then + table.insert(ALL_SAPLINGS, slot.key) + end + end + sapling = ALL_SAPLINGS[math.random(1, #ALL_SAPLINGS)] + end + + return sapling +end + +local function fellTree(pt) + + local function desperateRefuel(min) + if turtle.getFuelLevel() < min then + local logs = turtle.getItemCount(OAK_LOG) + if logs > 0 then + if craftItem(OAK_PLANK, math.min(8, logs * 4)) then + turtle.refuel(OAK_PLANK) + print('fuel: ' .. turtle.getFuelLevel()) + end + end + end + end + + turtle.setMoveCallback(function() desperateRefuel(FUEL_DIRE) end) + + desperateRefuel(FUEL_DIRE) + + if turtle.digUpAt(Point.above(pt)) then + Level( + { x = GRID_WIDTH-1, y = 1, z = GRID_WIDTH-1 }, + { x = -(GRID_WIDTH-1), y = 50, z = -(GRID_WIDTH-1) }, + Point.above(pt)) + end + + desperateRefuel(FUEL_BASE + 100) + turtle.clearMoveCallback() + turtle.setPolicy("attack") + + return true +end + +local function fell() + + local pts = Util.shallowCopy(state.trees) + + local pt = table.remove(pts, math.random(1, #pts)) + + -- give the pathfinder hints about what to avoid (state.trees) + if not turtle.faceAgainst(pt, { blocks = Util.shallowCopy(state.trees) }) or + not string.match(inspect(turtle.inspect), 'minecraft:log') then + return true + end + + print('Chopping') + + local fuel = turtle.getFuelLevel() + + -- push this point to the start of this list + table.insert(pts, 1, pt) + + Point.eachClosest(turtle.point, pts, function(pt) + if turtle.faceAgainst(pt, { blocks = Util.shallowCopy(state.trees) }) and + string.match(inspect(turtle.inspect), 'minecraft:log') then + turtle.dig() + fellTree(pt) + end + turtle.placeAt(pt, randomSapling()) + turtle.select(1) + end) + + print('Used ' .. (fuel - turtle.getFuelLevel()) .. ' fuel') + return true +end + +local function moreTrees() + + if #state.trees > 1 then + return + end + + if not state.chest_1 or turtle.getItemCount('minecraft:sapling') < 15 then + return true + end + + print('Adding more trees') + + local singleTree = state.trees[1] + + state.trees = { } + for x = -2, 2, 1 do + for z = -2, 2, 2 do + table.insert(state.trees, { x = x, y = 0, z = z }) + end + end + + turtle.digAt(singleTree) + fellTree(singleTree) + + setState('trees', state.trees) + + Point.eachClosest(turtle.point, state.trees, function(pt) + turtle.placeDownAt(pt, randomSapling()) + end) +end + +function getTurtleFacing(block) + local directions = { + [5] = 2, + [3] = 3, + [4] = 0, + [2] = 1, + } + + if not safePlaceBlock(block) then + error('unable to place chest above') + end + local _, bi = turtle.inspectUp() + turtle.digUp() + return directions[bi.metadata] +end + +function saveTurtleFacing() + if not state.facing then + setState('facing', getTurtleFacing(CHEST)) + end +end + +local function findGround() + print('Locating ground level') + turtle.setPoint(HOME_PT) + + while true do + local s, block = turtle.inspectDown() + + if not s then block = { name = 'minecraft:air', metadata = 0 } end + b = block.name .. ':' .. block.metadata + + if b == 'minecraft:dirt:0' or + b == 'minecraft:grass:0' or + block.name == 'minecraft:chest' then + break + end + + if b == COBBLESTONE or b == STONE then + error('lost') + end + + if b == TORCH or b == FURNACE then + turtle.forward() + else + turtle.digDown() + turtle.down() + end + + if turtle.point.y < -20 then + error('lost') + end + end + turtle.setPoint(HOME_PT) +end + +local function findHome() + + if not state.perimeter then + return + end + + print('Determining location') + + turtle.point.heading = getTurtleFacing(CHEST) + turtle.setHeading(state.facing) + turtle.point.heading = 0 + + local pt = Point.copy(turtle.point) + + while inspect(turtle.inspectDown) ~= COBBLESTONE do + pt.x = pt.x - 1 + turtle.pathfind(pt) + if pt.x < -20 then + error('lost') + end + end + while inspect(turtle.inspectDown) == COBBLESTONE do + pt.z = pt.z - 1 + turtle.pathfind(pt) + if pt.z < -20 then + error('lost') + end + end + + turtle.setPoint({ + x = -(GRID_LENGTH), + y = 0, + z = -GRID_WIDTH, + heading = turtle.point.heading + }) + + -- when pathfinding - don't leave this box + Pathing.setBox({ + x = GRID.TL.x, + y = GRID.TL.y, + z = GRID.TL.z, + ex = GRID.BR.x, + ey = 5, + ez = GRID.BR.z, + }) +end + +local function updateClock() + + local ONE_HOUR = 50 + + if os.clock() - clock > ONE_HOUR then + clock = os.clock() + else + print('sleeping for ' .. math.floor(ONE_HOUR - (os.clock() - clock))) + os.sleep(ONE_HOUR - (os.clock() - clock)) + clock = os.clock() + end + + return true +end + +local function startupCheck() + local slots = turtle.getSummedInventory() + + if not slots[CHEST] or not slots[CRAFTING_TABLE] then + error('A chest and crafting table must be in inventory') + end + + if state.facing and not state.perimeter then + print('Perimeter has not been established.') + print('Enter to continue if turtle is in the original starting position.') + read() + end +end + +local tasks = { + { desc = 'Startup check', fn = startupCheck }, + { desc = 'Finding ground', fn = findGround }, + { desc = 'Determine facing', fn = saveTurtleFacing }, + { desc = 'Finding home', fn = findHome }, + { desc = 'Emptying furnace', fn = emptyFurnace }, + { desc = 'Adding trees', fn = moreTrees }, + { desc = 'Chopping', fn = fell }, + { desc = 'Snacking', fn = eatSaplings }, + { desc = 'Creating chest', fn = createChests }, + { desc = 'Creating furnace', fn = createFurnace }, + { desc = 'Making charcoal', fn = makeSingleCharcoal }, + { desc = 'Making charcoal', fn = makeCharcoal }, + { desc = 'Creating perimeter', fn = createPerimeter }, + { desc = 'Placing torches', fn = placeTorches }, + { desc = 'Refueling', fn = refuel }, + { desc = 'Dropping off items', fn = dropOffItems }, + { desc = 'Condensing', fn = turtle.condense }, + { desc = 'Sleeping', fn = updateClock }, +} + +local s, m = turtle.run(function() + + turtle.setPolicy("attack") + + while not turtle.abort do + print('fuel: ' .. turtle.getFuelLevel()) + for _,task in ipairs(Util.shallowCopy(tasks)) do + --print(task.desc) + turtle.status = task.desc + turtle.select(1) + if not task.fn() then + Util.filterInplace(tasks, function(v) return v.fn ~= task.fn end) + end + end + end +end) + +if not s then + error('Failed') +end diff --git a/etc/blocks.json b/etc/blocks.json new file mode 100644 index 0000000..99b7ef3 --- /dev/null +++ b/etc/blocks.json @@ -0,0 +1,1370 @@ +{ + "air": { + "id": 0, + "name": "Air", + }, + "stone": { + "id": 1, + "name": ["Stone", + "Granite", + "Polished Granite", + "Diorite", + "Polished Diorite", + "Andesite", + "Polished Andesite"], + }, + "grass": { + "id": 2, + "name": "Grass Block", + }, + "dirt": { + "id": 3, + "name": ["Dirt", + "Coarse Dirt", + "Podzol"], + }, + "cobblestone": { + "id": 4, + "name": "Cobblestone", + }, + "planks": { + "id": 5, + "name": ["Oak Wood Planks", + "Spruce Wood Planks", + "Birch Wood Planks", + "Jungle Wood Planks", + "Acacia Wood Planks", + "Dark Oak Wood Planks"], + }, + "sapling": { + "id": 6, + "name": ["Oak Sapling", + "Spruce Sapling", + "Birch Sapling", + "Jungle Sapling", + "Acacia Sapling", + "Dark Oak Sapling"], + "place": "sapling", + }, + "bedrock": { + "id": 7, + "name": "Bedrock", + }, + "flowing_water": { + "id": 8, + "name": "Water", + "place": "flatten", + }, + "water": { + "id": 9, + "name": "Water", + "place": "flatten", + }, + "flowing_lava": { + "id": 10, + "name": "Lava", + }, + "lava": { + "id": 11, + "name": "Lava", + }, + "sand": { + "id": 12, + "name": ["Sand", + "Red Sand"], + }, + "gravel": { + "id": 13, + "name": "Gravel", + }, + "gold_ore": { + "id": 14, + "name": "Gold Ore", + }, + "iron_ore": { + "id": 15, + "name": "Iron Ore", + }, + "coal_ore": { + "id": 16, + "name": "Coal Ore", + }, + "log": { + "id": 17, + "name": ["Oak Wood", + "Spruce Wood", + "Birch Wood", + "Jungle Wood"], + "place": "wood", + }, + "leaves": { + "id": 18, + "name": ["Oak Leaves", + "Spruce Leaves", + "Birch Leaves", + "Jungle Leaves"], + "place": "leaves", + }, + "sponge": { + "id": 19, + "name": ["Sponge", + "Wet Sponge"], + }, + "glass": { + "id": 20, + "name": "Glass", + }, + "lapis_ore": { + "id": 21, + "name": "Lapis Lazuli Ore", + }, + "lapis_block": { + "id": 22, + "name": "Lapis Lazuli Block", + }, + "dispenser": { + "id": 23, + "name": "Dispenser", + "place": "dispenser", + }, + "sandstone": { + "id": 24, + "name": ["Sandstone", + "Chiseled Sandstone", + "Smooth Sandstone"], + }, + "noteblock": { + "id": 25, + "name": "Note Block", + }, + "bed": { + "id": 26, + "name": "Bed", + "place": "bed", + }, + "golden_rail": { + "id": 27, + "name": "Powered Rail", + "place": "adp-rail", + }, + "detector_rail": { + "id": 28, + "name": "Detector Rail", + "place": "adp-rail", + }, + "sticky_piston": { + "id": 29, + "name": "Sticky Piston", + "place": "piston", + }, + "web": { + "id": 30, + "name": "Cobweb", + }, + "tallgrass": { + "id": 31, + "name": ["Shrub", + "Grass", + "Fern"], + }, + "deadbush": { + "id": 32, + "name": "Dead Bush", + }, + "piston": { + "id": 33, + "name": "Piston", + "place": "piston", + }, + "piston_head": { + "id": 34, + "name": "Piston Extension", + "place": "flatten", + }, + "wool": { + "id": 35, + "name": ["White Wool", + "Orange Wool", + "Magenta Wool", + "Light Blue Wool", + "Yellow Wool", + "Lime Wool", + "Pink Wool", + "Gray Wool", + "Light Gray Wool", + "Cyan Wool", + "Purple Wool", + "Blue Wool", + "Brown Wool", + "Green Wool", + "Red Wool", + "Black Wool"], + }, + "piston_extension": { + "id": 36, + "name": "Block moved by Piston", + "place": "flatten", + }, + "yellow_flower": { + "id": 37, + "name": "Dandelion", + }, + "red_flower": { + "id": 38, + "name": ["Poppy", + "Blue Orchid", + "Allium", + "Azure Bluet", + "Red Tulip", + "Orange Tulip", + "White Tulip", + "Pink Tulip", + "Oxeye Daisy"], + }, + "brown_mushroom": { + "id": 39, + "name": "Brown Mushroom", + }, + "red_mushroom": { + "id": 40, + "name": "Red Mushroom", + }, + "gold_block": { + "id": 41, + "name": "Block of Gold", + }, + "iron_block": { + "id": 42, + "name": "Block of Iron", + }, + "double_stone_slab": { + "id": 43, + "name": "Double Stone Slab", + }, + "stone_slab": { + "id": 44, + "name": ["Stone Slab", + "Sandstone Slab", + "Wooden Slab", + "Cobblestone Slab", + "Bricks Slab", + "Stone Bricks Slab", + "Nether Brick Slab", + "Quartz Slab"], + "place": "slab", + }, + "brick_block": { + "id": 45, + "name": "Bricks", + }, + "tnt": { + "id": 46, + "name": "TNT", + }, + "bookshelf": { + "id": 47, + "name": "Bookshelf", + }, + "mossy_cobblestone": { + "id": 48, + "name": "Moss Stone", + }, + "obsidian": { + "id": 49, + "name": "Obsidian", + }, + "torch": { + "id": 50, + "name": "Torch", + "place": "torch", + }, + "fire": { + "id": 51, + "name": "Fire", + "place": "flatten", + }, + "mob_spawner": { + "id": 52, + "name": "Monster Spawner", + }, + "oak_stairs": { + "id": 53, + "name": "Oak Wood Stairs", + "place": "stairs", + }, + "chest": { + "id": 54, + "name": "Chest", + "place": "chest-furnace", + }, + "redstone_wire": { + "id": 55, + "name": "Redstone Wire", + "place": "flatten", + }, + "diamond_ore": { + "id": 56, + "name": "Diamond Ore", + }, + "diamond_block": { + "id": 57, + "name": "Block of Diamond", + }, + "crafting_table": { + "id": 58, + "name": "Crafting Table", + }, + "wheat": { + "id": 59, + "name": "Wheat", + "place": "flatten", + }, + "farmland": { + "id": 60, + "name": "Farmland", + "place": "flatten", + }, + "furnace": { + "id": 61, + "name": "Furnace", + "place": "chest-furnace", + }, + "lit_furnace": { + "id": 62, + "name": "Burning Furnace", + "place": "chest-furnace", + }, + "standing_sign": { + "id": 63, + "name": "Sign", + "place": "signpost", + }, + "wooden_door": { + "id": 64, + "name": "Oak Door", + "place": "door", + }, + "ladder": { + "id": 65, + "name": "Ladder", + "place": "wallsign-ladder", + }, + "rail": { + "id": 66, + "name": "Rail", + "place": "rail", + }, + "stone_stairs": { + "id": 67, + "name": "Cobblestone Stairs", + "place": "stairs", + }, + "wall_sign": { + "id": 68, + "name": "Sign", + "place": "wallsign-ladder", + }, + "lever": { + "id": 69, + "name": "Lever", + "place": "lever", + }, + "stone_pressure_plate": { + "id": 70, + "name": "Stone Pressure Plate", + }, + "iron_door": { + "id": 71, + "name": "Iron Door", + "place": "door", + }, + "wooden_pressure_plate": { + "id": 72, + "name": "Wooden Pressure Plate", + }, + "redstone_ore": { + "id": 73, + "name": "Redstone Ore", + }, + "lit_redstone_ore": { + "id": 74, + "name": "Redstone Ore", + }, + "unlit_redstone_torch": { + "id": 75, + "name": "Redstone Torch (inactive)", + "place": "torch", + }, + "redstone_torch": { + "id": 76, + "name": "Redstone Torch (active)", + "place": "torch", + }, + "stone_button": { + "id": 77, + "name": "Stone Button", + "place": "button", + }, + "snow_layer": { + "id": 78, + "name": "Snow", + "place": "flatten", + }, + "ice": { + "id": 79, + "name": "Ice", + }, + "snow": { + "id": 80, + "name": "Snow", + }, + "cactus": { + "id": 81, + "name": "Cactus", + "place": "flatten", + }, + "clay": { + "id": 82, + "name": "Clay", + }, + "reeds": { + "id": 83, + "name": "Sugar Cane", + "place": "flatten", + }, + "jukebox": { + "id": 84, + "name": "Jukebox", + "place": "flatten", + }, + "fence": { + "id": 85, + "name": "Fence", + }, + "pumpkin": { + "id": 86, + "name": "Pumpkin", + "place": "pumpkin", + }, + "netherrack": { + "id": 87, + "name": "Netherrack", + }, + "soul_sand": { + "id": 88, + "name": "Soul Sand", + }, + "glowstone": { + "id": 89, + "name": "Glowstone", + }, + "portal": { + "id": 90, + "name": "Portal", + "place": "flatten", + }, + "lit_pumpkin": { + "id": 91, + "name": "Jack o'Lantern", + "place": "pumpkin", + }, + "cake": { + "id": 92, + "name": "Cake", + "place": "flatten", + }, + "unpowered_repeater": { + "id": 93, + "name": "Redstone Repeater (inactive)", + "place": "repeater", + }, + "powered_repeater": { + "id": 94, + "name": "Redstone Repeater (active)", + "place": "repeater", + }, + "stained_glass": { + "id": 95, + "name": ["White Stained Glass", + "Orange Stained Glass", + "Magenta Stained Glass", + "Light Blue Stained Glass", + "Yellow Stained Glass", + "Lime Stained Glass", + "Pink Stained Glass", + "Gray Stained Glass", + "Light Gray Stained Glass", + "Cyan Stained Glass", + "Purple Stained Glass", + "Blue Stained Glass", + "Brown Stained Glass", + "Green Stained Glass", + "Red Stained Glass", + "Black Stained Glass"], + }, + "trapdoor": { + "id": 96, + "name": "Trapdoor", + "place": "trapdoor", + }, + "monster_egg": { + "id": 97, + "name": ["Stone Monster Egg", + "Cobblestone Monster Egg", + "Stone Brick Monster Egg", + "Mossy Stone Brick Monster Egg", + "Cracked Stone Brick Monster Egg", + "Chiseled Stone Brick Monster Egg"], + }, + "stonebrick": { + "id": 98, + "name": ["Stone Bricks", + "Mossy Stone Bricks", + "Cracked Stone Bricks", + "Chiseled Stone Bricks"], + }, + "brown_mushroom_block": { + "id": 99, + "name": "Brown Mushroom (block)", + "place": "flatten", + }, + "red_mushroom_block": { + "id": 100, + "name": "Red Mushroom (block)", + "place": "flatten", + }, + "iron_bars": { + "id": 101, + "name": "Iron Bars", + }, + "glass_pane": { + "id": 102, + "name": "Glass Pane", + }, + "melon_block": { + "id": 103, + "name": "Melon", + }, + "pumpkin_stem": { + "id": 104, + "name": "Pumpkin Stem", + }, + "melon_stem": { + "id": 105, + "name": "Melon Stem", + }, + "vine": { + "id": 106, + "name": "Vines", + "place": "vine", + }, + "fence_gate": { + "id": 107, + "name": "Fence Gate", + "place": "gate", + }, + "brick_stairs": { + "id": 108, + "name": "Brick Stairs", + "place": "stairs", + }, + "stone_brick_stairs": { + "id": 109, + "name": "Stone Brick Stairs", + "place": "stairs", + }, + "mycelium": { + "id": 110, + "name": "Mycelium", + }, + "waterlily": { + "id": 111, + "name": "Lily Pad", + }, + "nether_brick": { + "id": 112, + "name": "Nether Brick", + }, + "nether_brick_fence": { + "id": 113, + "name": "Nether Brick Fence", + }, + "nether_brick_stairs": { + "id": 114, + "name": "Nether Brick Stairs", + "place": "stairs", + }, + "nether_wart": { + "id": 115, + "name": "Nether Wart", + "place": "flatten", + }, + "enchanting_table": { + "id": 116, + "name": "Enchantment Table", + }, + "brewing_stand": { + "id": 117, + "name": "Brewing Stand", + "place": "flatten", + }, + "cauldron": { + "id": 118, + "name": "Cauldron", + "place": "cauldron", + }, + "end_portal": { + "id": 119, + "name": "End Portal", + }, + "end_portal_frame": { + "id": 120, + "name": "End Portal Block", + "place": "flatten", + }, + "end_stone": { + "id": 121, + "name": "End Stone", + }, + "dragon_egg": { + "id": 122, + "name": "Dragon Egg", + }, + "redstone_lamp": { + "id": 123, + "name": "Redstone Lamp (inactive)", + }, + "lit_redstone_lamp": { + "id": 124, + "name": "Redstone Lamp (active)", + }, + "double_wooden_slab": { + "id": 125, + "name": "Double Wooden Slab", + }, + "wooden_slab": { + "id": 126, + "name": ["Oak Wood Slab", + "Spruce Wood Slab", + "Birch Wood Slab", + "Jungle Wood Slab", + "Acacia Wood Slab", + "Dark Oak Wood Slab"], + "place": "slab", + }, + "cocoa": { + "id": 127, + "name": "Cocoa", + "place": "cocoa", + }, + "sandstone_stairs": { + "id": 128, + "name": "Sandstone Stairs", + "place": "stairs", + }, + "emerald_ore": { + "id": 129, + "name": "Emerald Ore", + }, + "ender_chest": { + "id": 130, + "name": "Ender Chest", + "place": "chest-furnace", + }, + "tripwire_hook": { + "id": 131, + "name": "Tripwire Hook", + "place": "tripwire", + }, + "tripwire": { + "id": 132, + "name": "Tripwire", + "place": "flatten", + }, + "emerald_block": { + "id": 133, + "name": "Block of Emerald", + }, + "spruce_stairs": { + "id": 134, + "name": "Spruce Wood Stairs", + "place": "stairs", + }, + "birch_stairs": { + "id": 135, + "name": "Birch Wood Stairs", + "place": "stairs", + }, + "jungle_stairs": { + "id": 136, + "name": "Jungle Wood Stairs", + "place": "stairs", + }, + "command_block": { + "id": 137, + "name": "Command Block", + }, + "beacon": { + "id": 138, + "name": "Beacon", + }, + "cobblestone_wall": { + "id": 139, + "name": ["Cobblestone Wall", + "Mossy Cobblestone Wall"], + }, + "flower_pot": { + "id": 140, + "name": "Flower Pot", + "place": "flatten", + }, + "carrots": { + "id": 141, + "name": "Carrot", + "place": "flatten", + }, + "potatoes": { + "id": 142, + "name": "Potato", + "place": "flatten", + }, + "wooden_button": { + "id": 143, + "name": "Wooden Button", + "place": "button", + }, + "skull": { + "id": 144, + "name": "Mob Head", + "place": "mobhead", + }, + "anvil": { + "id": 145, + "name": ["Anvil", + "Slightly Damaged Anvil", + "Very Damaged Anvil"], + "place": "anvil", + }, + "trapped_chest": { + "id": 146, + "name": "Trapped Chest", + "place": "chest-furnace", + }, + "light_weighted_pressure_plate": { + "id": 147, + "name": "Weighted Pressure Plate", + }, + "heavy_weighted_pressure_plate": { + "id": 148, + "name": "Weighted Pressure Plate", + }, + "unpowered_comparator": { + "id": 149, + "name": "Redstone Comparator", + "place": "comparator", + }, + "powered_comparator": { + "id": 150, + "name": "Redstone Comparator", + }, + "daylight_detector": { + "id": 151, + "name": "Daylight Sensor", + "place": "flatten", + }, + "redstone_block": { + "id": 152, + "name": "Block of Redstone", + }, + "quartz_ore": { + "id": 153, + "name": "Nether Quartz Ore", + }, + "hopper": { + "id": 154, + "name": "Hopper", + "place": "hopper", + }, + "quartz_block": { + "id": 155, + "name": ["Block of Quartz", + "Chiseled Quartz Block", + "Pillar Quartz Block"], + }, + "quartz_stairs": { + "id": 156, + "name": "Quartz Stairs", + "place": "stairs", + }, + "activator_rail": { + "id": 157, + "name": "Activator Rail", + "place": "adp-rail", + }, + "dropper": { + "id": 158, + "name": "Dropper", + "place": "dispenser", + }, + "stained_hardened_clay": { + "id": 159, + "name": ["White Stained Clay", + "Orange Stained Clay", + "Magenta Stained Clay", + "Light Blue Stained Clay", + "Yellow Stained Clay", + "Lime Stained Clay", + "Pink Stained Clay", + "Gray Stained Clay", + "Light Gray Stained Clay", + "Cyan Stained Clay", + "Purple Stained Clay", + "Blue Stained Clay", + "Brown Stained Clay", + "Green Stained Clay", + "Red Stained Clay", + "Black Stained Clay"], + }, + "stained_glass_pane": { + "id": 160, + "name": ["White Stained Glass Pane", + "Orange Stained Glass Pane", + "Magenta Stained Glass Pane", + "Light Blue Stained Glass Pane", + "Yellow Stained Glass Pane", + "Lime Stained Glass Pane", + "Pink Stained Glass Pane", + "Gray Stained Glass Pane", + "Light Gray Stained Glass Pane", + "Cyan Stained Glass Pane", + "Purple Stained Glass Pane", + "Blue Stained Glass Pane", + "Brown Stained Glass Pane", + "Green Stained Glass Pane", + "Red Stained Glass Pane", + "Black Stained Glass Pane"], + }, + "leaves2": { + "id": 161, + "name": ["Acacia Leaves", + "Dark Oak Leaves"], + "place": "leaves", + }, + "log2": { + "id": 162, + "name": ["Acacia Wood", + "Dark Oak Wood"], + "place": "wood", + }, + "acacia_stairs": { + "id": 163, + "name": "Acacia Wood Stairs", + "place": "stairs", + }, + "dark_oak_stairs": { + "id": 164, + "name": "Dark Oak Wood Stairs", + "place": "stairs", + }, + "slime": { + "id": 165, + "name": "Slime Block", + }, + "barrier": { + "id": 166, + "name": "Barrier", + }, + "iron_trapdoor": { + "id": 167, + "name": "Iron Trapdoor", + "place": "trapdoor", + }, + "prismarine": { + "id": 168, + "name": ["Prismarine", + "Prismarine Bricks", + "Dark Prismarine"], + }, + "sea_lantern": { + "id": 169, + "name": "Sea Lantern", + }, + "hay_block": { + "id": 170, + "name": "Hay Block", + "place": "hay-bale", + }, + "carpet": { + "id": 171, + "name": ["Carpet", + "Orange Carpet", + "Magenta Carpet", + "Light Blue Carpet", + "Yellow Carpet", + "Lime Carpet", + "Pink Carpet", + "Gray Carpet", + "Light Gray Carpet", + "Cyan Carpet", + "Purple Carpet", + "Blue Carpet", + "Brown Carpet", + "Green Carpet", + "Red Carpet", + "Black Carpet"], + }, + "hardened_clay": { + "id": 172, + "name": "Hardened Clay", + }, + "coal_block": { + "id": 173, + "name": "Block of Coal", + }, + "packed_ice": { + "id": 174, + "name": "Packed Ice", + }, + "double_plant": { + "id": 175, + "name": ["Sunflower", + "Lilac", + "Double Tallgrass", + "Large Fern", + "Rose Bush", + "Peony"], + "place": "largeplant", + }, + "standing_banner": { + "id": 176, + "name": "Banner", + "place": "signpost", + }, + "wall_banner": { + "id": 177, + "name": "Banner", + "place": "wallsign-ladder", + }, + "daylight_detector_inverted": { + "id": 178, + "name": "Inverted Daylight Sensor", + "place": "flatten", + }, + "red_sandstone": { + "id": 179, + "name": ["Red Sandstone", + "Chiseled Red Sandstone", + "Smooth Red Sandstone"], + }, + "red_sandstone_stairs": { + "id": 180, + "name": "Red Sandstone Stairs", + "place": "stairs", + }, + "double_stone_slab2": { + "id": 181, + "name": "Double Red Sandstone Slab", + }, + "stone_slab2": { + "id": 182, + "name": "Red Sandstone Slab", + "place": "slab", + }, + "spruce_fence_gate": { + "id": 183, + "name": "Spruce Fence Gate", + "place": "gate", + }, + "birch_fence_gate": { + "id": 184, + "name": "Birch Fence Gate", + "place": "gate", + }, + "jungle_fence_gate": { + "id": 185, + "name": "Jungle Fence Gate", + "place": "gate", + }, + "dark_oak_fence_gate": { + "id": 186, + "name": "Dark Oak Fence Gate", + "place": "gate", + }, + "acacia_fence_gate": { + "id": 187, + "name": "Acacia Fence Gate", + "place": "gate", + }, + "spruce_fence": { + "id": 188, + "name": "Spruce Fence", + }, + "birch_fence": { + "id": 189, + "name": "Birch Fence", + }, + "jungle_fence": { + "id": 190, + "name": "Jungle Fence", + }, + "dark_oak_fence": { + "id": 191, + "name": "Dark Oak Fence", + }, + "acacia_fence": { + "id": 192, + "name": "Acacia Fence", + }, + "spruce_door": { + "id": 193, + "name": "Spruce Door", + "place": "door", + }, + "birch_door": { + "id": 194, + "name": "Birch Door", + "place": "door", + }, + "jungle_door": { + "id": 195, + "name": "Jungle Door", + "place": "door", + }, + "acacia_door": { + "id": 196, + "name": "Acacia Door", + "place": "door", + }, + "dark_oak_door": { + "id": 197, + "name": "Dark Oak Door", + "place": "door", + }, + "end_rod": { + "id": 198 + "name": "End Rod", + "place": "end_rod", + }, + "chorus_plant": { + "id": 199 + "name": "Chorus Plant", + }, + "chorus_flower": { + "id": 200 + "name": "Chorus Flower", + }, + "purpur_block": { + "id": 201 + "name": "Purpur Block", + }, + "purpur_pillar": { + "id": 202 + "name": "Purpur Pillar", + }, + "purpur_stairs": { + "id": 203 + "name": "Purpur Stairs", + }, + "purpur_double_slab": { + "id": 204 + "name": "Double Purpur Slabs", + }, + "purpur_slab": { + "name": "Purpur Slab", + "id": 205, + "place": "slab", + }, + "end_bricks": { + "name": "End Stone Bricks", + "id": 206 + }, + "beetroots": { + "name": "Beetroot", + "id": 207 + }, + "grass_path": { + "name": "Path", + "id": 208 + }, + "end_gateway": { + "id": 209, + "name": "End Gateway" + }, + "repeating_command_block": { + "name": "Repeating Command Block", + "id": 210, + "place": "flatten", + }, + "chain_command_block": { + "name": "Chain Command Block", + "id": 211 + }, + "frosted_ice": { + "name": "Frosted Ice", + "id": 212 + }, + "magma": { + "id": 213, + "name": "Magma Block", + }, + "nether_wart_block": { + "id": 214, + "name": "Nether Wart Block", + }, + "red_nether_brick": { + "id": 215, + "name": "Red Nether Brick", + }, + "bone_block": { + "id": 216, + "name": "Bone Block", + }, + "structure_void": { + "id": 217, + "name": "Structure Void", + }, + "observer": { + "name": "Observer", + "id": 218 + }, + "white_shulker_box": { + "name": "White Shulker Box", + "id": 219 + }, + "orange_shulker_box": { + "name": "Orange Shulker Box", + "id": 220 + }, + "magenta_shulker_box": { + "name": "Magenta Shulker Box", + "id": 221 + }, + "light_blue_shulker_box": { + "name": "Light Blue Shulker Box", + "id": 222 + }, + "yellow_shulker_box": { + "name": "Yellow Shulker Box", + "id": 223 + }, + "lime_shulker_box": { + "name": "Lime Shulker Box", + "id": 224 + }, + "pink_shulker_box": { + "name": "Pink Shulker Box", + "id": 225 + }, + "gray_shulker_box": { + "name": "Gray Shulker Box", + "id": 226 + }, + "silver_shulker_box": { + "name": "Light Gray Shulker Box", + "id": 227 + }, + "cyan_shulker_box": { + "name": "Cyan Shulker Box", + "id": 228 + }, + "purple_shulker_box": { + "name": "Purple Shulker Box", + "id": 229 + }, + "blue_shulker_box": { + "name": "Blue Shulker Box", + "id": 230 + }, + "brown_shulker_box": { + "name": "Brown Shulker Box", + "id": 231 + }, + "green_shulker_box": { + "name": "Green Shulker Box", + "id": 232 + }, + "red_shulker_box": { + "name": "Red Shulker Box", + "id": 233 + }, + "black_shulker_box": { + "name": "Black Shulker Box", + "id": 234 + }, + "white_glazed_terracotta": { + "id": 235, + "name": "White glazed terracotta", + }, + "orange_glazed_terracotta": { + "id": 236, + "name": "Orange glazed terracotta", + }, + "magenta_glazed_terracotta": { + "id": 237, + "name": "Magenta glazed terracotta", + }, + "light_blue_glazed_terracotta": { + "id": 238, + "name": "Light blue glazed terracotta", + }, + "yellow_glazed_terracotta": { + "id": 239, + "name": "Yellow glazed terracotta", + }, + "lime_glazed_terracotta": { + "id": 240, + "name": "Lime glazed terracotta", + }, + "pink_glazed_terracotta": { + "id": 241, + "name": "Pink glazed terracotta", + }, + "gray_glazed_terracotta": { + "id": 242, + "name": "Gray glazed terracotta", + }, + "light_gray_glazed_terracotta": { + "id": 243, + "name": "Light gray glazed terracotta", + }, + "cyan_glazed_terracotta": { + "id": 244, + "name": "Cyan glazed terracotta", + }, + "purple_glazed_terracotta": { + "id": 245, + "name": "Purple glazed terracotta", + }, + "blue_glazed_terracotta": { + "id": 246, + "name": "Blue glazed terracotta", + }, + "brown_glazed_terracotta": { + "id": 247, + "name": "Brown glazed terracotta", + }, + "green_glazed_terracotta": { + "id": 248, + "name": "Green glazed terracotta", + }, + "red_glazed_terracotta": { + "id": 249, + "name": "Red glazed terracotta", + }, + "black_glazed_terracotta": { + "id": 250, + "name": "Black glazed terracotta", + }, + "concrete": { + "id": 251, + "name": ["White concrete", + "Orange concrete", + "Magenta concrete", + "Light blue concrete", + "Yellow concrete", + "Lime concrete", + "Pink concrete", + "Gray concrete", + "Silver concrete", + "Cyan concrete", + "Purple concrete", + "Blue concrete", + "Brown concrete", + "Green concrete", + "Red concrete", + "Black concrete"], + }, + "concrete_powder": { + "id": 252, + "name": ["White concrete powder", + "Orange concrete powder", + "Magenta concrete powder", + "Light blue concrete powder", + "Yellow concrete powder", + "Lime concrete powder", + "Pink concrete powder", + "Gray concrete powder", + "Silver concrete powder", + "Cyan concrete powder", + "Purple concrete powder", + "Blue concrete powder", + "Brown concrete powder", + "Green concrete powder", + "Red concrete powder", + "Black concrete powder"], + }, + "structure_block": { + "name": ["Structure Block (Save)", + "Structure Block (Load)", + "Structure Block (Corner)", + "Structure Block (Data)"], + "id": 255 + }, + "string": { + "name": "String", + "id": 287 + }, + "coal": { + "name": ["Coal", + "Charcoal"], + "id": 263 + }, + "wheat_seeds": { + "id": 295, + "name": "Wheat Seeds", + }, + "sign": { + "id": 323, + "name": "Sign", + }, + "redstone": { + "id": 331, + "name": "Redstone Dust", + }, + "dye": { + "id": 351, + "name": ["Ink Sack", + "Rose Red", + "Cactus Green", + "Cocoa Bean", + "Lapis Lazuli", + "Purple Dye", + "Cyan Dye", + "Light Gray Dye", + "Gray Dye", + "Pink Dye", + "Lime Dye", + "Dandelion Yellow", + "Light Blue Dye", + "Magenta Dye", + "Orange Dye", + "Bone Meal"] + }, + "bed-block": { + "id": 355, + "name": "Bed", + "place": "bed", + }, + "repeater": { + "id": 356, + "name": "Redstone Repeater", + "place": "repeater", + }, + "carrot": { + "id": 391, + "name": "Carrot", + }, + "potato": { + "id": 392, + "name": "Potato", + }, + "comparator": { + "id": 404, + "name": "Redstone Comparator", + "place": "comparator", + }, + "banner": { + "id": 425, + "name": "Banner", + } +} diff --git a/etc/recipes.db b/etc/recipes.db new file mode 100644 index 0000000..61611db --- /dev/null +++ b/etc/recipes.db @@ -0,0 +1,2108 @@ +{ + [ "minecraft:wool:4" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:11", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:stained_glass:9" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:6", + }, + }, + [ "minecraft:trapdoor:0" ] = { + count = 2, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + [ 7 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:dark_oak_fence:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:5", + "minecraft:stick:0", + "minecraft:planks:5", + [ 6 ] = "minecraft:stick:0", + [ 7 ] = "minecraft:planks:5", + [ 5 ] = "minecraft:planks:5", + }, + }, + [ "minecraft:dirt:1" ] = { + count = 4, + ingredients = { + "minecraft:dirt:0", + "minecraft:gravel:0", + [ 5 ] = "minecraft:gravel:0", + [ 6 ] = "minecraft:dirt:0", + }, + }, + [ "minecraft:carpet:4" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:4", + [ 6 ] = "minecraft:wool:4", + }, + }, + [ "minecraft:stained_glass:1" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:14", + }, + }, + [ "minecraft:wooden_pressure_plate:0" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:stained_hardened_clay:1" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:14", + }, + }, + [ "minecraft:stained_hardened_clay:13" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:2", + }, + }, + [ "minecraft:stained_glass_pane:11" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:11", + "minecraft:stained_glass:11", + "minecraft:stained_glass:11", + [ 6 ] = "minecraft:stained_glass:11", + [ 7 ] = "minecraft:stained_glass:11", + [ 5 ] = "minecraft:stained_glass:11", + }, + }, + [ "minecraft:dye:10" ] = { + count = 2, + ingredients = { + "minecraft:dye:15", + "minecraft:dye:2", + }, + }, + [ "minecraft:quartz_block:1" ] = { + count = 1, + ingredients = { + [ 2 ] = "minecraft:stone_slab:7", + [ 6 ] = "minecraft:stone_slab:7", + }, + }, + [ "minecraft:carpet:12" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:12", + [ 6 ] = "minecraft:wool:12", + }, + }, + [ "minecraft:birch_fence_gate:0" ] = { + count = 1, + ingredients = { + "minecraft:stick:0", + "minecraft:planks:2", + "minecraft:stick:0", + [ 6 ] = "minecraft:planks:2", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:acacia_fence:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:4", + "minecraft:stick:0", + "minecraft:planks:4", + [ 6 ] = "minecraft:stick:0", + [ 7 ] = "minecraft:planks:4", + [ 5 ] = "minecraft:planks:4", + }, + }, + [ "minecraft:diamond_block:0" ] = { + count = 1, + ingredients = { + "minecraft:diamond:0", + "minecraft:diamond:0", + "minecraft:diamond:0", + [ 7 ] = "minecraft:diamond:0", + [ 9 ] = "minecraft:diamond:0", + [ 10 ] = "minecraft:diamond:0", + [ 11 ] = "minecraft:diamond:0", + [ 5 ] = "minecraft:diamond:0", + [ 6 ] = "minecraft:diamond:0", + }, + }, + [ "minecraft:stone:1" ] = { + count = 1, + ingredients = { + "minecraft:stone:3", + "minecraft:quartz:0", + }, + }, + [ "minecraft:wooden_slab:2" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:planks:2", + [ 7 ] = "minecraft:planks:2", + [ 5 ] = "minecraft:planks:2", + }, + }, + [ "minecraft:stained_hardened_clay:14" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:1", + }, + }, + [ "minecraft:stained_glass_pane:6" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:6", + "minecraft:stained_glass:6", + "minecraft:stained_glass:6", + [ 6 ] = "minecraft:stained_glass:6", + [ 7 ] = "minecraft:stained_glass:6", + [ 5 ] = "minecraft:stained_glass:6", + }, + }, + [ "minecraft:carpet:9" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:9", + [ 6 ] = "minecraft:wool:9", + }, + }, + [ "minecraft:carpet:0" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:quartz_block:0" ] = { + count = 1, + ingredients = { + "minecraft:quartz:0", + "minecraft:quartz:0", + [ 5 ] = "minecraft:quartz:0", + [ 6 ] = "minecraft:quartz:0", + }, + }, + [ "minecraft:pumpkin_seeds:0" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:pumpkin:0", + }, + }, + [ "minecraft:stone_slab:4" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:brick_block:0", + [ 7 ] = "minecraft:brick_block:0", + [ 5 ] = "minecraft:brick_block:0", + }, + }, + [ "minecraft:carpet:10" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:10", + [ 6 ] = "minecraft:wool:10", + }, + }, + [ "minecraft:carpet:8" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:8", + [ 6 ] = "minecraft:wool:8", + }, + }, + [ "minecraft:stained_glass_pane:1" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:1", + "minecraft:stained_glass:1", + "minecraft:stained_glass:1", + [ 6 ] = "minecraft:stained_glass:1", + [ 7 ] = "minecraft:stained_glass:1", + [ 5 ] = "minecraft:stained_glass:1", + }, + }, + [ "minecraft:cobblestone_wall:0" ] = { + count = 6, + ingredients = { + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + [ 6 ] = "minecraft:cobblestone:0", + [ 7 ] = "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + }, + }, + [ "minecraft:wooden_button:0" ] = { + count = 1, + ingredients = { + [ 6 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:book:0" ] = { + count = 1, + ingredients = { + "minecraft:paper:0", + "minecraft:paper:0", + "minecraft:paper:0", + [ 5 ] = "minecraft:leather:0", + }, + }, + [ "minecraft:stone_pressure_plate:0" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:stone:0", + [ 6 ] = "minecraft:stone:0", + }, + }, + [ "minecraft:planks:4" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:log2:0", + }, + }, + [ "minecraft:stone_slab:7" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:quartz_block:0", + [ 7 ] = "minecraft:quartz_block:0", + [ 5 ] = "minecraft:quartz_block:0", + }, + }, + [ "minecraft:sandstone:1" ] = { + count = 1, + ingredients = { + [ 2 ] = "minecraft:stone_slab:1", + [ 6 ] = "minecraft:stone_slab:1", + }, + }, + [ "minecraft:jungle_fence_gate:0" ] = { + count = 1, + ingredients = { + "minecraft:stick:0", + "minecraft:planks:3", + "minecraft:stick:0", + [ 6 ] = "minecraft:planks:3", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:furnace:0" ] = { + count = 1, + ingredients = { + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + [ 7 ] = "minecraft:cobblestone:0", + [ 9 ] = "minecraft:cobblestone:0", + [ 10 ] = "minecraft:cobblestone:0", + [ 11 ] = "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + }, + }, + [ "minecraft:dye:5" ] = { + count = 2, + ingredients = { + [ 5 ] = "minecraft:dye:1", + [ 6 ] = "minecraft:dye:4", + }, + }, + [ "minecraft:stone_brick_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:stonebrick:0", + [ 9 ] = "minecraft:stonebrick:0", + [ 10 ] = "minecraft:stonebrick:0", + [ 11 ] = "minecraft:stonebrick:0", + [ 5 ] = "minecraft:stonebrick:0", + [ 6 ] = "minecraft:stonebrick:0", + }, + }, + [ "minecraft:nether_brick_fence:0" ] = { + count = 6, + ingredients = { + "minecraft:nether_brick:0", + "minecraft:nether_brick:0", + "minecraft:nether_brick:0", + [ 6 ] = "minecraft:nether_brick:0", + [ 7 ] = "minecraft:nether_brick:0", + [ 5 ] = "minecraft:nether_brick:0", + }, + }, + [ "minecraft:wool:11" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:4", + }, + }, + [ "minecraft:sign:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + [ 7 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + [ 10 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:light_weighted_pressure_plate:0" ] = { + count = 1, + ingredients = { + "minecraft:gold_ingot:0", + "minecraft:gold_ingot:0", + }, + }, + [ "minecraft:flower_pot:0" ] = { + count = 1, + ingredients = { + "minecraft:brick:0", + [ 6 ] = "minecraft:brick:0", + [ 3 ] = "minecraft:brick:0", + }, + }, + [ "minecraft:wooden_door:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + [ 10 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + [ 9 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:wool:0" ] = { + count = 1, + ingredients = { + "minecraft:string:0", + "minecraft:string:0", + [ 5 ] = "minecraft:string:0", + [ 6 ] = "minecraft:string:0", + }, + }, + [ "minecraft:carpet:2" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:2", + [ 6 ] = "minecraft:wool:2", + }, + }, + [ "minecraft:comparator:0" ] = { + count = 1, + ingredients = { + [ 7 ] = "minecraft:redstone_torch:0", + [ 2 ] = "minecraft:redstone_torch:0", + [ 10 ] = "minecraft:stone:0", + [ 11 ] = "minecraft:stone:0", + [ 5 ] = "minecraft:redstone_torch:0", + [ 6 ] = "minecraft:quartz:0", + [ 9 ] = "minecraft:stone:0", + }, + }, + [ "minecraft:crafting_table:0" ] = { + count = 1, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:stained_glass_pane:3" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:3", + "minecraft:stained_glass:3", + "minecraft:stained_glass:3", + [ 6 ] = "minecraft:stained_glass:3", + [ 7 ] = "minecraft:stained_glass:3", + [ 5 ] = "minecraft:stained_glass:3", + }, + }, + [ "minecraft:stick:0" ] = { + count = 4, + ingredients = { + [ 2 ] = "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:stained_glass_pane:0" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:0", + "minecraft:stained_glass:0", + "minecraft:stained_glass:0", + [ 6 ] = "minecraft:stained_glass:0", + [ 7 ] = "minecraft:stained_glass:0", + [ 5 ] = "minecraft:stained_glass:0", + }, + }, + [ "minecraft:wool:6" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:9", + }, + }, + [ "minecraft:stonebrick:3" ] = { + count = 1, + ingredients = { + [ 2 ] = "minecraft:stone_slab:5", + [ 6 ] = "minecraft:stone_slab:5", + }, + }, + [ "minecraft:emerald_block:0" ] = { + count = 1, + ingredients = { + "minecraft:emerald:0", + "minecraft:emerald:0", + "minecraft:emerald:0", + [ 7 ] = "minecraft:emerald:0", + [ 9 ] = "minecraft:emerald:0", + [ 10 ] = "minecraft:emerald:0", + [ 11 ] = "minecraft:emerald:0", + [ 5 ] = "minecraft:emerald:0", + [ 6 ] = "minecraft:emerald:0", + }, + }, + [ "minecraft:wool:13" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:2", + }, + }, + [ "minecraft:noteblock:0" ] = { + count = 1, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + "minecraft:planks:0", + [ 7 ] = "minecraft:planks:0", + [ 9 ] = "minecraft:planks:0", + [ 10 ] = "minecraft:planks:0", + [ 11 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + [ 6 ] = "minecraft:redstone:0", + }, + }, + [ "minecraft:spruce_door:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:1", + "minecraft:planks:1", + [ 9 ] = "minecraft:planks:1", + [ 10 ] = "minecraft:planks:1", + [ 5 ] = "minecraft:planks:1", + [ 6 ] = "minecraft:planks:1", + }, + }, + [ "minecraft:dye:9" ] = { + count = 2, + ingredients = { + [ 5 ] = "minecraft:dye:1", + [ 6 ] = "minecraft:dye:15", + }, + }, + [ "minecraft:stained_glass:15" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:0", + }, + }, + [ "minecraft:stained_hardened_clay:5" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:10", + }, + }, + [ "minecraft:fence:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:0", + "minecraft:stick:0", + "minecraft:planks:0", + [ 6 ] = "minecraft:stick:0", + [ 7 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:dispenser:0" ] = { + count = 1, + ingredients = { + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + [ 7 ] = "minecraft:cobblestone:0", + [ 9 ] = "minecraft:cobblestone:0", + [ 10 ] = "minecraft:redstone:0", + [ 11 ] = "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + [ 6 ] = "minecraft:bow:0", + }, + }, + [ "minecraft:beacon:0" ] = { + count = 1, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:obsidian:0", + [ 10 ] = "minecraft:obsidian:0", + [ 11 ] = "minecraft:obsidian:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:nether_star:0", + }, + }, + [ "minecraft:redstone_torch:0" ] = { + count = 1, + ingredients = { + [ 2 ] = "minecraft:redstone:0", + [ 6 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:snow_layer:0" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:snow:0", + [ 7 ] = "minecraft:snow:0", + [ 5 ] = "minecraft:snow:0", + }, + }, + [ "minecraft:stained_glass:2" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:13", + }, + }, + [ "minecraft:mossy_cobblestone:0" ] = { + count = 1, + ingredients = { + "minecraft:cobblestone:0", + "minecraft:vine:0", + }, + }, + [ "minecraft:stained_glass_pane:14" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:14", + "minecraft:stained_glass:14", + "minecraft:stained_glass:14", + [ 6 ] = "minecraft:stained_glass:14", + [ 7 ] = "minecraft:stained_glass:14", + [ 5 ] = "minecraft:stained_glass:14", + }, + }, + [ "minecraft:glowstone:0" ] = { + count = 1, + ingredients = { + "minecraft:glowstone_dust:0", + "minecraft:glowstone_dust:0", + [ 5 ] = "minecraft:glowstone_dust:0", + [ 6 ] = "minecraft:glowstone_dust:0", + }, + }, + [ "minecraft:bone_block:0" ] = { + count = 1, + ingredients = { + "minecraft:dye:15", + "minecraft:dye:15", + "minecraft:dye:15", + [ 7 ] = "minecraft:dye:15", + [ 9 ] = "minecraft:dye:15", + [ 10 ] = "minecraft:dye:15", + [ 11 ] = "minecraft:dye:15", + [ 5 ] = "minecraft:dye:15", + [ 6 ] = "minecraft:dye:15", + }, + }, + [ "minecraft:stained_hardened_clay:3" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:12", + }, + }, + [ "minecraft:spruce_fence:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:1", + "minecraft:stick:0", + "minecraft:planks:1", + [ 6 ] = "minecraft:stick:0", + [ 7 ] = "minecraft:planks:1", + [ 5 ] = "minecraft:planks:1", + }, + }, + [ "minecraft:paper:0" ] = { + count = 3, + ingredients = { + "minecraft:reeds:0", + "minecraft:reeds:0", + "minecraft:reeds:0", + }, + }, + [ "minecraft:stained_glass:3" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:12", + }, + }, + [ "minecraft:redstone_lamp:0" ] = { + count = 1, + ingredients = { + [ 7 ] = "minecraft:redstone:0", + [ 2 ] = "minecraft:redstone:0", + [ 10 ] = "minecraft:redstone:0", + [ 5 ] = "minecraft:redstone:0", + [ 6 ] = "minecraft:glowstone:0", + }, + }, + [ "minecraft:heavy_weighted_pressure_plate:0" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:wool:15" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:0", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:stained_glass:10" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:5", + }, + }, + [ "minecraft:tnt:0" ] = { + count = 1, + ingredients = { + "minecraft:gunpowder:0", + "minecraft:sand:0", + "minecraft:gunpowder:0", + [ 7 ] = "minecraft:sand:0", + [ 9 ] = "minecraft:gunpowder:0", + [ 10 ] = "minecraft:sand:0", + [ 11 ] = "minecraft:gunpowder:0", + [ 5 ] = "minecraft:sand:0", + [ 6 ] = "minecraft:gunpowder:0", + }, + }, + [ "minecraft:dye:7" ] = { + count = 3, + ingredients = { + "minecraft:dye:15", + "minecraft:dye:15", + [ 5 ] = "minecraft:dye:0", + }, + }, + [ "minecraft:stained_glass:14" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:1", + }, + }, + [ "minecraft:banner:0" ] = { + count = 1, + ingredients = { + "minecraft:wool:15", + "minecraft:wool:15", + "minecraft:wool:15", + [ 6 ] = "minecraft:wool:15", + [ 7 ] = "minecraft:wool:15", + [ 5 ] = "minecraft:wool:15", + [ 10 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:wooden_slab:5" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:planks:5", + [ 7 ] = "minecraft:planks:5", + [ 5 ] = "minecraft:planks:5", + }, + }, + [ "minecraft:anvil:0" ] = { + count = 1, + ingredients = { + "minecraft:iron_block:0", + "minecraft:iron_block:0", + "minecraft:iron_block:0", + [ 9 ] = "minecraft:iron_ingot:0", + [ 10 ] = "minecraft:iron_ingot:0", + [ 11 ] = "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:stained_glass_pane:4" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:4", + "minecraft:stained_glass:4", + "minecraft:stained_glass:4", + [ 6 ] = "minecraft:stained_glass:4", + [ 7 ] = "minecraft:stained_glass:4", + [ 5 ] = "minecraft:stained_glass:4", + }, + }, + [ "minecraft:stained_hardened_clay:4" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:11", + }, + }, + [ "minecraft:wooden_slab:1" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:planks:1", + [ 7 ] = "minecraft:planks:1", + [ 5 ] = "minecraft:planks:1", + }, + }, + [ "minecraft:bookshelf:0" ] = { + count = 1, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + "minecraft:planks:0", + [ 7 ] = "minecraft:book:0", + [ 9 ] = "minecraft:planks:0", + [ 10 ] = "minecraft:planks:0", + [ 11 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:book:0", + [ 6 ] = "minecraft:book:0", + }, + }, + [ "minecraft:stonebrick:0" ] = { + count = 4, + ingredients = { + "minecraft:stone:0", + "minecraft:stone:0", + [ 5 ] = "minecraft:stone:0", + [ 6 ] = "minecraft:stone:0", + }, + }, + [ "minecraft:ladder:0" ] = { + count = 3, + ingredients = { + "minecraft:stick:0", + [ 7 ] = "minecraft:stick:0", + [ 9 ] = "minecraft:stick:0", + [ 3 ] = "minecraft:stick:0", + [ 11 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + [ 6 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:trapped_chest:0" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:chest:0", + [ 6 ] = "minecraft:tripwire_hook:0", + }, + }, + [ "minecraft:wool:3" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:12", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:dropper:0" ] = { + count = 1, + ingredients = { + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + "minecraft:cobblestone:0", + [ 7 ] = "minecraft:cobblestone:0", + [ 9 ] = "minecraft:cobblestone:0", + [ 10 ] = "minecraft:redstone:0", + [ 11 ] = "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + }, + }, + [ "minecraft:stained_hardened_clay:7" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:8", + }, + }, + [ "minecraft:stained_hardened_clay:9" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:6", + }, + }, + [ "minecraft:stone:3" ] = { + count = 2, + ingredients = { + "minecraft:quartz:0", + "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + [ 6 ] = "minecraft:quartz:0", + }, + }, + [ "minecraft:wooden_slab:0" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:planks:0", + [ 7 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:prismarine:1" ] = { + count = 1, + ingredients = { + "minecraft:prismarine_shard:0", + "minecraft:prismarine_shard:0", + "minecraft:prismarine_shard:0", + [ 7 ] = "minecraft:prismarine_shard:0", + [ 9 ] = "minecraft:prismarine_shard:0", + [ 10 ] = "minecraft:prismarine_shard:0", + [ 11 ] = "minecraft:prismarine_shard:0", + [ 5 ] = "minecraft:prismarine_shard:0", + [ 6 ] = "minecraft:prismarine_shard:0", + }, + }, + [ "minecraft:acacia_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:planks:4", + [ 9 ] = "minecraft:planks:4", + [ 10 ] = "minecraft:planks:4", + [ 11 ] = "minecraft:planks:4", + [ 5 ] = "minecraft:planks:4", + [ 6 ] = "minecraft:planks:4", + }, + }, + [ "minecraft:iron_bars:0" ] = { + count = 16, + ingredients = { + "minecraft:iron_ingot:0", + "minecraft:iron_ingot:0", + "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:iron_ingot:0", + [ 7 ] = "minecraft:iron_ingot:0", + [ 5 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:birch_fence:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:2", + "minecraft:stick:0", + "minecraft:planks:2", + [ 6 ] = "minecraft:stick:0", + [ 7 ] = "minecraft:planks:2", + [ 5 ] = "minecraft:planks:2", + }, + }, + [ "minecraft:dark_oak_door:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:5", + "minecraft:planks:5", + [ 6 ] = "minecraft:planks:5", + [ 10 ] = "minecraft:planks:5", + [ 5 ] = "minecraft:planks:5", + [ 9 ] = "minecraft:planks:5", + }, + }, + [ "minecraft:stone:4" ] = { + count = 4, + ingredients = { + "minecraft:stone:3", + "minecraft:stone:3", + [ 5 ] = "minecraft:stone:3", + [ 6 ] = "minecraft:stone:3", + }, + }, + [ "minecraft:chest:0" ] = { + count = 1, + ingredients = { + "minecraft:planks:0", + "minecraft:planks:0", + "minecraft:planks:0", + [ 7 ] = "minecraft:planks:0", + [ 9 ] = "minecraft:planks:0", + [ 10 ] = "minecraft:planks:0", + [ 11 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:bow:0" ] = { + count = 1, + ingredients = { + "minecraft:string:0", + "minecraft:stick:0", + [ 9 ] = "minecraft:string:0", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:string:0", + [ 10 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:stained_glass_pane:15" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:15", + "minecraft:stained_glass:15", + "minecraft:stained_glass:15", + [ 6 ] = "minecraft:stained_glass:15", + [ 7 ] = "minecraft:stained_glass:15", + [ 5 ] = "minecraft:stained_glass:15", + }, + }, + [ "minecraft:red_sandstone:2" ] = { + count = 4, + ingredients = { + "minecraft:red_sandstone:0", + "minecraft:red_sandstone:0", + [ 5 ] = "minecraft:red_sandstone:0", + [ 6 ] = "minecraft:red_sandstone:0", + }, + }, + [ "minecraft:detector_rail:0" ] = { + count = 6, + ingredients = { + "minecraft:iron_ingot:0", + [ 7 ] = "minecraft:iron_ingot:0", + [ 9 ] = "minecraft:iron_ingot:0", + [ 10 ] = "minecraft:redstone:0", + [ 11 ] = "minecraft:iron_ingot:0", + [ 5 ] = "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:stone_pressure_plate:0", + [ 3 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:birch_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:planks:2", + [ 9 ] = "minecraft:planks:2", + [ 10 ] = "minecraft:planks:2", + [ 11 ] = "minecraft:planks:2", + [ 5 ] = "minecraft:planks:2", + [ 6 ] = "minecraft:planks:2", + }, + }, + [ "minecraft:redstone_block:0" ] = { + count = 1, + ingredients = { + "minecraft:redstone:0", + "minecraft:redstone:0", + "minecraft:redstone:0", + [ 7 ] = "minecraft:redstone:0", + [ 9 ] = "minecraft:redstone:0", + [ 10 ] = "minecraft:redstone:0", + [ 11 ] = "minecraft:redstone:0", + [ 5 ] = "minecraft:redstone:0", + [ 6 ] = "minecraft:redstone:0", + }, + }, + [ "minecraft:wool:8" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:7", + }, + }, + [ "minecraft:coal_block:0" ] = { + count = 1, + ingredients = { + "minecraft:coal:0", + "minecraft:coal:0", + "minecraft:coal:0", + [ 7 ] = "minecraft:coal:0", + [ 9 ] = "minecraft:coal:0", + [ 10 ] = "minecraft:coal:0", + [ 11 ] = "minecraft:coal:0", + [ 5 ] = "minecraft:coal:0", + [ 6 ] = "minecraft:coal:0", + }, + }, + [ "minecraft:stained_hardened_clay:15" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:0", + }, + }, + [ "minecraft:dye:14" ] = { + count = 2, + ingredients = { + [ 5 ] = "minecraft:dye:1", + [ 6 ] = "minecraft:dye:11", + }, + }, + [ "minecraft:dye:8" ] = { + count = 2, + ingredients = { + [ 5 ] = "minecraft:dye:15", + [ 6 ] = "minecraft:dye:0", + }, + }, + [ "minecraft:ender_eye:0" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:blaze_powder:0", + [ 6 ] = "minecraft:ender_pearl:0", + }, + }, + [ "minecraft:end_bricks:0" ] = { + count = 4, + ingredients = { + "minecraft:end_stone:0", + "minecraft:end_stone:0", + [ 5 ] = "minecraft:end_stone:0", + [ 6 ] = "minecraft:end_stone:0", + }, + }, + [ "minecraft:carpet:5" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:5", + [ 6 ] = "minecraft:wool:5", + }, + }, + [ "minecraft:jungle_fence:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:3", + "minecraft:stick:0", + "minecraft:planks:3", + [ 6 ] = "minecraft:stick:0", + [ 7 ] = "minecraft:planks:3", + [ 5 ] = "minecraft:planks:3", + }, + }, + [ "minecraft:stone_slab:1" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:sandstone:0", + [ 7 ] = "minecraft:sandstone:0", + [ 5 ] = "minecraft:sandstone:0", + }, + }, + [ "minecraft:stained_glass_pane:9" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:9", + "minecraft:stained_glass:9", + "minecraft:stained_glass:9", + [ 6 ] = "minecraft:stained_glass:9", + [ 7 ] = "minecraft:stained_glass:9", + [ 5 ] = "minecraft:stained_glass:9", + }, + }, + [ "minecraft:stained_glass_pane:7" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:7", + "minecraft:stained_glass:7", + "minecraft:stained_glass:7", + [ 6 ] = "minecraft:stained_glass:7", + [ 7 ] = "minecraft:stained_glass:7", + [ 5 ] = "minecraft:stained_glass:7", + }, + }, + [ "minecraft:spruce_fence_gate:0" ] = { + count = 1, + ingredients = { + "minecraft:stick:0", + "minecraft:planks:1", + "minecraft:stick:0", + [ 6 ] = "minecraft:planks:1", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:stained_glass:7" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:8", + }, + }, + [ "minecraft:stained_glass_pane:10" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:10", + "minecraft:stained_glass:10", + "minecraft:stained_glass:10", + [ 6 ] = "minecraft:stained_glass:10", + [ 7 ] = "minecraft:stained_glass:10", + [ 5 ] = "minecraft:stained_glass:10", + }, + }, + [ "minecraft:wool:10" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:5", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:dye:6" ] = { + count = 2, + ingredients = { + [ 5 ] = "minecraft:dye:2", + [ 6 ] = "minecraft:dye:4", + }, + }, + [ "minecraft:planks:5" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:log2:1", + }, + }, + [ "minecraft:stone_button:0" ] = { + count = 1, + ingredients = { + [ 6 ] = "minecraft:stone:0", + }, + }, + [ "minecraft:sugar:0" ] = { + count = 1, + ingredients = { + [ 6 ] = "minecraft:reeds:0", + }, + }, + [ "minecraft:stone_slab:5" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:stonebrick:0", + [ 7 ] = "minecraft:stonebrick:0", + [ 5 ] = "minecraft:stonebrick:0", + }, + }, + [ "minecraft:stone_slab:3" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:cobblestone:0", + [ 7 ] = "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + }, + }, + [ "minecraft:stone_slab:6" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:nether_brick:0", + [ 7 ] = "minecraft:nether_brick:0", + [ 5 ] = "minecraft:nether_brick:0", + }, + }, + [ "minecraft:stone:6" ] = { + count = 4, + ingredients = { + "minecraft:stone:5", + "minecraft:stone:5", + [ 5 ] = "minecraft:stone:5", + [ 6 ] = "minecraft:stone:5", + }, + }, + [ "minecraft:dye:13" ] = { + count = 3, + ingredients = { + "minecraft:dye:4", + "minecraft:dye:1", + [ 5 ] = "minecraft:dye:9", + }, + }, + [ "minecraft:stained_glass:5" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:10", + }, + }, + [ "minecraft:carpet:6" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:6", + [ 6 ] = "minecraft:wool:6", + }, + }, + [ "minecraft:stained_glass:6" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:9", + }, + }, + [ "minecraft:stained_glass:13" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:2", + }, + }, + [ "minecraft:sandstone:0" ] = { + count = 1, + ingredients = { + "minecraft:sand:0", + "minecraft:sand:0", + [ 5 ] = "minecraft:sand:0", + [ 6 ] = "minecraft:sand:0", + }, + }, + [ "minecraft:gold_block:0" ] = { + count = 1, + ingredients = { + "minecraft:gold_ingot:0", + "minecraft:gold_ingot:0", + "minecraft:gold_ingot:0", + [ 7 ] = "minecraft:gold_ingot:0", + [ 9 ] = "minecraft:gold_ingot:0", + [ 10 ] = "minecraft:gold_ingot:0", + [ 11 ] = "minecraft:gold_ingot:0", + [ 5 ] = "minecraft:gold_ingot:0", + [ 6 ] = "minecraft:gold_ingot:0", + }, + }, + [ "minecraft:stained_glass:0" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:15", + }, + }, + [ "minecraft:stained_glass_pane:8" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:8", + "minecraft:stained_glass:8", + "minecraft:stained_glass:8", + [ 6 ] = "minecraft:stained_glass:8", + [ 7 ] = "minecraft:stained_glass:8", + [ 5 ] = "minecraft:stained_glass:8", + }, + }, + [ "minecraft:wool:9" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:6", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:carpet:11" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:11", + [ 6 ] = "minecraft:wool:11", + }, + }, + [ "minecraft:stained_glass:11" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:4", + }, + }, + [ "minecraft:carpet:7" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:7", + [ 6 ] = "minecraft:wool:7", + }, + }, + [ "minecraft:wool:2" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:13", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:end_rod:0" ] = { + count = 4, + ingredients = { + [ 2 ] = "minecraft:blaze_rod:0", + [ 6 ] = "minecraft:chorus_fruit_popped:0", + }, + }, + [ "minecraft:sandstone_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:sandstone:0", + [ 9 ] = "minecraft:sandstone:0", + [ 10 ] = "minecraft:sandstone:0", + [ 11 ] = "minecraft:sandstone:0", + [ 5 ] = "minecraft:sandstone:0", + [ 6 ] = "minecraft:sandstone:0", + }, + }, + [ "minecraft:stained_hardened_clay:11" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:4", + }, + }, + [ "minecraft:stone_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:cobblestone:0", + [ 9 ] = "minecraft:cobblestone:0", + [ 10 ] = "minecraft:cobblestone:0", + [ 11 ] = "minecraft:cobblestone:0", + [ 5 ] = "minecraft:cobblestone:0", + [ 6 ] = "minecraft:cobblestone:0", + }, + }, + [ "minecraft:stained_glass_pane:2" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:2", + "minecraft:stained_glass:2", + "minecraft:stained_glass:2", + [ 6 ] = "minecraft:stained_glass:2", + [ 7 ] = "minecraft:stained_glass:2", + [ 5 ] = "minecraft:stained_glass:2", + }, + }, + [ "minecraft:hopper:0" ] = { + count = 1, + ingredients = { + "minecraft:iron_ingot:0", + [ 7 ] = "minecraft:iron_ingot:0", + [ 3 ] = "minecraft:iron_ingot:0", + [ 5 ] = "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:chest:0", + [ 10 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:nether_brick_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:nether_brick:0", + [ 9 ] = "minecraft:nether_brick:0", + [ 10 ] = "minecraft:nether_brick:0", + [ 11 ] = "minecraft:nether_brick:0", + [ 5 ] = "minecraft:nether_brick:0", + [ 6 ] = "minecraft:nether_brick:0", + }, + }, + [ "minecraft:stained_glass_pane:13" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:13", + "minecraft:stained_glass:13", + "minecraft:stained_glass:13", + [ 6 ] = "minecraft:stained_glass:13", + [ 7 ] = "minecraft:stained_glass:13", + [ 5 ] = "minecraft:stained_glass:13", + }, + }, + [ "minecraft:wool:12" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:3", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:birch_door:0" ] = { + count = 3, + ingredients = { + "minecraft:planks:2", + "minecraft:planks:2", + [ 9 ] = "minecraft:planks:2", + [ 10 ] = "minecraft:planks:2", + [ 5 ] = "minecraft:planks:2", + [ 6 ] = "minecraft:planks:2", + }, + }, + [ "minecraft:ender_chest:0" ] = { + count = 1, + ingredients = { + "minecraft:obsidian:0", + "minecraft:obsidian:0", + "minecraft:obsidian:0", + [ 7 ] = "minecraft:obsidian:0", + [ 9 ] = "minecraft:obsidian:0", + [ 10 ] = "minecraft:obsidian:0", + [ 11 ] = "minecraft:obsidian:0", + [ 5 ] = "minecraft:obsidian:0", + [ 6 ] = "minecraft:ender_eye:0", + }, + }, + [ "minecraft:brick_block:0" ] = { + count = 1, + ingredients = { + "minecraft:brick:0", + "minecraft:brick:0", + [ 5 ] = "minecraft:brick:0", + [ 6 ] = "minecraft:brick:0", + }, + }, + [ "minecraft:prismarine:0" ] = { + count = 1, + ingredients = { + "minecraft:prismarine_shard:0", + "minecraft:prismarine_shard:0", + [ 5 ] = "minecraft:prismarine_shard:0", + [ 6 ] = "minecraft:prismarine_shard:0", + }, + }, + [ "minecraft:stone:5" ] = { + count = 2, + ingredients = { + "minecraft:stone:3", + "minecraft:cobblestone:0", + }, + }, + [ "minecraft:torch:0" ] = { + count = 4, + ingredients = { + [ 2 ] = "minecraft:coal:1", + [ 6 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:iron_block:0" ] = { + count = 1, + ingredients = { + "minecraft:iron_ingot:0", + "minecraft:iron_ingot:0", + "minecraft:iron_ingot:0", + [ 7 ] = "minecraft:iron_ingot:0", + [ 9 ] = "minecraft:iron_ingot:0", + [ 10 ] = "minecraft:iron_ingot:0", + [ 11 ] = "minecraft:iron_ingot:0", + [ 5 ] = "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:stained_hardened_clay:12" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:3", + }, + }, + [ "minecraft:stained_glass:8" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:7", + }, + }, + [ "minecraft:stained_hardened_clay:0" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:15", + }, + }, + [ "minecraft:wooden_slab:4" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:planks:4", + [ 7 ] = "minecraft:planks:4", + [ 5 ] = "minecraft:planks:4", + }, + }, + [ "minecraft:carpet:3" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:3", + [ 6 ] = "minecraft:wool:3", + }, + }, + [ "minecraft:carpet:14" ] = { + count = 3, + ingredients = { + "minecraft:wool:14", + "minecraft:wool:14", + }, + }, + [ "minecraft:stained_glass_pane:12" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:12", + "minecraft:stained_glass:12", + "minecraft:stained_glass:12", + [ 6 ] = "minecraft:stained_glass:12", + [ 7 ] = "minecraft:stained_glass:12", + [ 5 ] = "minecraft:stained_glass:12", + }, + }, + [ "minecraft:planks:1" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:log:1", + }, + }, + [ "minecraft:spruce_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:planks:1", + [ 9 ] = "minecraft:planks:1", + [ 10 ] = "minecraft:planks:1", + [ 11 ] = "minecraft:planks:1", + [ 5 ] = "minecraft:planks:1", + [ 6 ] = "minecraft:planks:1", + }, + }, + [ "minecraft:quartz_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:quartz_block:0", + [ 9 ] = "minecraft:quartz_block:0", + [ 10 ] = "minecraft:quartz_block:0", + [ 11 ] = "minecraft:quartz_block:0", + [ 5 ] = "minecraft:quartz_block:0", + [ 6 ] = "minecraft:quartz_block:0", + }, + }, + [ "minecraft:cauldron:0" ] = { + count = 1, + ingredients = { + "minecraft:iron_ingot:0", + [ 7 ] = "minecraft:iron_ingot:0", + [ 9 ] = "minecraft:iron_ingot:0", + [ 10 ] = "minecraft:iron_ingot:0", + [ 11 ] = "minecraft:iron_ingot:0", + [ 5 ] = "minecraft:iron_ingot:0", + [ 3 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:fence_gate:0" ] = { + count = 1, + ingredients = { + "minecraft:stick:0", + "minecraft:planks:0", + "minecraft:stick:0", + [ 6 ] = "minecraft:planks:0", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:hay_block:0" ] = { + count = 1, + ingredients = { + "minecraft:wheat:0", + "minecraft:wheat:0", + "minecraft:wheat:0", + [ 7 ] = "minecraft:wheat:0", + [ 9 ] = "minecraft:wheat:0", + [ 10 ] = "minecraft:wheat:0", + [ 11 ] = "minecraft:wheat:0", + [ 5 ] = "minecraft:wheat:0", + [ 6 ] = "minecraft:wheat:0", + }, + }, + [ "minecraft:red_sandstone:0" ] = { + count = 1, + ingredients = { + "minecraft:sand:1", + "minecraft:sand:1", + [ 5 ] = "minecraft:sand:1", + [ 6 ] = "minecraft:sand:1", + }, + }, + [ "minecraft:enchanting_table:0" ] = { + count = 1, + ingredients = { + [ 7 ] = "minecraft:diamond:0", + [ 2 ] = "minecraft:book:0", + [ 10 ] = "minecraft:obsidian:0", + [ 11 ] = "minecraft:obsidian:0", + [ 5 ] = "minecraft:diamond:0", + [ 6 ] = "minecraft:obsidian:0", + [ 9 ] = "minecraft:obsidian:0", + }, + }, + [ "minecraft:lapis_block:0" ] = { + count = 1, + ingredients = { + "minecraft:dye:4", + "minecraft:dye:4", + "minecraft:dye:4", + [ 7 ] = "minecraft:dye:4", + [ 9 ] = "minecraft:dye:4", + [ 10 ] = "minecraft:dye:4", + [ 11 ] = "minecraft:dye:4", + [ 5 ] = "minecraft:dye:4", + [ 6 ] = "minecraft:dye:4", + }, + }, + [ "minecraft:quartz_block:2" ] = { + count = 2, + ingredients = { + [ 2 ] = "minecraft:quartz_block:0", + [ 6 ] = "minecraft:quartz_block:0", + }, + }, + [ "minecraft:wool:5" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:10", + }, + }, + [ "minecraft:dye:12" ] = { + count = 2, + ingredients = { + [ 5 ] = "minecraft:dye:4", + [ 6 ] = "minecraft:dye:15", + }, + }, + [ "minecraft:dark_oak_fence_gate:0" ] = { + count = 1, + ingredients = { + "minecraft:stick:0", + "minecraft:planks:5", + "minecraft:stick:0", + [ 6 ] = "minecraft:planks:5", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:dark_oak_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:planks:5", + [ 9 ] = "minecraft:planks:5", + [ 10 ] = "minecraft:planks:5", + [ 11 ] = "minecraft:planks:5", + [ 5 ] = "minecraft:planks:5", + [ 6 ] = "minecraft:planks:5", + }, + }, + [ "minecraft:stone_slab:0" ] = { + count = 6, + ingredients = { + [ 6 ] = "minecraft:stone:0", + [ 7 ] = "minecraft:stone:0", + [ 5 ] = "minecraft:stone:0", + }, + }, + [ "minecraft:glass_pane:0" ] = { + count = 16, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 6 ] = "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + }, + }, + [ "minecraft:bed:0" ] = { + count = 1, + ingredients = { + "minecraft:wool:14", + "minecraft:wool:14", + "minecraft:wool:14", + [ 6 ] = "minecraft:planks:0", + [ 7 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:stained_glass:4" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:11", + }, + }, + [ "minecraft:prismarine:2" ] = { + count = 1, + ingredients = { + "minecraft:prismarine_shard:0", + "minecraft:prismarine_shard:0", + "minecraft:prismarine_shard:0", + [ 7 ] = "minecraft:prismarine_shard:0", + [ 9 ] = "minecraft:prismarine_shard:0", + [ 10 ] = "minecraft:prismarine_shard:0", + [ 11 ] = "minecraft:prismarine_shard:0", + [ 5 ] = "minecraft:prismarine_shard:0", + [ 6 ] = "minecraft:dye:0", + }, + }, + [ "minecraft:carpet:15" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:15", + [ 6 ] = "minecraft:wool:15", + }, + }, + [ "minecraft:wool:7" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:8", + }, + }, + [ "minecraft:iron_trapdoor:0" ] = { + count = 1, + ingredients = { + "minecraft:iron_ingot:0", + "minecraft:iron_ingot:0", + [ 5 ] = "minecraft:iron_ingot:0", + [ 6 ] = "minecraft:iron_ingot:0", + }, + }, + [ "minecraft:planks:2" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:log:2", + }, + }, + [ "minecraft:stained_glass:12" ] = { + count = 8, + ingredients = { + "minecraft:glass:0", + "minecraft:glass:0", + "minecraft:glass:0", + [ 7 ] = "minecraft:glass:0", + [ 9 ] = "minecraft:glass:0", + [ 10 ] = "minecraft:glass:0", + [ 11 ] = "minecraft:glass:0", + [ 5 ] = "minecraft:glass:0", + [ 6 ] = "minecraft:dye:3", + }, + }, + [ "minecraft:stained_glass_pane:5" ] = { + count = 16, + ingredients = { + "minecraft:stained_glass:5", + "minecraft:stained_glass:5", + "minecraft:stained_glass:5", + [ 6 ] = "minecraft:stained_glass:5", + [ 7 ] = "minecraft:stained_glass:5", + [ 5 ] = "minecraft:stained_glass:5", + }, + }, + [ "minecraft:wool:14" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:wool:0", + [ 6 ] = "minecraft:dye:1", + }, + }, + [ "minecraft:lever:0" ] = { + count = 1, + ingredients = { + [ 2 ] = "minecraft:stick:0", + [ 6 ] = "minecraft:cobblestone:0", + }, + }, + [ "minecraft:nether_brick:0" ] = { + count = 1, + ingredients = { + "minecraft:netherbrick:0", + "minecraft:netherbrick:0", + [ 5 ] = "minecraft:netherbrick:0", + [ 6 ] = "minecraft:netherbrick:0", + }, + }, + [ "minecraft:acacia_fence_gate:0" ] = { + count = 1, + ingredients = { + "minecraft:stick:0", + "minecraft:planks:4", + "minecraft:stick:0", + [ 6 ] = "minecraft:planks:4", + [ 7 ] = "minecraft:stick:0", + [ 5 ] = "minecraft:stick:0", + }, + }, + [ "minecraft:stained_hardened_clay:8" ] = { + count = 8, + ingredients = { + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + "minecraft:hardened_clay:0", + [ 7 ] = "minecraft:hardened_clay:0", + [ 9 ] = "minecraft:hardened_clay:0", + [ 10 ] = "minecraft:hardened_clay:0", + [ 11 ] = "minecraft:hardened_clay:0", + [ 5 ] = "minecraft:hardened_clay:0", + [ 6 ] = "minecraft:dye:7", + }, + }, + [ "minecraft:carpet:13" ] = { + count = 3, + ingredients = { + [ 5 ] = "minecraft:wool:13", + [ 6 ] = "minecraft:wool:13", + }, + }, + [ "minecraft:sea_lantern:0" ] = { + count = 1, + ingredients = { + "minecraft:prismarine_shard:0", + "minecraft:prismarine_crystals:0", + "minecraft:prismarine_shard:0", + [ 7 ] = "minecraft:prismarine_crystals:0", + [ 9 ] = "minecraft:prismarine_shard:0", + [ 10 ] = "minecraft:prismarine_crystals:0", + [ 11 ] = "minecraft:prismarine_shard:0", + [ 5 ] = "minecraft:prismarine_crystals:0", + [ 6 ] = "minecraft:prismarine_crystals:0", + }, + }, + [ "minecraft:oak_stairs:0" ] = { + count = 8, + ingredients = { + "minecraft:planks:0", + [ 9 ] = "minecraft:planks:0", + [ 10 ] = "minecraft:planks:0", + [ 11 ] = "minecraft:planks:0", + [ 5 ] = "minecraft:planks:0", + [ 6 ] = "minecraft:planks:0", + }, + }, + [ "minecraft:wool:1" ] = { + count = 1, + ingredients = { + [ 5 ] = "minecraft:dye:14", + [ 6 ] = "minecraft:wool:0", + }, + }, + [ "minecraft:planks:3" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:log:3", + }, + }, + [ "minecraft:sandstone:2" ] = { + count = 4, + ingredients = { + "minecraft:sandstone:0", + "minecraft:sandstone:0", + [ 5 ] = "minecraft:sandstone:0", + [ 6 ] = "minecraft:sandstone:0", + }, + }, + [ "minecraft:planks:0" ] = { + count = 4, + ingredients = { + [ 6 ] = "minecraft:log:0", + }, + }, +} \ No newline at end of file