Files
Inventory-Manager-CC/inventoryManager.lua

524 lines
16 KiB
Lua

-- Inventory Manager: Auto-sort barrel & order items to dropper
-- 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 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
if peripheral.getType(name) == "minecraft:chest" then
table.insert(chests, name)
end
end
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
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
-- Build a full catalogue: itemName -> { {chest=name, total=N}, ... }
-- Scans all chests in parallel for speed
local function buildCatalogue()
local chests = getChests()
local results = {} -- index -> { chest=name, contents={} }
-- Build parallel scan tasks
local tasks = {}
for i, chest in ipairs(chests) do
results[i] = { chest = chest, contents = {} }
tasks[i] = function()
results[i].contents = scanInventory(chest)
end
end
-- Run all scans simultaneously
if #tasks > 0 then
parallel.waitForAll(table.unpack(tasks))
end
-- Merge results into catalogue
local catalogue = {}
for _, result in ipairs(results) do
for itemName, info in pairs(result.contents) do
if not catalogue[itemName] then
catalogue[itemName] = {}
end
table.insert(catalogue[itemName], { chest = result.chest, total = info.total })
end
end
return catalogue
end
-------------------------------------------------
-- Monitor Dashboard
-------------------------------------------------
local mon = nil
local buf = nil -- offscreen buffer to prevent flickering
local function setupMonitor()
mon = peripheral.wrap(MONITOR_SIDE)
if not mon then return false end
mon.setTextScale(0.5)
mon.clear()
return true
end
-- Drawing target (buf or mon, set in drawDashboard)
local draw = nil
-- 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)
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
-- Draw the full dashboard (double-buffered)
local function drawDashboard()
if not mon then return end
local w, h = mon.getSize()
-- Create offscreen buffer
buf = window.create(mon, 1, 1, w, h, false)
draw = buf
-- Clear buffer
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 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: --")
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
-- ===== Divider =====
monFill(3, colors.lightBlue)
monCenter(3, string.rep("\140", math.min(w - 4, 60)), colors.cyan, colors.lightBlue)
-- ===== Storage capacity bar =====
-- Count total slots and used slots across all chests (in parallel)
local totalSlots = 0
local usedSlots = 0
local capTasks = {}
local capResults = {}
for i, chestName in ipairs(chests) do
capResults[i] = { total = 0, used = 0 }
capTasks[i] = function()
local inv = peripheral.wrap(chestName)
if inv then
local size = inv.size()
capResults[i].total = size
local contents = inv.list()
for _ in pairs(contents) do
capResults[i].used = capResults[i].used + 1
end
end
end
end
if #capTasks > 0 then
parallel.waitForAll(table.unpack(capTasks))
end
for _, r in ipairs(capResults) do
totalSlots = totalSlots + r.total
usedSlots = usedSlots + r.used
end
local freeSlots = totalSlots - usedSlots
local usedRatio = totalSlots > 0 and (usedSlots / totalSlots) or 0
local capY = 4
monFill(capY, colors.black)
local capLabel = string.format(" Storage: %d/%d slots (%d free)", usedSlots, totalSlots, freeSlots)
monWrite(2, capY, capLabel, colors.lightGray, colors.black)
-- Draw capacity bar
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
end
monBar(barStart, capY, barWidth, usedRatio, barColor, colors.gray)
-- Show percentage on the bar
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)
end
-- ===== 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 })
end
table.sort(itemList, function(a, b) return a.total > b.total end)
-- 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
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
monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg)
-- Item name
monWrite(5, y, short, colors.white, rowBg)
-- Quantity
local qtyStr = tostring(item.total)
monWrite(w - 22, y, qtyStr, colors.yellow, rowBg)
-- Stock bar
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)
row = row + 1
end
-- ===== Footer =====
local footerY = h - 1
monFill(footerY, colors.gray)
monWrite(2, footerY,
string.format(" Total: %d items across %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)
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
local contents = barrel.list()
if not contents or not next(contents) then return end
local catalogue = buildCatalogue()
local chests = getChests()
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)
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 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)
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
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
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)
return false
end
local remaining = 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))
end
if remaining <= 0 then break end
end
end
end
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))
end
print("[OK] Items in dropper. Computer 1 will dispense.")
return true
end
-------------------------------------------------
-- Display
-------------------------------------------------
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
end
print("")
return items
end
-------------------------------------------------
-- Main loop
-------------------------------------------------
local function main()
print("=================================")
print(" Inventory Manager v2")
print("=================================")
print("")
-- Check dropper
if peripheral.wrap(DROPPER_NAME) then
print("[OK] Dropper found: " .. 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)
else
print("[WARN] Barrel not found: " .. BARREL_NAME)
end
-- Setup monitor
if setupMonitor() then
print("[OK] Monitor found on: " .. MONITOR_SIDE)
else
print("[WARN] No monitor on " .. MONITOR_SIDE .. ". Dashboard disabled.")
end
print("")
-- Run barrel watcher, dashboard, and command prompt in parallel
parallel.waitForAny(
-- Task 1: Watch barrel for new items
function()
while true do
sortBarrel()
sleep(POLL_INTERVAL)
end
end,
-- Task 2: Dashboard refresh
function()
while true do
pcall(drawDashboard)
sleep(DASH_REFRESH)
end
end,
-- Task 3: Interactive command prompt
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("")
end
end
)
end
main()