Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- AE Monitor Turtle Script v7.3
- Monitors AE2 system, provides chat alerts and commands.
- Changes:
- - (v7.2) Removed redundant meBridge.isItemCraftable() checks.
- - (v7.3) Corrected JSON text component error in MinStockCheck announcement
- by converting amountToCraft to string using tostring().
- ]]
- --#region Configuration
- local ME_BRIDGE_PERIPHERAL_NAME = "meBridge"
- local CHAT_BOX_PERIPHERAL_NAME = "chatBox"
- local COMMAND_PREFIX = "@ae"
- local CHAT_BOT_NAME = "AEMonitor"
- local CHAT_BOT_BRACKETS = "[]"
- local CHAT_BOT_BRACKET_COLOR_MOTD = "&5" -- Magenta
- local DEFAULT_STORAGE_ALERT_THRESHOLD_PERCENT = 20
- local STORAGE_CHECK_INTERVAL = 60 -- Seconds
- local ITEM_TRACKING_INTERVAL = 2 -- Seconds
- local HOT_ITEMS_DEFAULT_COUNT = 10
- local MIN_STOCK_CHECK_INTERVAL = 60 -- Seconds
- local MIN_STOCK_BUFFER_PERCENT = 0.05
- local DEBUG_MODE = true
- local DEBUG_LOG_FILE = "aemonitor.log"
- local MINIMUMS_FILE_PATH = "aemonitor_mins.json"
- --#endregion
- --#region Peripherals
- local meBridge = peripheral.find(ME_BRIDGE_PERIPHERAL_NAME)
- local chatBox = peripheral.find(CHAT_BOX_PERIPHERAL_NAME)
- --#endregion
- --#region Debug Logger & Safe Serializer
- local function logDebug(message) if not DEBUG_MODE then return end; local logMessage = string.format("[%s] %s\n", os.date("%Y-%m-%d %H:%M:%S"), message); local file, err = fs.open(DEBUG_LOG_FILE, "a"); if file then file.write(logMessage); file.close() else print("DEBUG LOG ERROR: Could not open " .. DEBUG_LOG_FILE .. ": " .. (err or "unknown error")) end end
- local function safeSerializeItemForLog(item_data, context_msg) context_msg = context_msg or ""; if not item_data or type(item_data) ~= "table" then return context_msg .. " (Not a table or nil)" end; local success, result = pcall(textutils.serialize, item_data, {compact = true, max_depth = 2}); if success then return context_msg .. result else local name = item_data.name or "N/A"; local displayName = item_data.displayName or "N/A"; local qty_val = item_data.count or item_data.amount; local nbt_present = item_data.nbt and "present" or "absent"; local fingerprint_present = item_data.fingerprint and "present" or "absent"; local fallback_str = string.format("%s [Serialization Fallback] Name: %s, DispName: %s, QtyField: %s (type: %s), NBT: %s, Fingerprint: %s. (Original Error: %s)", context_msg, tostring(name), tostring(displayName), tostring(qty_val), type(qty_val), nbt_present, fingerprint_present, tostring(result)); return fallback_str end end
- --#endregion
- --#region State Variables
- logDebug("Script initializing state variables..."); local currentStorageAlertThreshold = DEFAULT_STORAGE_ALERT_THRESHOLD_PERCENT / 100; local lastNotifiedFreePercentage = 1.0; local anAlertHasBeenSentSinceRecovery = false; local storageCheckTimerId = nil; local minStockCheckTimerId = nil; local trackedItems = {}; local minimum_stock_rules = {}; logDebug("State variables initialized.")
- --#endregion
- --#region Minimum Stock Persistence
- local function loadMinimumStockRules() logDebug("Loading minimum stock rules from " .. MINIMUMS_FILE_PATH); if fs.exists(MINIMUMS_FILE_PATH) then local file, err = fs.open(MINIMUMS_FILE_PATH, "r"); if file then local serializedData = file.readAll(); file.close(); local success, data = pcall(textutils.unserialiseJSON, serializedData); if success and type(data) == "table" then minimum_stock_rules = data; logDebug("Successfully loaded " .. #minimum_stock_rules .. " minimum stock rules.") else logDebug("Failed to unserialise minimum stock rules or data is not a table. Error: " .. tostring(data)); minimum_stock_rules = {} end else logDebug("Could not open minimums file for reading: " .. (err or "unknown")); minimum_stock_rules = {} end else logDebug("Minimums file does not exist. Starting with empty rules."); minimum_stock_rules = {} end end
- local function saveMinimumStockRules() logDebug("Saving " .. #minimum_stock_rules .. " minimum stock rules to " .. MINIMUMS_FILE_PATH); local serializedData = textutils.serialiseJSON(minimum_stock_rules, {compact = false}); if not serializedData then logDebug("Failed to serialize minimum stock rules for saving."); announce({{text="CRITICAL ERROR: Could not serialize minimum stock rules to save them!", color=COLORS.RED, bold=true}}); return false end; local file, err = fs.open(MINIMUMS_FILE_PATH, "w"); if file then file.write(serializedData); file.close(); logDebug("Minimum stock rules saved successfully."); return true else logDebug("Could not open minimums file for writing: " .. (err or "unknown")); announce({{text="ERROR: Could not save minimum stock rules to disk!", color=COLORS.RED}}); return false end end
- --#endregion
- --#region Minecraft JSON Text Component Colors
- local COLORS = { BLACK = "black", DARK_BLUE = "dark_blue", DARK_GREEN = "dark_green", DARK_AQUA = "dark_aqua", DARK_RED = "dark_red", DARK_PURPLE = "dark_purple", GOLD = "gold", GRAY = "gray", DARK_GRAY = "dark_gray", BLUE = "blue", GREEN = "green", AQUA = "aqua", RED = "red", LIGHT_PURPLE = "light_purple", YELLOW = "yellow", WHITE = "white", RESET = "reset" }
- --#endregion
- --#region Helper Functions
- local function sendFormattedChat(messageComponents, recipientUsername) logDebug("Attempting to send formatted chat. Recipient: " .. (recipientUsername or "ALL")); if not chatBox then local plainText = ""; for _, comp in ipairs(messageComponents) do plainText = plainText .. (comp.text or "") end; local noChatMsg = "[" .. CHAT_BOT_NAME .. "-NoChatBox" .. (recipientUsername and (" to " .. recipientUsername) or "") .. "] " .. plainText; print(noChatMsg); logDebug("ChatBox not found. Printed to console: " .. noChatMsg); return end; local jsonMessage = textutils.serialiseJSON(messageComponents); if not jsonMessage then local fallbackMsg = "Error: Could not serialize message for formatted sending."; logDebug("JSON Serialization Error for message components. Fallback: " .. fallbackMsg .. " Data: " .. textutils.serialize(messageComponents, {max_depth=3})); if recipientUsername then chatBox.sendMessageToPlayer(fallbackMsg, recipientUsername, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR_MOTD) else chatBox.sendMessage(fallbackMsg, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR_MOTD) end; return end; local success, err; if recipientUsername then success, err = chatBox.sendFormattedMessageToPlayer(jsonMessage, recipientUsername, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR_MOTD) else success, err = chatBox.sendFormattedMessage(jsonMessage, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR_MOTD) end; if not success then logDebug("Error sending formatted message: " .. (err or "Unknown error") .. ". Attempting fallback to plain text."); local plainText = ""; for _, comp in ipairs(messageComponents) do plainText = plainText .. (comp.text or "") end; if recipientUsername then chatBox.sendMessageToPlayer(plainText, recipientUsername, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR_MOTD) else chatBox.sendMessage(plainText, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR_MOTD) end else logDebug("Formatted message sent successfully.") end; os.sleep(0.6) end
- local function announce(messageComponents) sendFormattedChat(messageComponents) end
- local function findSystemItem(itemNamePart) logDebug("findSystemItem called with part: '" .. itemNamePart .. "'"); if not meBridge then logDebug("findSystemItem: ME Bridge not available."); return nil, "ME Bridge not available." end; itemNamePart = string.lower(itemNamePart); local candidates = {}; logDebug("findSystemItem: Calling meBridge.listItems()"); local allItems, errItems = meBridge.listItems(); if errItems then logDebug("findSystemItem: Error listing items from meBridge: " .. errItems) else logDebug("findSystemItem: meBridge.listItems() returned " .. (#allItems or 0) .. " items."); if DEBUG_MODE then for i = 1, math.min(5, #allItems or 0) do logDebug(safeSerializeItemForLog(allItems[i], string.format(" listItems #%d: ", i))) end end; for i, item in ipairs(allItems or {}) do if item and item.name and (string.find(string.lower(item.name), itemNamePart, 1, true) or (item.displayName and string.find(string.lower(item.displayName), itemNamePart, 1, true))) then item.source = "listItems"; table.insert(candidates, item) end end end; logDebug("findSystemItem: Calling meBridge.listCraftableItems()"); local craftableItems, errCraftable = meBridge.listCraftableItems(); if errCraftable then logDebug("findSystemItem: Error listing craftable items: " .. errCraftable) else logDebug("findSystemItem: meBridge.listCraftableItems() returned " .. (#craftableItems or 0) .. " items."); if DEBUG_MODE then for i = 1, math.min(5, #craftableItems or 0) do logDebug(safeSerializeItemForLog(craftableItems[i], string.format(" craftableItems #%d: ", i))) end end; for _, item in ipairs(craftableItems or {}) do local alreadyFound = false; if item and item.name then for _, existingCandidate in ipairs(candidates) do if (existingCandidate.fingerprint and item.fingerprint and existingCandidate.fingerprint == item.fingerprint) or (existingCandidate.name == item.name and existingCandidate.nbt == item.nbt) then alreadyFound = true; existingCandidate.isCraftable = true; break end end; if not alreadyFound and (string.find(string.lower(item.name), itemNamePart, 1, true) or (item.displayName and string.find(string.lower(item.displayName), itemNamePart, 1, true))) then item.isCraftable = true; item.source = "listCraftableItems"; table.insert(candidates, item) end end end end; logDebug("findSystemItem: Total candidates found: " .. #candidates); if #candidates == 0 then logDebug("findSystemItem: No candidates found."); return nil, "No item found matching '" .. itemNamePart .. "'." elseif #candidates > 1 then logDebug("findSystemItem: Multiple candidates. Attempting to find exact match."); local exactMatches = {}; for _, item in ipairs(candidates) do if item and item.name and (string.lower(item.name) == itemNamePart or (item.displayName and string.lower(item.displayName) == itemNamePart)) then table.insert(exactMatches, item) end end; if #exactMatches == 1 then logDebug("findSystemItem: Exact match found."); return exactMatches[1] end; if #exactMatches > 1 then candidates = exactMatches; logDebug("findSystemItem: Multiple exact matches, using those.") end; local displayNames = {}; for i = 1, math.min(#candidates, 5) do table.insert(displayNames, "'" .. (candidates[i].displayName or candidates[i].name) .. "'") end; local msg = "Multiple items found: " .. table.concat(displayNames, ", "); if #candidates > 5 then msg = msg .. " and " .. (#candidates - 5) .. " more." end; logDebug("findSystemItem: Returning multiple items message: " .. msg); return nil, msg .. " Please be more specific." end; logDebug("findSystemItem: Single candidate found: " .. safeSerializeItemForLog(candidates[1])); return candidates[1] end
- local function getItemFilter(itemData) if itemData.fingerprint then return {fingerprint = itemData.fingerprint} else local filter = {name = itemData.name}; if itemData.nbt then filter.nbt = itemData.nbt end; return filter end end
- --#endregion
- --#region Core Logic: Storage Check & Minimum Stock Check
- local function checkStorageAndAlert() logDebug("checkStorageAndAlert: Starting storage check."); if not meBridge then logDebug("checkStorageAndAlert: ME Bridge not found."); print("ME Bridge not found for storage check."); return end; logDebug("checkStorageAndAlert: Calling getTotalItemStorage and getUsedItemStorage."); local totalBytes, errTotal = meBridge.getTotalItemStorage(); local usedBytes, errUsed = meBridge.getUsedItemStorage(); logDebug(string.format("checkStorageAndAlert: TotalBytes=%s (err:%s), UsedBytes=%s (err:%s)", tostring(totalBytes), tostring(errTotal), tostring(usedBytes), tostring(errUsed))); if errTotal or errUsed or totalBytes == nil or usedBytes == nil then logDebug("checkStorageAndAlert: Error getting ME storage byte counts."); print("Error getting ME storage byte counts: " .. textutils.serialize({errTotal, errUsed})); return end; totalBytes = totalBytes or 0; usedBytes = usedBytes or 0; local currentFreePercentage; if totalBytes > 0 then currentFreePercentage = (totalBytes - usedBytes) / totalBytes else currentFreePercentage = 1.0 end; logDebug(string.format("checkStorageAndAlert: currentFreePercentage=%.2f, currentStorageAlertThreshold=%.2f, lastNotifiedFreePercentage=%.2f, anAlertHasBeenSentSinceRecovery=%s", currentFreePercentage, currentStorageAlertThreshold, lastNotifiedFreePercentage, tostring(anAlertHasBeenSentSinceRecovery))); local defaultThreshold = DEFAULT_STORAGE_ALERT_THRESHOLD_PERCENT / 100; if currentFreePercentage < currentStorageAlertThreshold then if currentFreePercentage < lastNotifiedFreePercentage then logDebug("checkStorageAndAlert: Storage low, sending alert."); local usedPercentDisplay = (1 - currentFreePercentage) * 100; local currentThresholdDisplay = currentStorageAlertThreshold * 100; announce({{text = "WARNING: ME Item Storage at ", color = COLORS.YELLOW},{text = string.format("%.1f%% full (%.1f%% free)", usedPercentDisplay, currentFreePercentage * 100), color = COLORS.RED, bold = true},{text = ". Below alert level of ", color = COLORS.YELLOW},{text = string.format("%.0f%% free.", currentThresholdDisplay), color = COLORS.RED, bold = true}}); lastNotifiedFreePercentage = currentFreePercentage; currentStorageAlertThreshold = math.max(0.01, math.floor(currentFreePercentage * 100 - 1) / 100); anAlertHasBeenSentSinceRecovery = true; logDebug("checkStorageAndAlert: Alert sent. New threshold=" .. currentStorageAlertThreshold) else logDebug("checkStorageAndAlert: Storage low, but not lower than last notification or new threshold. No new alert.") end elseif currentFreePercentage >= defaultThreshold and anAlertHasBeenSentSinceRecovery then logDebug("checkStorageAndAlert: Storage recovered above default threshold."); announce({{text = "RESOLVED: ME Item Storage now at ", color = COLORS.GREEN},{text = string.format("%.1f%% free.", currentFreePercentage * 100), color = COLORS.AQUA},{text = " Above default alert level of ", color = COLORS.GREEN},{text = string.format("%.0f%%.", DEFAULT_STORAGE_ALERT_THRESHOLD_PERCENT), color = COLORS.AQUA}}); currentStorageAlertThreshold = defaultThreshold; lastNotifiedFreePercentage = 1.0; anAlertHasBeenSentSinceRecovery = false; logDebug("checkStorageAndAlert: Recovery alert sent. Threshold reset to default.") elseif currentFreePercentage >= currentStorageAlertThreshold and currentFreePercentage < defaultThreshold and anAlertHasBeenSentSinceRecovery then logDebug("checkStorageAndAlert: Storage improved but not above default. Resetting current alert threshold to default for next cycle."); currentStorageAlertThreshold = defaultThreshold; lastNotifiedFreePercentage = currentFreePercentage else logDebug("checkStorageAndAlert: Storage OK. No alert action needed.") end end
- local function checkMinimumStockLevels()
- logDebug("MinStockCheck: Starting check for " .. #minimum_stock_rules .. " rules.")
- if not meBridge then logDebug("MinStockCheck: ME Bridge not found."); return end
- if #minimum_stock_rules == 0 then logDebug("MinStockCheck: No minimum stock rules defined."); return end
- for i, rule in ipairs(minimum_stock_rules) do
- logDebug("MinStockCheck: Checking rule #" .. i .. " for " .. rule.displayName .. " (Min: " .. rule.minAmount .. ") Filter: " .. safeSerializeItemForLog(rule.itemFilter))
- local itemInfo, errGet = meBridge.getItem(rule.itemFilter)
- local currentQuantity = 0
- if itemInfo and itemInfo.count then currentQuantity = itemInfo.count
- elseif itemInfo and itemInfo.amount then currentQuantity = itemInfo.amount end
- logDebug("MinStockCheck: " .. rule.displayName .. " - Current quantity: " .. currentQuantity)
- if errGet and not itemInfo then logDebug("MinStockCheck: Error getting item info for " .. rule.displayName .. " (or 0 in stock): " .. tostring(errGet)) end
- if currentQuantity < rule.minAmount then
- local targetStock = rule.minAmount * (1 + MIN_STOCK_BUFFER_PERCENT)
- local amountToCraft = math.ceil(targetStock - currentQuantity)
- if amountToCraft < 1 then amountToCraft = 1 end
- logDebug("MinStockCheck: " .. rule.displayName .. " is low (Current: " .. currentQuantity .. ", Min: " .. rule.minAmount .. "). Target stock with buffer: " .. targetStock .. ". Need to craft: " .. amountToCraft)
- logDebug("MinStockCheck: Proceeding to craft " .. rule.displayName .. " based on existing rule (assumed craftable).")
- local craftArg = {count = amountToCraft}
- if rule.itemFilter.fingerprint then craftArg.fingerprint = rule.itemFilter.fingerprint
- else craftArg.name = rule.itemFilter.name; if rule.itemFilter.nbt then craftArg.nbt = rule.itemFilter.nbt end end
- logDebug("MinStockCheck: Attempting to craft " .. amountToCraft .. " of " .. rule.displayName .. ". Arg: " .. safeSerializeItemForLog(craftArg))
- local success, errCraft = meBridge.craftItem(craftArg)
- if success then
- announce({ -- *** THIS IS THE CORRECTED PART ***
- {text="MinStock: ", color=COLORS.BLUE}, {text="Auto-crafting ", color=COLORS.GRAY},
- {text=tostring(amountToCraft), color=COLORS.WHITE}, -- Used tostring() here
- {text=" of ", color=COLORS.GRAY},
- {text=rule.displayName, color=COLORS.AQUA}, {text=" to meet minimum.", color=COLORS.GRAY}
- })
- logDebug("MinStockCheck: Crafting job started for " .. rule.displayName)
- else
- announce({{text="MinStock: ", color=COLORS.RED}, {text="Failed to auto-craft ", color=COLORS.YELLOW},{text=rule.displayName, color=COLORS.AQUA}, {text=". Error: ", color=COLORS.YELLOW},{text=errCraft or "Unknown ME error", color=COLORS.WHITE}})
- logDebug("MinStockCheck: Crafting job FAILED for " .. rule.displayName .. ". Error: " .. (errCraft or "Unknown"))
- end
- os.sleep(1)
- end
- end
- logDebug("MinStockCheck: Finished checking all rules.")
- end
- --#endregion
- --#region Core Logic: Item Tracking
- local function updateTrackedItem(trackedIndex) local trackData = trackedItems[trackedIndex]; logDebug("updateTrackedItem: Updating index " .. trackedIndex .. ", item: " .. (trackData and trackData.displayName or "N/A")); if not trackData or not meBridge then if trackData and trackData.timerId then os.cancelTimer(trackData.timerId) end; if trackData then table.remove(trackedItems, trackedIndex); logDebug("updateTrackedItem: Removed invalid trackData.") end; return end; logDebug("updateTrackedItem: Calling meBridge.getItem for " .. safeSerializeItemForLog(trackData.itemFilter)); local itemInfo, err = meBridge.getItem(trackData.itemFilter); logDebug("updateTrackedItem: meBridge.getItem returned: info=" .. safeSerializeItemForLog(itemInfo) .. ", err=" .. tostring(err)); local currentQuantity = 0; if itemInfo and itemInfo.count then currentQuantity = itemInfo.count; logDebug("updateTrackedItem: Using itemInfo.count = " .. currentQuantity) elseif itemInfo and itemInfo.amount then currentQuantity = itemInfo.amount; logDebug("updateTrackedItem: itemInfo.count was nil, using itemInfo.amount = " .. currentQuantity) elseif err then logDebug("updateTrackedItem: Error or item not found. Stopping tracking for " .. trackData.displayName); announce({{text = "Stopped tracking '", color = COLORS.YELLOW}, {text = trackData.displayName, color = COLORS.AQUA},{text = "' for ", color = COLORS.YELLOW}, {text = trackData.username, color = COLORS.DARK_AQUA},{text = ": Item no longer found or error: " .. err, color = COLORS.YELLOW}}); os.cancelTimer(trackData.timerId); table.remove(trackedItems, trackedIndex); return else logDebug("updateTrackedItem: No quantity field (count or amount) found for " .. trackData.displayName) end; if currentQuantity ~= trackData.lastCount then announce({{text = "[Track] ", color = COLORS.DARK_AQUA},{text = trackData.displayName .. " (for " .. trackData.username .. "): ", color = COLORS.GRAY},{text = tostring(currentQuantity), color = COLORS.WHITE}}); trackedItems[trackedIndex].lastCount = currentQuantity end; if trackedItems[trackedIndex] then trackedItems[trackedIndex].timerId = os.startTimer(ITEM_TRACKING_INTERVAL); logDebug("updateTrackedItem: Restarted timer for " .. trackData.displayName) else logDebug("updateTrackedItem: Track data removed, not restarting timer for " .. trackData.displayName) end end
- local function stopAllTracking(requestingUsername) logDebug("stopAllTracking called by " .. (requestingUsername or "SYSTEM")); if #trackedItems == 0 then announce({{text = "No items are currently being tracked.", color = COLORS.YELLOW}}); logDebug("stopAllTracking: No items to stop."); return end; for i = #trackedItems, 1, -1 do if trackedItems[i] and trackedItems[i].timerId then os.cancelTimer(trackedItems[i].timerId); logDebug("stopAllTracking: Cancelled timer for " .. trackedItems[i].displayName) end; table.remove(trackedItems, i) end; if requestingUsername then announce({{text = "All item tracking has been stopped by ", color = COLORS.YELLOW}, {text=requestingUsername, color=COLORS.AQUA}, {text=".", color=COLORS.YELLOW}}) else announce({{text = "All item tracking has been stopped.", color = COLORS.YELLOW}}) end; logDebug("stopAllTracking: All tracking stopped.") end
- --#endregion
- --#region Command Handlers
- local commandHandlers = {}
- commandHandlers.help = function(_,_) logDebug("Executing command: help"); announce({{text = "--- AE Monitor Turtle Commands ---",color = COLORS.GOLD,bold = true}}); announce({{text = COMMAND_PREFIX .. " help",color = COLORS.AQUA},{text = " - Shows this help.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " capacity",color = COLORS.AQUA},{text = " - ME system storage report.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " craft <item_part> <amount>",color = COLORS.AQUA},{text = " - Crafts items.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " setalert <%>",color = COLORS.AQUA},{text = " - Sets storage free % alert (e.g., 15 for 15%).",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " available <item_part>",color = COLORS.AQUA},{text = " - Checks item quantity.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " hotitems [count]",color = COLORS.AQUA},{text = " - Lists most abundant items.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " track <item_part>",color = COLORS.AQUA},{text = " - Tracks item quantity (global updates).",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " stoptracking",color = COLORS.AQUA},{text = " - Stops ALL item tracking.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " min <item_part> <amount>",color = COLORS.AQUA},{text = " - Sets minimum stock for an item.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " listmins",color = COLORS.AQUA},{text = " - Lists all minimum stock rules.",color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " rmin <item_part>",color = COLORS.AQUA},{text = " - Removes a minimum stock rule.",color = COLORS.GRAY}}) end
- commandHandlers.capacity = function(_,_) logDebug("Executing command: capacity"); if not meBridge then announce({{text = "ME Bridge not available.",color = COLORS.RED}}); return end; announce({{text = "--- ME System Capacity Report ---",color = COLORS.GOLD,bold = true}}); local byteInfo = {}; local tB,eTB = meBridge.getTotalItemStorage(); local uB,eUB = meBridge.getUsedItemStorage(); local aB,eAB = meBridge.getAvailableItemStorage(); logDebug(string.format("Capacity: TotalB=%s, UsedB=%s, AvailB=%s",tostring(tB),tostring(uB),tostring(aB))); if eTB or eUB or eAB or tB == nil or uB == nil or aB == nil then table.insert(byteInfo, {text="Error retrieving item storage byte counts.", color=COLORS.RED}) else table.insert(byteInfo, {text=string.format("Item Storage (Bytes): %s used / %s total (%s available).",uB,tB,aB),color = COLORS.GREEN}) end; if #byteInfo > 0 then announce(byteInfo) end; local typeInfoMsg = {}; local cells,eC = meBridge.listCells(); local totalTypeSlots = 0; logDebug("Capacity: Listing cells for type calculation. Found " .. (#cells or 0) .. " cells. Error: " .. tostring(eC)); if not eC and cells then for i,cell_data in ipairs(cells) do logDebug(string.format("Capacity: Cell #%d - Type: %s, Item: %s", i, tostring(cell_data.cellType), tostring(cell_data.item))); if cell_data.cellType == "item" then totalTypeSlots = totalTypeSlots + 63; logDebug("Capacity: Added 63 type slots. Total now: " .. totalTypeSlots) end end else table.insert(typeInfoMsg, {text="Could not accurately determine total type slots (cell error): " .. (eC or "N/A"),color = COLORS.YELLOW}) end; local itemsList,eI = meBridge.listItems(); local usedTypes = 0; if not eI and itemsList then usedTypes = #itemsList else table.insert(typeInfoMsg,{text="Could not accurately determine used types (item list error): " .. (eI or "N/A"),color = COLORS.YELLOW}) end; if totalTypeSlots > 0 then table.insert(typeInfoMsg,{text=string.format("Item Storage (Types): %d used / %d total slots (~%d available).",usedTypes,totalTypeSlots,math.max(0,totalTypeSlots - usedTypes)),color = COLORS.GREEN}) else table.insert(typeInfoMsg,{text=string.format("Item Storage (Types): %d used types. Total type slots calculation might be incorrect or zero.",usedTypes),color = COLORS.YELLOW}) end; if #typeInfoMsg > 0 then announce(typeInfoMsg) end end
- commandHandlers.craft = function(username, args) logDebug("Executing command: craft, user: "..username..", args: "..safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge not available.",color = COLORS.RED}}); return end; if #args < 2 then announce({{text = "Usage: "..COMMAND_PREFIX.." craft <item_name_part> <amount>",color = COLORS.YELLOW}}); return end; local itemNamePart = args[1]; local amount = tonumber(args[2]); if not amount or amount <= 0 or math.floor(amount) ~= amount then announce({{text = "Invalid amount: Must be a positive whole number.",color = COLORS.RED}}); return end; local itemToCraft, errFind = findSystemItem(itemNamePart); if not itemToCraft then announce({{text = errFind or "Could not find item to craft.",color = COLORS.RED}}); return end; if not itemToCraft.isCraftable then logDebug("CraftCmd: Item " .. (itemToCraft.displayName or itemToCraft.name) .. " is not marked as craftable by findSystemItem."); announce({{text = "'",color = COLORS.YELLOW},{text = itemToCraft.displayName or itemToCraft.name,color = COLORS.AQUA},{text = "' does not have a known crafting pattern.",color = COLORS.YELLOW}}); return end; logDebug("CraftCmd: Item " .. (itemToCraft.displayName or itemToCraft.name) .. " is marked as craftable. Proceeding."); local itemArg = getItemFilter(itemToCraft); itemArg.count = amount; announce({{text = "Attempting to craft " .. amount .. " of ",color = COLORS.GRAY},{text = itemToCraft.displayName or itemToCraft.name,color = COLORS.AQUA},{text = " (requested by ",color = COLORS.GRAY},{text = username,color = COLORS.DARK_AQUA},{text = ")...",color = COLORS.GRAY}}); local success, errCraft = meBridge.craftItem(itemArg); logDebug("Craft attempt: item="..safeSerializeItemForLog(itemArg)..", success="..tostring(success)..", err="..tostring(errCraft)); if success then announce({{text = "Successfully started crafting job for ",color = COLORS.GREEN},{text = itemToCraft.displayName or itemToCraft.name,color = COLORS.AQUA},{text = ".",color = COLORS.GREEN}}) else announce({{text = "Failed to start crafting: ",color = COLORS.RED},{text = errCraft or "Unknown ME error.",color = COLORS.YELLOW}}) end end
- commandHandlers.setalert = function(username,args) logDebug("Executing command: setalert, user: "..username..", args: "..safeSerializeItemForLog(args)); if #args < 1 then announce({{text = "Usage: "..COMMAND_PREFIX.." setalert <percentage_free>",color = COLORS.YELLOW}}); return end; local per = tonumber(args[1]); if not per or per < 1 or per > 99 then announce({{text = "Percentage must be between 1 and 99.",color = COLORS.RED}}); return end; DEFAULT_STORAGE_ALERT_THRESHOLD_PERCENT = per; currentStorageAlertThreshold = per / 100; lastNotifiedFreePercentage = 1.0; anAlertHasBeenSentSinceRecovery = false; logDebug("Alert threshold set to "..per.."%. currentStorageAlertThreshold="..currentStorageAlertThreshold); announce({{text = "ME storage alert threshold set to ",color = COLORS.GREEN},{text = per.."% free space ",color = COLORS.AQUA,bold = true},{text = "by ",color = COLORS.GREEN},{text = username,color = COLORS.AQUA},{text = ".",color = COLORS.GREEN}}); checkStorageAndAlert() end
- commandHandlers.available = function(_,args) logDebug("Executing command: available, args: "..safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge not available.",color = COLORS.RED}}); return end; if #args < 1 then announce({{text = "Usage: "..COMMAND_PREFIX.." available <item_name_part>",color = COLORS.YELLOW}}); return end; local iD,errF = findSystemItem(args[1]); if not iD then announce({{text = errF or "Could not find item.",color = COLORS.RED}}); return end; local iF = getItemFilter(iD); logDebug("Available: Calling meBridge.getItem with filter: "..safeSerializeItemForLog(iF)); local iI,errG = meBridge.getItem(iF); logDebug("Available: meBridge.getItem returned: info="..safeSerializeItemForLog(iI)..", err="..tostring(errG)); if errG or not iI then announce({{text = "Error retrieving info for '",color = COLORS.RED},{text = iD.displayName or iD.name,color = COLORS.AQUA},{text = "': "..(errG or "Not found/Unknown error"),color = COLORS.RED}}) else local quantity = iI.count or iI.amount or 0; logDebug("Available: Quantity determined as: " .. quantity .. " (from .count or .amount)"); announce({{text = iD.displayName or iD.name,color = COLORS.AQUA,bold = true},{text = ": ",color = COLORS.GRAY},{text = tostring(quantity),color = COLORS.WHITE},{text = " available. Craftable: ",color = COLORS.GRAY},{text = tostring(iI.isCraftable or false),color = iI.isCraftable and COLORS.GREEN or COLORS.YELLOW}}) end end
- commandHandlers.hotitems = function(_, args) logDebug("Executing command: hotitems, args: " .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge not available.", color = COLORS.RED}}); return end; local count_to_display = tonumber(args[1]) or HOT_ITEMS_DEFAULT_COUNT; if count_to_display <= 0 then count_to_display = HOT_ITEMS_DEFAULT_COUNT end; logDebug("HotItems: Count set to " .. count_to_display); logDebug("HotItems: Calling meBridge.listItems()"); local allSystemItems, errList = meBridge.listItems(); if errList then logDebug("HotItems: Error from meBridge.listItems(): " .. errList); announce({{text = "Could not retrieve item list: " .. errList, color = COLORS.RED}}); return end; if not allSystemItems then logDebug("HotItems: meBridge.listItems() returned nil (unexpected)."); announce({{text = "System returned no item data (unexpected nil).", color = COLORS.RED}}); return end; logDebug("HotItems: meBridge.listItems() returned " .. #allSystemItems .. " items. Logging summary..."); if DEBUG_MODE then if #allSystemItems == 0 then logDebug("HotItems: Detailed log - listItems is empty.") else logDebug("HotItems - Detailed - First few items (up to 5):"); for i = 1, math.min(5, #allSystemItems) do logDebug(safeSerializeItemForLog(allSystemItems[i], string.format(" Item #%d: ", i))) end; if #allSystemItems > 5 then logDebug("HotItems - Detailed - Last few items (up to 3 if list > 5):"); for i = math.max(1, #allSystemItems - 2), #allSystemItems do logDebug(safeSerializeItemForLog(allSystemItems[i], string.format(" Item #%d: ", i))) end end end end; if #allSystemItems == 0 then logDebug("HotItems: System empty."); announce({{text = "System is empty.", color = COLORS.YELLOW}}); return end; local positiveCountItems = {}; logDebug("HotItems: Filtering for items with count > 0..."); for i, item_data in ipairs(allSystemItems) do local itemNameForLog = (item_data and (item_data.displayName or item_data.name)) or "Unknown Item"; local itemQuantity = (item_data and item_data.count); local itemQuantityType = type(itemQuantity); logDebug(string.format("HotItems: Processing item #%d: Name='%s', Quantity=%s (Type: %s)", i, itemNameForLog, tostring(itemQuantity), itemQuantityType )); if item_data and itemQuantity and itemQuantityType == "number" and itemQuantity > 0 then table.insert(positiveCountItems, item_data); logDebug("HotItems: Added to positiveCountItems: " .. itemNameForLog .. " with quantity " .. itemQuantity) else logDebug("HotItems: Skipped (quantity not > 0 or invalid type): " .. itemNameForLog) end end; logDebug("HotItems: Found " .. #positiveCountItems .. " items with quantity > 0."); if #positiveCountItems == 0 then announce({{text = "No items with a quantity greater than 0 found in the system.", color = COLORS.YELLOW}}); return end; logDebug("HotItems: Sorting positiveCountItems..."); table.sort(positiveCountItems, function(a, b) return (a.count or 0) > (b.count or 0) end); logDebug("HotItems: Sorting complete. Top item quantity: " .. (positiveCountItems[1] and positiveCountItems[1].count or "N/A")); announce({{text = "--- Top " .. math.min(count_to_display, #positiveCountItems) .. " Most Abundant Items ---", color = COLORS.GOLD, bold=true}}); for i = 1, math.min(count_to_display, #positiveCountItems) do local item = positiveCountItems[i]; announce({{text = string.format("%d. ", i), color = COLORS.GRAY},{text = item.displayName or item.name, color = COLORS.AQUA},{text = string.format(": %d", item.count or 0), color = COLORS.WHITE}}) end end
- commandHandlers.track = function(username,args) logDebug("Executing command: track, user: "..username..", args: "..safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge not available.",color = COLORS.RED}}); return end; if #args < 1 then announce({{text = "Usage: "..COMMAND_PREFIX.." track <item_name_part>",color = COLORS.YELLOW}}); return end; local iD,errF = findSystemItem(args[1]); if not iD then announce({{text = errF or "Could not find item to track.",color = COLORS.RED}}); return end; for _,eT in ipairs(trackedItems) do if eT.username == username then local ef = eT.itemFilter; local nf = getItemFilter(iD); if(ef.fingerprint and nf.fingerprint and ef.fingerprint == nf.fingerprint)or(ef.name == nf.name and ef.nbt == nf.nbt)then announce({{text = username.." is already tracking '",color = COLORS.YELLOW},{text = iD.displayName or iD.name,color = COLORS.AQUA},{text = "'.",color = COLORS.YELLOW}}); return end end end; local nT = {username = username,itemFilter = getItemFilter(iD),displayName = iD.displayName or iD.name,lastCount = -1,timerId = nil}; table.insert(trackedItems,nT); logDebug("Track: Added new track for "..username..": "..safeSerializeItemForLog(nT)); announce({{text = "Now tracking '",color = COLORS.GREEN},{text = nT.displayName,color = COLORS.AQUA},{text = "' for ",color = COLORS.GREEN},{text = username,color = COLORS.DARK_AQUA},{text = ". Global updates every "..ITEM_TRACKING_INTERVAL.."s.",color = COLORS.GREEN}}); updateTrackedItem(#trackedItems) end
- commandHandlers.stoptracking = function(username,_) logDebug("Executing command: stoptracking, user: "..username); stopAllTracking(username) end
- commandHandlers.min = function(username, args) logDebug("Executing command: min, user: " .. username .. ", args: " .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge not available.", color = COLORS.RED}}); return end; if #args < 2 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " min <item_name_part> <amount>", color = COLORS.YELLOW}}); return end; local itemNamePart = args[1]; local minAmount = tonumber(args[2]); if not minAmount or minAmount <= 0 or math.floor(minAmount) ~= minAmount then announce({{text = "Invalid minimum amount: Must be a positive whole number.", color = COLORS.RED}}); return end; local itemData, errFind = findSystemItem(itemNamePart); if not itemData then announce({{text = errFind or "Could not find item to set minimum for.", color = COLORS.RED}}); return end; if not itemData.isCraftable then logDebug("MinSet: Item " .. (itemData.displayName or itemData.name) .. " is not marked as craftable by findSystemItem. Source: " .. (itemData.source or "unknown")); announce({{text = "Item '", color = COLORS.YELLOW}, {text=itemData.displayName or itemData.name, color=COLORS.AQUA}, {text="' must have a known crafting pattern in the ME system to set a minimum.", color=COLORS.YELLOW}}); return end; logDebug("MinSet: Item " .. (itemData.displayName or itemData.name) .. " is marked as craftable by findSystemItem."); local rule_id = itemData.fingerprint or (itemData.name .. ":" .. (itemData.nbt or "")); local itemFilterForStoring = getItemFilter(itemData); for i = #minimum_stock_rules, 1, -1 do if minimum_stock_rules[i].id == rule_id then logDebug("MinSet: Overwriting existing minimum for " .. itemData.displayName); table.remove(minimum_stock_rules, i); break end end; local newRule = {id = rule_id, displayName = itemData.displayName or itemData.name, minAmount = minAmount, itemFilter = itemFilterForStoring}; table.insert(minimum_stock_rules, newRule); logDebug("MinSet: Added/Updated rule: " .. safeSerializeItemForLog(newRule)); if saveMinimumStockRules() then announce({{text="Minimum stock for '", color=COLORS.GREEN}, {text=newRule.displayName, color=COLORS.AQUA}, {text="' set to ", color=COLORS.GREEN}, {text=tostring(newRule.minAmount), color=COLORS.WHITE}, {text=".", color=COLORS.GREEN}}); checkMinimumStockLevels() else announce({{text="Failed to save new minimum stock rule for '", color=COLORS.RED}, {text=newRule.displayName, color=COLORS.AQUA}, {text="'.", color=COLORS.RED}}) end end
- commandHandlers.listmins = function(_, _) logDebug("Executing command: listmins"); if #minimum_stock_rules == 0 then announce({{text="No minimum stock rules are currently defined.", color=COLORS.YELLOW}}); return end; announce({{text="--- Current Minimum Stock Rules ---", color=COLORS.GOLD, bold=true}}); for i, rule in ipairs(minimum_stock_rules) do announce({{text=string.format("%d. ", i), color=COLORS.GRAY}, {text=rule.displayName, color=COLORS.AQUA},{text=" - Min: ", color=COLORS.GRAY}, {text=tostring(rule.minAmount), color=COLORS.WHITE}}) end end
- commandHandlers.rmin = function(username, args) logDebug("Executing command: rmin, user: " .. username .. ", args: " .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge not available.", color = COLORS.RED}}); return end; if #args < 1 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " rmin <item_name_part>", color = COLORS.YELLOW}}); return end; local itemNamePart = args[1]; local foundRuleIndex = -1; local tempRuleId = nil; local itemDataForLookup, _ = findSystemItem(itemNamePart); if itemDataForLookup then tempRuleId = itemDataForLookup.fingerprint or (itemDataForLookup.name .. ":" .. (itemDataForLookup.nbt or "")) end; if tempRuleId then for i = #minimum_stock_rules, 1, -1 do if minimum_stock_rules[i].id == tempRuleId then foundRuleIndex = i; break end end end; if foundRuleIndex == -1 then logDebug("RMin: Could not find rule by precise ID based on '" .. itemNamePart .. "'. Trying display name match."); for i = #minimum_stock_rules, 1, -1 do if string.find(string.lower(minimum_stock_rules[i].displayName), string.lower(itemNamePart), 1, true) then if foundRuleIndex ~= -1 then announce({{text="Multiple minimums match '", color=COLORS.RED},{text=itemNamePart, color=COLORS.AQUA},{text="'. Please be more specific or use a more complete name.",color=COLORS.RED}}); return end; foundRuleIndex = i; end end end; if foundRuleIndex ~= -1 then local removedRule = table.remove(minimum_stock_rules, foundRuleIndex); logDebug("RMin: Removed rule: " .. safeSerializeItemForLog(removedRule)); if saveMinimumStockRules() then announce({{text="Minimum stock rule for '", color=COLORS.GREEN}, {text=removedRule.displayName, color=COLORS.AQUA}, {text="' has been removed.", color=COLORS.GREEN}}) else announce({{text="Failed to save after removing minimum stock rule for '", color=COLORS.RED}, {text=removedRule.displayName, color=COLORS.AQUA}, {text="'.", color=COLORS.RED}}) end else announce({{text="No minimum stock rule found matching '", color=COLORS.RED}, {text=itemNamePart, color=COLORS.AQUA}, {text="'.", color=COLORS.RED}}) end end
- --#endregion
- --#region Main Loop
- local function run()
- term.clear(); term.setCursorPos(1,1); logDebug("Script run() started. Terminal cleared.")
- if DEBUG_MODE then local file, err = fs.open(DEBUG_LOG_FILE, "w"); if file then file.write(string.format("[%s] AE Monitor Script Initializing - DEBUG MODE ENABLED (v7.3)\n", os.date("%Y-%m-%d %H:%M:%S"))); file.write("======================================================================\n"); file.close() else print("DEBUG LOG ERROR: Could not clear/initialize " .. DEBUG_LOG_FILE .. ": " .. (err or "unknown")) end end
- loadMinimumStockRules()
- if not meBridge then logDebug("FATAL: ME Bridge not found during init!"); print("FATAL: ME Bridge peripheral (" .. ME_BRIDGE_PERIPHERAL_NAME .. ") not found!"); return end
- if not chatBox then logDebug("WARNING: Chat Box not found during init!"); print("WARNING: Chat Box peripheral (" .. CHAT_BOX_PERIPHERAL_NAME .. ") not found! Chat features disabled.") end
- logDebug("Peripherals checked. Announcing online status."); print("AE Monitor Turtle script started. Type '" .. COMMAND_PREFIX .. " help' in chat.")
- if chatBox then announce({{text = "AE Monitor Turtle online.", color = COLORS.GREEN, bold=true}, {text = " Type '", color = COLORS.GRAY}, {text = COMMAND_PREFIX .. " help", color = COLORS.AQUA}, {text = "' for commands.", color = COLORS.GRAY}}) end
- checkStorageAndAlert(); storageCheckTimerId = os.startTimer(STORAGE_CHECK_INTERVAL); logDebug("Initial storage check complete. Storage timer ID: " .. tostring(storageCheckTimerId))
- checkMinimumStockLevels(); minStockCheckTimerId = os.startTimer(MIN_STOCK_CHECK_INTERVAL); logDebug("Initial minimum stock check complete. Min stock timer ID: " .. tostring(minStockCheckTimerId))
- while true do
- local eventData = {os.pullEvent()}; local eventType = eventData[1]; logDebug("Event received: " .. eventType .. " - Data: " .. safeSerializeItemForLog(eventData, "EventData: "))
- if eventType == "chat" then
- local eUsername, eMessage, _, eIsHidden = eventData[2], eventData[3], eventData[4], eventData[5]
- if not eIsHidden and eMessage and string.sub(eMessage, 1, #COMMAND_PREFIX) == COMMAND_PREFIX then
- logDebug("Chat command received from " .. eUsername .. ": " .. eMessage); local parts = {}; for part in string.gmatch(eMessage, "[^%s]+") do table.insert(parts, part) end
- local commandName = ""; if parts[2] then commandName = string.lower(parts[2]) end; local cmdArgs = {}; for i = 3, #parts do table.insert(cmdArgs, parts[i]) end
- logDebug("Parsed command: '" .. commandName .. "', Args: " .. safeSerializeItemForLog(cmdArgs))
- if commandHandlers[commandName] then commandHandlers[commandName](eUsername, cmdArgs)
- elseif commandName ~= "" then announce({{text = "Unknown command: '", color = COLORS.RED}, {text=commandName, color=COLORS.YELLOW}, {text="'. Try '", color=COLORS.RED}, {text=COMMAND_PREFIX .. " help", color=COLORS.AQUA}, {text="'.", color=COLORS.RED}}) end
- end
- elseif eventType == "timer" then
- local timerId = eventData[2]; logDebug("Timer event received for ID: " .. timerId)
- if timerId == storageCheckTimerId then logDebug("Storage check timer triggered."); checkStorageAndAlert(); storageCheckTimerId = os.startTimer(STORAGE_CHECK_INTERVAL); logDebug("Storage timer restarted. New ID: " .. tostring(storageCheckTimerId))
- elseif timerId == minStockCheckTimerId then logDebug("Minimum stock check timer triggered."); checkMinimumStockLevels(); minStockCheckTimerId = os.startTimer(MIN_STOCK_CHECK_INTERVAL); logDebug("Minimum stock timer restarted. New ID: " .. tostring(minStockCheckTimerId))
- else
- local foundTrackIndex = nil; for i = #trackedItems, 1, -1 do if trackedItems[i] and trackedItems[i].timerId == timerId then foundTrackIndex = i; break end end
- if foundTrackIndex then logDebug("Item tracking timer triggered for index: " .. foundTrackIndex .. ", item: " .. trackedItems[foundTrackIndex].displayName); updateTrackedItem(foundTrackIndex)
- else logDebug("Unknown timer ID: " .. timerId) end
- end
- elseif eventType == "terminate" then
- logDebug("Terminate event received. Shutting down."); if chatBox then announce({{text = "AE Monitor Turtle shutting down...", color = COLORS.YELLOW, bold=true}}) end
- for _, trackData in ipairs(trackedItems) do if trackData.timerId then os.cancelTimer(trackData.timerId) end end; if storageCheckTimerId then os.cancelTimer(storageCheckTimerId) end; if minStockCheckTimerId then os.cancelTimer(minStockCheckTimerId) end
- logDebug("All timers cancelled. Script terminated."); print("AE Monitor Turtle terminated."); return
- end
- end
- end
- run()
- --#endregion
Add Comment
Please, Sign In to add comment