diff --git a/inventoryManager.lua b/inventoryManager.lua index 5879999..edd6e7a 100644 --- a/inventoryManager.lua +++ b/inventoryManager.lua @@ -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 [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 )