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 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")