Refactor inventory manager for touch UI support and improved dashboard functionality

This commit is contained in:
MayaTheShy
2026-03-15 16:44:32 -04:00
parent efb41ecd2f
commit 0d5a533fd0

View File

@@ -1,4 +1,4 @@
-- Inventory Manager: Auto-sort barrel & order items to dropper -- Inventory Manager: Touch UI on monitor
-- Main computer (networked). Computer 1 sits next to dropper_0 and auto-dispenses. -- Main computer (networked). Computer 1 sits next to dropper_0 and auto-dispenses.
local DROPPER_NAME = "minecraft:dropper_9" local DROPPER_NAME = "minecraft:dropper_9"
@@ -11,7 +11,6 @@ local DASH_REFRESH = 3 -- seconds between dashboard refreshes
-- Inventory helpers -- Inventory helpers
------------------------------------------------- -------------------------------------------------
-- Get all chest peripheral names on the network
local function getChests() local function getChests()
local chests = {} local chests = {}
for _, name in ipairs(peripheral.getNames()) do for _, name in ipairs(peripheral.getNames()) do
@@ -22,7 +21,6 @@ local function getChests()
return chests return chests
end end
-- Scan a single inventory: returns { [itemName] = { total=N, slots={ [slot]={name,count} } } }
local function scanInventory(deviceName) local function scanInventory(deviceName)
local inv = peripheral.wrap(deviceName) local inv = peripheral.wrap(deviceName)
if not inv then return {} end if not inv then return {} end
@@ -37,7 +35,6 @@ local function scanInventory(deviceName)
return result return result
end end
-- Build a full catalogue: itemName -> { {chest=name, total=N}, ... }
local function buildCatalogue() local function buildCatalogue()
local catalogue = {} local catalogue = {}
for _, chest in ipairs(getChests()) do for _, chest in ipairs(getChests()) do
@@ -53,11 +50,10 @@ local function buildCatalogue()
end end
------------------------------------------------- -------------------------------------------------
-- Monitor Dashboard -- Monitor setup
------------------------------------------------- -------------------------------------------------
local mon = nil local mon = nil
local buf = nil -- offscreen buffer to prevent flickering
local function setupMonitor() local function setupMonitor()
mon = peripheral.wrap(MONITOR_SIDE) mon = peripheral.wrap(MONITOR_SIDE)
@@ -67,52 +63,91 @@ local function setupMonitor()
return true return true
end end
-- Drawing target (buf or mon, set in drawDashboard) -------------------------------------------------
local draw = nil -- UI State
-------------------------------------------------
local selectedAmount = 1
local amountOptions = {1, 4, 8, 16, 32, 64}
local statusMessage = ""
local statusColor = colors.white
local statusTimer = 0
-- Touch zones: list of {x1, y1, x2, y2, action, data}
local touchZones = {}
local function addZone(x1, y1, x2, y2, action, data)
table.insert(touchZones, {
x1 = x1, y1 = y1, x2 = x2, y2 = y2,
action = action, data = data
})
end
local function hitTest(x, y)
for _, zone in ipairs(touchZones) do
if x >= zone.x1 and x <= zone.x2 and y >= zone.y1 and y <= zone.y2 then
return zone.action, zone.data
end
end
return nil, nil
end
-------------------------------------------------
-- Drawing helpers
-------------------------------------------------
-- Helpers (draw to buffer)
local function monWrite(x, y, text, fg, bg) local function monWrite(x, y, text, fg, bg)
draw.setCursorPos(x, y) mon.setCursorPos(x, y)
if fg then draw.setTextColor(fg) end if fg then mon.setTextColor(fg) end
if bg then draw.setBackgroundColor(bg) end if bg then mon.setBackgroundColor(bg) end
draw.write(text) mon.write(text)
end end
local function monFill(y, color) local function monFill(y, color)
local w, _ = draw.getSize() local w, _ = mon.getSize()
draw.setCursorPos(1, y) mon.setCursorPos(1, y)
draw.setBackgroundColor(color) mon.setBackgroundColor(color)
draw.write(string.rep(" ", w)) mon.write(string.rep(" ", w))
end end
local function monCenter(y, text, fg, bg) local function monCenter(y, text, fg, bg)
local w, _ = draw.getSize() local w, _ = mon.getSize()
local x = math.floor((w - #text) / 2) + 1 local x = math.floor((w - #text) / 2) + 1
monWrite(x, y, text, fg, bg) monWrite(x, y, text, fg, bg)
end end
local function monBar(x, y, width, ratio, barColor, bgColor) local function monBar(x, y, width, ratio, barColor, bgColor)
local filled = math.floor(ratio * width) local filled = math.floor(ratio * width)
draw.setCursorPos(x, y) mon.setCursorPos(x, y)
draw.setBackgroundColor(barColor) mon.setBackgroundColor(barColor)
draw.write(string.rep(" ", filled)) mon.write(string.rep(" ", filled))
draw.setBackgroundColor(bgColor) mon.setBackgroundColor(bgColor)
draw.write(string.rep(" ", width - filled)) mon.write(string.rep(" ", width - filled))
end end
-- Draw the full dashboard (double-buffered) local function drawButton(x, y, text, fg, bg, padLeft, padRight)
padLeft = padLeft or 1
padRight = padRight or 1
local full = string.rep(" ", padLeft) .. text .. string.rep(" ", padRight)
monWrite(x, y, full, fg, bg)
return x, y, x + #full - 1, y
end
-------------------------------------------------
-- Dashboard
-------------------------------------------------
-- 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()
touchZones = {}
-- Create offscreen buffer mon.setBackgroundColor(colors.black)
buf = window.create(mon, 1, 1, w, h, false) mon.clear()
draw = buf
-- Clear buffer
draw.setBackgroundColor(colors.black)
draw.clear()
-- ===== Title bar ===== -- ===== Title bar =====
monFill(1, colors.blue) monFill(1, colors.blue)
@@ -128,23 +163,13 @@ local function drawDashboard()
table.insert(statusParts, string.format(" Chests: %d", #chests)) table.insert(statusParts, string.format(" Chests: %d", #chests))
table.insert(statusParts, dropperOk and "Dropper: OK" or "Dropper: --") table.insert(statusParts, dropperOk and "Dropper: OK" or "Dropper: --")
table.insert(statusParts, barrelOk and "Barrel: OK" or "Barrel: --") table.insert(statusParts, barrelOk and "Barrel: OK" or "Barrel: --")
local statusLine = table.concat(statusParts, " | ") monWrite(2, 2, table.concat(statusParts, " | "), colors.white, colors.gray)
monWrite(2, 2, statusLine, colors.white, colors.gray)
-- Dropper/Barrel status indicators
local indicatorX = w - 10
if indicatorX > #statusLine + 3 then
monWrite(indicatorX, 2, dropperOk and " \7 " or " x ", dropperOk and colors.lime or colors.red, colors.gray)
monWrite(indicatorX + 4, 2, barrelOk and " \7 " or " x ", barrelOk and colors.lime or colors.red, colors.gray)
end
-- ===== Divider ===== -- ===== Divider =====
monFill(3, colors.lightBlue) monFill(3, colors.lightBlue)
monCenter(3, string.rep("\140", math.min(w - 4, 60)), colors.cyan, colors.lightBlue) monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue)
-- ===== Storage capacity bar ===== -- ===== Storage capacity =====
-- Count total slots and used slots across all chests
local totalSlots = 0 local totalSlots = 0
local usedSlots = 0 local usedSlots = 0
for _, chestName in ipairs(chests) do for _, chestName in ipairs(chests) do
@@ -159,12 +184,10 @@ local function drawDashboard()
local freeSlots = totalSlots - usedSlots local freeSlots = totalSlots - usedSlots
local usedRatio = totalSlots > 0 and (usedSlots / totalSlots) or 0 local usedRatio = totalSlots > 0 and (usedSlots / totalSlots) or 0
local capY = 4 monFill(4, colors.black)
monFill(capY, 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)", usedSlots, totalSlots, freeSlots)
monWrite(2, capY, capLabel, colors.lightGray, colors.black) monWrite(2, 4, capLabel, colors.lightGray, colors.black)
-- Draw capacity bar
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
@@ -173,73 +196,82 @@ local function drawDashboard()
elseif usedRatio > 0.7 then barColor = colors.orange elseif usedRatio > 0.7 then barColor = colors.orange
elseif usedRatio > 0.5 then barColor = colors.yellow elseif usedRatio > 0.5 then barColor = colors.yellow
end end
monBar(barStart, capY, barWidth, usedRatio, barColor, colors.gray) monBar(barStart, 4, barWidth, usedRatio, barColor, colors.gray)
-- Show percentage on the bar
local pctStr = string.format(" %d%% ", math.floor(usedRatio * 100)) local pctStr = string.format(" %d%% ", math.floor(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, capY, pctStr, colors.white, barColor) monWrite(pctX, 4, pctStr, colors.white, barColor)
end end
-- ===== Amount selector (row 5) =====
monFill(5, colors.black)
monWrite(2, 5, "Qty:", colors.lightGray, colors.black)
local btnX = 7
for _, amt in ipairs(amountOptions) do
local label = tostring(amt)
local bg = (amt == selectedAmount) and colors.cyan or colors.gray
local fg = (amt == selectedAmount) and colors.white or colors.lightGray
local x1, y1, x2, y2 = drawButton(btnX, 5, label, fg, bg)
addZone(x1, y1, x2, y2, "amount", amt)
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)
addZone(sx1, sy1, sx2, sy2, "scan", nil)
-- ===== Column headers (row 6) =====
local row = 7
monFill(row, 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 ===== -- ===== Item catalogue =====
local catalogue = buildCatalogue() local catalogue = buildCatalogue()
-- Collect and sort items
local itemList = {} local itemList = {}
local grandTotal = 0 local grandTotal = 0
for itemName, sources in pairs(catalogue) do for itemName, sources in pairs(catalogue) do
local total = 0 local total = 0
for _, s in ipairs(sources) do total = total + s.total end for _, s in ipairs(sources) do total = total + s.total end
grandTotal = grandTotal + total grandTotal = grandTotal + total
table.insert(itemList, { name = itemName, total = total, sources = #sources }) table.insert(itemList, { name = itemName, total = total })
end end
table.sort(itemList, function(a, b) return a.total > b.total end) table.sort(itemList, function(a, b) return a.total > b.total end)
cachedItems = itemList
-- Header row
local row = 6
monFill(row, 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)
row = row + 1
-- Find max for bar scaling
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
end end
if maxCount == 0 then maxCount = 1 end if maxCount == 0 then maxCount = 1 end
-- Item rows local maxRows = h - row - 3
local maxRows = h - row - 3 -- leave space for footer
for i, item in ipairs(itemList) do for i, item in ipairs(itemList) do
if i > maxRows then break end if i > maxRows then break end
local y = row local y = row
local short = item.name:gsub("^minecraft:", ""):gsub("_", " ") local short = item.name:gsub("^minecraft:", ""):gsub("_", " ")
-- Capitalize first letter
short = short:sub(1,1):upper() .. short:sub(2) short = short:sub(1,1):upper() .. short:sub(2)
-- Truncate if too long
local maxNameLen = w - 30 local maxNameLen = w - 30
if #short > maxNameLen then if #short > maxNameLen then
short = short:sub(1, maxNameLen - 2) .. ".." short = short:sub(1, maxNameLen - 2) .. ".."
end end
-- Alternating row background
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 number -- Index
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
-- Name
-- Item name
monWrite(5, y, short, colors.white, rowBg) monWrite(5, y, short, colors.white, rowBg)
-- Quantity -- Quantity
local qtyStr = tostring(item.total) monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg)
monWrite(w - 22, y, qtyStr, colors.yellow, rowBg)
-- Stock bar -- Stock bar
local ratio = item.total / maxCount local ratio = item.total / maxCount
@@ -249,34 +281,41 @@ 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)
-- Entire row is a touch zone
addZone(1, y, w, y, "order", i)
row = row + 1 row = row + 1
end end
-- ===== Status message (above footer) =====
local msgY = h - 2
monFill(msgY, colors.black)
if statusTimer > 0 and #statusMessage > 0 then
monCenter(msgY, statusMessage, statusColor, colors.black)
end
-- ===== Footer ===== -- ===== Footer =====
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 across %d types ", grandTotal, #itemList), string.format(" Total: %d items | %d types ", grandTotal, #itemList),
colors.white, colors.gray) colors.white, colors.gray)
-- Timestamp
local timeStr = textutils.formatTime(os.time(), true) local timeStr = textutils.formatTime(os.time(), true)
monWrite(w - #timeStr - 1, footerY, timeStr, colors.lightGray, colors.gray) monWrite(w - #timeStr - 1, footerY, timeStr, colors.lightGray, colors.gray)
-- Bottom accent -- Bottom accent
monFill(h, colors.blue) monFill(h, colors.blue)
monCenter(h, " \4 Auto-Sort Active \4 ", colors.lightBlue, colors.blue) monCenter(h, " Tap item to order ", colors.lightBlue, colors.blue)
-- Flush buffer to monitor in one go (no flicker)
buf.setVisible(true)
buf.setVisible(false)
end end
------------------------------------------------- -------------------------------------------------
-- Barrel auto-sort -- Barrel auto-sort
------------------------------------------------- -------------------------------------------------
-- Move all items from the barrel into matching chests (or first chest with space)
local function sortBarrel() local function sortBarrel()
local barrel = peripheral.wrap(BARREL_NAME) local barrel = peripheral.wrap(BARREL_NAME)
if not barrel then return end if not barrel then return end
@@ -290,7 +329,6 @@ local function sortBarrel()
for slot, item in pairs(contents) do for slot, item in pairs(contents) do
local moved = 0 local moved = 0
-- Try chests that already hold this item first
if catalogue[item.name] then if catalogue[item.name] then
for _, entry in ipairs(catalogue[item.name]) do for _, entry in ipairs(catalogue[item.name]) do
local n = barrel.pushItems(entry.chest, slot) local n = barrel.pushItems(entry.chest, slot)
@@ -302,7 +340,6 @@ local function sortBarrel()
end end
end end
-- If some remain, push to any chest with space
if moved < item.count then if moved < item.count then
for _, chest in ipairs(chests) do for _, chest in ipairs(chests) do
local n = barrel.pushItems(chest, slot) local n = barrel.pushItems(chest, slot)
@@ -324,19 +361,23 @@ end
-- Order -- Order
------------------------------------------------- -------------------------------------------------
-- Order items: move from chest(s) to dropper
-- Computer 1 will auto-detect and dispense
local function orderItem(itemName, amount) local function orderItem(itemName, amount)
local catalogue = buildCatalogue() local catalogue = buildCatalogue()
if not catalogue[itemName] then if not catalogue[itemName] then
statusMessage = "Not found: " .. itemName:gsub("^minecraft:", "")
statusColor = colors.red
statusTimer = 3
print("[ERR] Item not found: " .. itemName) print("[ERR] Item not found: " .. itemName)
return false return false
end end
local dropper = peripheral.wrap(DROPPER_NAME) local dropper = peripheral.wrap(DROPPER_NAME)
if not dropper then if not dropper then
print("[ERR] Dropper not found: " .. DROPPER_NAME) statusMessage = "Dropper offline!"
statusColor = colors.red
statusTimer = 3
print("[ERR] Dropper not found")
return false return false
end end
@@ -344,14 +385,13 @@ local function orderItem(itemName, amount)
for _, entry in ipairs(catalogue[itemName]) do for _, entry in ipairs(catalogue[itemName]) do
local chest = peripheral.wrap(entry.chest) local chest = peripheral.wrap(entry.chest)
if chest then if chest then
-- Find slots in this chest with the target item
for slot, slotItem in pairs(chest.list()) do for slot, slotItem in pairs(chest.list()) do
if slotItem.name == itemName then if slotItem.name == itemName then
local toMove = math.min(remaining, slotItem.count) local toMove = math.min(remaining, slotItem.count)
local moved = chest.pushItems(DROPPER_NAME, slot, toMove) local moved = chest.pushItems(DROPPER_NAME, slot, toMove)
if moved and moved > 0 then if moved and moved > 0 then
remaining = remaining - moved remaining = remaining - moved
print(string.format("[ORDER] %s x%d from %s -> dropper", itemName, moved, entry.chest)) print(string.format("[ORDER] %s x%d from %s", itemName, moved, entry.chest))
end end
if remaining <= 0 then break end if remaining <= 0 then break end
end end
@@ -360,129 +400,109 @@ local function orderItem(itemName, amount)
if remaining <= 0 then break end if remaining <= 0 then break end
end end
if remaining > 0 then local sent = amount - remaining
print(string.format("[WARN] Only moved %d/%d of %s", amount - remaining, amount, itemName)) local short = itemName:gsub("^minecraft:", ""):gsub("_", " ")
if sent > 0 then
statusMessage = string.format("Ordered %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 end
statusTimer = 3
print("[OK] Items in dropper. Computer 1 will dispense.") return sent > 0
return true
end end
------------------------------------------------- -------------------------------------------------
-- Display -- Touch handler
------------------------------------------------- -------------------------------------------------
local function showCatalogue() local function handleTouch(x, y)
local catalogue = buildCatalogue() local action, data = hitTest(x, y)
print("") if not action then return end
print("=== Item Catalogue ===")
print("") if action == "amount" then
local index = 1 selectedAmount = data
local items = {} print("[UI] Amount set to " .. data)
for itemName, sources in pairs(catalogue) do
local total = 0 elseif action == "order" then
for _, s in ipairs(sources) do total = total + s.total end local idx = data
-- Strip "minecraft:" prefix for cleaner display if cachedItems[idx] then
local short = itemName:gsub("^minecraft:", "") local item = cachedItems[idx]
print(string.format(" %2d. %-30s x%d", index, short, total)) orderItem(item.name, selectedAmount)
items[index] = itemName end
index = index + 1
elseif action == "scan" then
statusMessage = "Refreshing..."
statusColor = colors.cyan
statusTimer = 2
print("[UI] Manual refresh")
end end
print("")
return items -- Redraw immediately after touch
pcall(drawDashboard)
end end
------------------------------------------------- -------------------------------------------------
-- Main loop -- Main
------------------------------------------------- -------------------------------------------------
local function main() local function main()
print("=================================") print("=================================")
print(" Inventory Manager v2") print(" Inventory Manager v2 (Touch)")
print("=================================") print("=================================")
print("") print("")
-- Check dropper
if peripheral.wrap(DROPPER_NAME) then if peripheral.wrap(DROPPER_NAME) then
print("[OK] Dropper found: " .. DROPPER_NAME) print("[OK] Dropper: " .. DROPPER_NAME)
else else
print("[WARN] Dropper not found: " .. DROPPER_NAME) print("[WARN] Dropper not found: " .. DROPPER_NAME)
end end
-- Check barrel
if peripheral.wrap(BARREL_NAME) then if peripheral.wrap(BARREL_NAME) then
print("[OK] Barrel found: " .. BARREL_NAME) print("[OK] Barrel: " .. BARREL_NAME)
else else
print("[WARN] Barrel not found: " .. BARREL_NAME) print("[WARN] Barrel not found: " .. BARREL_NAME)
end end
-- Setup monitor
if setupMonitor() then if setupMonitor() then
print("[OK] Monitor found on: " .. MONITOR_SIDE) print("[OK] Monitor: " .. MONITOR_SIDE)
else else
print("[WARN] No monitor on " .. MONITOR_SIDE .. ". Dashboard disabled.") print("[WARN] No monitor on " .. MONITOR_SIDE)
end end
print("")
print("Console shows log output. Use the monitor to order items.")
print("") print("")
-- Run barrel watcher, dashboard, and command prompt in parallel
parallel.waitForAny( parallel.waitForAny(
-- Task 1: Watch barrel for new items -- Task 1: Barrel auto-sort
function() function()
while true do while true do
sortBarrel() pcall(sortBarrel)
sleep(POLL_INTERVAL) sleep(POLL_INTERVAL)
end end
end, end,
-- Task 2: Dashboard refresh -- Task 2: Dashboard refresh + status timer
function() function()
while true do while true do
pcall(drawDashboard) pcall(drawDashboard)
if statusTimer > 0 then
statusTimer = statusTimer - DASH_REFRESH
end
sleep(DASH_REFRESH) sleep(DASH_REFRESH)
end end
end, end,
-- Task 3: Interactive command prompt -- Task 3: Touch event listener
function() function()
while true do while true do
local items = showCatalogue() local event, side, x, y = os.pullEvent("monitor_touch")
print(string.format("[TOUCH] x=%d y=%d", x, y))
print("Commands:") handleTouch(x, y)
print(" order <number> [amount] - Dispense an item")
print(" scan - Refresh catalogue")
print(" quit - Exit")
print("")
write("> ")
local input = read()
local parts = {}
for word in input:gmatch("%S+") do
table.insert(parts, word)
end
local cmd = parts[1]
if cmd == "order" or cmd == "o" then
local idx = tonumber(parts[2])
local amt = tonumber(parts[3]) or 1
if idx and items[idx] then
orderItem(items[idx], amt)
else
print("[ERR] Invalid item number.")
end
elseif cmd == "scan" or cmd == "s" then
print("Rescanning...")
elseif cmd == "quit" or cmd == "q" then
print("Shutting down.")
return
else
print("[ERR] Unknown command: " .. tostring(cmd))
end
print("")
end end
end end
) )