builder upgrade

This commit is contained in:
kepler155c@gmail.com 2017-04-01 19:21:49 -04:00
parent 74e93068fb
commit 72bd16502b
14 changed files with 977 additions and 160 deletions

View File

@ -68,9 +68,6 @@ end
function page:enable() function page:enable()
self:setFocus(self.prompt) self:setFocus(self.prompt)
UI.Page.enable(self) UI.Page.enable(self)
if not device then
self.menuBar.Device:disable()
end
end end
local function autocomplete(env, oLine, x) local function autocomplete(env, oLine, x)
@ -131,6 +128,13 @@ function page:eventHandler(event)
self.prompt:updateCursor() self.prompt:updateCursor()
elseif event.type == 'device' then elseif event.type == 'device' then
if not _G.device then
sandboxEnv.device = { }
for _,side in pairs(peripheral.getNames()) do
local key = string.format('%s:%s', peripheral.getType(side), side)
sandboxEnv.device[ key ] = peripheral.wrap(side)
end
end
self:setPrompt('device', true) self:setPrompt('device', true)
self:executeStatement('device') self:executeStatement('device')

View File

@ -86,7 +86,7 @@ local systemPage = UI.Page {
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 4, y = 4,
values = { values = {
{ name = 'CC version', value = os.version() }, { name = 'CC version', value = os.getVersion() },
{ name = 'Lua version', value = _VERSION }, { name = 'Lua version', value = _VERSION },
{ name = 'MC version', value = _MC_VERSION or 'unknown' }, { name = 'MC version', value = _MC_VERSION or 'unknown' },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },

View File

@ -11,11 +11,15 @@ local UI = require('ui')
local Schematic = require('schematic') local Schematic = require('schematic')
local Profile = require('profile') local Profile = require('profile')
local TableDB = require('tableDB') local TableDB = require('tableDB')
local ChestProvider = require('chestProvider')
local MEProvider = require('meProvider') local MEProvider = require('meProvider')
local Blocks = require('blocks') local Blocks = require('blocks')
local Point = require('point') local Point = require('point')
local ChestProvider = require('chestProvider')
if os.getVersion() == 1.8 then
ChestProvider = require('chestProvider18')
end
Logger.filter('modem_send', 'event', 'ui') Logger.filter('modem_send', 'event', 'ui')
if device.wireless_modem then if device.wireless_modem then
@ -38,24 +42,10 @@ local Builder = {
fuelItem = { id = 'minecraft:coal', dmg = 0 }, fuelItem = { id = 'minecraft:coal', dmg = 0 },
resourceSlots = 15, resourceSlots = 15,
facing = 'south', facing = 'south',
confirmFacing = false,
} }
-- these wrenches work relative to the turtle local pistonFacings
local GoodWrenches = {
[ 'appliedEnergistics2:item.ToolCertusQuartzWrench' ] = true,
[ 'appliedEnergistics2:item.ToolNetherQuartzWrench' ] = true,
[ 'EnderIO:itemYetaWrench' ] = true,
}
--[[
-- these wrenches work but take more hits to turn a piston
local BadButUsableWrenches = {
[ 'ThermalExpansion:wrench' ] = true,
[ 'MineFactoryReloaded:hammer' ] = true,
[ 'ImmersiveEngineering:tool' ] = true,
}
--]]
--[[-- SubDB --]]-- --[[-- SubDB --]]--
subDB = TableDB({ subDB = TableDB({
@ -359,7 +349,7 @@ function Builder:getBlockCounts()
local blocks = { } local blocks = { }
-- add a couple essential items to the supply list to allow replacements -- add a couple essential items to the supply list to allow replacements
local wrench = subDB:getSubstitutedItem('ThermalExpansion:wrench', 0) local wrench = subDB:getSubstitutedItem('SubstituteAWrench', 0)
wrench.qty = 0 wrench.qty = 0
wrench.need = 1 wrench.need = 1
blocks[wrench.id .. ':' .. wrench.dmg] = wrench blocks[wrench.id .. ':' .. wrench.dmg] = wrench
@ -461,7 +451,7 @@ function Builder:getSupplyList(blockIndex)
index = 15, index = 15,
} }
local wrench = subDB:getSubstitutedItem('ThermalExpansion:wrench', 0) local wrench = subDB:getSubstitutedItem('SubstituteAWrench', 0)
slots[16] = { slots[16] = {
id = wrench.id, id = wrench.id,
dmg = wrench.dmg, dmg = wrench.dmg,
@ -612,9 +602,7 @@ function Builder:getSupplies()
if s.need > 0 then if s.need > 0 then
local item = self.itemProvider:getItemInfo(s.id, s.dmg) local item = self.itemProvider:getItemInfo(s.id, s.dmg)
if item then if item then
if item.name then s.name = item.name
s.name = item.name
end
local qty = math.min(s.need - s.qty, item.qty) local qty = math.min(s.need - s.qty, item.qty)
@ -629,6 +617,8 @@ function Builder:getSupplies()
self.itemProvider:provide(item, qty, s.index) self.itemProvider:provide(item, qty, s.index)
s.qty = turtle.getItemCount(s.index) s.qty = turtle.getItemCount(s.index)
end end
else
s.name = blocks.blockDB:getName(s.id, s.dmg)
end end
end end
if s.qty < s.need then if s.qty < s.need then
@ -855,26 +845,83 @@ end
function Builder:getWrenchSlot() function Builder:getWrenchSlot()
local wrench = subDB:getSubstitutedItem('ThermalExpansion:wrench', 0) local wrench = subDB:getSubstitutedItem('SubstituteAWrench', 0)
return Builder:selectItem(wrench.id, wrench.dmg) return Builder:selectItem(wrench.id, wrench.dmg)
end end
function Builder:wrenchBlock(side, count) function Builder:getTurtleFacing()
if os.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)
local s = Builder:getWrenchSlot() local s = Builder:getWrenchSlot()
if not s then if not s then
b.needResupply = true b.needResupply = true
return return false
end end
turtle.select(s.index) local key = turtle.point.heading .. '-' .. facing
for i = 1,count do local count = pistonFacings[side][key]
if count then
turtle.select(s.index)
for i = 1,count do
turtle.getAction(side).place()
end
return true
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()
local pistonFacing = directions[bi.metadata]
if facing == pistonFacing then
pistonFacings[side][key] = count
return true
end
count = count + 1
turtle.getAction(side).place() turtle.getAction(side).place()
end end
return true return false
end end
-- place piston, wrench piston to face downward, extend, remove piston -- place piston, wrench piston to face downward, extend, remove piston
@ -890,15 +937,10 @@ function Builder:placePiston(b)
end end
if not turtle.place(ps.index) then if not turtle.place(ps.index) then
return false return
end end
local wrenchCount = 5 local success = self:wrenchBlock('forward', 'down') --wrench piston to point downwards
if GoodWrenches[ws.id] then
wrenchCount = 2
end
local success = self:wrenchBlock('forward', wrenchCount) --wrench piston to point downwards
rs.setOutput('front', true) rs.setOutput('front', true)
os.sleep(.25) os.sleep(.25)
@ -907,7 +949,7 @@ function Builder:placePiston(b)
turtle.select(ps.index) turtle.select(ps.index)
turtle.dig() turtle.dig()
return true return success
end end
function Builder:goto(x, z, y, heading) function Builder:goto(x, z, y, heading)
@ -1008,6 +1050,10 @@ function Builder:placeDirectionalBlock(b, slot, travelPlane)
local isSouth = (turtle.getHeadingInfo(Builder.facing).heading + local isSouth = (turtle.getHeadingInfo(Builder.facing).heading +
turtle.getHeadingInfo(stairUpDirections[d]).heading) % 4 == 1 turtle.getHeadingInfo(stairUpDirections[d]).heading) % 4 == 1
if os.getVersion() == 1.8 then
isSouth = false -- no stair bug in this version
end
if isSouth then if isSouth then
-- for some reason, the south facing stair doesn't place correctly -- for some reason, the south facing stair doesn't place correctly
@ -1058,6 +1104,7 @@ function Builder:placeDirectionalBlock(b, slot, travelPlane)
[ 'piston-west' ] = 'west', [ 'piston-west' ] = 'west',
[ 'piston-east' ] = 'east', [ 'piston-east' ] = 'east',
[ 'piston-down' ] = 'down', [ 'piston-down' ] = 'down',
[ 'piston-up' ] = 'up',
} }
if pistonDirections[d] then if pistonDirections[d] then
@ -1071,55 +1118,23 @@ function Builder:placeDirectionalBlock(b, slot, travelPlane)
return false return false
end end
if GoodWrenches[ws.id] then -- piston turns relative to turtle position :)
-- piston turns relative to turtle position :) local rotatedPistonDirections = {
local rotatedPistonDirections = { [ 'piston-east' ] = 0,
[ 'piston-east' ] = 'south', [ 'piston-south' ] = 1,
[ 'piston-south' ] = 'west', [ 'piston-west' ] = 2,
[ 'piston-west' ] = 'north', [ 'piston-north' ] = 3,
[ 'piston-north' ] = 'east', }
[ 'piston-down' ] = 'down',
}
local wrenchCount self:gotoEx(b.x, b.z, b.y, nil, travelPlane)
if d == 'piston-down' then local heading = rotatedPistonDirections[d]
self:gotoEx(b.x -1, b.z, b.y, 0, travelPlane) if heading and turtle.point.heading % 2 ~= heading % 2 then
wrenchCount = 2 turtle.setHeading(heading)
else end
local hi = turtle.getHeadingInfo(rotatedPistonDirections[d])
self:gotoEx(b.x + hi.xd, b.z + hi.zd, b.y, (hi.heading + 2) % 4, travelPlane)
wrenchCount = 1
end
if self:place(slot) then if self:placeDown(slot) then
self:wrenchBlock('forward', wrenchCount) b.placed = self:wrenchBlock('down', pistonDirections[d])
turtle.up()
b.placed = self:placePiston(b)
end
else -- cresent wrench
-- piston turns relative to the world :(
local wrenchCounts = {
[ 1 ] = 4, -- east
[ 2 ] = 2, -- south
[ 3 ] = 3, -- west
[ 4 ] = 1, -- north
}
self:goto(b.x, b.z, b.y, nil, travelPlane)
local wrenchCount = 5
if d ~= 'piston-down' then
local offsetDirection = (turtle.getHeadingInfo(Builder.facing).heading +
turtle.getHeadingInfo(pistonDirections[d]).heading) % 4
wrenchCount = wrenchCounts[offsetDirection + 1]
end
if self:placeDown(slot) then
b.placed = self:wrenchBlock('down', wrenchCount)
end
end end
end end
@ -1254,6 +1269,13 @@ function Builder:build()
else else
travelPlane = self:findTravelPlane(self.index) travelPlane = self:findTravelPlane(self.index)
turtle.status = 'building' turtle.status = 'building'
if not self.confirmFacing then
local facing = self:getTurtleFacing()
if facing then
self.confirmFacing = true
self.facing = facing
end
end
end end
UI:setPage('blank') UI:setPage('blank')
@ -1366,6 +1388,11 @@ function blankPage:draw()
self:setCursorPos(1, 1) self:setCursorPos(1, 1)
end end
function blankPage:enable()
self:sync()
UI.Page.enable(self)
end
--[[-- selectSubstitutionPage --]]-- --[[-- selectSubstitutionPage --]]--
selectSubstitutionPage = UI.Page({ selectSubstitutionPage = UI.Page({
titleBar = UI.TitleBar({ titleBar = UI.TitleBar({
@ -1524,6 +1551,7 @@ function substitutionPage:eventHandler(event)
elseif event.type == 'accept' or event.type == 'air' or event.type == 'revert' then elseif event.type == 'accept' or event.type == 'air' or event.type == 'revert' then
self.statusBar:setStatus('Saving changes...') self.statusBar:setStatus('Saving changes...')
self.statusBar:draw() self.statusBar:draw()
self:sync()
if event.type == 'air' then if event.type == 'air' then
self:applySubstitute('minecraft:air', 0) self:applySubstitute('minecraft:air', 0)
@ -1674,8 +1702,8 @@ listingPage = UI.Page({
grid = UI.ScrollingGrid({ grid = UI.ScrollingGrid({
columns = { columns = {
{ heading = 'Name', key = 'name', width = UI.term.width - 14 }, { heading = 'Name', key = 'name', width = UI.term.width - 14 },
{ heading = 'Need', key = 'fNeed', width = 5 }, { heading = 'Need', key = 'need', width = 5 },
{ heading = 'Have', key = 'fQty', width = 5 }, { heading = 'Have', key = 'qty', width = 5 },
}, },
sortColumn = 'name', sortColumn = 'name',
y = 3, y = 3,
@ -1743,6 +1771,13 @@ function listingPage:eventHandler(event)
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end 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) function listingPage.grid:getRowTextColor(row, selected)
if row.is_craftable then if row.is_craftable then
return colors.yellow return colors.yellow
@ -1759,13 +1794,15 @@ function listingPage:refresh()
for _,b in pairs(supplyList) do for _,b in pairs(supplyList) do
if b.need > 0 then if b.need > 0 then
local item = Builder.itemProvider:getItemInfo(b.id, b.dmg) local item = Builder.itemProvider:getItemInfo(b.id, b.dmg)
if item then if item then
local block = blocks.blockDB:lookup(b.id, b.dmg) local block = blocks.blockDB:lookup(b.id, b.dmg)
if not block then if not block then
blocks.blockDB:add(b.id, b.dmg, item.name) blocks.blockDB:add(b.id, b.dmg, item.name, b.id)
elseif not blocks.name and item.name then elseif not blocks.name and item.name then
blocks.blockDB:add(b.id, b.dmg, item.name) blocks.blockDB:add(b.id, b.dmg, item.name)
end end
b.name = item.name
b.qty = item.qty b.qty = item.qty
b.is_craftable = item.is_craftable b.is_craftable = item.is_craftable
elseif not b.name then elseif not b.name then
@ -1775,19 +1812,17 @@ function listingPage:refresh()
end end
blocks.blockDB:flush() blocks.blockDB:flush()
local t = {} if self.fullList then
for _,b in pairs(supplyList) do self.grid:setValues(supplyList)
local block = blocks.blockDB:lookup(b.id, b.dmg) else
if block then local t = {}
b.name = block.name for _,b in pairs(supplyList) do
if self.fullList or b.qty < b.need then
table.insert(t, b)
end
end end
if self.fullList or b.qty < b.need then self.grid:setValues(t)
table.insert(t, b)
end
b.fNeed = Util.toBytes(b.need)
b.fQty = Util.toBytes(b.qty)
end end
self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
end end
@ -1882,13 +1917,19 @@ function startPage:eventHandler(event)
if event.type == 'startLevel' then if event.type == 'startLevel' then
local dialog = UI.Dialog({ local dialog = UI.Dialog({
text = UI.Text({ x = 5, y = 3, value = '0 - ' .. schematic.height }), title = 'Enter Starting Level',
textEntry = UI.TextEntry({ x = 15, y = 3, '0 - 11' }) 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) dialog.eventHandler = function(self, event)
if event.type == 'accept' then if event.type == 'form_complete' then
local l = tonumber(self.textEntry.value) local l = tonumber(self.form.textEntry.value)
if l and l < schematic.height and l >= 0 then if l and l < schematic.height and l >= 0 then
for k,v in pairs(schematic.blocks) do for k,v in pairs(schematic.blocks) do
if v.y >= l then if v.y >= l then
@ -1901,25 +1942,32 @@ function startPage:eventHandler(event)
else else
self.statusBar:timedStatus('Invalid Level', 3) self.statusBar:timedStatus('Invalid Level', 3)
end end
return true elseif event.type == 'form_cancel' or event.type == 'cancel' then
UI:setPreviousPage()
else
return UI.Dialog.eventHandler(self, event)
end end
return true
return UI.Dialog.eventHandler(self, event)
end end
dialog.titleBar.title = 'Enter Starting Level' dialog:setFocus(dialog.form.textEntry)
dialog:setFocus(dialog.textEntry)
UI:setPage(dialog) UI:setPage(dialog)
elseif event.type == 'startBlock' then elseif event.type == 'startBlock' then
local dialog = UI.Dialog({ local dialog = UI.Dialog {
text = UI.Text({ x = 5, y = 3, value = '1 - ' .. #schematic.blocks }), title = 'Enter Block Number',
textEntry = UI.TextEntry({ x = 15, y = 3, value = tostring(Builder.index) }) height = 7,
}) form = UI.Form {
y = 3, x = 2, height = 4,
text = UI.Text { x = 5, y = 1, value = '1 - ' .. #schematic.blocks, textColor = colors.gray },
textEntry = UI.TextEntry { x = 15, y = 1, value = tostring(Builder.index), width = 7 }
},
statusBar = UI.StatusBar(),
}
dialog.eventHandler = function(self, event) dialog.eventHandler = function(self, event)
if event.type == 'accept' then if event.type == 'form_complete' then
local bn = tonumber(self.textEntry.value) local bn = tonumber(self.form.textEntry.value)
if bn and bn < #schematic.blocks and bn >= 0 then if bn and bn < #schematic.blocks and bn >= 0 then
Builder.index = bn Builder.index = bn
Builder:saveProgress(Builder.index) Builder:saveProgress(Builder.index)
@ -1927,14 +1975,15 @@ function startPage:eventHandler(event)
else else
self.statusBar:timedStatus('Invalid Block', 3) self.statusBar:timedStatus('Invalid Block', 3)
end end
return true elseif event.type == 'form_cancel' or event.type == 'cancel' then
UI:setPreviousPage()
else
return UI.Dialog.eventHandler(self, event)
end end
return true
return UI.Dialog.eventHandler(self, event)
end end
dialog.titleBar.title = 'Enter Block Number' dialog:setFocus(dialog.form.textEntry)
dialog:setFocus(dialog.textEntry)
UI:setPage(dialog) UI:setPage(dialog)
elseif event.type == 'assignBlocks' then elseif event.type == 'assignBlocks' then
@ -1991,6 +2040,12 @@ function startPage:eventHandler(event)
else else
print('Starting build') print('Starting build')
end end
-- reset piston cache in case wrench was substituted
pistonFacings = {
down = { },
forward = { },
}
Builder:build() Builder:build()
Profile.display() Profile.display()

603
apps/refinedManager.lua Normal file
View File

@ -0,0 +1,603 @@
local injector = requireInjector or load(http.get('http://pastebin.com/raw/c0TWsScv').readAll())()
require = injector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Peripheral = require('peripheral')
local controller = Peripheral.getByType('refinedstorage:controller')
if not controller then
error('Refined storage controller not found')
end
multishell.setTitle(multishell.getCurrent(), 'Storage Manager')
-- 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 listItems()
local items = { }
local list
pcall(function()
list = controller.listAvailableItems()
end)
if list then
for k,v in pairs(list) do
local item = controller.findItem(v)
if item then
Util.merge(item, item.getMetadata())
item.displayName = safeString(item.displayName)
if item.maxDamage and item.maxDamage > 0 and item.damage > 0 then
item.displayName = item.displayName .. ' (damaged)'
end
item.lname = item.displayName:lower()
table.insert(items, item)
end
end
end
return items
end
function getItem(items, inItem, ignoreDamage)
for _,item in pairs(items) do
if item.name == inItem.name then
if ignoreDamage and ignoreDamage == 'yes' then
return item
elseif item.damage == inItem.damage and item.nbtHash == inItem.nbtHash then
return item
end
end
end
end
local function uniqueKey(item)
local key = item.name .. ':' .. item.damage
if item.nbtHash then
key = key .. ':' .. item.nbtHash
end
return key
end
function mergeResources(t)
local resources = Util.readTable('resource.limits')
resources = resources or { }
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.ignoreDamage = v.ignoreDamage
else
v.count = 0
v.limit = tonumber(v.limit)
v.low = tonumber(v.low)
v.auto = v.auto
v.ignoreDamage = v.ignoreDamage
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 getJobList()
local list = { }
for _,task in pairs(controller.getCraftingTasks()) do
table.insert(list, task.getPattern().outputs[1])
end
return list
end
function craftItems(itemList, allItems)
for _,item in pairs(itemList) do
local alreadyCrafting = false
local jobList = getJobList()
for _,v in pairs(jobList) do
if v.name == item.name and v.damage == item.damage and v.nbtHash == item.nbtHash then
alreadyCrafting = true
end
end
local cItem = getItem(allItems, item)
if alreadyCrafting then
item.status = '(crafting)'
elseif not cItem then
item.status = '(no recipe)'
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 cItem.craft(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
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.count = 4 -- this could be higher to increase autocrafting speed
table.insert(itemList, res)
end
end
return itemList
end
local function getItemWithQty(items, res, ignoreDamage)
local item = getItem(items, res, ignoreDamage)
if item then
if ignoreDamage and ignoreDamage == 'yes' 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
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.ignoreDamage)
res.limit = tonumber(res.limit)
res.low = tonumber(res.low)
if not item then
item = {
damage = res.damage,
nbtHash = res.nbtHash,
name = res.name,
displayName = res.displayName,
count = 0
}
end
if res.low and item.count < res.low then
if res.ignoreDamage and res.ignoreDamage == 'yes' then
item.damage = 0
end
table.insert(itemList, {
damage = item.damage,
nbtHash = item.nbtHash,
count = res.low - item.count,
name = item.name,
displayName = item.displayName,
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
},
displayName = UI.Window {
x = 5, y = 3, width = UI.term.width - 10, height = 3,
},
form = UI.Form {
x = 4, y = 6, 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.Chooser {
width = 7,
formLabel = 'Autocraft', formKey = 'auto',
nochoice = 'No',
choices = {
{ name = 'Yes', value = 'yes' },
{ name = 'No', value = 'no' },
},
help = 'Craft until out of ingredients'
},
[3] = UI.Chooser {
width = 7,
formLabel = 'Ignore Dmg', formKey = 'ignoreDamage',
nochoice = 'No',
choices = {
{ name = 'Yes', value = 'yes' },
{ name = 'No', value = 'no' },
},
help = 'Ignore damage of item'
},
},
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('\nNBT: %s\n', item.nbtHash)
end
debug(str)
self:setCursorPos(1, 1)
self:print(str)
end
function itemPage:enable(item)
self.item = item
self.form:setValues(item)
self.titleBar.title = item.name
self.displayName.value = item.displayName
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.name == values.name and v.damage == values.damage then
t[k] = nil
break
end
end
local keys = { 'name', 'displayName', 'auto', 'low', 'damage', 'maxDamage', 'nbtHash', 'limit', 'ignoreDamage' }
local filtered = { }
for _,key in pairs(keys) do
filtered[key] = values[key]
end
if filtered.ignoreDamage and filtered.ignoreDamage == 'yes' then
filtered.damage = 0
end
t[uniqueKey(filtered)] = filtered
--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 = 'Forget', event = 'forget' },
},
},
grid = UI.Grid {
y = 2, height = UI.term.height - 2,
columns = {
{ heading = 'Name', key = 'displayName', width = UI.term.width - 14 },
{ heading = 'Qty', key = 'count', width = 5 },
{ heading = 'Min', key = 'low', width = 4 },
},
sortColumn = 'lname',
},
statusBar = UI.StatusBar {
backgroundColor = colors.gray,
width = UI.term.width,
filterText = UI.Text {
x = 2, width = 6,
value = 'Filter',
},
filter = UI.TextEntry {
x = 9, rex = -12,
limit = 50,
},
refresh = UI.Button {
rx = -9, width = 8,
text = 'Refresh',
event = 'refresh',
},
},
accelerators = {
r = 'refresh',
q = 'quit',
}
}
function listingPage.grid:getRowTextColor(row, selected)
if row.is_craftable then -- not implemented
return colors.yellow
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
Event.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()
elseif event.type == 'forget' then
local item = self.grid:getSelected()
if item then
local resources = Util.readTable('resource.limits') or { }
resources[uniqueKey(item)] = nil
Util.writeTable('resource.limits', resources)
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 = listItems()
mergeResources(self.allItems)
self:applyFilter()
end
function listingPage:applyFilter()
local t = filterItems(self.allItems, self.filter)
self.grid:setValues(t)
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,
sync = function(...) 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 = nullDevice
})
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 jobMonitor(jobList)
local mon = Peripheral.getByType('monitor')
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,
sync = function(...) end,
blit = function(...) end,
}
if mon then
mon = UI.Device({
device = mon,
textScale = .5,
})
else
mon = UI.Device({
device = nullDevice
})
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 },
},
}
return jobListGrid
end
UI:setPages({
listing = listingPage,
item = itemPage,
})
UI:setPage(listingPage)
listingPage:setFocus(listingPage.statusBar.filter)
local jobListGrid = jobMonitor()
jobListGrid:draw()
jobListGrid:sync()
function craftingThread()
while true do
os.sleep(5)
pcall(function()
local items = listItems()
if controller.getNetworkEnergyStored() == 0 then
jobListGrid.parent:clear()
jobListGrid.parent:centeredWrite(math.ceil(jobListGrid.parent.height/2), 'Power failure')
jobListGrid:sync()
elseif 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 itemList = watchResources(items)
jobListGrid:setValues(itemList)
jobListGrid:draw()
jobListGrid:sync()
craftItems(itemList, items)
jobListGrid:update()
jobListGrid:draw()
jobListGrid:sync()
itemList = getAutocraftItems(items) -- autocrafted items don't show on job monitor
craftItems(itemList, items)
end
end)
end
end
Event.pullEvents(craftingThread)
UI.term:reset()
jobListGrid.parent:reset()

View File

@ -24,6 +24,11 @@ local options = {
local MIN_FUEL = 7500 local MIN_FUEL = 7500
local LOW_FUEL = 1500 local LOW_FUEL = 1500
local MAX_FUEL = 100000
if not term.isColor() then
MAX_FUEL = 20000
end
local mining = { local mining = {
diameter = 1, diameter = 1,
@ -175,6 +180,7 @@ end
function refuel() function refuel()
if turtle.getFuelLevel() < MIN_FUEL then if turtle.getFuelLevel() < MIN_FUEL then
local oldStatus = turtle.status
status('refueling') status('refueling')
if turtle.selectSlot('minecraft:coal:0') then if turtle.selectSlot('minecraft:coal:0') then
@ -193,7 +199,7 @@ function refuel()
end) end)
end end
log('Fuel: ' .. turtle.getFuelLevel()) log('Fuel: ' .. turtle.getFuelLevel())
status('boring') status(oldStatus)
end end
turtle.select(1) turtle.select(1)
@ -315,7 +321,7 @@ function mineable(action)
collectDrops(action.suck) collectDrops(action.suck)
end end
if turtle.getFuelLevel() < 99000 then if turtle.getFuelLevel() < (MAX_FUEL - 1000) then
if block.name == 'minecraft:lava' or block.name == 'minecraft:flowing_lava' then if block.name == 'minecraft:lava' or block.name == 'minecraft:flowing_lava' then
if turtle.selectSlot('minecraft:bucket:0') then if turtle.selectSlot('minecraft:bucket:0') then
if action.place() then if action.place() then
@ -546,6 +552,7 @@ end
local function main() local function main()
repeat repeat
while #mining.locations > 0 do while #mining.locations > 0 do
status('searching')
if not boreCommand() then if not boreCommand() then
return return
end end

View File

@ -6,6 +6,11 @@ local Point = require('point')
local TableDB = require('tableDB') local TableDB = require('tableDB')
local MEProvider = require('meProvider') local MEProvider = require('meProvider')
local ChestProvider = require('chestProvider')
if os.getVersion() == 1.8 then
ChestProvider = require('chestProvider18')
end
if not device.wireless_modem then if not device.wireless_modem then
error('No wireless modem detected') error('No wireless modem detected')
end end
@ -324,9 +329,9 @@ Message.addHandler('needSupplies',
turtle.select(15) turtle.select(15)
turtle.placeDown() turtle.placeDown()
os.sleep(.1) -- random computer not connected error os.sleep(.1) -- random computer not connected error
local p = peripheral.wrap('bottom') local p = ChestProvider({ direction = 'up', wrapSide = 'bottom' })
for i = 1, 16 do for i = 1, 16 do
p.pullItem('up', i, 64) p:insert(i, 64)
end end
Message.send(__BUILDER_ID, 'gotSupplies', { supplies = true, point = pt }) Message.send(__BUILDER_ID, 'gotSupplies', { supplies = true, point = pt })
@ -334,9 +339,9 @@ Message.addHandler('needSupplies',
Message.waitForMessage('thanks', 5, __BUILDER_ID) Message.waitForMessage('thanks', 5, __BUILDER_ID)
--os.sleep(0) --os.sleep(0)
p.condenseItems() --p.condenseItems()
for i = 1, 16 do for i = 1, 16 do
p.pushItem('up', i, 64) p:extract(i, 64)
end end
turtle.digDown() turtle.digDown()
turtle.status = 'waiting' turtle.status = 'waiting'
@ -386,6 +391,12 @@ turtle.setPoint({ x = -1, z = -2, y = 0, heading = 0 })
turtle.saveLocation('supplies') turtle.saveLocation('supplies')
Builder.itemProvider = MEProvider() Builder.itemProvider = MEProvider()
if not Builder.itemProvider:isValid() then
Builder.itemProvider = ChestProvider()
if not Builder.itemProvider:isValid() then
error('A chest or ME interface must be below turtle')
end
end
turtle.run(function() turtle.run(function()
Event.pullEvents(onTheWay) Event.pullEvents(onTheWay)

View File

@ -96,15 +96,13 @@ function blockDB:seedDB(dir)
end end
lastID = strId lastID = strId
local sep = string.find(line[2], ':') local t = { }
if not sep then string.gsub(line[2], '(%d+)', function(d) table.insert(t, d) end)
nid = tonumber(line[2]) nid = tonumber(t[1])
dmg = 0 dmg = 0
else if t[2] then
nid = tonumber(string.sub(line[2], 1, sep - 1)) dmg = tonumber(t[2])
dmg = tonumber(string.sub(line[2], sep + 1, #line[2]))
end end
self:add(nid, dmg, name, strId) self:add(nid, dmg, name, strId)
end end
@ -653,13 +651,13 @@ function blockTypeDB:seedDB()
}) })
blockTypeDB:addTemp('piston', { -- piston placement is broken in 1.7 -- need to add work around blockTypeDB:addTemp('piston', { -- piston placement is broken in 1.7 -- need to add work around
{ 0, nil, 0, 'piston-down' }, { 0, nil, 0, 'piston-down' },
{ 1, nil, 0 }, { 1, nil, 0, 'piston-up' },
{ 2, nil, 0, 'piston-north' }, { 2, nil, 0, 'piston-north' },
{ 3, nil, 0, 'piston-south' }, { 3, nil, 0, 'piston-south' },
{ 4, nil, 0, 'piston-west' }, { 4, nil, 0, 'piston-west' },
{ 5, nil, 0, 'piston-east' }, { 5, nil, 0, 'piston-east' },
{ 8, nil, 0, 'piston-down' }, { 8, nil, 0, 'piston-down' },
{ 9, nil, 0 }, { 9, nil, 0, 'piston-up' },
{ 10, nil, 0, 'piston-north' }, { 10, nil, 0, 'piston-north' },
{ 11, nil, 0, 'piston-south' }, { 11, nil, 0, 'piston-south' },
{ 12, nil, 0, 'piston-west' }, { 12, nil, 0, 'piston-west' },

View File

@ -54,7 +54,9 @@ function ChestProvider:getItemInfo(id, dmg)
item.max_size = stack.max_size item.max_size = stack.max_size
end end
end end
return item if item.name then
return item
end
end end
function ChestProvider:craft(id, dmg, qty) function ChestProvider:craft(id, dmg, qty)
@ -79,7 +81,13 @@ function ChestProvider:provide(item, qty, slot)
end 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) function ChestProvider:insert(slot, qty)
if self.p then if self.p then
local s, m = pcall(function() self.p.pullItem(self.direction, slot, qty) end) local s, m = pcall(function() self.p.pullItem(self.direction, slot, qty) end)

View File

@ -0,0 +1,103 @@
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.list
end
function ChestProvider:refresh()
if self.p then
--self.p.condenseItems()
self.stacks = self.p.list()
local t = { }
for _,s in pairs(self.stacks) do
s.id = s.name
s.dmg = s.damage
s.qty = s.count
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 k,stack in pairs(self.stacks) do
if stack.id == id and stack.dmg == dmg then
local meta = self.p.getItemMeta(k)
if meta then
item.name = meta.displayName
item.qty = item.qty + meta.count
item.max_size = meta.maxCount
end
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:refresh()
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.pushItems(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.pushItems(self.direction, slot, qty)
end
end
function ChestProvider:insert(slot, qty)
if self.p then
self.p.pullItems(self.direction, slot, qty)
end
end
return ChestProvider

View File

@ -1,6 +1,21 @@
local Peripheral = { } local Peripheral = { }
function Peripheral.addDevice(side) local function getDeviceList()
if _G.device then
return _G.device
end
local deviceList = { }
for _,side in pairs(peripheral.getNames()) do
Peripheral.addDevice(deviceList, side)
end
return deviceList
end
function Peripheral.addDevice(deviceList, side)
local name = side local name = side
local ptype = peripheral.getType(side) local ptype = peripheral.getType(side)
@ -28,33 +43,33 @@ function Peripheral.addDevice(side)
if sides[name] then if sides[name] then
local i = 1 local i = 1
local uniqueName = ptype local uniqueName = ptype
while device[uniqueName] do while deviceList[uniqueName] do
uniqueName = ptype .. '_' .. i uniqueName = ptype .. '_' .. i
i = i + 1 i = i + 1
end end
name = uniqueName name = uniqueName
end end
device[name] = peripheral.wrap(side) deviceList[name] = peripheral.wrap(side)
Util.merge(device[name], { Util.merge(deviceList[name], {
name = name, name = name,
type = ptype, type = ptype,
side = side, side = side,
}) })
return device[name] return deviceList[name]
end end
function Peripheral.getBySide(side) function Peripheral.getBySide(side)
return Util.find(device, 'side', side) return Util.find(getDeviceList(), 'side', side)
end end
function Peripheral.getByType(typeName) function Peripheral.getByType(typeName)
return Util.find(device, 'type', typeName) return Util.find(getDeviceList(), 'type', typeName)
end end
function Peripheral.getByMethod(method) function Peripheral.getByMethod(method)
for _,p in pairs(device) do for _,p in pairs(getDeviceList()) do
if p[method] then if p[method] then
return p return p
end end

View File

@ -438,6 +438,7 @@ end
function Manager:pullEvents(...) function Manager:pullEvents(...)
Event.pullEvents(...) Event.pullEvents(...)
self.term:reset()
end end
function Manager:exitPullEvents() function Manager:exitPullEvents()
@ -1075,7 +1076,7 @@ function UI.TransitionSlideLeft:update(device)
self.canvas:blit(device, { self.canvas:blit(device, {
x = self.x, x = self.x,
y = self.y, y = self.y,
ex = self.ex - x + self.x + 1, ex = self.ex - x + self.x,
ey = self.ey }, ey = self.ey },
{ x = x, y = self.y }) { x = x, y = self.y })
end end
@ -1108,13 +1109,13 @@ function UI.TransitionSlideRight:update(device)
self.lastScreen:blit(device, { self.lastScreen:blit(device, {
x = self.x, x = self.x,
y = self.y, y = self.y,
ex = self.ex - x + self.x + 1, ex = self.ex - x + self.x,
ey = self.ey }, ey = self.ey },
{ x = x, y = self.y }) { x = x, y = self.y })
self.canvas:blit(device, { self.canvas:blit(device, {
x = self.ex - x + self.x, x = self.ex - x + self.x,
y = self.y, y = self.y,
ex = self.ex + 1, ex = self.ex,
ey = self.ey }, ey = self.ey },
{ x = self.x, y = self.y }) { x = self.x, y = self.y })
end end
@ -1431,8 +1432,6 @@ end
UI.Grid = class(UI.Window) UI.Grid = class(UI.Window)
UI.Grid.defaults = { UI.Grid.defaults = {
UIElement = 'Grid', UIElement = 'Grid',
x = 1,
y = 1,
index = 1, index = 1,
inverseSort = false, inverseSort = false,
disableHeader = false, disableHeader = false,
@ -2571,7 +2570,7 @@ UI.StatusBar.defaults = {
function UI.StatusBar:init(args) function UI.StatusBar:init(args)
local defaults = UI:getDefaults(UI.StatusBar, args) local defaults = UI:getDefaults(UI.StatusBar, args)
UI.GridLayout.init(self, defaults) UI.GridLayout.init(self, defaults)
self:setStatus(self.status) self:setStatus(self.status, true)
end end
function UI.StatusBar:setParent() function UI.StatusBar:setParent()
@ -2583,12 +2582,15 @@ function UI.StatusBar:setParent()
end end
end end
function UI.StatusBar:setStatus(status) function UI.StatusBar:setStatus(status, noDraw)
if type(status) == 'string' then if type(status) == 'string' then
self.values[1] = { status = status } self.values[1] = { status = status }
else else
self.values[1] = status self.values[1] = status
end end
if not noDraw then
self:draw()
end
end end
function UI.StatusBar:setValue(name, value) function UI.StatusBar:setValue(name, value)
@ -2796,7 +2798,8 @@ function UI.TextEntry:updateScroll()
elseif self.pos < self.scroll then elseif self.pos < self.scroll then
self.scroll = self.pos self.scroll = self.pos
end end
-- debug('p:%d s:%d w:%d l:%d', self.pos, self.scroll, self.width, self.limit)
--debug('p:%d s:%d w:%d l:%d', self.pos, self.scroll, self.width, self.limit)
end end
function UI.TextEntry:draw() function UI.TextEntry:draw()
@ -2910,7 +2913,7 @@ function UI.TextEntry:eventHandler(event)
return true return true
elseif event.type == 'mouse_click' then elseif event.type == 'mouse_click' then
if self.focused then if self.focused and event.x > 1 then
self.pos = event.x + self.scroll - 2 self.pos = event.x + self.scroll - 2
self:updateCursor() self:updateCursor()
return true return true
@ -3166,7 +3169,7 @@ function UI.Dialog:init(args)
titleBar = UI.TitleBar({ previousPage = true, title = defaults.title }), titleBar = UI.TitleBar({ previousPage = true, title = defaults.title }),
}) })
UI.setProperties(defaults, args) --UI.setProperties(defaults, args)
UI.Page.init(self, defaults) UI.Page.init(self, defaults)
end end
@ -3266,8 +3269,8 @@ function UI.NftImage:setImage(image)
end end
UI:loadTheme('config/ui.theme') UI:loadTheme('config/ui.theme')
if _HOST and string.find(_HOST, 'CCEmuRedux') then if os.getVersion() >= 1.79 then
UI:loadTheme('config/ccemuredux.theme') UI:loadTheme('config/ext.theme')
end end
UI:setDefaultDevice(UI.Device({ device = term.current() })) UI:setDefaultDevice(UI.Device({ device = term.current() }))

View File

@ -4,5 +4,5 @@ require = requireInjector(getfenv(1))
local Peripheral = require('peripheral') local Peripheral = require('peripheral')
for _,side in pairs(peripheral.getNames()) do for _,side in pairs(peripheral.getNames()) do
Peripheral.addDevice(side) Peripheral.addDevice(device, side)
end end

View File

@ -90,6 +90,16 @@ function os.isPocket()
return not not pocket return not not pocket
end end
function os.getVersion()
if _CC_VERSION then
return tonumber(_CC_VERSION)
end
if _HOST then
return tonumber(_HOST:gmatch('[%d]+%.?[%d][%d]', '%1')())
end
return 1.7
end
function os.registerApp(entry) function os.registerApp(entry)
local apps = { } local apps = { }
Config.load('apps', apps) Config.load('apps', apps)

View File

@ -14,7 +14,7 @@ end
Event.addHandler('peripheral', function(event, side) Event.addHandler('peripheral', function(event, side)
if side then if side then
local dev = Peripheral.addDevice(side) local dev = Peripheral.addDevice(device, side)
if dev then if dev then
term.setTextColor(attachColor) term.setTextColor(attachColor)
Util.print('[%s] %s attached', dev.side, dev.name) Util.print('[%s] %s attached', dev.side, dev.name)