From 0db3ad47bb59c5b7efa9e304f0ac46e0786cc73a Mon Sep 17 00:00:00 2001 From: kepler155c Date: Sat, 20 Oct 2018 20:36:16 -0400 Subject: [PATCH] plethora networked autocrafting --- apis/chestAdapter18.lua | 11 +- inventoryManager/.package | 12 + inventoryManager/apis/craft.lua | 319 ++++ inventoryManager/apis/inventoryAdapter.lua | 55 + inventoryManager/apis/networkedAdapter18.lua | 145 ++ inventoryManager/apps/Crafter.lua | 1130 ++++++++++++++ inventoryManager/apps/inventoryManager.lua | 1400 ++++++++++++++++++ 7 files changed, 3068 insertions(+), 4 deletions(-) create mode 100644 inventoryManager/.package create mode 100644 inventoryManager/apis/craft.lua create mode 100644 inventoryManager/apis/inventoryAdapter.lua create mode 100644 inventoryManager/apis/networkedAdapter18.lua create mode 100644 inventoryManager/apps/Crafter.lua create mode 100644 inventoryManager/apps/inventoryManager.lua diff --git a/apis/chestAdapter18.lua b/apis/chestAdapter18.lua index a0829d1..3053aa4 100644 --- a/apis/chestAdapter18.lua +++ b/apis/chestAdapter18.lua @@ -122,6 +122,8 @@ function ChestAdapter:getPercentUsed() end function ChestAdapter:provide(item, qty, slot, direction) + local total = 0 + local s, m = pcall(function() local stacks = self.list() for key,stack in Util.rpairs(stacks) do @@ -130,24 +132,25 @@ function ChestAdapter:provide(item, qty, slot, direction) (not item.nbtHash or stack.nbtHash == item.nbtHash) then local amount = math.min(qty, stack.count) if amount > 0 then - self.pushItems(direction or self.direction, key, amount, slot) + amount = self.pushItems(direction or self.direction, key, amount, slot) end qty = qty - amount + total = total + amount if qty <= 0 then break end end end end) - return s, m + return total, m end function ChestAdapter:extract(slot, qty, toSlot) - self.pushItems(self.direction, slot, qty, toSlot) + return self.pushItems(self.direction, slot, qty, toSlot) end function ChestAdapter:insert(slot, qty, toSlot) - self.pullItems(self.direction, slot, qty, toSlot) + return self.pullItems(self.direction, slot, qty, toSlot) end return ChestAdapter diff --git a/inventoryManager/.package b/inventoryManager/.package new file mode 100644 index 0000000..f1522ad --- /dev/null +++ b/inventoryManager/.package @@ -0,0 +1,12 @@ +{ + required = { + 'opus-inventory-manager', + }, + title = 'Inventory manager for Opus OS', + description = [[ +Unstable Branch +]], + licence = 'MIT', + --location = '', + --mount = 'packages/opus-neural gitfs kepler155c/opus-neural/master', +} diff --git a/inventoryManager/apis/craft.lua b/inventoryManager/apis/craft.lua new file mode 100644 index 0000000..1b7b676 --- /dev/null +++ b/inventoryManager/apis/craft.lua @@ -0,0 +1,319 @@ +local itemDB = require('itemDB') +local Util = require('util') + +local fs = _G.fs +local turtle = _G.turtle + +local RECIPES_DIR = 'usr/etc/recipes' +local USER_RECIPES = 'usr/config/recipes.db' + +local Craft = { } + +local function clearGrid(inventoryAdapter) + for i = 1, 16 do + local count = turtle.getItemCount(i) + if count > 0 then + inventoryAdapter:insert(i, count) + if turtle.getItemCount(i) ~= 0 then + -- inventory is possibly full + return false + end + end + end + return true +end + +local function splitKey(key) + local t = Util.split(key, '(.-):') + local item = { } + if #t[#t] > 8 then + item.nbtHash = table.remove(t) + end + item.damage = tonumber(table.remove(t)) + item.name = table.concat(t, ':') + return item +end + +function Craft.getItemCount(items, item) + if type(item) == 'string' then + item = splitKey(item) + end + + local count = 0 + for _,v in pairs(items) do + if v.name == item.name and + (not item.damage or v.damage == item.damage) and + v.nbtHash == item.nbtHash then + if item.damage then + return v.count + end + count = count + v.count + end + end + return count +end + +local function turtleCraft(recipe, qty, inventoryAdapter) + if not clearGrid(inventoryAdapter) then + return false + end + + for k,v in pairs(recipe.ingredients) do + local item = splitKey(v) + local provideQty = qty + --[[ + Turtles can only craft 1 item at a time when using a tool. + + if recipe.craftingTools and recipe.craftingTools[k] then + provideQty = 1 + end + ]]-- + inventoryAdapter:provide(item, provideQty, k) + if turtle.getItemCount(k) == 0 then -- ~= qty then + -- FIX: ingredients cannot be stacked +--debug('failed ' .. v .. ' - ' .. provideQty) + return false + end + end + + return turtle.craft() +end + +function Craft.loadRecipes() + Craft.recipes = { } + + Util.merge(Craft.recipes, (Util.readTable(fs.combine(RECIPES_DIR, 'minecraft.db')) or { }).recipes) + + local config = Util.readTable('usr/config/recipeBooks.db') or { } + for _, book in pairs(config) do + local recipeFile = Util.readTable(book) + Util.merge(Craft.recipes, recipeFile.recipes) + end + + local recipes = Util.readTable(USER_RECIPES) or { } + Util.merge(Craft.recipes, recipes) +end + +function Craft.sumIngredients(recipe) + -- produces { ['minecraft:planks:0'] = 8 } + local t = { } + for _,item in pairs(recipe.ingredients) do + t[item] = (t[item] or 0) + 1 + end +-- need a check for crafting tool + return t +end + +function Craft.craftRecipe(recipe, count, inventoryAdapter) + if type(recipe) == 'string' then + recipe = Craft.recipes[recipe] + if not recipe then + return 0, 'No recipe' + end + end + + local items = inventoryAdapter:listItems() + if not items then + return 0, 'Inventory changed' + end + + count = math.ceil(count / recipe.count) + local maxCount = recipe.maxCount or math.floor(64 / recipe.count) + + for key,icount in pairs(Craft.sumIngredients(recipe)) do + local itemCount = Craft.getItemCount(items, key) + local need = icount * count + if recipe.craftingTools and recipe.craftingTools[key] then + need = 1 + end + maxCount = math.min(maxCount, itemDB:getMaxCount(key)) + if itemCount < need then + local irecipe = Craft.findRecipe(key) + if irecipe then + local iqty = need - itemCount + local crafted = Craft.craftRecipe(irecipe, iqty, inventoryAdapter) + if crafted ~= iqty then + turtle.select(1) + return 0 + end + end + end + end + + local crafted = 0 + repeat + if not turtleCraft(recipe, math.min(count, maxCount), inventoryAdapter) then + turtle.select(1) + break + end + crafted = crafted + math.min(count, maxCount) + count = count - maxCount + until count <= 0 + + turtle.select(1) + return crafted * recipe.count +end + +local function makeRecipeKey(item) + if type(item) == 'string' then + item = splitKey(item) + end + return table.concat({ item.name, item.damage or 0, item.nbtHash }, ':') +end + +function Craft.findRecipe(key) + if type(key) ~= 'string' then + key = itemDB:makeKey(key) + end + + local item = itemDB:splitKey(key) + if item.damage then + return Craft.recipes[makeRecipeKey(item)] + end + + -- handle cases where the request is like : IC2:reactorVent:* + for rkey,recipe in pairs(Craft.recipes) do + local r = itemDB:splitKey(rkey) + if item.name == r.name and + (not item.nbtHash or r.nbtHash == item.nbtHash) then + return recipe + end + end +end + +-- determine the full list of ingredients needed to craft +-- a quantity of a recipe. +function Craft.getResourceList(inRecipe, items, inCount) + local summed = { } + + local function sumItems(recipe, key, count) + local item = itemDB:splitKey(key) + local summedItem = summed[key] + if not summedItem then + summedItem = Util.shallowCopy(item) + summedItem.recipe = Craft.findRecipe(key) + summedItem.count = Craft.getItemCount(items, item) + summedItem.displayName = itemDB:getName(item) + summedItem.total = 0 + summedItem.need = 0 + summedItem.used = 0 + summed[key] = summedItem + end + local total = count + local used = math.min(summedItem.count, total) + local need = total - used + + if recipe.craftingTools and recipe.craftingTools[key] then + summedItem.total = 1 + 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.total = summedItem.total + total + summedItem.count = summedItem.count - used + summedItem.used = summedItem.used + used + if not summedItem.recipe then + summedItem.need = summedItem.need + need + end + end + + if need > 0 and summedItem.recipe then + need = math.ceil(need / summedItem.recipe.count) + for ikey,iqty in pairs(Craft.sumIngredients(summedItem.recipe)) do + sumItems(summedItem.recipe, ikey, math.ceil(need * iqty)) + end + end + end + + inCount = math.ceil(inCount / inRecipe.count) + for ikey,iqty in pairs(Craft.sumIngredients(inRecipe)) do + sumItems(inRecipe, ikey, math.ceil(inCount * iqty)) + end + + return summed +end + +function Craft.getResourceList4(inRecipe, items, count) + local summed = Craft.getResourceList(inRecipe, items, count) +-- filter down to just raw materials + return Util.filter(summed, function(a) return a.used > 0 or a.need > 0 end) +end + +-- given a certain quantity, return how many of those can be crafted +function Craft.getCraftableAmount(inRecipe, count, items, missing) + local function sumItems(recipe, summedItems, count) + local canCraft = 0 + + for _ = 1, count do + for _,item in pairs(recipe.ingredients) do + local summedItem = summedItems[item] or Craft.getItemCount(items, item) + + local irecipe = Craft.findRecipe(item) + if irecipe and summedItem <= 0 then + summedItem = summedItem + sumItems(irecipe, summedItems, 1) + end + if summedItem <= 0 then + if missing and not irecipe then + missing.name = item + end + return canCraft + end + if not recipe.craftingTools or not recipe.craftingTools[item] then + summedItems[item] = summedItem - 1 + end + end + canCraft = canCraft + recipe.count + end + + return canCraft + end + + return sumItems(inRecipe, { }, math.ceil(count / inRecipe.count)) +end + +function Craft.canCraft(item, count, items) + return Craft.getCraftableAmount(Craft.recipes[item], count, items) == count +end + +function Craft.setRecipes(recipes) + Craft.recipes = recipes +end + +function Craft.getCraftableAmountTest() + local results = { } + Craft.setRecipes(Util.readTable('usr/etc/recipes.db')) + + local items = { + { name = 'minecraft:planks', damage = 0, count = 5 }, + { name = 'minecraft:log', damage = 0, count = 2 }, + } + results[1] = { item = 'chest', expected = 1, + got = Craft.getCraftableAmount(Craft.recipes['minecraft:chest:0'], 2, items) } + + items = { + { name = 'minecraft:log', damage = 0, count = 1 }, + { name = 'minecraft:coal', damage = 1, count = 1 }, + } + results[2] = { item = 'torch', expected = 4, + got = Craft.getCraftableAmount(Craft.recipes['minecraft:torch:0'], 4, items) } + + return results +end + +function Craft.craftRecipeTest(name, count) + local ChestAdapter = require('chestAdapter18') + local chestAdapter = ChestAdapter({ wrapSide = 'top', direction = 'down' }) + Craft.setRecipes(Util.readTable('usr/etc/recipes.db')) + return { Craft.craftRecipe(Craft.recipes[name], count, chestAdapter) } +end + +Craft.loadRecipes() + +return Craft diff --git a/inventoryManager/apis/inventoryAdapter.lua b/inventoryManager/apis/inventoryAdapter.lua new file mode 100644 index 0000000..3f16327 --- /dev/null +++ b/inventoryManager/apis/inventoryAdapter.lua @@ -0,0 +1,55 @@ +local Adapter = { } + +function Adapter.wrap(args) + local adapters = { + 'networkedAdapter18', + 'refinedAdapter', + 'meAdapter18', + 'chestAdapter18', + + -- adapters for version 1.7 + 'meAdapter', + 'chestAdapter', + } + + for _,adapterType in ipairs(adapters) do + local adapter = require(adapterType)(args) + + if adapter:isValid() then + + -- figure out which direction to push/pull items from an inventory + -- based on the side the inventory is attached and which way the + -- turtle/computer is facing + if args and args.facing and adapter.side and not adapter.direction then + local horz = { top = 'down', bottom = 'up' } + adapter.direction = horz[adapter.side] + + if not adapter.direction then + local sides = { + front = 0, + right = 1, + back = 2, + left = 3, + } + -- pretty sure computer/turtle have sides reversed + local cards = { + east = 0, + south = 1, + west = 2, + north = 3, + } + local icards = { + [ 0 ] = 'west', + [ 1 ] = 'north', + [ 2 ] = 'east', + [ 3 ] = 'south', + } + adapter.direction = icards[(cards[args.facing] + sides[adapter.side]) % 4] + end + end + return adapter + end + end +end + +return Adapter diff --git a/inventoryManager/apis/networkedAdapter18.lua b/inventoryManager/apis/networkedAdapter18.lua new file mode 100644 index 0000000..2e3305f --- /dev/null +++ b/inventoryManager/apis/networkedAdapter18.lua @@ -0,0 +1,145 @@ +local class = require('class') +local Util = require('util') +local InventoryAdapter = require('inventoryAdapter') +local itemDB = require('itemDB') +local Peripheral = require('peripheral') + +local NetworkedAdapter = class() + +function NetworkedAdapter:init(args) + local defaults = { + name = 'Networked Adapter', + remotes = { }, + } + Util.merge(self, defaults) + Util.merge(self, args) + + if not self.side or self.side == 'network' then + self.chests = { } + self.modem = Peripheral.get('wired_modem') + + if self.modem and self.modem.getNameLocal then + self.localName = self.modem.getNameLocal() + for _, v in pairs(self.modem.getNamesRemote()) do + local remote = Peripheral.get({ name = v }) + if remote and remote.size and remote.size() >= 27 and remote.list then + + local adapter = InventoryAdapter.wrap({ side = v, direction = self.localName }) + if adapter then + table.insert(self.remotes, adapter) + end + end + end + end + end + + _G._p = self +end + +function NetworkedAdapter:isValid() + return #self.remotes > 0 +end + +function NetworkedAdapter:refresh(throttle) + return self:listItems(throttle) +end + +-- provide a consolidated list of items +function NetworkedAdapter:listItems(throttle) + local cache = { } + local items = { } + throttle = throttle or Util.throttle() + + for _, v in pairs(self.remotes) do + v.__cache = v:listItems(throttle) + + for k,v in pairs(v.__cache) do + if v.count > 0 then + local key = table.concat({ v.name, v.damage, v.nbtHash }, ':') + + local entry = cache[key] + if not entry then + entry = Util.shallowCopy(v) + entry.count = v.count + cache[key] = entry + table.insert(items, entry) + else + entry.count = entry.count + v.count + end + + throttle() + end + end + end + + if not Util.empty(items) then + self.cache = cache + return items + end +end + +function NetworkedAdapter:getItemInfo(item) + if not self.cache then + self:listItems() + end + local key = table.concat({ item.name, item.damage, item.nbtHash }, ':') + local items = self.cache or { } + return items[key] +end + +function NetworkedAdapter:getPercentUsed() + if self.cache and self.getDrawerCount then + return math.floor(Util.size(self.cache) / self.getDrawerCount() * 100) + end + return 0 +end + +function NetworkedAdapter:provide(item, qty, slot, direction) + local total = 0 + + for _, remote in pairs(self.remotes) do +debug('%s -> slot %d: %d %s', remote.side, slot, qty, item.name) + local amount = remote:provide(item, qty, slot) + qty = qty - amount + total = total + amount + if qty <= 0 then + break + end + end + + return total +end + +function NetworkedAdapter:extract(slot, qty, toSlot) + + error('extract not supported') + local total = 0 + for _, remote in pairs(self.remotes) do +debug('extract %d slot:%d', qty, slot) + local amount = remote:extract(slot, qty, toSlot) + qty = qty - amount + total = total + amount + if qty <= 0 then + break + end + end + + return total +end + +function NetworkedAdapter:insert(slot, qty, toSlot) + local total = 0 + for _, remote in pairs(self.remotes) do +debug('slot %d -> %s: %s', slot, remote.side, qty) + local amount = remote:insert(slot, qty, toSlot) + qty = qty - amount + total = total + amount + if qty <= 0 then + break + end + end + + return total +end + +return NetworkedAdapter diff --git a/inventoryManager/apps/Crafter.lua b/inventoryManager/apps/Crafter.lua new file mode 100644 index 0000000..0589d0e --- /dev/null +++ b/inventoryManager/apps/Crafter.lua @@ -0,0 +1,1130 @@ +--[[ + Turtle/machine crafting. + + Requirements: + Turtle must be restricted forward and back by some obstacle. + The turtle must have access to the main inventory at the most forward location. + Machines must be placed above or below the line along the turtle's backwards travel. + + Optional: + Monitors can be placed touching the turtle at the most forward position to + display crafting status. + + Sample setups: + M = machine, I = inventory, O = obstacle, T = turtle + + Turtle facing <--- + + MMMM + IT O + MMM + + IMMMM + O O + MMMMMM +]]-- + +_G.requireInjector() + +local InventoryAdapter = require('inventoryAdapter') +local Config = require('config') +local Event = require('event') +local itemDB = require('itemDB') +local Peripheral = require('peripheral') +local UI = require('ui') +local Terminal = require('terminal') +local Util = require('util') + +local colors = _G.colors +local os = _G.os +local term = _G.term +local turtle = _G.turtle + +local config = { + computerFacing = 'north', + inventorySide = 'front', + monitor = 'type/monitor', +} +Config.load('crafter', config) + +repeat until not turtle.forward() + +local inventoryAdapter = InventoryAdapter.wrap({ + side = config.inventorySide, + facing = config.computerFacing +}) +if not inventoryAdapter then + error('Invalid inventory configuration') +end + +local RESOURCE_FILE = 'usr/config/resources.db' +local RECIPES_FILE = 'usr/config/recipes2.db' +local MACHINES_FILE = 'usr/config/machines.db' + +local STATUS_ERROR = 'error' +local STATUS_INFO = 'info' +local STATUS_SUCCESS = 'success' +local STATUS_WARNING = 'warning' + +local recipes = Util.readTable(RECIPES_FILE) or { } +local resources +local machines = { } +local jobListGrid +local listing, docked = false, false + +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 getItemQuantity(items, res) + local count = 0 + for _,v in pairs(items) do + if res.name == v.name and + ((not res.damage and v.maxDamage > 0) or res.damage == v.damage) and + ((not res.nbtHash and v.nbtHash) or res.nbtHash == v.nbtHash) then + count = count + v.count + end + end + return count +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 = 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 filterItems(t, filter) + if filter then + local r = {} + filter = filter:lower() + for _,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 clearGrid() + 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 + +local function undock() + while listing do + os.sleep(.5) + end + docked = false +end + +local function gotoMachine(machine) + undock() + for _ = 1, machine.index do + if not turtle.back() then + return + end + end + + return true +end + +local function dock() + if not docked then + repeat until not turtle.forward() + end + docked = true +end + +local function getItems() + while not docked do + os.sleep(.5) + end + + listing = true + + local items + for _ = 1, 5 do + items = inventoryAdapter:listItems() + if items then + break + end + end + if not items then + error('could not check inventory') + end + + listing = false + + return items +end + +local function isMachineEmpty(machine, item) + local list = { true } + + pcall(function() -- fails randomly in 1.7x + local side = turtle.getAction(machine.dir).side + local methods = Util.transpose(Peripheral.getMethods(side)) + + if methods.getAllStacks then -- 1.7x + list = Peripheral.call(side, 'getAllStacks', false) + elseif methods.list then + list = Peripheral.call(side, 'list') + elseif methods.getProgress then + if Peripheral.call(side, 'getProgress') == 0 then + return true + end + else + item.statusCode = STATUS_ERROR + item.status = 'Unable to check empty status' + return + end + + if tonumber(machine.ignoreSlot) then + list[tonumber(machine.ignoreSlot)] = nil + end + end) + + if Util.empty(list) then + return true + end + item.statusCode = STATUS_INFO + item.status = 'machine busy' +end + +local function craftItem(ikey, item, items, machineStatus) + dock() + + local resource = resources[ikey] + if not resource or not resource.machine then + item.statusCode = STATUS_ERROR + item.status = 'machine not defined' + return + end + + local machine = Util.find(machines, 'order', resource.machine) + if not machine then + item.statusCode = STATUS_ERROR + item.status = 'invalid machine' + return + end + + local ms = machineStatus[machine.order] + if not ms then + ms = { count = 0 } + machineStatus[machine.order] = ms + end + + local slot = 1 + local maxCount = math.ceil(item.need / item.recipe.count) + maxCount = math.min(machine.maxCount or 64, maxCount) + + if maxCount > 0 and maxCount - ms.count <= 0 then + item.statusCode = STATUS_INFO + item.status = 'machine busy' + return + end + + maxCount = maxCount - ms.count + + for key,qty in pairs(item.recipe.ingredients) do + local ingredient = itemDB:get(key) + local c = math.min(maxCount * qty, getItemQuantity(items, ingredient)) + c = math.min(c, ingredient.maxCount) + c = math.floor(c / qty) + if c < maxCount then + maxCount = c + end + if maxCount <= 0 then + item.status = 'Missing ' .. ingredient.displayName + item.statusCode = STATUS_WARNING + return + end + end + + if machine.maxCount then + ms.count = ms.count + maxCount + end + + turtle.setStatus('Craft: ' .. itemDB:getName(ikey)) + for key,qty in pairs(item.recipe.ingredients) do + local ingredient = itemDB:get(key) +-- local c = item.craftable * qty +-- while c > 0 do +--debug(key) + inventoryAdapter:provide(ingredient, maxCount * qty, slot) + if turtle.getItemCount(slot) ~= maxCount * qty then + item.status = 'Extract failed: ' .. (ingredient.displayName or itemDB:getName(ingredient)) + item.statusCode = STATUS_ERROR + return + end +-- c = c - maxCount + slot = slot + 1 + --end + end + + if not gotoMachine(machine) then + item.status = 'failed to find machine' + item.statusCode = STATUS_ERROR + else + if machine.empty and not isMachineEmpty(machine, item) then + return + end + + if machine.dir == 'up' then + turtle.emptyInventory(turtle.dropUp) + else + turtle.emptyInventory(turtle.dropDown) + end + if #turtle.getFilledSlots() ~= 0 then + item.statusCode = STATUS_INFO + item.status = 'machine busy' + else + item.statusCode = STATUS_SUCCESS + item.status = 'crafting' + end + end +end + +local function expandList(list, items) + local summed = { } + + local function sumItems(key, count) + local item = itemDB:splitKey(key) + local summedItem = summed[key] + if not summedItem then + summedItem = Util.shallowCopy(item) + summedItem.recipe = recipes[key] + summedItem.count = getItemQuantity(items, item) + summedItem.displayName = itemDB:getName(item) + summedItem.total = 0 + summedItem.need = 0 + summedItem.used = 0 + summedItem.craftable = 0 + summed[key] = summedItem + end + local total = count + local used = math.min(summedItem.count, total) + local need = total - used + + summedItem.total = summedItem.total + total + summedItem.count = summedItem.count - used + summedItem.used = summedItem.used + used + summedItem.need = summedItem.need + need + + if need > 0 and summedItem.recipe then + need = math.ceil(need / summedItem.recipe.count) + for ikey,iqty in pairs(summedItem.recipe.ingredients) do + sumItems(ikey, math.ceil(need * iqty)) + end + end + end + + for key, item in pairs(list) do + sumItems(key, item.count) + end + + return Util.filter(summed, function(a) return a.need > 0 end) +end + +local function watchResources(items) + local craftList = { } + + for _,res in pairs(resources) do + if res.low then + local item = Util.shallowCopy(res) + item.nbtHash = res.nbtHash + item.damage = res.damage + if res.ignoreDamage then + item.damage = nil + end + item.count = getItemQuantity(items, item) + if item.count < res.low then + item.displayName = itemDB:getName(res) + item.count = res.low -- - item.count + craftList[uniqueKey(res)] = item + end + end + end + + return craftList +end + +local function craftItems() + local machineStatus = { } + local items = getItems() + local craftList = watchResources(items) + local list = expandList(craftList, items) + jobListGrid:setValues(list) + jobListGrid:update() + jobListGrid:draw() + jobListGrid:sync() + for key, item in pairs(list) do + if item.need > 0 and item.recipe then + craftItem(key, item, items, machineStatus) + dock() + items = getItems() + clearGrid() + elseif item.need > 0 then + item.status = 'no recipe' + item.statusCode = STATUS_ERROR + end + jobListGrid:update() + jobListGrid:draw() + jobListGrid:sync() + end + turtle.setStatus('idle') +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) + + v.name = nil + v.damage = nil + v.nbtHash = nil + t[k] = v + end + + Util.writeTable(RESOURCE_FILE, t) +end + +local function findMachines() + turtle.setStatus('Inspecting machines') + + dock() + + local function getName(dir) + local side = turtle.getAction(dir).side + if Peripheral.isPresent(side) then + local methods = Util.transpose(Peripheral.getMethods(side)) + if methods.getMetadata then + local name = Peripheral.call(side, 'getMetadata').displayName + if name and not string.find(name, '.', 1, true) then + return name + end + elseif methods.getInventoryName then -- 1.7x + return Peripheral.call(side, 'getInventoryName') + end + return Peripheral.getType(side) + end + local _, machine = turtle.getAction(dir).inspect() + if not machine or type(machine) ~= 'table' then + return 'Unknown' + end + return machine.name or 'Unknown' + end + + local index = 0 + + local function getMachine(dir) + local name = getName(dir) + table.insert(machines, { + name = name, + rawName = name, + index = index, + dir = dir, + order = #machines + 1 + }) + end + + repeat + getMachine('down') + getMachine('up') + index = index + 1 + undock() + until not turtle.back() + + local mf = Util.readTable(MACHINES_FILE) or { } + for _,m in pairs(machines) do + local m2 = Util.find(mf, 'order', m.order) + if m2 then + if not m2.rawName then + m2.rawName = m.rawName + end + if m.rawName == m2.rawName then + m.name = m2.name or m.name + end + m.empty = m2.empty + m.ignore = m2.ignore + m.ignoreSlot = m2.ignoreSlot + m.maxCount = m2.maxCount + 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 + + jobListGrid = UI.Grid({ + parent = mon, + sortColumn = 'displayName', + columns = { + { heading = 'Qty', key = 'need', width = 6 }, + { heading = 'Crafting', key = 'displayName', width = (mon.width - 18) / 2 }, + { heading = 'Status', key = 'status', }, + }, + }) + + function jobListGrid: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_SUCCESS then + return colors.lime + end + + return UI.Grid:getRowTextColor(row, selected) + end + + jobListGrid:draw() + jobListGrid:sync() +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.Chooser { + width = 7, + formLabel = 'Ignore Dmg', formKey = 'ignoreDamage', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Ignore damage of item' + }, + [3] = UI.Button { + text = 'Select', event= 'selectMachine', + formLabel = 'Machine' + }, + info = UI.TextArea { + x = 2, ex = -2, y = 6, height = 3, + textColor = colors.gray, + }, + button = UI.Button { + x = 2, y = 9, + text = 'Recipe', event = 'learn', + }, + }, + machines = UI.SlideOut { + backgroundColor = colors.cyan, + titleBar = UI.TitleBar { + title = 'Select Machine', + previousPage = true, + }, + grid = UI.ScrollingGrid { + y = 2, ey = -4, + values = machines, + disableHeader = true, + columns = { + { heading = '', key = 'index', width = 2 }, + { heading = 'Name', key = 'name'}, + }, + sortColumn = 'order', + }, + button1 = UI.Button { + x = -14, y = -2, + text = 'Ok', event = 'setMachine', + }, + button2 = UI.Button { + x = -9, y = -2, + text = 'Cancel', event = 'cancelMachine', + }, + }, + statusBar = UI.StatusBar { } +} + +function itemPage:enable(item) + if item then + self.item = Util.shallowCopy(item) + self.form:setValues(item) + self.titleBar.title = item.displayName or item.name + end + UI.Page.enable(self) + self:focusFirst() +end + +function itemPage.form.info:draw() + local recipe = recipes[uniqueKey(itemPage.item)] + self.value = '' + if recipe and itemPage.item.machine then + self.value = string.format('Crafts %d using the %s machine', + recipe.count, + machines[itemPage.item.machine].name) + end + UI.TextArea.draw(self) +end + +function itemPage.machines.grid:getRowTextColor(row, selected) + if itemPage.item.machine == row.order then + return colors.yellow + end + return UI.Grid:getRowTextColor(row, selected) +end + +--[[ +function itemPage.machines:eventHandler(event) + if event.type == 'grid_focus_row' then + self.statusBar:setStatus(string.format('%d %s', event.selected.index, event.selected.dir)) + 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 == 'learn' then + UI:setPage('learn', self.item) + + elseif event.type == 'setMachine' then + self.item.machine = self.machines.grid:getSelected().order + self.machines:hide() + + elseif event.type == 'cancelMachine' then + self.machines:hide() + + elseif event.type == 'selectMachine' then + local machineCopy = Util.shallowCopy(machines) + Util.filterInplace(machineCopy, function(m) return not m.ignore end) + self.machines.grid:setValues(machineCopy) + if self.item.machine then + local _, index = Util.find(machineCopy, 'order', self.item.machine) + if index then + self.machines.grid:setIndex(index) + end + else + self.machines.grid:setIndex(1) + end + self.machines:show() + + 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', 'low', 'damage', 'nbtHash', 'machine' } + + local filtered = { } + for _,key in pairs(keys) do + filtered[key] = values[key] + end + filtered.low = tonumber(filtered.low) + filtered.machine = self.item.machine + + if values.ignoreDamage == true then + filtered.damage = 0 + filtered.ignoreDamage = true + end + + local key = uniqueKey(filtered) + + resources[key] = filtered + saveResources() + + UI:setPreviousPage() + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +local learnPage = UI.Page { + ingredients = UI.ScrollingGrid { + y = 2, height = 3, + disableHeader = true, + columns = { + { heading = 'Name', key = 'displayName', width = 31 }, + { heading = 'Qty', key = 'count' , width = 5 }, + }, + sortColumn = 'displayName', + }, + grid = UI.ScrollingGrid { + y = 6, height = 5, + disableHeader = true, + columns = { + { heading = 'Name', key = 'displayName', width = 31 }, + { heading = 'Qty', key = 'count' , width = 5 }, + }, + sortColumn = 'displayName', + }, + filter = UI.TextEntry { + x = 20, ex = -2, y = 5, + limit = 50, + shadowText = 'filter', + backgroundColor = colors.lightGray, + backgroundFocusColor = colors.lightGray, + }, + count = UI.TextEntry { + x = 11, y = -1, width = 5, + limit = 50, + }, + button1 = UI.Button { + x = -14, y = -1, + text = 'Ok', event = 'accept', + }, + button2 = UI.Button { + x = -9, y = -1, + text = 'Cancel', event = 'cancel', + }, +} + +function learnPage:enable(target) + self.target = target + self.allItems = getItems() + mergeResources(self.allItems) + + self.filter.value = '' + self.grid.values = self.allItems + self.grid:update() + self.ingredients.values = { } + self.count.value = 1 + + if target.has_recipe then + local recipe = recipes[uniqueKey(target)] + self.count.value = recipe.count + for k,v in pairs(recipe.ingredients) do + self.ingredients.values[k] = + { name = k, count = v, displayName = itemDB:getName(k) } + end + end + self.ingredients:update() + + self:setFocus(self.filter) + UI.Page.enable(self) +end + +function learnPage:draw() + UI.Window.draw(self) + self:write(2, 1, 'Ingredients', nil, colors.yellow) + self:write(2, 5, 'Inventory', nil, colors.yellow) + self:write(2, 12, 'Produces') +end + +function learnPage:eventHandler(event) + + if event.type == 'text_change' and event.element == self.filter then + local t = filterItems(learnPage.allItems, event.text) + self.grid:setValues(t) + self.grid:draw() + + elseif event.type == 'cancel' then + UI:setPreviousPage() + + elseif event.type == 'accept' then + + local recipe = { + count = tonumber(self.count.value) or 1, + ingredients = { }, + } + for key, item in pairs(self.ingredients.values) do + recipe.ingredients[key] = item.count + end + recipes[uniqueKey(self.target)] = recipe + Util.writeTable(RECIPES_FILE, recipes) + + UI:setPreviousPage() + + elseif event.type == 'grid_select' then + if event.element == self.grid then + local key = uniqueKey(event.selected) + if not self.ingredients.values[key] then + self.ingredients.values[key] = Util.shallowCopy(event.selected) + self.ingredients.values[key].count = 0 + end + self.ingredients.values[key].count = self.ingredients.values[key].count + 1 + self.ingredients:update() + self.ingredients:draw() + elseif event.element == self.ingredients then + event.selected.count = event.selected.count - 1 + if event.selected.count == 0 then + self.ingredients.values[uniqueKey(event.selected)] = nil + self.ingredients:update() + end + self.ingredients:draw() + end + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +local machinesPage = UI.Page { + titleBar = UI.TitleBar { + previousPage = true, + title = 'Machines', + }, + grid = UI.ScrollingGrid { + y = 2, ey = -2, + values = machines, + columns = { + { heading = 'Name', key = 'name' }, + { heading = 'Side', key = 'dir', width = 5 }, + { heading = 'Index', key = 'index', width = 5 }, + }, + sortColumn = 'order', + }, + detail = UI.SlideOut { + backgroundColor = colors.cyan, + form = UI.Form { + x = 1, y = 2, ex = -1, ey = -2, + [1] = UI.TextEntry { + formLabel = 'Name', formKey = 'name', help = '...', + limit = 64, + }, + [2] = UI.Chooser { + width = 7, + formLabel = 'Hidden', formKey = 'ignore', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Do not show this machine' + }, + [3] = UI.Chooser { + width = 7, + formLabel = 'Empty', formKey = 'empty', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Check if machine is empty before crafting' + }, + [4] = UI.TextEntry { + formLabel = 'Ignore Slot', formKey = 'ignoreSlot', help = '...', + limit = 4, + }, + [5] = UI.TextEntry { + formLabel = 'Max Craft', formKey = 'maxCount', help = '...', + limit = 4, + }, + }, + statusBar = UI.StatusBar(), + }, + statusBar = UI.StatusBar { + values = 'Select Machine', + }, + accelerators = { + h = 'toggle_hidden', + } +} + +function machinesPage:enable() + self.grid:update() + UI.Page.enable(self) +end + +function machinesPage.detail:eventHandler(event) + if event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + end + return UI.SlideOut.eventHandler(self, event) +end + +function machinesPage.grid:getRowTextColor(row, selected) + if row.ignore then + return colors.yellow + end + return UI.Grid:getRowTextColor(row, selected) +end + +function machinesPage:eventHandler(event) + if event.type == 'grid_select' then + self.detail.form:setValues(event.selected) + self.detail:show() + + elseif event.type == 'toggle_hidden' then + local selected = self.grid:getSelected() + if selected then + selected.ignore = not selected.ignore + Util.writeTable(MACHINES_FILE, machines) + self:draw() + end + + elseif event.type == 'form_complete' then + self.detail.form.values.empty = self.detail.form.values.empty == true + self.detail.form.values.ignore = self.detail.form.values.ignore == true + self.detail.form.values.ignoreSlot = tonumber(self.detail.form.values.ignoreSlot) + self.detail.form.values.maxCount = tonumber(self.detail.form.values.maxCount) + Util.writeTable(MACHINES_FILE, machines) + self.detail:hide() + + elseif event.type == 'form_cancel' then + self.detail:hide() + + else + UI.Page.eventHandler(self, event) + end + return true +end + +local listingPage = UI.Page { + menuBar = UI.MenuBar { + buttons = { + { text = 'Forget', event = 'forget' }, + { text = 'Machines', event = 'machines' }, + { text = 'Refresh', event = 'refresh', x = -9 }, + }, + }, + grid = UI.Grid { + y = 2, height = UI.term.height - 2, + columns = { + { heading = 'Name', key = 'displayName' }, + { heading = 'Qty', key = 'count' , width = 5 }, + { heading = 'Min', key = 'low' , width = 4 }, + }, + sortColumn = 'displayName', + }, + statusBar = UI.Window { + y = -1, + filter = UI.TextEntry { + limit = 50, + shadowText = 'filter', + shadowTextColor = colors.lightGray, + backgroundColor = colors.gray, + backgroundFocusColor = colors.gray, + }, + }, + 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 + 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 + return row +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 == 'machines' then + UI:setPage('machines') + + elseif event.type == 'craft' then + UI:setPage('craft', self.grid:getSelected()) + + 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 = getItems() + mergeResources(self.allItems) + self:applyFilter() +end + +function listingPage:applyFilter() + local t = filterItems(self.allItems, self.filter) + self.grid:setValues(t) +end + +-- randomly errors in 1.7x with "you are not attached to this computer" +print('Inspecting machines') +local retryCount = 0 +while true do + Util.clear(machines) + local s, m = pcall(findMachines) + if not s and m then + _G.printError(m) + else + break + end + retryCount = retryCount + 1 + if retryCount > 3 then + error(m) + end + print('retrying...') +end + +loadResources() +dock() +clearGrid() +jobMonitor() + +UI:setPages({ + listing = listingPage, + machines = machinesPage, + item = itemPage, + learn = learnPage, +}) + +UI:setPage(listingPage) +listingPage:setFocus(listingPage.statusBar.filter) + +Event.on('turtle_abort', function() + UI:exitPullEvents() +end) + +Event.onInterval(30, function() + dock() + if turtle.getFuelLevel() < 100 then + turtle.select(1) + inventoryAdapter:provide({ name = 'minecraft:coal', damage = 1 }, 16, 1) + turtle.refuel() + end + craftItems() +end) + +turtle.setStatus('idle') +UI:pullEvents() +jobListGrid.parent:reset() diff --git a/inventoryManager/apps/inventoryManager.lua b/inventoryManager/apps/inventoryManager.lua new file mode 100644 index 0000000..a4808eb --- /dev/null +++ b/inventoryManager/apps/inventoryManager.lua @@ -0,0 +1,1400 @@ +--[[ + 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. 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. + + 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 + 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 = 'network', -- main inventory + controller = 'none', -- AE / RS controller + + 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 = config.inventory == 'network' and + InventoryAdapter.wrap() or + InventoryAdapter.wrap({ side = config.inventory, facing = config.computerFacing }) + +if not inventoryAdapter then + error('Invalid inventory configuration') +end + +local introspectionModule = device['plethora:introspection'] + +local controllerAdapter +if inventoryAdapter.craft then + controllerAdapter = inventoryAdapter +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 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 + + -- 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 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', + backgroundFocusColor = colors.black, + 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() + local list = { } + for i = 1,16 do + list[i] = introspectionModule.getInventory().getItemMeta(i) + end + return list +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 + 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()