From 041e67f2479f92e5d3f2825b3aa98edad7b7a205 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 15 Mar 2026 16:59:47 -0400 Subject: [PATCH] Refactor inventory management logic to enhance cache handling and dashboard updates --- inventoryManager.lua | 308 ++++++++++++++++++++++++++++--------------- 1 file changed, 202 insertions(+), 106 deletions(-) diff --git a/inventoryManager.lua b/inventoryManager.lua index a923b82..0f6b4af 100644 --- a/inventoryManager.lua +++ b/inventoryManager.lua @@ -5,7 +5,34 @@ local DROPPER_NAME = "minecraft:dropper_9" local BARREL_NAME = "minecraft:barrel_0" local POLL_INTERVAL = 2 -- seconds between barrel checks local MONITOR_SIDE = "left" -local DASH_REFRESH = 3 -- seconds between dashboard refreshes +local SCAN_INTERVAL = 3 -- seconds between background scans + +------------------------------------------------- +-- Cached data (updated by background scanner) +------------------------------------------------- + +local cache = { + catalogue = {}, -- itemName -> { {chest=name, total=N}, ... } + itemList = {}, -- sorted list of { name, total } + grandTotal = 0, + chestCount = 0, + totalSlots = 0, + usedSlots = 0, + freeSlots = 0, + usedRatio = 0, + dropperOk = false, + barrelOk = false, +} + +------------------------------------------------- +-- Activity state (shown on monitor) +------------------------------------------------- + +local activity = { + sorting = false, -- barrel sort in progress + dispensing = false, -- order in progress + scanning = false, -- background scan in progress +} ------------------------------------------------- -- Inventory helpers @@ -35,18 +62,65 @@ local function scanInventory(deviceName) return result end -local function buildCatalogue() +-- Full scan: updates the global cache +local function refreshCache() + activity.scanning = true + + local chests = getChests() local catalogue = {} - for _, chest in ipairs(getChests()) do - local contents = scanInventory(chest) - for itemName, info in pairs(contents) do - if not catalogue[itemName] then - catalogue[itemName] = {} + local totalSlots = 0 + local usedSlots = 0 + + for _, chest in ipairs(chests) do + local inv = peripheral.wrap(chest) + if inv then + totalSlots = totalSlots + inv.size() + local contents = inv.list() + for slot, item in pairs(contents) do + usedSlots = usedSlots + 1 + if not catalogue[item.name] then + catalogue[item.name] = {} + end + -- Accumulate per-chest totals + local found = false + for _, entry in ipairs(catalogue[item.name]) do + if entry.chest == chest then + entry.total = entry.total + item.count + found = true + break + end + end + if not found then + table.insert(catalogue[item.name], { chest = chest, total = item.count }) + end end - table.insert(catalogue[itemName], { chest = chest, total = info.total }) end end - return catalogue + + -- Build sorted item list + local itemList = {} + local grandTotal = 0 + for itemName, sources in pairs(catalogue) do + local total = 0 + for _, s in ipairs(sources) do total = total + s.total end + grandTotal = grandTotal + total + table.insert(itemList, { name = itemName, total = total }) + end + table.sort(itemList, function(a, b) return a.total > b.total end) + + -- Update cache atomically + cache.catalogue = catalogue + cache.itemList = itemList + cache.grandTotal = grandTotal + cache.chestCount = #chests + cache.totalSlots = totalSlots + cache.usedSlots = usedSlots + cache.freeSlots = totalSlots - usedSlots + cache.usedRatio = totalSlots > 0 and (usedSlots / totalSlots) or 0 + cache.dropperOk = peripheral.wrap(DROPPER_NAME) ~= nil + cache.barrelOk = peripheral.wrap(BARREL_NAME) ~= nil + + activity.scanning = false end ------------------------------------------------- @@ -73,10 +147,9 @@ local statusMessage = "" local statusColor = colors.white local statusTimer = 0 --- Touch zones: list of {x1, y1, x2, y2, action, data} local touchZones = {} -local pendingZones = {} -- built during draw, swapped in at end -local needsRedraw = false +local pendingZones = {} +local needsRedraw = true local function addZone(x1, y1, x2, y2, action, data) table.insert(pendingZones, { @@ -95,10 +168,9 @@ local function hitTest(x, y) end ------------------------------------------------- --- Drawing helpers +-- Drawing helpers (write to draw target) ------------------------------------------------- --- Drawing target (set to offscreen buffer in drawDashboard) local draw = nil local function monWrite(x, y, text, fg, bg) @@ -139,19 +211,16 @@ local function drawButton(x, y, text, fg, bg, padLeft, padRight) end ------------------------------------------------- --- Dashboard +-- Dashboard (reads ONLY from cache — no peripheral calls — instant) ------------------------------------------------- --- Cached item list for touch mapping -local cachedItems = {} - local function drawDashboard() if not mon then return end local w, h = mon.getSize() pendingZones = {} - -- Create offscreen buffer (invisible) + -- Offscreen buffer draw = window.create(mon, 1, 1, w, h, false) draw.setBackgroundColor(colors.black) draw.clear() @@ -162,49 +231,44 @@ local function drawDashboard() -- ===== Status bar ===== monFill(2, colors.gray) - local chests = getChests() - local dropperOk = peripheral.wrap(DROPPER_NAME) ~= nil - local barrelOk = peripheral.wrap(BARREL_NAME) ~= nil - local statusParts = {} - table.insert(statusParts, string.format(" Chests: %d", #chests)) - table.insert(statusParts, dropperOk and "Dropper: OK" or "Dropper: --") - table.insert(statusParts, barrelOk and "Barrel: OK" or "Barrel: --") + table.insert(statusParts, string.format(" Chests: %d", cache.chestCount)) + table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --") + table.insert(statusParts, cache.barrelOk and "Barrel: OK" or "Barrel: --") + + -- Activity indicators + local actParts = {} + if activity.sorting then table.insert(actParts, "SORTING") end + if activity.dispensing then table.insert(actParts, "DISPENSING") end + if activity.scanning then table.insert(actParts, "SCANNING") end + monWrite(2, 2, table.concat(statusParts, " | "), colors.white, colors.gray) + if #actParts > 0 then + local actStr = " " .. table.concat(actParts, " | ") .. " " + monWrite(w - #actStr, 2, actStr, colors.white, colors.orange) + end + -- ===== Divider ===== monFill(3, colors.lightBlue) monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue) -- ===== Storage capacity ===== - local totalSlots = 0 - local usedSlots = 0 - for _, chestName in ipairs(chests) do - local inv = peripheral.wrap(chestName) - if inv then - totalSlots = totalSlots + inv.size() - for _ in pairs(inv.list()) do - usedSlots = usedSlots + 1 - end - end - end - local freeSlots = totalSlots - usedSlots - local usedRatio = totalSlots > 0 and (usedSlots / totalSlots) or 0 - monFill(4, colors.black) - local capLabel = string.format(" Storage: %d/%d slots (%d free)", usedSlots, totalSlots, freeSlots) + local capLabel = string.format(" Storage: %d/%d slots (%d free)", + cache.usedSlots, cache.totalSlots, cache.freeSlots) monWrite(2, 4, capLabel, colors.lightGray, colors.black) local barStart = #capLabel + 4 local barWidth = w - barStart - 2 if barWidth > 4 then local barColor = colors.lime - if usedRatio > 0.9 then barColor = colors.red - elseif usedRatio > 0.7 then barColor = colors.orange - elseif usedRatio > 0.5 then barColor = colors.yellow + if cache.usedRatio > 0.9 then barColor = colors.red + elseif cache.usedRatio > 0.7 then barColor = colors.orange + elseif cache.usedRatio > 0.5 then barColor = colors.yellow end - monBar(barStart, 4, barWidth, usedRatio, barColor, colors.gray) - local pctStr = string.format(" %d%% ", math.floor(usedRatio * 100)) + monBar(barStart, 4, barWidth, cache.usedRatio, barColor, colors.gray) + local pctStr = string.format(" %d%% ", math.floor(cache.usedRatio * 100)) local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2) monWrite(pctX, 4, pctStr, colors.white, barColor) end @@ -222,35 +286,29 @@ local function drawDashboard() btnX = x2 + 2 end - -- Scan button on the right - local scanLabel = " Refresh " - local scanX = w - #scanLabel - 1 - local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, "Refresh", colors.white, colors.green, 1, 1) + -- Refresh button + local refreshBg = activity.scanning and colors.yellow or colors.green + local refreshFg = activity.scanning and colors.black or colors.white + local refreshTxt = activity.scanning and "Scanning" or "Refresh" + local scanX = w - #refreshTxt - 3 + local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, refreshTxt, refreshFg, refreshBg, 1, 1) addZone(sx1, sy1, sx2, sy2, "scan", nil) - -- ===== Column headers (row 6) ===== + -- ===== Row 6: blank spacer ===== + monFill(6, colors.black) + + -- ===== Column headers (row 7) ===== local row = 7 monFill(row, colors.gray) - monWrite(2, row, "#", colors.lightGray, colors.gray) - monWrite(5, row, "Item", colors.lightGray, colors.gray) + monWrite(2, row, "#", colors.lightGray, colors.gray) + monWrite(5, row, "Item", colors.lightGray, colors.gray) monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray) monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray) monWrite(w - 1, row, ">", colors.lightGray, colors.gray) row = row + 1 - -- ===== Item catalogue ===== - local catalogue = buildCatalogue() - local itemList = {} - local grandTotal = 0 - for itemName, sources in pairs(catalogue) do - local total = 0 - for _, s in ipairs(sources) do total = total + s.total end - grandTotal = grandTotal + total - table.insert(itemList, { name = itemName, total = total }) - end - table.sort(itemList, function(a, b) return a.total > b.total end) - cachedItems = itemList - + -- ===== Item rows ===== + local itemList = cache.itemList local maxCount = 0 for _, item in ipairs(itemList) do if item.total > maxCount then maxCount = item.total end @@ -273,14 +331,10 @@ local function drawDashboard() local rowBg = (i % 2 == 0) and colors.gray or colors.black monFill(y, rowBg) - -- Index monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) - -- Name monWrite(5, y, short, colors.white, rowBg) - -- Quantity monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg) - -- Stock bar local ratio = item.total / maxCount local barColor = colors.lime if ratio < 0.25 then barColor = colors.red @@ -288,16 +342,13 @@ local function drawDashboard() end monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray) - -- Order button monWrite(w - 1, y, ">", colors.orange, rowBg) - - -- Entire row is a touch zone addZone(1, y, w, y, "order", i) row = row + 1 end - -- ===== Status message (above footer) ===== + -- ===== Status message ===== local msgY = h - 2 monFill(msgY, colors.black) if statusTimer > 0 and #statusMessage > 0 then @@ -308,7 +359,7 @@ local function drawDashboard() local footerY = h - 1 monFill(footerY, colors.gray) monWrite(2, footerY, - string.format(" Total: %d items | %d types ", grandTotal, #itemList), + string.format(" Total: %d items | %d types ", cache.grandTotal, #itemList), colors.white, colors.gray) local timeStr = textutils.formatTime(os.time(), true) @@ -316,12 +367,18 @@ local function drawDashboard() -- Bottom accent monFill(h, colors.blue) - monCenter(h, " Tap item to order ", colors.lightBlue, colors.blue) + local bottomMsg = " Tap item to order " + if activity.dispensing then + bottomMsg = " DISPENSING... " + elseif activity.sorting then + bottomMsg = " SORTING BARREL... " + end + monCenter(h, bottomMsg, colors.lightBlue, colors.blue) - -- Flush buffer to monitor in one go + -- Flush to monitor draw.setVisible(true) - -- Swap touch zones atomically (no yield between these two lines) + -- Swap zones touchZones = pendingZones end @@ -336,7 +393,10 @@ local function sortBarrel() local contents = barrel.list() if not contents or not next(contents) then return end - local catalogue = buildCatalogue() + activity.sorting = true + needsRedraw = true + + local catalogue = cache.catalogue local chests = getChests() for slot, item in pairs(contents) do @@ -368,6 +428,9 @@ local function sortBarrel() print(string.format("[WARN] Could not sort %d remaining %s", item.count - moved, item.name)) end end + + activity.sorting = false + needsRedraw = true end ------------------------------------------------- @@ -375,13 +438,17 @@ end ------------------------------------------------- local function orderItem(itemName, amount) - local catalogue = buildCatalogue() + activity.dispensing = true + needsRedraw = true + + local catalogue = cache.catalogue if not catalogue[itemName] then statusMessage = "Not found: " .. itemName:gsub("^minecraft:", "") statusColor = colors.red - statusTimer = 3 - print("[ERR] Item not found: " .. itemName) + statusTimer = 5 + activity.dispensing = false + needsRedraw = true return false end @@ -389,8 +456,9 @@ local function orderItem(itemName, amount) if not dropper then statusMessage = "Dropper offline!" statusColor = colors.red - statusTimer = 3 - print("[ERR] Dropper not found") + statusTimer = 5 + activity.dispensing = false + needsRedraw = true return false end @@ -416,20 +484,22 @@ local function orderItem(itemName, amount) local sent = amount - remaining local short = itemName:gsub("^minecraft:", ""):gsub("_", " ") if sent > 0 then - statusMessage = string.format("Ordered %s x%d", short, sent) + statusMessage = string.format("Dispensing %s x%d", short, sent) statusColor = colors.lime print(string.format("[OK] Ordered %s x%d", short, sent)) else statusMessage = "Could not order " .. short statusColor = colors.red end - statusTimer = 3 + statusTimer = 5 + activity.dispensing = false + needsRedraw = true return sent > 0 end ------------------------------------------------- --- Touch handler +-- Touch handler (no peripheral calls — instant) ------------------------------------------------- local function handleTouch(x, y) @@ -442,23 +512,31 @@ local function handleTouch(x, y) if action == "amount" then selectedAmount = data print("[UI] Amount set to " .. data) + -- Instant visual feedback: just redraw (no peripheral calls) + needsRedraw = true elseif action == "order" then local idx = data - if cachedItems[idx] then - local item = cachedItems[idx] + if cache.itemList[idx] then + local item = cache.itemList[idx] + local short = item.name:gsub("^minecraft:", ""):gsub("_", " ") + -- Immediate feedback + statusMessage = string.format("Ordering %s x%d...", short, selectedAmount) + statusColor = colors.cyan + statusTimer = 10 + activity.dispensing = true + needsRedraw = true + -- Actual order happens next — draw will flush before peripheral calls orderItem(item.name, selectedAmount) end elseif action == "scan" then statusMessage = "Refreshing..." statusColor = colors.cyan - statusTimer = 2 + statusTimer = 3 + needsRedraw = true print("[UI] Manual refresh") end - - -- Flag redraw instead of drawing here (avoids yielding in touch handler) - needsRedraw = true end ------------------------------------------------- @@ -490,11 +568,26 @@ local function main() end print("") - print("Console shows log output. Use the monitor to order items.") + print("Console shows log. Use the monitor to interact.") + print("") + + -- Initial scan before starting + print("[INIT] Scanning inventories...") + refreshCache() + print("[INIT] Done. Found " .. #cache.itemList .. " item types.") print("") parallel.waitForAny( - -- Task 1: Barrel auto-sort + -- Task 1: Background inventory scanner + function() + while true do + sleep(SCAN_INTERVAL) + pcall(refreshCache) + needsRedraw = true + end + end, + + -- Task 2: Barrel auto-sort function() while true do pcall(sortBarrel) @@ -502,24 +595,27 @@ local function main() end end, - -- Task 2: Dashboard refresh + status timer + -- Task 3: Dashboard redraw (event-driven, checks every 0.1s) function() + needsRedraw = true while true do - pcall(drawDashboard) - needsRedraw = false + if needsRedraw then + needsRedraw = false + pcall(drawDashboard) + end + -- Decrement status timer if statusTimer > 0 then - statusTimer = statusTimer - 1 - end - -- Short sleep so redraws triggered by touch happen quickly - local elapsed = 0 - while elapsed < DASH_REFRESH and not needsRedraw do - sleep(0.5) - elapsed = elapsed + 0.5 + statusTimer = statusTimer - 0.1 + if statusTimer <= 0 then + statusTimer = 0 + needsRedraw = true + end end + sleep(0.1) end end, - -- Task 3: Touch event listener + -- Task 4: Touch event listener function() while true do local event, side, x, y = os.pullEvent("monitor_touch")