-- ********************************************************************************** -- -- ** ** -- -- ** Minecraft AE2 Auto-Stocker by RandomBlue (E.J. Wilburn) ** -- -- ** ---------------------------------------------------- ** -- -- ** ** -- -- ** This program automatically crafts items necessary to maintain a minimum ** -- -- ** stock level of specific items. The items are configured in a file on ** -- -- ** a computercraft computer named stock_list.txt in the stocker directory. ** -- -- ** Examine that file for example formatting and details. ** -- -- ** ** -- -- ** Minimum stock levels and crafting batch sizes are configurable per item. ** -- -- ** ** -- -- ** The computer must be placed adjacent to a full block ME Interface attached ** -- -- ** to an ME Network where both the items are stored and the crafting CPUs are ** -- -- ** located. Each item you wish to maintain a stock level for must have ** -- -- ** autocrafting enabled for it. ** -- -- ** ** -- -- ** Arguments ** -- -- ** ---------------------------------------------------- ** -- -- ** checkFrequency (optional) - How often inventory levels are checked in ** -- -- ** seconds. ** -- -- ** attachSide (optional) - Side the computer is attached to the ** -- -- ** ME Interface (full block version). ** -- -- ** stockFileName (optional) - Full path to the file containing stocking ** -- -- ** requirements. ** -- -- ** ** -- -- ** Change Log: ** -- -- ** 8th Sep 2015: [v0.1] Initial Release ** -- -- ** 11th Sep 2015: [v0.11] Minor bug fix - attempting to crafting 0 items ** -- -- ** when current quantity equals minQuantity ** -- -- ** ** -- -- ** TODO: ** -- -- ** 1) Save command line parameters to startup script. ** -- -- ** ** -- -- ********************************************************************************** -- -- Parameters with default values. local checkFrequency = 15 -- How often inventory levels are checked in seconds. Overridden by passing as the first argument. local attachSide = "bottom" -- Side the computer is attached to the ME Interface (full block version). -- Overridden by passing as the second argument. local stockFileName = "stocker/stock_list.txt" -- Change this if you want the file somewhere else. Can be -- overridden via a parameter. local recraftDelay = 300 -- Delay, in seconds, before allowing an item to be crafted again. If them item in question exceeds -- its min quantity before the delay expires, the delay is reset as it's assumed the job -- completed. 300 seconds = 5 minutes local delayedItems = {} -- List of delayed items by id:variant with delay time in seconds. Decremented each loop by -- checkFrequency ammount. When the delay hits 0 or lower then the item is removed from -- the list. local DEBUG = false -- Process the input arguments - storing them to global variables local args = { ... } function main(args) processArgs(args) local ae2 = attachToAe2(attachSide) local stocks = loadStockFile(stockFileName) displayStockingInfo(stocks) enableAutoRestart() while (true) do print("[" .. getDisplayTime() .. "] Checking inventory.") updateDelayedItems(delayedItems) local allItems = getAllItems(ae2) for i=1, #allItems do if (allItems[i].is_craftable == true) then stockItem(allItems[i], stocks, ae2) end end os.sleep(checkFrequency) end end function isValidSide(side) if (side == "left" or side == "right" or side == "top" or side == "bottom" or side == "front" or side == "back") then return true else return false end end function processArgs(args) if (#args >= 1) then assert(type(args[1]) == "number", "The first parameter (checkFrequency) must be a number.") checkFrequency = args[1] end if (#args > 1) then assert(type(args[2]) == "string", "The second parameter (attachSide) must be a string.") attachSide = args[2]:lower() end assert(isValidSide(attachSide), "The attachSide parameter must be a valid side: left, right, front, back, top, bottom") if (#args > 2) then assert(type(args[3]) == "string", "The third parameter (stockFileName) must be a string.") stockFileName = args[3] end assert(fs.exists(stockFileName), "The stock file does not exist: " .. stockFileName) end function attachToAe2(attachSide) -- Make sure the attached device is actually an ME Interface. assert(peripheral.getType(attachSide) == "tileinterface", "The computer must be attached to a full block " .. "ME Inteface on the specified side.") return peripheral.wrap(attachSide) end function loadStockFile(stockFileName) local stockFile = fs.open(stockFileName, "r") local stockFileContents = stockFile.readAll(); stockFile.close(); local outputStocks = textutils.unserialize(stockFileContents) if (DEBUG) then print("Stock file: ") print(stockFileContents) print("Output stocks length: " .. #outputStocks) print("Output stocks: ") for i=1, #outputStocks do print("itemId: " .. outputStocks[i].itemId) print("variant: " .. outputStocks[i].variant) print("minQuantity: " .. outputStocks[i].minQuantity) print("batchSize: " .. outputStocks[i].batchSize) end end assert(#outputStocks > 0, "There are no entries in the " .. stockFileName .. " file.") return outputStocks end function displayStockingInfo(stocks) print("Stocking info:") for i=1, #stocks do print(" itemId: " .. stocks[i].itemId .. ":" .. stocks[i].variant .. " minQuantity: " .. stocks[i].minQuantity .. " batchSize: " .. stocks[i].batchSize) end end function getAllItems(ae2) local outputAllItems = ae2.getAvailableItems() assert(outputAllItems ~= nil, "No craftable items found in this AE2 network.") assert(#outputAllItems > 0, "No craftable items found in this AE2 network.") return outputAllItems end function isCpuAvailable(ae2) local cpus = ae2.getCraftingCPUs() for i=1, #cpus do if (cpus[i].busy == false) then return true end end return false end function findStockSetting(fingerprint, stocks) for i=1, #stocks do if (stocks[i].itemId == fingerprint.id and stocks[i].variant == fingerprint.dmg) then return stocks[i] end end return nil end function stockItem(currItem, stocks, ae2) local stockSetting = findStockSetting(currItem.fingerprint, stocks) if (stockSetting == nil or currItem.size >= stockSetting.minQuantity or isDelayed(currItem.fingerprint, delayedItems) or isCpuAvailable(ae2) == false) then return end local neededAmount = math.ceil((stockSetting.minQuantity - currItem.size) / stockSetting.batchSize) * stockSetting.batchSize ae2.requestCrafting(currItem.fingerprint, neededAmount) delayItem(currItem.fingerprint, delayedItems) print("[" .. getDisplayTime() .. "] Item " .. stockSetting.displayName .. " is below its min stock level of " .. stockSetting.minQuantity .. ". Crafting " .. neededAmount .. " more.") end function getDisplayTime() return textutils.formatTime(os.time(), false) end function delayItem(fingerprint, delayedItems) local fullItemName = fingerprintToFullName(fingerprint) if(delayedItems == nil) then delayedItems = {} end for i=1, #delayedItems do if (delayedItems[i].fullName == fullItemName) then delayedItems[i].delay = recraftDelay return end end local delayedItem = {fullName = fullItemName, delay = recraftDelay} delayedItems[#delayedItems+1] = delayedItem end function updateDelayedItems(delayedItems) if (delayedItems == nil or #delayedItems < 1) then return end local removeIndexes = {} for i=1, #delayedItems do currItem = delayedItems[i] currItem.delay = currItem.delay - checkFrequency if (currItem.delay < 0) then table.insert(removeIndexes, i) end end -- This should remove items from the end of the list towards the beginning -- so the list being reordered won't matter. for i=1, #removeIndexes do table.remove(delayedItems, removeIndexes[i]) end end function fingerprintToFullName(fingerprint) return fingerprint.id .. ":" .. fingerprint.dmg end function isDelayed(fingerprint, delayedItems) if (delayedItems == nil or #delayedItems < 1) then return false end local fullItemName = fingerprintToFullName(fingerprint) for i=1, #delayedItems do if (delayedItems[i].fullName == fullItemName and delayedItems[i].delay > 0) then return true end end return false end function enableAutoRestart() -- Skip this if any startup file already exists. -- Let the user manaully delete or edit the startup file at that point. -- Notify the user. if (fs.exists("startup") == true) then print("Startup file already exists.") return end outputFile = fs.open("startup", "w") -- Write an info message so that people know how to get out of auto-resume outputFile.write("\nprint(\"Running auto-restart...\")\n") outputFile.write("print(\"If you want to stop auto-resume and restore original state:\")\n") outputFile.write("print(\"1) Hold Ctrl-T until the program terminates\")\n") outputFile.write("print(\"2) Type \\\"rm startup\\\" (without quotes) and hit Enter\")\n") outputFile.write("print(\"\")\n\n") -- Write the code required to restart the turtle outputFile.write("shell.run(\"") outputFile.write(shell.getRunningProgram()) outputFile.write("\")\n") outputFile.close() end -- Start the actual program main(args)