SHOW:
|
|
- or go back to the newest paste.
| 1 | -- ********************************************************************************** -- | |
| 2 | -- ** ** -- | |
| 3 | -- ** Minecraft AE2 Auto-Stocker by RandomBlue (E.J. Wilburn) ** -- | |
| 4 | -- ** ---------------------------------------------------- ** -- | |
| 5 | -- ** ** -- | |
| 6 | -- ** This program automatically crafts items necessary to maintain a minimum ** -- | |
| 7 | -- ** stock level of specific items. The items are configured in a file on ** -- | |
| 8 | -- ** a computercraft computer named stock_list.txt in the stocker directory. ** -- | |
| 9 | -- ** Examine that file for example formatting and details. ** -- | |
| 10 | -- ** ** -- | |
| 11 | -- ** Minimum stock levels and crafting batch sizes are configurable per item. ** -- | |
| 12 | -- ** ** -- | |
| 13 | -- ** The computer must be placed adjacent to a full block ME Interface attached ** -- | |
| 14 | -- ** to an ME Network where both the items are stored and the crafting CPUs are ** -- | |
| 15 | -- ** located. Each item you wish to maintain a stock level for must have ** -- | |
| 16 | -- ** autocrafting enabled for it. ** -- | |
| 17 | -- ** ** -- | |
| 18 | -- ** Arguments ** -- | |
| 19 | -- ** ---------------------------------------------------- ** -- | |
| 20 | -- ** checkFrequency (optional) - How often inventory levels are checked in ** -- | |
| 21 | -- ** seconds. ** -- | |
| 22 | -- ** attachSide (optional) - Side the computer is attached to the ** -- | |
| 23 | -- ** ME Interface (full block version). ** -- | |
| 24 | -- ** stockFileName (optional) - Full path to the file containing stocking ** -- | |
| 25 | -- ** requirements. ** -- | |
| 26 | -- ** ** -- | |
| 27 | -- ** Change Log: ** -- | |
| 28 | -- ** 8th Sep 2015: [v0.1] Initial Release ** -- | |
| 29 | -- ** 11th Sep 2015: [v0.11] Minor bug fix - attempting to crafting 0 items ** -- | |
| 30 | -- ** when current quantity equals minQuantity ** -- | |
| 31 | -- ** ** -- | |
| 32 | -- ** TODO: ** -- | |
| 33 | -- ** 1) Save command line parameters to startup script. ** -- | |
| 34 | -- ** ** -- | |
| 35 | -- ********************************************************************************** -- | |
| 36 | ||
| 37 | -- Parameters with default values. | |
| 38 | local checkFrequency = 15 -- How often inventory levels are checked in seconds. Overridden by passing as the first argument. | |
| 39 | local attachSide = "bottom" -- Side the computer is attached to the ME Interface (full block version). | |
| 40 | -- Overridden by passing as the second argument. | |
| 41 | local stockFileName = "stocker/stock_list.txt" -- Change this if you want the file somewhere else. Can be | |
| 42 | -- overridden via a parameter. | |
| 43 | local recraftDelay = 300 -- Delay, in seconds, before allowing an item to be crafted again. If them item in question exceeds | |
| 44 | -- its min quantity before the delay expires, the delay is reset as it's assumed the job | |
| 45 | -- completed. 300 seconds = 5 minutes | |
| 46 | local delayedItems = {} -- List of delayed items by id:variant with delay time in seconds. Decremented each loop by
| |
| 47 | -- checkFrequency ammount. When the delay hits 0 or lower then the item is removed from | |
| 48 | -- the list. | |
| 49 | ||
| 50 | local DEBUG = false | |
| 51 | ||
| 52 | -- Process the input arguments - storing them to global variables | |
| 53 | local args = { ... }
| |
| 54 | ||
| 55 | function main(args) | |
| 56 | processArgs(args) | |
| 57 | local ae2 = attachToAe2(attachSide) | |
| 58 | local stocks = loadStockFile(stockFileName) | |
| 59 | displayStockingInfo(stocks) | |
| 60 | enableAutoRestart() | |
| 61 | ||
| 62 | while (true) do | |
| 63 | print("[" .. getDisplayTime() .. "] Checking inventory.")
| |
| 64 | updateDelayedItems(delayedItems) | |
| 65 | local allItems = getAllItems(ae2) | |
| 66 | for i=1, #allItems do | |
| 67 | if (allItems[i].is_craftable == true) then | |
| 68 | stockItem(allItems[i], stocks, ae2) | |
| 69 | end | |
| 70 | end | |
| 71 | os.sleep(checkFrequency) | |
| 72 | end | |
| 73 | end | |
| 74 | ||
| 75 | function isValidSide(side) | |
| 76 | if (side == "left" or side == "right" or side == "top" or side == "bottom" or side == "front" or side == "back") then | |
| 77 | return true | |
| 78 | else | |
| 79 | return false | |
| 80 | end | |
| 81 | end | |
| 82 | ||
| 83 | function processArgs(args) | |
| 84 | if (#args >= 1) then | |
| 85 | assert(type(args[1]) == "number", "The first parameter (checkFrequency) must be a number.") | |
| 86 | checkFrequency = args[1] | |
| 87 | end | |
| 88 | ||
| 89 | if (#args > 1) then | |
| 90 | assert(type(args[2]) == "string", "The second parameter (attachSide) must be a string.") | |
| 91 | attachSide = args[2]:lower() | |
| 92 | end | |
| 93 | assert(isValidSide(attachSide), "The attachSide parameter must be a valid side: left, right, front, back, top, bottom") | |
| 94 | ||
| 95 | if (#args > 2) then | |
| 96 | assert(type(args[3]) == "string", "The third parameter (stockFileName) must be a string.") | |
| 97 | stockFileName = args[3] | |
| 98 | end | |
| 99 | assert(fs.exists(stockFileName), "The stock file does not exist: " .. stockFileName) | |
| 100 | end | |
| 101 | ||
| 102 | function attachToAe2(attachSide) | |
| 103 | -- Make sure the attached device is actually an ME Interface. | |
| 104 | assert(peripheral.getType(attachSide) == "tileinterface", "The computer must be attached to a full block " .. | |
| 105 | "ME Inteface on the specified side.") | |
| 106 | return peripheral.wrap(attachSide) | |
| 107 | end | |
| 108 | ||
| 109 | function loadStockFile(stockFileName) | |
| 110 | local stockFile = fs.open(stockFileName, "r") | |
| 111 | local stockFileContents = stockFile.readAll(); | |
| 112 | stockFile.close(); | |
| 113 | local outputStocks = textutils.unserialize(stockFileContents) | |
| 114 | ||
| 115 | if (DEBUG) then | |
| 116 | print("Stock file: ")
| |
| 117 | print(stockFileContents) | |
| 118 | print("Output stocks length: " .. #outputStocks)
| |
| 119 | print("Output stocks: ")
| |
| 120 | for i=1, #outputStocks do | |
| 121 | print("itemId: " .. outputStocks[i].itemId)
| |
| 122 | print("variant: " .. outputStocks[i].variant)
| |
| 123 | print("minQuantity: " .. outputStocks[i].minQuantity)
| |
| 124 | print("batchSize: " .. outputStocks[i].batchSize)
| |
| 125 | end | |
| 126 | end | |
| 127 | ||
| 128 | assert(#outputStocks > 0, "There are no entries in the " .. stockFileName .. " file.") | |
| 129 | return outputStocks | |
| 130 | end | |
| 131 | ||
| 132 | function displayStockingInfo(stocks) | |
| 133 | print("Stocking info:")
| |
| 134 | for i=1, #stocks do | |
| 135 | print(" itemId: " .. stocks[i].itemId .. ":" .. stocks[i].variant .. " minQuantity: " .. stocks[i].minQuantity ..
| |
| 136 | " batchSize: " .. stocks[i].batchSize) | |
| 137 | end | |
| 138 | end | |
| 139 | ||
| 140 | function getAllItems(ae2) | |
| 141 | local outputAllItems = ae2.getAvailableItems() | |
| 142 | assert(outputAllItems ~= nil, "No craftable items found in this AE2 network.") | |
| 143 | assert(#outputAllItems > 0, "No craftable items found in this AE2 network.") | |
| 144 | return outputAllItems | |
| 145 | end | |
| 146 | ||
| 147 | function isCpuAvailable(ae2) | |
| 148 | local cpus = ae2.getCraftingCPUs() | |
| 149 | for i=1, #cpus do | |
| 150 | if (cpus[i].busy == false) then return true end | |
| 151 | end | |
| 152 | return false | |
| 153 | end | |
| 154 | ||
| 155 | function findStockSetting(fingerprint, stocks) | |
| 156 | for i=1, #stocks do | |
| 157 | if (stocks[i].itemId == fingerprint.id and stocks[i].variant == fingerprint.dmg) then | |
| 158 | return stocks[i] | |
| 159 | end | |
| 160 | end | |
| 161 | return nil | |
| 162 | end | |
| 163 | ||
| 164 | function stockItem(currItem, stocks, ae2) | |
| 165 | local stockSetting = findStockSetting(currItem.fingerprint, stocks) | |
| 166 | ||
| 167 | if (stockSetting == nil or currItem.size >= stockSetting.minQuantity or isDelayed(currItem.fingerprint, delayedItems) | |
| 168 | or isCpuAvailable(ae2) == false) then return end | |
| 169 | ||
| 170 | local neededAmount = math.ceil((stockSetting.minQuantity - currItem.size) / stockSetting.batchSize) * stockSetting.batchSize | |
| 171 | ||
| 172 | ae2.requestCrafting(currItem.fingerprint, neededAmount) | |
| 173 | delayItem(currItem.fingerprint, delayedItems) | |
| 174 | print("[" .. getDisplayTime() .. "] Item " .. stockSetting.displayName ..
| |
| 175 | " is below its min stock level of " .. stockSetting.minQuantity .. ". Crafting " .. neededAmount .. " more.") | |
| 176 | end | |
| 177 | ||
| 178 | function getDisplayTime() | |
| 179 | return textutils.formatTime(os.time(), false) | |
| 180 | end | |
| 181 | ||
| 182 | function delayItem(fingerprint, delayedItems) | |
| 183 | local fullItemName = fingerprintToFullName(fingerprint) | |
| 184 | ||
| 185 | if(delayedItems == nil) then | |
| 186 | delayedItems = {}
| |
| 187 | end | |
| 188 | ||
| 189 | for i=1, #delayedItems do | |
| 190 | if (delayedItems[i].fullName == fullItemName) then | |
| 191 | delayedItems[i].delay = recraftDelay | |
| 192 | return | |
| 193 | end | |
| 194 | end | |
| 195 | ||
| 196 | local delayedItem = {fullName = fullItemName, delay = recraftDelay}
| |
| 197 | delayedItems[#delayedItems+1] = delayedItem | |
| 198 | end | |
| 199 | ||
| 200 | function updateDelayedItems(delayedItems) | |
| 201 | if (delayedItems == nil or #delayedItems < 1) then return end | |
| 202 | ||
| 203 | local removeIndexes = {}
| |
| 204 | for i=1, #delayedItems do | |
| 205 | currItem = delayedItems[i] | |
| 206 | currItem.delay = currItem.delay - checkFrequency | |
| 207 | if (currItem.delay < 0) then | |
| 208 | table.insert(removeIndexes, i) | |
| 209 | end | |
| 210 | end | |
| 211 | ||
| 212 | -- This should remove items from the end of the list towards the beginning | |
| 213 | -- so the list being reordered won't matter. | |
| 214 | for i=1, #removeIndexes do | |
| 215 | table.remove(delayedItems, removeIndexes[i]) | |
| 216 | end | |
| 217 | end | |
| 218 | ||
| 219 | function fingerprintToFullName(fingerprint) | |
| 220 | return fingerprint.id .. ":" .. fingerprint.dmg | |
| 221 | end | |
| 222 | ||
| 223 | function isDelayed(fingerprint, delayedItems) | |
| 224 | if (delayedItems == nil or #delayedItems < 1) then return false end | |
| 225 | ||
| 226 | local fullItemName = fingerprintToFullName(fingerprint) | |
| 227 | for i=1, #delayedItems do | |
| 228 | if (delayedItems[i].fullName == fullItemName and delayedItems[i].delay > 0) then | |
| 229 | return true | |
| 230 | end | |
| 231 | end | |
| 232 | ||
| 233 | return false | |
| 234 | end | |
| 235 | ||
| 236 | function enableAutoRestart() | |
| 237 | -- Skip this if any startup file already exists. | |
| 238 | -- Let the user manaully delete or edit the startup file at that point. | |
| 239 | -- Notify the user. | |
| 240 | if (fs.exists("startup") == true) then
| |
| 241 | print("Startup file already exists.")
| |
| 242 | return | |
| 243 | end | |
| 244 | ||
| 245 | outputFile = fs.open("startup", "w")
| |
| 246 | ||
| 247 | -- Write an info message so that people know how to get out of auto-resume | |
| 248 | outputFile.write("\nprint(\"Running auto-restart...\")\n")
| |
| 249 | outputFile.write("print(\"If you want to stop auto-resume and restore original state:\")\n")
| |
| 250 | outputFile.write("print(\"1) Hold Ctrl-T until the program terminates\")\n")
| |
| 251 | outputFile.write("print(\"2) Type \\\"rm startup\\\" (without quotes) and hit Enter\")\n")
| |
| 252 | outputFile.write("print(\"\")\n\n")
| |
| 253 | ||
| 254 | -- Write the code required to restart the turtle | |
| 255 | outputFile.write("shell.run(\"")
| |
| 256 | outputFile.write(shell.getRunningProgram()) | |
| 257 | outputFile.write("\")\n")
| |
| 258 | outputFile.close() | |
| 259 | end | |
| 260 | ||
| 261 | -- Start the actual program | |
| 262 | main(args) |