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