Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- AE Monitor Turtle Script v7.8
- Monitors AE2 system, provides chat alerts and commands.
- Changes:
- - (v7.7) Corrected syntax error in updateTrackedItem.
- - (v7.8) Added sanity checks for byte storage reporting in capacity and alerts.
- - (v7.8) Improved logging for meBridge.listCells() in the capacity command.
- ]]
- --#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
- local ITEM_TRACKING_INTERVAL = 2
- local HOT_ITEMS_DEFAULT_COUNT = 10
- local MIN_STOCK_CHECK_INTERVAL = 60
- 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, isCritical) if not DEBUG_MODE and not isCritical then return end; 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; if not DEBUG_MODE then return context_msg .. " (Debug off, not serializing)" end; local success, result = pcall(textutils.serialize, item_data, {compact = true, max_depth = 1}); 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 fallback_str = string.format("%s [Serialization Fallback] Name: %s, DispName: %s, Qty: %s. (Err: %s)", context_msg, tostring(name), tostring(displayName), tostring(qty_val), tostring(result)); return fallback_str end end
- --#endregion
- --#region State Variables
- logDebug("Script initializing state variables (v7.8)..."); 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), true); minimum_stock_rules = {} end else logDebug("Could not open minimums file for reading: " .. (err or "unknown"), true); minimum_stock_rules = {} end else logDebug("Minimums file '" .. MINIMUMS_FILE_PATH .. "' 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.", true); 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"), true); 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) if not chatBox then local plainText = ""; for _, comp in ipairs(messageComponents) do plainText = plainText .. (comp.text or "") end; print("[" .. CHAT_BOT_NAME .. "-NoChatBox" .. (recipientUsername and (" to " .. recipientUsername) or "") .. "] " .. plainText); 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 chat components. Fallback: " .. fallbackMsg .. " Components: " .. textutils.serialize(messageComponents, {max_depth=3}), true); 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.", true); 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 end; os.sleep(0.6) end
- local function announce(messageComponents) sendFormattedChat(messageComponents) end
- local function findSystemItem(itemNamePart) logDebug("findSystemItem: Looking for part='" .. itemNamePart .. "'"); if not meBridge then logDebug("findSystemItem: ME Bridge not available.", true); return nil, "ME Bridge not available." end; itemNamePart = string.lower(itemNamePart); local candidates = {}; local allItems, errItems = meBridge.listItems(); if errItems then logDebug("findSystemItem: Error from meBridge.listItems(): " .. errItems, true) else logDebug("findSystemItem: meBridge.listItems() returned " .. (#allItems or 0) .. " items."); for _, 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; local craftableItems, errCraftable = meBridge.listCraftableItems(); if errCraftable then logDebug("findSystemItem: Error from meBridge.listCraftableItems(): " .. errCraftable, true) else logDebug("findSystemItem: meBridge.listCraftableItems() returned " .. (#craftableItems or 0) .. " items."); 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 return nil, "No item found matching '" .. itemNamePart .. "'." elseif #candidates > 1 then 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 return exactMatches[1] end; if #exactMatches > 1 then candidates = exactMatches end; local displayNames = {}; for i = 1, math.min(#candidates, 3) do table.insert(displayNames, "'" .. (candidates[i].displayName or candidates[i].name) .. "'") end; local msg = "Multiple items found: " .. table.concat(displayNames, ", "); if #candidates > 3 then msg = msg .. " and " .. (#candidates - 3) .. " more." end; return nil, msg .. " Please be more specific." end; 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.")
- if not meBridge then logDebug("checkStorageAndAlert: ME Bridge not available.", true); return end
- local totalBytes, errTotal = meBridge.getTotalItemStorage()
- local usedBytes, errUsed = meBridge.getUsedItemStorage()
- logDebug(string.format("checkStorageAndAlert: Raw 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 byte counts: " .. safeSerializeItemForLog({errTotal, errUsed}), true)
- return
- end
- totalBytes = totalBytes or 0; usedBytes = usedBytes or 0
- if usedBytes > totalBytes and totalBytes > 0 then -- totalBytes > 0 to avoid division by zero if total is genuinely 0
- logDebug("checkStorageAndAlert: Inconsistent byte data from ME Bridge (used > total). Reporting raw and skipping percentage alert.")
- announce({
- {text="WARNING: ME Storage data inconsistent (Used: ", color=COLORS.YELLOW}, {text=tostring(usedBytes), color=COLORS.RED},
- {text=" > Total: ", color=COLORS.YELLOW}, {text=tostring(totalBytes), color=COLORS.RED}, {text="). Percentages unreliable.", color=COLORS.YELLOW}
- })
- -- We could still try to adapt threshold based on total if needed, but percent free is meaningless here
- -- For now, just skip the percentage-based alert logic if data is bad.
- return
- end
- local freePercentage; if totalBytes > 0 then freePercentage = (totalBytes - usedBytes) / totalBytes else freePercentage = 1.0 end
- local defaultThreshold = DEFAULT_STORAGE_ALERT_THRESHOLD_PERCENT / 100
- if freePercentage < currentStorageAlertThreshold then
- if freePercentage < lastNotifiedFreePercentage then
- local usedPercentDisplay = (1 - freePercentage) * 100
- local currentThresholdDisplay = currentStorageAlertThreshold * 100
- announce({{text = "WARNING: ME Item Storage at ", color = COLORS.YELLOW},{text = string.format("%.1f%% full (%.1f%% free)", usedPercentDisplay, freePercentage * 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 = freePercentage; currentStorageAlertThreshold = math.max(0.01, math.floor(freePercentage * 100 - 1) / 100); anAlertHasBeenSentSinceRecovery = true
- logDebug("checkStorageAndAlert: Alert sent. New threshold=" .. currentStorageAlertThreshold)
- end
- elseif freePercentage >= defaultThreshold and anAlertHasBeenSentSinceRecovery then
- announce({{text = "RESOLVED: ME Item Storage now at ", color = COLORS.GREEN},{text = string.format("%.1f%% free.", freePercentage * 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.")
- elseif freePercentage >= currentStorageAlertThreshold and freePercentage < defaultThreshold and anAlertHasBeenSentSinceRecovery then
- currentStorageAlertThreshold = defaultThreshold; lastNotifiedFreePercentage = freePercentage
- end
- end
- local function checkMinimumStockLevels() logDebug("MinStockCheck: Starting, #" .. #minimum_stock_rules .. " rules."); if not meBridge then logDebug("MinStockCheck: ME Bridge not available.", true); return end; if #minimum_stock_rules == 0 then return end; for i, rule in ipairs(minimum_stock_rules) do logDebug("MinStockCheck: Rule #" .. i .. " for " .. rule.displayName); local itemInfo, errGet = meBridge.getItem(rule.itemFilter); local currentQty = 0; if itemInfo and itemInfo.count then currentQty = itemInfo.count elseif itemInfo and itemInfo.amount then currentQty = itemInfo.amount end; logDebug("MinStockCheck: " .. rule.displayName .. " current=" .. currentQty); if errGet and not itemInfo then logDebug("MinStockCheck: getItem error for " .. rule.displayName .. ": " .. tostring(errGet), true) end; if currentQty < rule.minAmount then local targetStock = rule.minAmount * (1 + MIN_STOCK_BUFFER_PERCENT); local amountToCraft = math.ceil(targetStock - currentQty); if amountToCraft < 1 then amountToCraft = 1 end; logDebug("MinStockCheck: " .. rule.displayName .. " low. Crafting " .. amountToCraft); 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; local success, errCraft = meBridge.craftItem(craftArg); if success then announce({{text="MinStock: ",color=COLORS.BLUE},{text="Auto-crafting ",color=COLORS.GRAY},{text=tostring(amountToCraft),color=COLORS.WHITE},{text=" of ",color=COLORS.GRAY},{text=rule.displayName,color=COLORS.AQUA},{text=".",color=COLORS.GRAY}}); logDebug("MinStockCheck: Craft job for " .. rule.displayName .. " OK.") else announce({{text="MinStock: ",color=COLORS.RED},{text="Failed auto-craft ",color=COLORS.YELLOW},{text=rule.displayName,color=COLORS.AQUA},{text=" Err: ",color=COLORS.YELLOW},{text=errCraft or "Unknown",color=COLORS.WHITE}}); logDebug("MinStockCheck: Craft job FAILED for " .. rule.displayName .. ": " .. (errCraft or "Unknown"), true) end; os.sleep(1) end end; logDebug("MinStockCheck: Finished.") end
- --#endregion
- --#region Core Logic: Item Tracking
- local function updateTrackedItem(idx) local tD = trackedItems[idx]; logDebug("TrackUpdate: idx=" .. idx .. " item=" .. (tD and tD.displayName or "NA")); if not tD or not meBridge then if tD and tD.timerId then os.cancelTimer(tD.timerId) end; if tD then table.remove(trackedItems, idx); logDebug("TrackUpdate: Removed invalid.") end; return end; local iI, err = meBridge.getItem(tD.itemFilter); logDebug("TrackUpdate: getItem ret: " .. safeSerializeItemForLog(iI) .. " E:" .. tostring(err)); local cQ = 0; if iI and iI.count then cQ = iI.count elseif iI and iI.amount then cQ = iI.amount end; if err then logDebug("TrackUpdate: getItem err for " .. tD.displayName .. ": " .. tostring(err), true); local errorMessage = ": Error " .. tostring(err); announce({{text = "Stopped tracking '", color = COLORS.YELLOW},{text = tD.displayName, color = COLORS.AQUA},{text = "' for ", color = COLORS.YELLOW},{text = tD.username, color = COLORS.DARK_AQUA},{text = errorMessage, color = COLORS.YELLOW}}); os.cancelTimer(tD.timerId); table.remove(trackedItems, idx); return end; if cQ ~= tD.lastCount then announce({{text = "[Track] ", color = COLORS.DARK_AQUA},{text = tD.displayName .. " (" .. tD.username .. "): ", color = COLORS.GRAY},{text = tostring(cQ), color = COLORS.WHITE}}); tD.lastCount = cQ end; if trackedItems[idx] then trackedItems[idx].timerId = os.startTimer(ITEM_TRACKING_INTERVAL) else logDebug("TrackUpdate: Track removed, no timer restart for " .. tD.displayName) end end
- local function stopAllTracking(reqUser) logDebug("stopAllTracking by " .. (reqUser or "SYS")); if #trackedItems == 0 then announce({{text="No items tracked.",color=COLORS.YELLOW}}); return end; for i = #trackedItems, 1, -1 do if trackedItems[i] and trackedItems[i].timerId then os.cancelTimer(trackedItems[i].timerId) end; table.remove(trackedItems, i) end; if reqUser then announce({{text="All tracking stopped by ",color=COLORS.YELLOW},{text=reqUser,color=COLORS.AQUA},{text=".",color=COLORS.YELLOW}}) else announce({{text="All tracking stopped.",color=COLORS.YELLOW}}) end end
- --#endregion
- --#region Command Handlers
- local commandHandlers = {}
- commandHandlers.help = function(_, _) logDebug("Cmd: help"); announce({{text = "--- AE Monitor Commands (v7.8) ---", color = COLORS.GOLD, bold = true}}); announce({{text = COMMAND_PREFIX .. " help", color = COLORS.AQUA}, {text = " Shows help", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " capacity", color = COLORS.AQUA}, {text = " Storage report", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " craft <item> <amt>", color = COLORS.AQUA}, {text = " Crafts items", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " setalert <%>", color = COLORS.AQUA}, {text = " Sets storage alert %", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " available <item>", color = COLORS.AQUA}, {text = " Checks item qty", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " hotitems [cnt]", color = COLORS.AQUA}, {text = " Most abundant items", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " track <item>", color = COLORS.AQUA}, {text = " Tracks item qty", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " stoptracking", color = COLORS.AQUA}, {text = " Stops ALL tracking", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " min <item> <amt>", color = COLORS.AQUA}, {text = " Sets min stock", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " listmins", color = COLORS.AQUA}, {text = " Lists min rules", color = COLORS.GRAY}}); announce({{text = COMMAND_PREFIX .. " rmin <item>", color = COLORS.AQUA}, {text = " Removes min rule", color = COLORS.GRAY}}) end
- commandHandlers.capacity = function(_, _)
- logDebug("Cmd: 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 byteInfoMsg = {}
- local totalBytes, errTB = meBridge.getTotalItemStorage()
- local usedBytes, errUB = meBridge.getUsedItemStorage()
- local availableBytes, errAB = meBridge.getAvailableItemStorage() -- Get available directly too
- logDebug(string.format("Capacity: Raw TotalB=%s, UsedB=%s, AvailB=%s",tostring(totalBytes),tostring(usedBytes),tostring(availableBytes)))
- if errTB or errUB or errAB or totalBytes==nil or usedBytes==nil or availableBytes==nil then
- table.insert(byteInfoMsg, {text="Error retrieving some byte counts from ME Bridge.", color=COLORS.RED})
- else
- if usedBytes > totalBytes and totalBytes > 0 then
- table.insert(byteInfoMsg, {text=string.format("Bytes: %s used / %s total (%s avail).",usedBytes,totalBytes,availableBytes),color=COLORS.GREEN})
- table.insert(byteInfoMsg, {text="WARNING: Used bytes > Total bytes! Data may be unreliable.", color=COLORS.YELLOW})
- else
- table.insert(byteInfoMsg, {text=string.format("Bytes: %s used / %s total (%s avail).",usedBytes,totalBytes,availableBytes),color=COLORS.GREEN})
- end
- end
- if #byteInfoMsg > 0 then announce(byteInfoMsg) end
- local typeInfoMsg = {}
- local cells, eC = meBridge.listCells(); local totalTypeSlots = 0
- logDebug("Capacity: meBridge.listCells() returned #" .. (#cells or 0) .. " cells. Error: " .. tostring(eC))
- if not eC and cells then
- for i, cellData in ipairs(cells) do
- logDebug(string.format("Capacity: Processing Cell #%d - Item: %s, Type: %s, totalBytes: %s, bytesPerType: %s",
- i, tostring(cellData.item), tostring(cellData.cellType), tostring(cellData.totalBytes), tostring(cellData.bytesPerType)))
- if cellData.cellType == "item" then
- totalTypeSlots = totalTypeSlots + 63
- logDebug("Capacity: Cell #" .. i .. " is item type. Added 63 slots. TotalTypeSlots 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("Types: %d used / %d total slots (~%d avail).", usedTypes, totalTypeSlots, math.max(0, totalTypeSlots - usedTypes)), color = COLORS.GREEN})
- else
- table.insert(typeInfoMsg, {text = string.format("Types: %d used. 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(user, args) logDebug("Cmd: craft U:" .. user .. " A:" .. 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> <amt>", color = COLORS.YELLOW}}); return end; local iNP = args[1]; local amt = tonumber(args[2]); if not amt or amt <= 0 or math.floor(amt) ~= amt then announce({{text = "Invalid amount.", color = COLORS.RED}}); return end; local iTC, errF = findSystemItem(iNP); if not iTC then announce({{text = errF or "Item not found.", color = COLORS.RED}}); return end; if not iTC.isCraftable then logDebug("CraftCmd: " .. (iTC.displayName or iTC.name) .. " not flagged as craftable by findSystemItem."); announce({{text = "'", color = COLORS.YELLOW}, {text = iTC.displayName or iTC.name, color = COLORS.AQUA}, {text = "' does not have a known crafting pattern.", color = COLORS.YELLOW}}); return end; logDebug("CraftCmd: " .. (iTC.displayName or iTC.name) .. " flagged as craftable. Proceeding."); local iArg = getItemFilter(iTC); iArg.count = amt; announce({{text = "Crafting " .. amt .. " of ", color = COLORS.GRAY}, {text = iTC.displayName or iTC.name, color = COLORS.AQUA}, {text = " for " .. user, color = COLORS.DARK_AQUA}, {text = "...", color = COLORS.GRAY}}); local suc, errC = meBridge.craftItem(iArg); logDebug("Craft attempt: " .. safeSerializeItemForLog(iArg) .. " Success:" .. tostring(suc) .. " Error:" .. tostring(errC), not suc); if suc then announce({{text = "Craft job started for ", color = COLORS.GREEN}, {text = iTC.displayName or iTC.name, color = COLORS.AQUA}, {text = ".", color = COLORS.GREEN}}) else announce({{text = "Craft FAILED: ", color = COLORS.RED}, {text = errC or "Unknown ME error.", color = COLORS.YELLOW}}) end end
- commandHandlers.setalert = function(user, args) logDebug("Cmd: setalert U:" .. user .. " A:" .. safeSerializeItemForLog(args)); if #args < 1 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " setalert <%>", 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 1-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 .. "%"); announce({{text = "Storage alert set to ", color = COLORS.GREEN}, {text = per .. "% free", color = COLORS.AQUA, bold = true}, {text = " by " .. user, color = COLORS.AQUA}, {text = ".", color = COLORS.GREEN}}); checkStorageAndAlert() end
- commandHandlers.available = function(_, args) logDebug("Cmd: available A:" .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge NA.", color = COLORS.RED}}); return end; if #args < 1 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " available <item>", color = COLORS.YELLOW}}); return end; local itemData, errFind = findSystemItem(args[1]); if not itemData then announce({{text = errFind or "Item not found.", color = COLORS.RED}}); return end; local itemFilter = getItemFilter(itemData); local itemInfo, errGet = meBridge.getItem(itemFilter); logDebug("Available: getItem ret: " .. safeSerializeItemForLog(itemInfo) .. " E:" .. tostring(errGet)); if errGet or not itemInfo then local errorMessage = ": " .. (errGet or "Not found/Unknown error"); announce({{text = "Error retrieving info for '", color = COLORS.RED},{text = itemData.displayName or itemData.name, color = COLORS.AQUA},{text = "'" .. errorMessage, color = COLORS.RED}}) else local qty = itemInfo.count or itemInfo.amount or 0; announce({{text = itemData.displayName or itemData.name, color = COLORS.AQUA, bold = true},{text = ": ", color = COLORS.GRAY},{text = tostring(qty), color = COLORS.WHITE},{text = " avail. Craftable: ", color = COLORS.GRAY},{text = tostring(itemInfo.isCraftable or false), color = (itemInfo.isCraftable and COLORS.GREEN or COLORS.YELLOW)}}) end end
- commandHandlers.hotitems = function(_, args) logDebug("Cmd: hotitems A:" .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge NA.", color = COLORS.RED}}); return end; local ctd = tonumber(args[1]) or HOT_ITEMS_DEFAULT_COUNT; if ctd <= 0 then ctd = HOT_ITEMS_DEFAULT_COUNT end; local aSI, errL = meBridge.listItems(); if errL then logDebug("HotItems: listItems err: " .. errL, true); announce({{text = "Item list err: " .. errL, color = COLORS.RED}}); return end; if not aSI then logDebug("HotItems: listItems nil ret.", true); announce({{text = "System no item data.", color = COLORS.RED}}); return end; logDebug("HotItems: listItems() OK, #" .. #aSI); if #aSI == 0 then announce({{text = "System empty.", color = COLORS.YELLOW}}); return end; local pCI = {}; for i, iD in ipairs(aSI) do local iQty = iD and iD.count; if iD and iQty and type(iQty) == "number" and iQty > 0 then table.insert(pCI, iD) end end; logDebug("HotItems: Filtered #" .. #pCI); if #pCI == 0 then announce({{text = "No items >0 qty.", color = COLORS.YELLOW}}); return end; table.sort(pCI, function(a, b) return (a.count or 0) > (b.count or 0) end); announce({{text = "--- Top " .. math.min(ctd, #pCI) .. " Items ---", color = COLORS.GOLD, bold = true}}); for i = 1, math.min(ctd, #pCI) do local item = pCI[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(user, args) logDebug("Cmd: track U:" .. user .. " A:" .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge NA.", color = COLORS.RED}}); return end; if #args < 1 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " track <item>", color = COLORS.YELLOW}}); return end; local iD, errF = findSystemItem(args[1]); if not iD then announce({{text = errF or "Item not found.", color = COLORS.RED}}); return end; for _, eT in ipairs(trackedItems) do if eT.username == user 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 = user .. " 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 = user, itemFilter = getItemFilter(iD), displayName = iD.displayName or iD.name, lastCount = -1, timerId = nil}; table.insert(trackedItems, nT); logDebug("Track: Added: " .. safeSerializeItemForLog(nT)); announce({{text = "Tracking '", color = COLORS.GREEN}, {text = nT.displayName, color = COLORS.AQUA}, {text = "' for " .. user, color = COLORS.DARK_AQUA}, {text = ". Global updates.", color = COLORS.GREEN}}); updateTrackedItem(#trackedItems) end
- commandHandlers.stoptracking = function(user, _) logDebug("Cmd: stoptracking U:" .. user); stopAllTracking(user) end
- commandHandlers.min = function(user, args) logDebug("Cmd: min U:" .. user .. " A:" .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge NA.", color = COLORS.RED}}); return end; if #args < 2 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " min <item> <amt>", color = COLORS.YELLOW}}); return end; local iNP = args[1]; local minAmt = tonumber(args[2]); if not minAmt or minAmt <= 0 or math.floor(minAmt) ~= minAmt then announce({{text = "Invalid min amount.", color = COLORS.RED}}); return end; local iD, errF = findSystemItem(iNP); if not iD then announce({{text = errF or "Item not found.", color = COLORS.RED}}); return end; if not iD.isCraftable then logDebug("MinSet: " .. (iD.displayName or iD.name) .. " not craftable flag by findSystemItem."); announce({{text = "Item '", color = COLORS.YELLOW}, {text = iD.displayName or iD.name, color = COLORS.AQUA}, {text = "' no known pattern.", color = COLORS.YELLOW}}); return end; logDebug("MinSet: " .. (iD.displayName or iD.name) .. " craftable flag OK."); local rID = iD.fingerprint or (iD.name .. ":" .. (iD.nbt or "")); local iFStore = getItemFilter(iD); for i = #minimum_stock_rules, 1, -1 do if minimum_stock_rules[i].id == rID then logDebug("MinSet: Overwriting for " .. iD.displayName); table.remove(minimum_stock_rules, i); break end end; local nR = {id = rID, displayName = iD.displayName or iD.name, minAmount = minAmt, itemFilter = iFStore}; table.insert(minimum_stock_rules, nR); logDebug("MinSet: Added/Updated: " .. safeSerializeItemForLog(nR)); if saveMinimumStockRules() then announce({{text = "Min stock for '", color = COLORS.GREEN}, {text = nR.displayName, color = COLORS.AQUA}, {text = "' set to " .. tostring(nR.minAmount), color = COLORS.GREEN}, {text = ".", color = COLORS.GREEN}}); checkMinimumStockLevels() else announce({{text = "Failed to save min rule for '", color = COLORS.RED}, {text = nR.displayName, color = COLORS.AQUA}, {text = "'.", color = COLORS.RED}}) end end
- commandHandlers.listmins = function(_, _) logDebug("Cmd: listmins"); if #minimum_stock_rules == 0 then announce({{text = "No min rules defined.", color = COLORS.YELLOW}}); return end; announce({{text = "--- Min 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(user, args) logDebug("Cmd: rmin U:" .. user .. " A:" .. safeSerializeItemForLog(args)); if not meBridge then announce({{text = "ME Bridge NA.", color = COLORS.RED}}); return end; if #args < 1 then announce({{text = "Usage: " .. COMMAND_PREFIX .. " rmin <item>", color = COLORS.YELLOW}}); return end; local iNP = args[1]; local fRI = -1; local tID = nil; local iDL, _ = findSystemItem(iNP); if iDL then tID = iDL.fingerprint or (iDL.name .. ":" .. (iDL.nbt or "")) end; if tID then for i = #minimum_stock_rules, 1, -1 do if minimum_stock_rules[i].id == tID then fRI = i; break end end end; if fRI == -1 then logDebug("RMin: No precise match for '" .. iNP .. "'. Try display name."); for i = #minimum_stock_rules, 1, -1 do if string.find(string.lower(minimum_stock_rules[i].displayName), string.lower(iNP), 1, true) then if fRI ~= -1 then announce({{text = "Multiple matches for '", color = COLORS.RED}, {text = iNP, color = COLORS.AQUA}, {text = "'. Be specific.", color = COLORS.RED}}); return end; fRI = i end end end; if fRI ~= -1 then local rR = table.remove(minimum_stock_rules, fRI); logDebug("RMin: Removed: " .. safeSerializeItemForLog(rR)); if saveMinimumStockRules() then announce({{text = "Min rule for '", color = COLORS.GREEN}, {text = rR.displayName, color = COLORS.AQUA}, {text = "' removed.", color = COLORS.GREEN}}) else announce({{text = "Failed save after rmin '", color = COLORS.RED}, {text = rR.displayName, color = COLORS.AQUA}, {text = "'.", color = COLORS.RED}}) end else announce({{text = "No min rule matching '", color = COLORS.RED}, {text = iNP, 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.8)\n",os.date("%Y-%m-%d %H:%M:%S")));file.write("======================================================================\n");file.close()else print("DEBUG LOG ERROR: "..DEBUG_LOG_FILE..": ".. (err or "?"))end end
- loadMinimumStockRules()
- if not meBridge then logDebug("FATAL: ME Bridge NA!",true);print("FATAL: ME Bridge not found!");return end
- if not chatBox then logDebug("WARNING: Chat Box NA!",true);print("WARNING: Chat Box not found!")end
- logDebug("Peripherals OK. Announcing online.");print("AE Monitor Turtle ("..CHAT_BOT_NAME..") started. '"..COMMAND_PREFIX .. " help'")
- if chatBox then announce({{text=CHAT_BOT_NAME.." online.",color=COLORS.GREEN,bold=true},{text=" '"..COMMAND_PREFIX.." help'",color=COLORS.AQUA},{text=" for commands.",color=COLORS.GRAY}})end
- checkStorageAndAlert();storageCheckTimerId=os.startTimer(STORAGE_CHECK_INTERVAL);logDebug("Init storage check done. TimerID: "..tostring(storageCheckTimerId))
- checkMinimumStockLevels();minStockCheckTimerId=os.startTimer(MIN_STOCK_CHECK_INTERVAL);logDebug("Init min_stock check done. TimerID: "..tostring(minStockCheckTimerId))
- while true do
- local eventData={os.pullEvent()};local eventType=eventData[1]
- if eventType == "timer" or eventType == "chat" or eventType == "terminate" then logDebug("Event: "..eventType.." Data: "..safeSerializeItemForLog(eventData,"EventData: ")) end
- if eventType=="chat"then
- local eU,eM,_,eIH=eventData[2],eventData[3],eventData[4],eventData[5]
- if not eIH and eM then
- if string.lower(eM) == "@all" then logDebug("@all command from " .. eU); announce({{text = CHAT_BOT_NAME, color = COLORS.LIGHT_PURPLE, bold=true}, {text = ": Hello! Use '", color = COLORS.GREEN}, {text = COMMAND_PREFIX .. " help", color = COLORS.AQUA}, {text = "' for my commands.", color = COLORS.GREEN}})
- elseif string.sub(eM,1,#COMMAND_PREFIX)==COMMAND_PREFIX then
- logDebug("Chat cmd from "..eU..": "..eM);local parts={};for part in string.gmatch(eM,"[^%s]+")do table.insert(parts,part)end
- local cmdName="";if parts[2]then cmdName=string.lower(parts[2])end;local cmdArgs={};for i=3,#parts do table.insert(cmdArgs,parts[i])end
- logDebug("Parsed cmd: '"..cmdName.."', Args: "..safeSerializeItemForLog(cmdArgs))
- if commandHandlers[cmdName]then commandHandlers[cmdName](eU,cmdArgs)
- elseif cmdName~=""then announce({{text="Unknown cmd: '",color=COLORS.RED},{text=cmdName,color=COLORS.YELLOW},{text="'. Try '",color=COLORS.RED},{text=COMMAND_PREFIX.." help",color=COLORS.AQUA},{text="'.",color=COLORS.RED}})end
- end
- end
- elseif eventType=="timer"then
- local tID=eventData[2]
- if tID==storageCheckTimerId then logDebug("Storage timer.");checkStorageAndAlert();storageCheckTimerId=os.startTimer(STORAGE_CHECK_INTERVAL);logDebug("Storage timer restartID: "..tostring(storageCheckTimerId))
- elseif tID==minStockCheckTimerId then logDebug("MinStock timer.");checkMinimumStockLevels();minStockCheckTimerId=os.startTimer(MIN_STOCK_CHECK_INTERVAL);logDebug("MinStock timer restartID: "..tostring(minStockCheckTimerId))
- else
- local fTI=nil;for i=#trackedItems,1,-1 do if trackedItems[i]and trackedItems[i].timerId==tID then fTI=i;break end end
- if fTI then logDebug("Track timer idx:"..fTI.." item:"..trackedItems[fTI].displayName);updateTrackedItem(fTI)
- else logDebug("Unknown timerID: "..tID)end
- end
- elseif eventType=="terminate"then
- logDebug("Terminate event.",true);if chatBox then announce({{text=CHAT_BOT_NAME.." shutting down...",color=COLORS.YELLOW,bold=true}})end
- for _,tD in ipairs(trackedItems)do if tD.timerId then os.cancelTimer(tD.timerId)end end;if storageCheckTimerId then os.cancelTimer(storageCheckTimerId)end;if minStockCheckTimerId then os.cancelTimer(minStockCheckTimerId)end
- logDebug("Timers cancelled. Script exit.",true);print(CHAT_BOT_NAME.." terminated.");return
- end
- end
- end
- run()
- --#endregion
Add Comment
Please, Sign In to add comment