Refactor inventory management logic to enhance cache handling and dashboard updates

This commit is contained in:
MayaTheShy
2026-03-15 16:59:47 -04:00
parent b50733846a
commit 041e67f247

View File

@@ -5,7 +5,34 @@ local DROPPER_NAME = "minecraft:dropper_9"
local BARREL_NAME = "minecraft:barrel_0" local BARREL_NAME = "minecraft:barrel_0"
local POLL_INTERVAL = 2 -- seconds between barrel checks local POLL_INTERVAL = 2 -- seconds between barrel checks
local MONITOR_SIDE = "left" 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 -- Inventory helpers
@@ -35,18 +62,65 @@ local function scanInventory(deviceName)
return result return result
end end
local function buildCatalogue() -- Full scan: updates the global cache
local function refreshCache()
activity.scanning = true
local chests = getChests()
local catalogue = {} local catalogue = {}
for _, chest in ipairs(getChests()) do local totalSlots = 0
local contents = scanInventory(chest) local usedSlots = 0
for itemName, info in pairs(contents) do
if not catalogue[itemName] then for _, chest in ipairs(chests) do
catalogue[itemName] = {} 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 end
table.insert(catalogue[itemName], { chest = chest, total = info.total })
end end
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 end
------------------------------------------------- -------------------------------------------------
@@ -73,10 +147,9 @@ local statusMessage = ""
local statusColor = colors.white local statusColor = colors.white
local statusTimer = 0 local statusTimer = 0
-- Touch zones: list of {x1, y1, x2, y2, action, data}
local touchZones = {} local touchZones = {}
local pendingZones = {} -- built during draw, swapped in at end local pendingZones = {}
local needsRedraw = false local needsRedraw = true
local function addZone(x1, y1, x2, y2, action, data) local function addZone(x1, y1, x2, y2, action, data)
table.insert(pendingZones, { table.insert(pendingZones, {
@@ -95,10 +168,9 @@ local function hitTest(x, y)
end end
------------------------------------------------- -------------------------------------------------
-- Drawing helpers -- Drawing helpers (write to draw target)
------------------------------------------------- -------------------------------------------------
-- Drawing target (set to offscreen buffer in drawDashboard)
local draw = nil local draw = nil
local function monWrite(x, y, text, fg, bg) local function monWrite(x, y, text, fg, bg)
@@ -139,19 +211,16 @@ local function drawButton(x, y, text, fg, bg, padLeft, padRight)
end end
------------------------------------------------- -------------------------------------------------
-- Dashboard -- Dashboard (reads ONLY from cache — no peripheral calls — instant)
------------------------------------------------- -------------------------------------------------
-- Cached item list for touch mapping
local cachedItems = {}
local function drawDashboard() local function drawDashboard()
if not mon then return end if not mon then return end
local w, h = mon.getSize() local w, h = mon.getSize()
pendingZones = {} pendingZones = {}
-- Create offscreen buffer (invisible) -- Offscreen buffer
draw = window.create(mon, 1, 1, w, h, false) draw = window.create(mon, 1, 1, w, h, false)
draw.setBackgroundColor(colors.black) draw.setBackgroundColor(colors.black)
draw.clear() draw.clear()
@@ -162,49 +231,44 @@ local function drawDashboard()
-- ===== Status bar ===== -- ===== Status bar =====
monFill(2, colors.gray) monFill(2, colors.gray)
local chests = getChests()
local dropperOk = peripheral.wrap(DROPPER_NAME) ~= nil
local barrelOk = peripheral.wrap(BARREL_NAME) ~= nil
local statusParts = {} local statusParts = {}
table.insert(statusParts, string.format(" Chests: %d", #chests)) table.insert(statusParts, string.format(" Chests: %d", cache.chestCount))
table.insert(statusParts, dropperOk and "Dropper: OK" or "Dropper: --") table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --")
table.insert(statusParts, barrelOk and "Barrel: OK" or "Barrel: --") 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) 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 ===== -- ===== Divider =====
monFill(3, colors.lightBlue) monFill(3, colors.lightBlue)
monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue) monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue)
-- ===== Storage capacity ===== -- ===== 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) 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) monWrite(2, 4, capLabel, colors.lightGray, colors.black)
local barStart = #capLabel + 4 local barStart = #capLabel + 4
local barWidth = w - barStart - 2 local barWidth = w - barStart - 2
if barWidth > 4 then if barWidth > 4 then
local barColor = colors.lime local barColor = colors.lime
if usedRatio > 0.9 then barColor = colors.red if cache.usedRatio > 0.9 then barColor = colors.red
elseif usedRatio > 0.7 then barColor = colors.orange elseif cache.usedRatio > 0.7 then barColor = colors.orange
elseif usedRatio > 0.5 then barColor = colors.yellow elseif cache.usedRatio > 0.5 then barColor = colors.yellow
end end
monBar(barStart, 4, barWidth, usedRatio, barColor, colors.gray) monBar(barStart, 4, barWidth, cache.usedRatio, barColor, colors.gray)
local pctStr = string.format(" %d%% ", math.floor(usedRatio * 100)) local pctStr = string.format(" %d%% ", math.floor(cache.usedRatio * 100))
local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2) local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2)
monWrite(pctX, 4, pctStr, colors.white, barColor) monWrite(pctX, 4, pctStr, colors.white, barColor)
end end
@@ -222,35 +286,29 @@ local function drawDashboard()
btnX = x2 + 2 btnX = x2 + 2
end end
-- Scan button on the right -- Refresh button
local scanLabel = " Refresh " local refreshBg = activity.scanning and colors.yellow or colors.green
local scanX = w - #scanLabel - 1 local refreshFg = activity.scanning and colors.black or colors.white
local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, "Refresh", colors.white, colors.green, 1, 1) 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) 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 local row = 7
monFill(row, colors.gray) monFill(row, colors.gray)
monWrite(2, row, "#", colors.lightGray, colors.gray) monWrite(2, row, "#", colors.lightGray, colors.gray)
monWrite(5, row, "Item", colors.lightGray, colors.gray) monWrite(5, row, "Item", colors.lightGray, colors.gray)
monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray) monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray)
monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray) monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray)
monWrite(w - 1, row, ">", colors.lightGray, colors.gray) monWrite(w - 1, row, ">", colors.lightGray, colors.gray)
row = row + 1 row = row + 1
-- ===== Item catalogue ===== -- ===== Item rows =====
local catalogue = buildCatalogue() local itemList = cache.itemList
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
local maxCount = 0 local maxCount = 0
for _, item in ipairs(itemList) do for _, item in ipairs(itemList) do
if item.total > maxCount then maxCount = item.total end 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 local rowBg = (i % 2 == 0) and colors.gray or colors.black
monFill(y, rowBg) monFill(y, rowBg)
-- Index
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
-- Name
monWrite(5, y, short, colors.white, rowBg) monWrite(5, y, short, colors.white, rowBg)
-- Quantity
monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg) monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg)
-- Stock bar
local ratio = item.total / maxCount local ratio = item.total / maxCount
local barColor = colors.lime local barColor = colors.lime
if ratio < 0.25 then barColor = colors.red if ratio < 0.25 then barColor = colors.red
@@ -288,16 +342,13 @@ local function drawDashboard()
end end
monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray) 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) monWrite(w - 1, y, ">", colors.orange, rowBg)
-- Entire row is a touch zone
addZone(1, y, w, y, "order", i) addZone(1, y, w, y, "order", i)
row = row + 1 row = row + 1
end end
-- ===== Status message (above footer) ===== -- ===== Status message =====
local msgY = h - 2 local msgY = h - 2
monFill(msgY, colors.black) monFill(msgY, colors.black)
if statusTimer > 0 and #statusMessage > 0 then if statusTimer > 0 and #statusMessage > 0 then
@@ -308,7 +359,7 @@ local function drawDashboard()
local footerY = h - 1 local footerY = h - 1
monFill(footerY, colors.gray) monFill(footerY, colors.gray)
monWrite(2, footerY, 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) colors.white, colors.gray)
local timeStr = textutils.formatTime(os.time(), true) local timeStr = textutils.formatTime(os.time(), true)
@@ -316,12 +367,18 @@ local function drawDashboard()
-- Bottom accent -- Bottom accent
monFill(h, colors.blue) 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) draw.setVisible(true)
-- Swap touch zones atomically (no yield between these two lines) -- Swap zones
touchZones = pendingZones touchZones = pendingZones
end end
@@ -336,7 +393,10 @@ local function sortBarrel()
local contents = barrel.list() local contents = barrel.list()
if not contents or not next(contents) then return end 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() local chests = getChests()
for slot, item in pairs(contents) do 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)) print(string.format("[WARN] Could not sort %d remaining %s", item.count - moved, item.name))
end end
end end
activity.sorting = false
needsRedraw = true
end end
------------------------------------------------- -------------------------------------------------
@@ -375,13 +438,17 @@ end
------------------------------------------------- -------------------------------------------------
local function orderItem(itemName, amount) local function orderItem(itemName, amount)
local catalogue = buildCatalogue() activity.dispensing = true
needsRedraw = true
local catalogue = cache.catalogue
if not catalogue[itemName] then if not catalogue[itemName] then
statusMessage = "Not found: " .. itemName:gsub("^minecraft:", "") statusMessage = "Not found: " .. itemName:gsub("^minecraft:", "")
statusColor = colors.red statusColor = colors.red
statusTimer = 3 statusTimer = 5
print("[ERR] Item not found: " .. itemName) activity.dispensing = false
needsRedraw = true
return false return false
end end
@@ -389,8 +456,9 @@ local function orderItem(itemName, amount)
if not dropper then if not dropper then
statusMessage = "Dropper offline!" statusMessage = "Dropper offline!"
statusColor = colors.red statusColor = colors.red
statusTimer = 3 statusTimer = 5
print("[ERR] Dropper not found") activity.dispensing = false
needsRedraw = true
return false return false
end end
@@ -416,20 +484,22 @@ local function orderItem(itemName, amount)
local sent = amount - remaining local sent = amount - remaining
local short = itemName:gsub("^minecraft:", ""):gsub("_", " ") local short = itemName:gsub("^minecraft:", ""):gsub("_", " ")
if sent > 0 then 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 statusColor = colors.lime
print(string.format("[OK] Ordered %s x%d", short, sent)) print(string.format("[OK] Ordered %s x%d", short, sent))
else else
statusMessage = "Could not order " .. short statusMessage = "Could not order " .. short
statusColor = colors.red statusColor = colors.red
end end
statusTimer = 3 statusTimer = 5
activity.dispensing = false
needsRedraw = true
return sent > 0 return sent > 0
end end
------------------------------------------------- -------------------------------------------------
-- Touch handler -- Touch handler (no peripheral calls — instant)
------------------------------------------------- -------------------------------------------------
local function handleTouch(x, y) local function handleTouch(x, y)
@@ -442,23 +512,31 @@ local function handleTouch(x, y)
if action == "amount" then if action == "amount" then
selectedAmount = data selectedAmount = data
print("[UI] Amount set to " .. data) print("[UI] Amount set to " .. data)
-- Instant visual feedback: just redraw (no peripheral calls)
needsRedraw = true
elseif action == "order" then elseif action == "order" then
local idx = data local idx = data
if cachedItems[idx] then if cache.itemList[idx] then
local item = cachedItems[idx] 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) orderItem(item.name, selectedAmount)
end end
elseif action == "scan" then elseif action == "scan" then
statusMessage = "Refreshing..." statusMessage = "Refreshing..."
statusColor = colors.cyan statusColor = colors.cyan
statusTimer = 2 statusTimer = 3
needsRedraw = true
print("[UI] Manual refresh") print("[UI] Manual refresh")
end end
-- Flag redraw instead of drawing here (avoids yielding in touch handler)
needsRedraw = true
end end
------------------------------------------------- -------------------------------------------------
@@ -490,11 +568,26 @@ local function main()
end end
print("") 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("") print("")
parallel.waitForAny( 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() function()
while true do while true do
pcall(sortBarrel) pcall(sortBarrel)
@@ -502,24 +595,27 @@ local function main()
end end
end, end,
-- Task 2: Dashboard refresh + status timer -- Task 3: Dashboard redraw (event-driven, checks every 0.1s)
function() function()
needsRedraw = true
while true do while true do
pcall(drawDashboard) if needsRedraw then
needsRedraw = false needsRedraw = false
pcall(drawDashboard)
end
-- Decrement status timer
if statusTimer > 0 then if statusTimer > 0 then
statusTimer = statusTimer - 1 statusTimer = statusTimer - 0.1
end if statusTimer <= 0 then
-- Short sleep so redraws triggered by touch happen quickly statusTimer = 0
local elapsed = 0 needsRedraw = true
while elapsed < DASH_REFRESH and not needsRedraw do end
sleep(0.5)
elapsed = elapsed + 0.5
end end
sleep(0.1)
end end
end, end,
-- Task 3: Touch event listener -- Task 4: Touch event listener
function() function()
while true do while true do
local event, side, x, y = os.pullEvent("monitor_touch") local event, side, x, y = os.pullEvent("monitor_touch")