Files
opus-apps/apps/chestManager.lua

1487 lines
38 KiB
Lua

--[[
Provides: autocrafting, resource limits, on-demand crafting, storage stocker.
Using a turtle allows for crafting of items eliminating the need for AE/RS
molecular assemblers / crafters.
Inventory setup:
Turtle/computer must be touching at least one type of inventory
Generic inventory block such as:
Vanilla chest
RFTools modular storage
Storage drawers controller
and many others...
Applied energistics
AE cable or interface (depending upon AE/MC version)
Refined storage
TODO: add required block
Turtle crafting (optional):
1. The turtle must have a crafting table equipped.
2. Requires a vanilla chest beside the turtle with the "craftingChest"
configuration variable defined.
-- or --
If using MC 1.7x, you can equip the turtle with a duck antenna and
set the duckAntenna configuration value to true.
-- or --
If plethora mod is available, equip the turtle with an introspection
module.
Controller (optional):
Provides the ability to request crafting from AE / RS
Applied Energistics
In versions 1.7x, AE can be used for both inventory access and crafting
requests.
In versions 1.8+, AE can only be used to request crafting.
Refined Storage
In versions 1.8x, inventory access works depending upon version.
Turtle/computer must be touching an interface for inventory access. If only
requesting crafting, the controller must be either be touching or connected
via CC cables.
Restocking (optional):
If using a limited inventory block, such as RFTools modular storage, you
can have the main inventory automatically replenish items from the restocking
inventory. Each cycle, the restocking inventory will be checked and if the
main inventory has less than a stack of an item, it will pull that item
from the restocking inventory into the main inventory.
Configuration:
Configuration file is usr/config/inventoryManager
valid sides:
top, bottom, left, right, front, back
valid directions:
up, down, north, south, east, west
Required:
computerFacing : direction turtle is facing
inventory : side for the main inventory (can be the same as the controller)
Optional:
craftingChest : side for the chest used for crafting
controller : side for AE cable/interface or RS controller
stock : side for restocking inventory
duckAntenna : true/false if the 1.7 openperipherals duck antenna is equipped
trashDirection : direction of trash block (trashcan/inventory/etc) in
relationship to the main inventory. This block does not
need to touch the turtle, only the main inventory block.
monitor : valid options include:
type/monitor - will use the first monitor found
side/north - specify a direction (top/bottom/east/etc)
name/monitor_1 - specify the exact name of the peripheral
]]--
_G.requireInjector()
local Ansi = require('ansi')
local Config = require('config')
local Craft = require('turtle.craft')
local Event = require('event')
local itemDB = require('itemDB')
local Peripheral = require('peripheral')
local Terminal = require('terminal')
local UI = require('ui')
local Util = require('util')
local ControllerAdapter = require('controllerAdapter')
local InventoryAdapter = require('inventoryAdapter')
local colors = _G.colors
local device = _G.device
local multishell = _ENV.multishell
local os = _G.os
local term = _G.term
local turtle = _G.turtle
if multishell then
multishell.setTitle(multishell.getCurrent(), 'Resource Manager')
end
local config = {
computerFacing = 'north', -- direction turtle is facing
inventory = 'top', -- main inventory
craftingChest = 'bottom', -- required in 1.8+ for crafting
duckAntenna = false,
controller = 'none', -- AE / RS controller
stock = 'none', -- another inventory for restocking main inventory
trashDirection = 'up', -- trash/chest in relation to inventory
monitor = 'type/monitor',
}
Config.loadWithCheck('inventoryManager', config)
local controllerAdapter = ControllerAdapter.wrap({ side = config.controller, facing = config.computerFacing })
local inventoryAdapter = InventoryAdapter.wrap({ side = config.inventory, facing = config.computerFacing })
local stockAdapter = InventoryAdapter.wrap({ side = config.stock, facing = config.computerFacing })
local turtleChestAdapter = InventoryAdapter.wrap({ side = config.craftingChest, facing = config.computerFacing })
local modem = device.modem
local introspectionModule = device['plethora:introspection']
local duckAntenna
if not inventoryAdapter then
error('Invalid inventory configuration')
end
if device.workbench and config.duckAntenna then
local oppositeSide = {
[ 'left' ] = 'right',
[ 'right' ] = 'left',
}
local duckAntennaSide = oppositeSide[device.workbench.side]
duckAntenna = Peripheral.wrap(duckAntennaSide)
if duckAntenna and not duckAntenna.getAllStacks then
duckAntenna = nil
end
end
local STATUS_INFO = 'info'
local STATUS_WARNING = 'warning'
local STATUS_ERROR = 'error'
local RESOURCE_FILE = 'usr/config/resources.db'
local RECIPES_FILE = 'usr/config/recipes.db'
local craftingPaused = false
local canCraft = not not (turtle and turtle.craft)
local canLearn = not not (canCraft and (duckAntenna or turtleChestAdapter or introspectionModule))
local userRecipes = Util.readTable(RECIPES_FILE) or { }
local jobList
local resources
local demandCrafting = { }
local function getItem(items, inItem, ignoreDamage, ignoreNbtHash)
for _,item in pairs(items) do
if item.name == inItem.name and
(ignoreDamage or item.damage == inItem.damage) and
(ignoreNbtHash or item.nbtHash == inItem.nbtHash) then
return item
end
end
end
local function uniqueKey(item)
return table.concat({ item.name, item.damage, item.nbtHash }, ':')
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(Craft.recipes) do
local v = itemDB: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 = itemDB:getName(v)
end
v.lname = v.displayName:lower()
end
end
local function listItems()
local items
for _ = 1, 5 do
items = inventoryAdapter:listItems()
if items then
break
end
jobList:showError('Error - retrying in 3 seconds')
os.sleep(3)
end
if not items then
-- error('could not check inventory')
term.clear()
jobList:showError('Error - rebooting in 5 seconds')
print('Communication failure')
print('rebooting in 5 secs')
os.sleep(5)
os.reboot()
end
return items
end
local function filterItems(t, filter, displayMode)
if filter or displayMode > 0 then
local r = { }
if filter then
filter = filter:lower()
end
for _,v in pairs(t) do
if not filter or string.find(v.lname, filter, 1, true) then
if not displayMode or
displayMode == 0 or
displayMode == 1 and v.count > 0 or
displayMode == 2 and v.has_recipe then
table.insert(r, v)
end
end
end
return r
end
return t
end
local function clearGrid()
local function clear()
for i = 1, 16 do
local count = turtle.getItemCount(i)
if count > 0 then
inventoryAdapter:insert(i, count)
if turtle.getItemCount(i) ~= 0 then
return false
end
end
end
return true
end
return clear() or clear()
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 = item.nbtHash, count = 0 }
request.displayName = itemDB:getName(request)
craftList[key] = request
end
request.count = request.count + count
return request
end
-- Craft
local function craftItem(recipe, items, originalItem, craftList, count)
local missing = { }
local toCraft = Craft.getCraftableAmount(recipe, count, items, missing)
if missing.name then
originalItem.status = string.format('%s missing', itemDB:getName(missing.name))
originalItem.statusCode = STATUS_WARNING
end
local crafted = 0
if toCraft > 0 then
crafted = Craft.craftRecipe(recipe, toCraft, inventoryAdapter)
clearGrid()
items = listItems()
count = count - crafted
end
if count > 0 and items then
local ingredients = Craft.getResourceList4(recipe, items, count)
for _,ingredient in pairs(ingredients) do
if ingredient.need > 0 then
local item = addCraftingRequest(ingredient, craftList, ingredient.need)
if Craft.findRecipe(item) then
item.status = string.format('%s missing', itemDB:getName(ingredient))
item.statusCode = STATUS_WARNING
else
item.status = 'no recipe'
item.statusCode = STATUS_ERROR
end
end
end
end
return crafted
end
-- Craft as much as possible regardless if all ingredients are available
local function forceCraftItem(inRecipe, items, originalItem, craftList, inCount)
local summed = { }
local throttle = Util.throttle()
local function sumItems(recipe, count)
count = math.ceil(count / recipe.count)
local craftable = count
for key,iqty in pairs(Craft.sumIngredients(recipe)) do
throttle()
local item = itemDB:splitKey(key)
local summedItem = summed[key]
if not summedItem then
summedItem = Util.shallowCopy(item)
summedItem.recipe = Craft.findRecipe(item)
summedItem.count = Craft.getItemCount(items, key)
summedItem.need = 0
summedItem.used = 0
summedItem.craftable = 0
summed[key] = summedItem
end
local total = count * iqty -- 4 * 2
local used = math.min(summedItem.count, total) -- 5
local need = total - used -- 3
if recipe.craftingTools and recipe.craftingTools[key] then
if summedItem.count > 0 then
summedItem.used = 1
summedItem.need = 0
need = 0
elseif not summedItem.recipe then
summedItem.need = 1
need = 1
else
need = 1
end
else
summedItem.count = summedItem.count - used
summedItem.used = summedItem.used + used
end
if need > 0 then
if not summedItem.recipe then
craftable = math.min(craftable, math.floor(used / iqty))
summedItem.need = summedItem.need + need
else
local c = sumItems(summedItem.recipe, need) -- 4
craftable = math.min(craftable, math.floor((used + c) / iqty))
summedItem.craftable = summedItem.craftable + c
end
end
end
if craftable > 0 then
craftable = Craft.craftRecipe(recipe, craftable * recipe.count, inventoryAdapter) / recipe.count
clearGrid()
end
return craftable * recipe.count
end
local count = sumItems(inRecipe, inCount)
if count < inCount then
for _,ingredient in pairs(summed) do
if ingredient.need > 0 then
local item = addCraftingRequest(ingredient, craftList, ingredient.need)
if Craft.findRecipe(item) then
item.status = string.format('%s missing', itemDB:getName(ingredient))
item.statusCode = STATUS_WARNING
else
item.status = '(no recipe)'
item.statusCode = STATUS_ERROR
end
end
end
end
return count
end
local function craft(recipe, items, item, craftList)
item.status = nil
item.statusCode = nil
item.crafted = 0
if craftingPaused or not canCraft then
return
end
if not clearGrid() then
item.status = 'Grid obstructed'
item.statusCode = STATUS_ERROR
return
end
if item.forceCrafting then
item.crafted = forceCraftItem(recipe, items, item, craftList, item.count)
else
item.crafted = craftItem(recipe, items, item, craftList, item.count)
end
end
local function craftItems(craftList, allItems)
-- turtle crafting
if canCraft then
for _,key in pairs(Util.keys(craftList)) do
local item = craftList[key]
local recipe = Craft.recipes[key]
if recipe then
craft(recipe, allItems, item, craftList)
allItems = listItems() -- refresh counts
if not allItems then
break
end
elseif not controllerAdapter then
item.status = '(no recipe)'
item.statusCode = STATUS_ERROR
end
end
end
-- redstone control
for _,item in pairs(craftList) do
if item.rsControl then
item.status = '(activated)'
item.statusCode = STATUS_INFO
end
end
-- remote processing
if modem then
for key,item in pairs(craftList) do
if Craft.recipes[key] and item.crafted < item.count then
end
end
end
-- controller
if controllerAdapter then
for key,item in pairs(craftList) do
if (not canCraft or not Craft.recipes[key]) and not item.rsControl then
if controllerAdapter:isCrafting(item) then
item.status = '(crafting)'
item.statusCode = STATUS_INFO
elseif not controllerAdapter:isCPUAvailable() then
item.status = '(waiting)'
item.statusCode = STATUS_WARNING
else
local count = item.count
item.crafted = 0
while count >= 1 do -- try to request smaller quantities until successful
local s = pcall(function()
item.status = '(no recipe)'
item.statusCode = STATUS_ERROR
if not controllerAdapter:craft(item, count) then
item.status = '(missing ingredients)'
item.statusCode = STATUS_WARNING
error('failed')
end
item.status = '(crafting)'
item.statusCode = STATUS_INFO
item.crafted = count
end)
if s then
break -- successfully requested crafting
end
count = math.floor(count / 2)
end
end
end
end
end
if not controllerAdapter and not canCraft then
for _,item in pairs(craftList) do
if not item.rsControl then
item.status = 'Invalid setup'
item.statusCode = STATUS_INFO
end
end
end
end
local function restock()
if turtle and stockAdapter then
local items = inventoryAdapter:listItems()
local stock = stockAdapter:listItems()
if items and stock then
for _,v in pairs(stock) do
local count = Craft.getItemCount(items, v)
if count < v.maxCount then
stockAdapter:provide(v, v.maxCount - count)
clearGrid()
end
end
end
end
end
local function eject(item, qty)
if _G.turtle then
local s, m = pcall(function()
inventoryAdapter:provide(item, qty)
_G.turtle.emptyInventory()
end)
end
end
local function jobMonitor()
local mon = Peripheral.lookup(config.monitor)
if mon then
mon = UI.Device({
device = mon,
textScale = .5,
})
else
mon = UI.Device({
device = Terminal.getNullTerm(term.current())
})
end
jobList = UI.Page {
parent = mon,
grid = UI.Grid {
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 },
},
},
}
function jobList:showError(msg)
self.grid:clear()
self.grid:centeredWrite(math.ceil(self.grid.height / 2), msg)
self:sync()
end
function jobList:updateList(craftList)
self.grid:setValues(craftList)
self.grid:update()
self:draw()
self:sync()
end
function jobList.grid:getRowTextColor(row, selected)
if row.statusCode == STATUS_ERROR then
return colors.red
elseif row.statusCode == STATUS_WARNING then
return colors.yellow
elseif row.statusCode == STATUS_INFO then
return colors.lime
end
return UI.Grid:getRowTextColor(row, selected)
end
jobList:enable()
jobList:draw()
jobList:sync()
end
local function getAutocraftItems()
local craftList = { }
for _,res in pairs(resources) do
if res.auto then
res = Util.shallowCopy(res)
res.count = 256 -- 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, ignoreNbtHash)
local item = getItem(items, res, ignoreDamage, ignoreNbtHash)
if item and (ignoreDamage or ignoreNbtHash) then
local count = 0
for _,v in pairs(items) do
if item.name == v.name and
(ignoreDamage or item.damage == v.damage) and
(ignoreNbtHash or item.nbtHash == v.nbtHash) then
count = count + v.count
end
end
item.count = count
end
return item
end
local function watchResources(items)
local craftList = { }
local outputs = { }
for _,res in pairs(resources) do
local item = getItemWithQty(items, res, res.ignoreDamage, res.ignoreNbtHash)
if not item then
item = {
damage = res.damage,
nbtHash = res.nbtHash,
name = res.name,
displayName = itemDB:getName(res),
count = 0
}
end
if res.limit and item.count > res.limit then
inventoryAdapter:provide(
{ name = item.name, damage = item.damage, nbtHash = item.nbtHash },
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
local enable = item.count < res.low
if not outputs[res.rsDevice] then
outputs[res.rsDevice] = { }
end
outputs[res.rsDevice][res.rsSide] = outputs[res.rsDevice][res.rsSide] or enable
end
end
for rsDevice, sides in pairs(outputs) do
for side, enable in pairs(sides) do
pcall(function()
device[rsDevice].setOutput(side, enable)
end)
end
end
return craftList
end
local function loadResources()
resources = Util.readTable(RESOURCE_FILE) or { }
for k,v in pairs(resources) do
Util.merge(v, itemDB:splitKey(k))
end
end
local function saveResources()
local t = { }
for k,v in pairs(resources) do
v = Util.shallowCopy(v)
local keys = Util.transpose({ 'auto', 'low', 'limit',
'ignoreDamage', 'ignoreNbtHash',
'rsControl', 'rsDevice', 'rsSide' })
for _,key in pairs(Util.keys(v)) do
if not keys[key] then
v[key] = nil
end
end
if not Util.empty(v) then
t[k] = v
end
end
Util.writeTable(RESOURCE_FILE, t)
end
local itemPage = UI.Page {
titleBar = UI.TitleBar {
title = 'Limit Resource',
previousPage = true,
event = 'form_cancel',
},
form = UI.Form {
x = 1, y = 2, height = 10, ex = -1,
[1] = UI.TextEntry {
width = 7,
formLabel = 'Min', formKey = 'low', help = 'Craft if below min'
},
[2] = UI.TextEntry {
width = 7,
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 = 'ignoreDamage',
nochoice = 'No',
choices = {
{ name = 'Yes', value = true },
{ name = 'No', value = false },
},
help = 'Ignore damage of item'
},
[5] = UI.Chooser {
width = 7,
formLabel = 'Ignore NBT', formKey = 'ignoreNbtHash',
nochoice = 'No',
choices = {
{ name = 'Yes', value = true },
{ name = 'No', value = false },
},
help = 'Ignore NBT of item'
},
--[[
[6] = UI.Button {
x = 2, y = -2, width = 10,
formLabel = 'Redstone',
event = 'show_rs',
text = 'Configure',
},
]]
infoButton = UI.Button {
x = 2, y = -2,
event = 'show_info',
text = 'Info',
},
},
rsControl = UI.SlideOut {
backgroundColor = colors.cyan,
titleBar = UI.TitleBar {
title = "Redstone Control",
},
form = UI.Form {
y = 2,
[1] = UI.Chooser {
width = 7,
formLabel = 'RS Control', formKey = 'rsControl',
nochoice = 'No',
choices = {
{ name = 'Yes', value = true },
{ name = 'No', value = false },
},
help = 'Control via redstone'
},
[2] = UI.Chooser {
width = 25,
formLabel = 'RS Device', formKey = 'rsDevice',
--choices = devices,
help = 'Redstone Device'
},
[3] = 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'
},
},
},
info = UI.SlideOut {
titleBar = UI.TitleBar {
title = "Information",
},
textArea = UI.TextArea {
x = 2, ex = -2, y = 3, ey = -4,
backgroundColor = colors.black,
},
cancel = UI.Button {
ex = -2, y = -2, width = 6,
text = 'Okay',
event = 'hide_info',
},
},
statusBar = UI.StatusBar { }
}
function itemPage:enable(item)
self.item = item
self.form:setValues(item)
self.titleBar.title = item.displayName or item.name
UI.Page.enable(self)
self:focusFirst()
end
function itemPage.rsControl:enable()
local devices = self.form[1].choices
Util.clear(devices)
for _,dev in pairs(device) do
if dev.setOutput then
table.insert(devices, { name = dev.name, value = dev.name })
end
end
if Util.size(devices) == 0 then
table.insert(devices, { name = 'None found', values = '' })
end
UI.SlideOut.enable(self)
end
function itemPage.rsControl:eventHandler(event)
if event.type == 'form_cancel' then
self:hide()
elseif event.type == 'form_complete' then
self:hide()
else
return UI.SlideOut.eventHandler(self, event)
end
return true
end
function itemPage:eventHandler(event)
if event.type == 'form_cancel' then
UI:setPreviousPage()
elseif event.type == 'show_rs' then
self.rsControl:show()
elseif event.type == 'show_info' then
local value =
string.format('%s%s%s\n%s\n',
Ansi.orange, self.item.displayName, Ansi.reset,
self.item.name)
if self.item.nbtHash then
value = value .. self.item.nbtHash .. '\n'
end
value = value .. string.format('\n%sDamage:%s %s',
Ansi.yellow, Ansi.reset, self.item.damage)
if self.item.maxDamage and self.item.maxDamage > 0 then
value = value .. string.format(' (max: %s)', self.item.maxDamage)
end
if self.item.maxCount then
value = value .. string.format('\n%sStack Size: %s%s',
Ansi.yellow, Ansi.reset, self.item.maxCount)
end
self.info.textArea.value = value
self.info:show()
elseif event.type == 'hide_info' then
self.info:hide()
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 originalKey = uniqueKey(self.item)
local filtered = Util.shallowCopy(values)
filtered.low = tonumber(filtered.low)
filtered.limit = tonumber(filtered.limit)
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 filtered.ignoreDamage == true then
filtered.damage = 0
else
filtered.ignoreDamage = nil
end
if filtered.ignoreNbtHash == true then
filtered.nbtHash = nil
else
filtered.ignoreNbtHash = nil
end
resources[originalKey] = nil
resources[uniqueKey(filtered)] = filtered
filtered.count = nil
saveResources()
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' },
{ text = 'Refresh', event = 'refresh', x = -9 },
},
},
grid = UI.Grid {
y = 2, ey = -2,
columns = {
{ heading = 'Name', key = 'displayName' },
{ heading = 'Qty', key = 'count' , width = 4 },
{ heading = 'Min', key = 'low' , width = 4 },
{ heading = 'Max', key = 'limit' , width = 4 },
},
sortColumn = 'displayName',
},
statusBar = UI.StatusBar {
filter = UI.TextEntry {
x = 1, ex = -4,
limit = 50,
shadowText = 'filter',
shadowTextColor = colors.gray,
backgroundColor = colors.cyan,
backgroundFocusColor = colors.cyan,
},
display = UI.Button {
x = -3,
event = 'toggle_display',
value = 0,
text = 'A',
},
},
notification = UI.Notification(),
accelerators = {
r = 'refresh',
q = 'quit',
grid_select_right = 'craft',
},
displayMode = 0,
}
function listingPage.statusBar:draw()
return UI.Window.draw(self)
end
function listingPage.grid:getRowTextColor(row, selected)
if row.is_craftable then
return colors.yellow
end
if canCraft and row.has_recipe then
return colors.cyan
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: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 == 'toggle_display' then
local values = {
[0] = 'A',
[1] = 'I',
[2] = 'C',
}
event.button.value = (event.button.value + 1) % 3
self.displayMode = event.button.value
event.button.text = values[event.button.value]
event.button:draw()
self:applyFilter()
self.grid:draw()
elseif event.type == 'learn' then
if canLearn then
UI:setPage('learn')
else
self.notification:error('Missing a crafting chest or workbench\nCheck configuration')
end
elseif event.type == 'craft' or event.type == 'grid_select_right' then
local item = self.grid:getSelected()
if Craft.findRecipe(item) or true then -- or item.is_craftable then
UI:setPage('craft', self.grid:getSelected())
else
self.notification:error('No recipe defined')
end
elseif event.type == 'forget' then
local item = self.grid:getSelected()
if item then
local key = uniqueKey(item)
if userRecipes[key] then
userRecipes[key] = nil
Util.writeTable(RECIPES_FILE, userRecipes)
Craft.loadRecipes()
end
if resources[key] then
resources[key] = nil
saveResources()
end
self.notification:info('Forgot: ' .. item.name)
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.displayMode)
self.grid:setValues(t)
end
local function getTurtleInventory()
if duckAntenna then
local list = duckAntenna.getAllStacks(false)
for _,v in pairs(list) do
v.name = v.id
v.damage = v.dmg
v.displayName = v.display_name
v.count = v.qty
v.maxDamage = v.max_dmg
v.maxCount = v.max_size
v.nbtHash = v.nbt_hash
if not itemDB:get(v) then
itemDB:add(v)
end
end
itemDB:flush()
return list
end
if introspectionModule then
local list = { }
for i = 1,16 do
list[i] = introspectionModule.getInventory().getItemMeta(i)
end
return list
end
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 learnRecipe(page)
local ingredients = getTurtleInventory()
if ingredients then
turtle.select(1)
if canLearn and turtle.craft() then
local results = getTurtleInventory()
if results and results[1] then
clearGrid()
local maxCount
local newRecipe = {
ingredients = ingredients,
}
local numResults = 0
for _,v in pairs(results) do
if v.count > 0 then
numResults = numResults + 1
end
end
if numResults > 1 then
for _,v1 in pairs(results) do
for _,v2 in pairs(ingredients) do
if v1.name == v2.name and
v1.nbtHash == v2.nbtHash and
(v1.damage == v2.damage or
(v1.maxDamage > 0 and v2.maxDamage > 0 and
v1.damage ~= v2.damage)) then
if not newRecipe.crafingTools then
newRecipe.craftingTools = { }
end
local tool = Util.shallowCopy(v2)
if tool.maxDamage > 0 then
tool.damage = '*'
end
--[[
Turtles can only craft one item at a time using a tool :(
]]--
maxCount = 1
newRecipe.craftingTools[uniqueKey(tool)] = true
v1.craftingTool = true
break
end
end
end
end
local recipe
for _,v in pairs(results) do
if not v.craftingTool then
recipe = v
if maxCount then
recipe.maxCount = maxCount
end
break
end
end
if not recipe then
debug(results)
debug(newRecipe)
error('Failed - view system log')
end
newRecipe.count = recipe.count
local key = uniqueKey(recipe)
if recipe.maxCount ~= 64 then
newRecipe.maxCount = recipe.maxCount
end
for k,ingredient in pairs(Util.shallowCopy(ingredients)) do
if ingredient.maxDamage > 0 then
-- ingredient.damage = '*' -- I don't think this is right
end
ingredients[k] = uniqueKey(ingredient)
end
userRecipes[key] = newRecipe
Util.writeTable(RECIPES_FILE, userRecipes)
Craft.loadRecipes()
local displayName = itemDB:getName(recipe)
listingPage.statusBar.filter:setValue(displayName)
listingPage.notification:success('Learned: ' .. displayName)
listingPage.filter = displayName
listingPage:refresh()
listingPage.grid:draw()
eject(recipe, recipe.count)
return true
end
else
listingPage.notification:error('Failed to craft', 3)
end
else
listingPage.notification:error('No recipe defined', 3)
end
end
local learnPage = UI.Dialog {
height = 7, width = UI.term.width - 6,
title = 'Learn Recipe',
idField = UI.Text {
x = 5,
y = 3,
width = UI.term.width - 10,
value = 'Place recipe in turtle'
},
accept = UI.Button {
x = -14, y = -3,
text = 'Ok', event = 'accept',
},
cancel = UI.Button {
x = -9, y = -3,
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.Page {
titleBar = UI.TitleBar { },
wizard = UI.Wizard {
y = 2, ey = -2,
pages = {
quantity = UI.Window {
index = 1,
text = UI.Text {
x = 6, y = 3,
value = 'Quantity',
},
count = UI.TextEntry {
x = 15, y = 3, width = 10,
limit = 6,
value = 1,
},
ejectText = UI.Text {
x = 6, y = 4,
value = 'Eject',
},
eject = UI.Chooser {
x = 15, y = 4, width = 7,
value = true,
nochoice = 'No',
choices = {
{ name = 'Yes', value = true },
{ name = 'No', value = false },
},
},
},
resources = UI.Window {
index = 2,
grid = UI.ScrollingGrid {
y = 2, ey = -2,
columns = {
{ heading = 'Name', key = 'displayName' },
{ heading = 'Total', key = 'total' , width = 5 },
{ heading = 'Used', key = 'used' , width = 5 },
{ heading = 'Need', key = 'need' , width = 5 },
},
sortColumn = 'displayName',
},
},
},
},
}
function craftPage:enable(item)
self.item = item
self:focusFirst()
self.titleBar.title = itemDB:getName(item)
-- self.wizard.pages.quantity.eject.value = true
UI.Page.enable(self)
end
function craftPage.wizard.pages.resources.grid:getDisplayValues(row)
local function dv(v)
if v == 0 then
return ''
end
return Util.toBytes(v)
end
row = Util.shallowCopy(row)
row.total = Util.toBytes(row.total)
row.used = dv(row.used)
row.need = dv(row.need)
return row
end
function craftPage.wizard.pages.resources.grid:getRowTextColor(row, selected)
if row.need > 0 then
return colors.orange
end
return UI.Grid:getRowTextColor(row, selected)
end
function craftPage.wizard:eventHandler(event)
if event.type == 'nextView' then
local count = tonumber(self.pages.quantity.count.value)
if not count or count <= 0 then
self.pages.quantity.count.backgroundColor = colors.red
self.pages.quantity.count:draw()
return false
end
self.pages.quantity.count.backgroundColor = colors.black
end
return UI.Wizard.eventHandler(self, event)
end
function craftPage.wizard.pages.resources:enable()
local items = listItems()
local count = tonumber(self.parent.quantity.count.value)
local recipe = Craft.findRecipe(craftPage.item)
if recipe then
local ingredients = Craft.getResourceList4(recipe, items, count)
for _,v in pairs(ingredients) do
v.displayName = itemDB:getName(v)
end
self.grid:setValues(ingredients)
else
self.grid:setValues({ })
end
return UI.Window.enable(self)
end
function craftPage:eventHandler(event)
if event.type == 'cancel' then
UI:setPreviousPage()
elseif event.type == 'accept' then
local key = uniqueKey(self.item)
demandCrafting[key] = Util.shallowCopy(self.item)
demandCrafting[key].count = tonumber(self.wizard.pages.quantity.count.value)
demandCrafting[key].ocount = demandCrafting[key].count
demandCrafting[key].forceCrafting = true
demandCrafting[key].eject = self.wizard.pages.quantity.eject.value == true
UI:setPreviousPage()
else
return UI.Page.eventHandler(self, event)
end
return true
end
loadResources()
if canCraft then
clearGrid()
end
UI:setPages({
listing = listingPage,
item = itemPage,
learn = learnPage,
craft = craftPage,
})
jobMonitor()
UI:setPage(listingPage)
listingPage:setFocus(listingPage.statusBar.filter)
Event.onInterval(5, function()
if not craftingPaused then
restock()
local items = listItems()
if not items or Util.size(items) == 0 then
jobList:showError('No items in system')
else
local demandCrafted
if Util.size(demandCrafting) > 0 then
items = listItems()
if items then
demandCrafted = Util.shallowCopy(demandCrafting)
craftItems(demandCrafted, items)
end
end
items = listItems()
local craftList
if items then
craftList = watchResources(items)
craftItems(craftList, items)
end
if demandCrafted and craftList then
for k,v in pairs(demandCrafted) do
craftList[k] = v
end
end
for _,key in pairs(Util.keys(demandCrafting)) do
local item = demandCrafting[key]
if item.crafted then
item.count = math.max(0, item.count - item.crafted)
if item.count <= 0 then
demandCrafting[key] = nil
item.statusCode = 'success'
if item.eject then
eject(item, item.ocount)
end
end
end
end
jobList:updateList(craftList)
craftList = getAutocraftItems(items) -- autocrafted items don't show on job monitor
craftItems(craftList, items)
end
end
end)
UI:pullEvents()
jobList.parent:reset()