Files
Inventory-Manager-CC/inventoryManager.lua

825 lines
26 KiB
Lua

-- 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"
local BARREL_NAME = "minecraft:barrel_0"
local POLL_INTERVAL = 2 -- seconds between barrel checks
local MONITOR_SIDE = "left"
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
-------------------------------------------------
local function getChests()
local chests = {}
for _, name in ipairs(peripheral.getNames()) do
if peripheral.getType(name) == "minecraft:chest" then
table.insert(chests, name)
end
end
return chests
end
local function scanInventory(deviceName)
local inv = peripheral.wrap(deviceName)
if not inv then return {} end
local result = {}
for slot, item in pairs(inv.list()) do
if not result[item.name] then
result[item.name] = { total = 0, slots = {} }
end
result[item.name].total = result[item.name].total + item.count
result[item.name].slots[slot] = { name = item.name, count = item.count }
end
return result
end
-- Full scan: updates the global cache
local function refreshCache()
activity.scanning = true
local chests = getChests()
local catalogue = {}
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
end
end
-- 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
-------------------------------------------------
-- Monitor setup
-------------------------------------------------
local mon = nil
local function setupMonitor()
mon = peripheral.wrap(MONITOR_SIDE)
if not mon then return false end
mon.setTextScale(0.5)
mon.clear()
return true
end
-------------------------------------------------
-- UI State
-------------------------------------------------
local selectedAmount = 1
local amountOptions = {1, 4, 8, 16, 32, 64}
local statusMessage = ""
local statusColor = colors.white
local statusTimer = 0
local touchZones = {}
local pendingZones = {}
local needsRedraw = true
local currentPage = 1
local totalPages = 1
local searchQuery = ""
local showKeyboard = false
-- Keyboard layout
local kbRows = {
{"Q","W","E","R","T","Y","U","I","O","P"},
{"A","S","D","F","G","H","J","K","L"},
{"Z","X","C","V","B","N","M"},
}
-- Get items filtered by search query
local function getFilteredItems()
local filtered = {}
for _, item in ipairs(cache.itemList) do
if searchQuery == "" then
table.insert(filtered, item)
else
local lower = item.name:lower():gsub("minecraft:", ""):gsub("_", " ")
if lower:find(searchQuery:lower(), 1, true) then
table.insert(filtered, item)
end
end
end
return filtered
end
local function addZone(x1, y1, x2, y2, action, data)
table.insert(pendingZones, {
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 (write to draw target)
-------------------------------------------------
local draw = nil
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)
end
local function monFill(y, color)
local w, _ = draw.getSize()
draw.setCursorPos(1, y)
draw.setBackgroundColor(color)
draw.write(string.rep(" ", w))
end
local function monCenter(y, text, fg, bg)
local w, _ = draw.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))
end
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 (reads ONLY from cache — no peripheral calls — instant)
-------------------------------------------------
local function drawDashboard()
if not mon then return end
local w, h = mon.getSize()
pendingZones = {}
-- Offscreen buffer
draw = window.create(mon, 1, 1, w, h, false)
draw.setBackgroundColor(colors.black)
draw.clear()
-- ===== Title bar =====
monFill(1, colors.blue)
monCenter(1, " ** INVENTORY MANAGER ** ", colors.white, colors.blue)
-- ===== Status bar =====
monFill(2, colors.gray)
local statusParts = {}
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 =====
monFill(4, colors.black)
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 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, 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
-- ===== 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
-- 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)
-- ===== Search bar + Pagination (row 6) =====
monFill(6, colors.black)
-- Keyboard toggle button
local kbLabel = showKeyboard and " X " or " ? "
local kbBg = showKeyboard and colors.red or colors.purple
monWrite(2, 6, kbLabel, colors.white, kbBg)
addZone(2, 6, 4, 6, "kb_toggle", nil)
-- Search query display
local queryDisplay = searchQuery
if showKeyboard then
queryDisplay = queryDisplay .. "|"
elseif queryDisplay == "" then
queryDisplay = "search..."
end
local fieldW = math.floor(w * 0.4)
if fieldW < 10 then fieldW = 10 end
local displayText = queryDisplay:sub(1, fieldW)
displayText = displayText .. string.rep("_", math.max(0, fieldW - #displayText))
monWrite(6, 6, displayText,
(searchQuery == "" and not showKeyboard) and colors.gray or colors.white,
colors.black)
addZone(6, 6, 5 + fieldW, 6, "kb_toggle", nil)
-- Filter items
local filteredItems = getFilteredItems()
-- Pagination
local maxRows = h - 10
if maxRows < 1 then maxRows = 1 end
totalPages = math.max(1, math.ceil(#filteredItems / maxRows))
if currentPage > totalPages then currentPage = totalPages end
if currentPage < 1 then currentPage = 1 end
-- Page controls (right side of row 6)
local pageStr = string.format("Pg %d/%d", currentPage, totalPages)
local navW = 3 + 1 + #pageStr + 1 + 3
local navX = w - navW
if currentPage > 1 then
monWrite(navX, 6, " < ", colors.white, colors.gray)
addZone(navX, 6, navX + 2, 6, "page_prev", nil)
else
monWrite(navX, 6, " < ", colors.lightGray, colors.black)
end
monWrite(navX + 4, 6, pageStr, colors.lightGray, colors.black)
local nextX = navX + 4 + #pageStr + 1
if currentPage < totalPages then
monWrite(nextX, 6, " > ", colors.white, colors.gray)
addZone(nextX, 6, nextX + 2, 6, "page_next", nil)
else
monWrite(nextX, 6, " > ", colors.lightGray, colors.black)
end
-- ===== 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(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 rows (paginated + filtered) =====
local maxCount = 0
for _, item in ipairs(filteredItems) do
if item.total > maxCount then maxCount = item.total end
end
if maxCount == 0 then maxCount = 1 end
local startIdx = (currentPage - 1) * maxRows + 1
local endIdx = math.min(startIdx + maxRows - 1, #filteredItems)
if #filteredItems == 0 then
monFill(8, colors.black)
monFill(9, colors.black)
if searchQuery ~= "" then
monCenter(9, "No items match \"" .. searchQuery .. "\"", colors.gray, colors.black)
else
monCenter(9, "No items in storage", colors.gray, colors.black)
end
row = 10
else
for i = startIdx, endIdx do
local item = filteredItems[i]
local y = row
local short = item.name:gsub("^minecraft:", ""):gsub("_", " ")
short = short:sub(1,1):upper() .. short:sub(2)
local maxNameLen = w - 30
if #short > maxNameLen then
short = short:sub(1, maxNameLen - 2) .. ".."
end
local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray
monFill(y, rowBg)
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
monWrite(5, y, short, colors.white, rowBg)
monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg)
local ratio = item.total / maxCount
local barColor = colors.lime
if ratio < 0.25 then barColor = colors.red
elseif ratio < 0.5 then barColor = colors.orange
end
monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray)
monWrite(w - 1, y, ">", colors.orange, rowBg)
addZone(1, y, w, y, "order", item.name)
row = row + 1
end
end
-- Fill remaining empty item rows
local lastItemRow = h - 3
while row <= lastItemRow do
monFill(row, colors.black)
row = row + 1
end
if showKeyboard then
-- ===== Keyboard overlay (bottom 3 rows: h-2, h-1, h) =====
local keyW = 3
local keyGap = 1
local kbDefs = {
{ keys = kbRows[1], specials = {{ label = " Bksp ", action = "kb_bksp", bg = colors.red }} },
{ keys = kbRows[2], specials = {{ label = " Done ", action = "kb_done", bg = colors.green }} },
{ keys = kbRows[3], specials = {
{ label = " Space ", action = "kb_space", bg = colors.lightGray },
{ label = " Clr ", action = "kb_clear", bg = colors.orange },
}},
}
for rowIdx, def in ipairs(kbDefs) do
local y = h - 3 + rowIdx
monFill(y, colors.black)
-- Calculate total row width for centering
local keysW = #def.keys * keyW + math.max(0, #def.keys - 1) * keyGap
local specialsW = 0
for _, sp in ipairs(def.specials) do
specialsW = specialsW + keyGap + #sp.label
end
local rowW = keysW + specialsW
local x = math.floor((w - rowW) / 2) + 1
-- Draw letter keys
for ki, key in ipairs(def.keys) do
monWrite(x, y, " " .. key .. " ", colors.white, colors.gray)
addZone(x, y, x + keyW - 1, y, "kb_key", key:lower())
x = x + keyW
if ki < #def.keys then x = x + keyGap end
end
-- Draw special keys
for _, sp in ipairs(def.specials) do
x = x + keyGap
monWrite(x, y, sp.label, colors.white, sp.bg)
addZone(x, y, x + #sp.label - 1, y, sp.action, nil)
x = x + #sp.label
end
end
else
-- ===== Status message (h-2) =====
monFill(h - 2, colors.black)
if statusTimer > 0 and #statusMessage > 0 then
monCenter(h - 2, statusMessage, statusColor, colors.black)
end
-- ===== Footer (h-1) =====
monFill(h - 1, colors.gray)
local footerLeft = string.format(" Total: %d items | %d types ",
cache.grandTotal, #cache.itemList)
monWrite(2, h - 1, footerLeft, colors.white, colors.gray)
if searchQuery ~= "" then
local filterNote = string.format("| Showing %d ", #filteredItems)
monWrite(2 + #footerLeft + 1, h - 1, filterNote, colors.yellow, colors.gray)
end
local timeStr = textutils.formatTime(os.time(), true)
monWrite(w - #timeStr - 1, h - 1, timeStr, colors.lightGray, colors.gray)
-- ===== Bottom accent (h) =====
monFill(h, 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)
end
-- Flush to monitor
draw.setVisible(true)
-- Swap zones
touchZones = pendingZones
end
-------------------------------------------------
-- Barrel auto-sort
-------------------------------------------------
local function sortBarrel()
local barrel = peripheral.wrap(BARREL_NAME)
if not barrel then return end
local contents = barrel.list()
if not contents or not next(contents) then return end
activity.sorting = true
needsRedraw = true
local catalogue = cache.catalogue
local chests = getChests()
for slot, item in pairs(contents) do
local moved = 0
if catalogue[item.name] then
for _, entry in ipairs(catalogue[item.name]) do
local n = barrel.pushItems(entry.chest, slot)
if n and n > 0 then
moved = moved + n
print(string.format("[SORT] %s x%d -> %s", item.name, n, entry.chest))
end
if moved >= item.count then break end
end
end
if moved < item.count then
for _, chest in ipairs(chests) do
local n = barrel.pushItems(chest, slot)
if n and n > 0 then
moved = moved + n
print(string.format("[SORT] %s x%d -> %s", item.name, n, chest))
end
if moved >= item.count then break end
end
end
if moved < item.count then
print(string.format("[WARN] Could not sort %d remaining %s", item.count - moved, item.name))
end
end
activity.sorting = false
needsRedraw = true
end
-------------------------------------------------
-- Order
-------------------------------------------------
local function orderItem(itemName, amount)
activity.dispensing = true
needsRedraw = true
local catalogue = cache.catalogue
if not catalogue[itemName] then
statusMessage = "Not found: " .. itemName:gsub("^minecraft:", "")
statusColor = colors.red
statusTimer = 5
activity.dispensing = false
needsRedraw = true
return false
end
local dropper = peripheral.wrap(DROPPER_NAME)
if not dropper then
statusMessage = "Dropper offline!"
statusColor = colors.red
statusTimer = 5
activity.dispensing = false
needsRedraw = true
return false
end
local remaining = amount
for _, entry in ipairs(catalogue[itemName]) do
local chest = peripheral.wrap(entry.chest)
if chest then
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", itemName, moved, entry.chest))
end
if remaining <= 0 then break end
end
end
end
if remaining <= 0 then break end
end
local sent = amount - remaining
local short = itemName:gsub("^minecraft:", ""):gsub("_", " ")
if sent > 0 then
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 = 5
activity.dispensing = false
needsRedraw = true
return sent > 0
end
-------------------------------------------------
-- Touch handler (no peripheral calls — instant)
-------------------------------------------------
local function handleTouch(x, y)
local action, data = hitTest(x, y)
if not action then
print("[TOUCH] No zone hit")
return
end
if action == "amount" then
selectedAmount = data
print("[UI] Amount set to " .. data)
needsRedraw = true
elseif action == "order" then
local itemName = data
if itemName then
local short = itemName:gsub("^minecraft:", ""):gsub("_", " ")
statusMessage = string.format("Ordering %s x%d...", short, selectedAmount)
statusColor = colors.cyan
statusTimer = 10
activity.dispensing = true
needsRedraw = true
orderItem(itemName, selectedAmount)
end
elseif action == "scan" then
statusMessage = "Refreshing..."
statusColor = colors.cyan
statusTimer = 3
needsRedraw = true
print("[UI] Manual refresh")
elseif action == "kb_toggle" then
showKeyboard = not showKeyboard
print("[UI] Keyboard " .. (showKeyboard and "open" or "closed"))
needsRedraw = true
elseif action == "kb_key" then
if #searchQuery < 30 then
searchQuery = searchQuery .. data
end
currentPage = 1
needsRedraw = true
elseif action == "kb_bksp" then
if #searchQuery > 0 then
searchQuery = searchQuery:sub(1, -2)
end
currentPage = 1
needsRedraw = true
elseif action == "kb_space" then
if #searchQuery < 30 then
searchQuery = searchQuery .. " "
end
currentPage = 1
needsRedraw = true
elseif action == "kb_done" then
showKeyboard = false
print("[UI] Keyboard closed")
needsRedraw = true
elseif action == "kb_clear" then
searchQuery = ""
currentPage = 1
print("[UI] Search cleared")
needsRedraw = true
elseif action == "page_prev" then
if currentPage > 1 then
currentPage = currentPage - 1
print("[UI] Page " .. currentPage)
end
needsRedraw = true
elseif action == "page_next" then
if currentPage < totalPages then
currentPage = currentPage + 1
print("[UI] Page " .. currentPage)
end
needsRedraw = true
end
end
-------------------------------------------------
-- Main
-------------------------------------------------
local function main()
print("=================================")
print(" Inventory Manager v2 (Touch)")
print("=================================")
print("")
if peripheral.wrap(DROPPER_NAME) then
print("[OK] Dropper: " .. DROPPER_NAME)
else
print("[WARN] Dropper not found: " .. DROPPER_NAME)
end
if peripheral.wrap(BARREL_NAME) then
print("[OK] Barrel: " .. BARREL_NAME)
else
print("[WARN] Barrel not found: " .. BARREL_NAME)
end
if setupMonitor() then
print("[OK] Monitor: " .. MONITOR_SIDE)
else
print("[WARN] No monitor on " .. MONITOR_SIDE)
end
print("")
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: 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)
sleep(POLL_INTERVAL)
end
end,
-- Task 3: Dashboard redraw (event-driven, checks every 0.1s)
function()
needsRedraw = true
while true do
if needsRedraw then
needsRedraw = false
pcall(drawDashboard)
end
-- Decrement status timer
if statusTimer > 0 then
statusTimer = statusTimer - 0.1
if statusTimer <= 0 then
statusTimer = 0
needsRedraw = true
end
end
sleep(0.1)
end
end,
-- Task 4: Touch event listener
function()
while true do
local event, side, x, y = os.pullEvent("monitor_touch")
print(string.format("[TOUCH] x=%d y=%d", x, y))
handleTouch(x, y)
end
end
)
end
main()