Refactor inventory management logic to enhance cache handling and dashboard updates
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user