-- 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 local SMELT_INTERVAL = 3 -- seconds between furnace checks local SMELT_RESERVE = 64 -- keep at least 1 stack of each raw material local CACHE_FILE = ".inventory_cache" -- persistent cache file local SMELTER_MONITOR_SIDE = "top" local DISABLED_RECIPES_FILE = ".disabled_recipes" ------------------------------------------------- -- Furnace types to manage ------------------------------------------------- local FURNACE_TYPES = { "minecraft:furnace", "minecraft:smoker", "minecraft:blast_furnace", } -- Furnace slots: 1 = input, 2 = fuel, 3 = output (standard Minecraft) local SLOT_INPUT = 1 local SLOT_FUEL = 2 local SLOT_OUTPUT = 3 ------------------------------------------------- -- Smeltable items: input -> output -- Items in chests matching a key here get auto-smelted. -- Add/remove entries to control what gets cooked. ------------------------------------------------- local SMELTABLE = { -- Ores (furnace + blast furnace only) ["minecraft:raw_iron"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:raw_gold"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:raw_copper"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:iron_ore"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:gold_ore"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:copper_ore"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:deepslate_iron_ore"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:deepslate_gold_ore"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:deepslate_copper_ore"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:ancient_debris"] = { result = "minecraft:netherite_scrap",furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, -- Sand / stone (furnace + blast furnace) ["minecraft:sand"] = { result = "minecraft:glass", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:red_sand"] = { result = "minecraft:glass", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:cobblestone"] = { result = "minecraft:stone", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:stone"] = { result = "minecraft:smooth_stone", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:clay_ball"] = { result = "minecraft:brick", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:netherrack"] = { result = "minecraft:nether_brick", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:sandstone"] = { result = "minecraft:smooth_sandstone", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, -- Food (furnace + smoker only) ["minecraft:beef"] = { result = "minecraft:cooked_beef", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:porkchop"] = { result = "minecraft:cooked_porkchop", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:chicken"] = { result = "minecraft:cooked_chicken", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:mutton"] = { result = "minecraft:cooked_mutton", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:rabbit"] = { result = "minecraft:cooked_rabbit", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:cod"] = { result = "minecraft:cooked_cod", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:salmon"] = { result = "minecraft:cooked_salmon", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:potato"] = { result = "minecraft:baked_potato", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:kelp"] = { result = "minecraft:dried_kelp", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, -- Misc (furnace only) ["minecraft:wet_sponge"] = { result = "minecraft:sponge", furnaces = {"minecraft:furnace"} }, ["minecraft:cactus"] = { result = "minecraft:green_dye", furnaces = {"minecraft:furnace"} }, ["minecraft:sea_pickle"] = { result = "minecraft:lime_dye", furnaces = {"minecraft:furnace"} }, ["minecraft:log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:oak_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:spruce_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:birch_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:jungle_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:acacia_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:dark_oak_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:mangrove_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:cherry_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, } -- Fuel items, ordered by preference (best first) -- burn_time = how many items one fuel smelts local FUEL_LIST = { { name = "minecraft:coal", burn_time = 8 }, { name = "minecraft:charcoal", burn_time = 8 }, { name = "minecraft:coal_block", burn_time = 80 }, { name = "minecraft:blaze_rod", burn_time = 12 }, { name = "minecraft:dried_kelp_block", burn_time = 20 }, { name = "minecraft:lava_bucket", burn_time = 100 }, { name = "minecraft:oak_planks", burn_time = 1.5 }, { name = "minecraft:spruce_planks",burn_time = 1.5 }, { name = "minecraft:birch_planks", burn_time = 1.5 }, { name = "minecraft:jungle_planks",burn_time = 1.5 }, { name = "minecraft:acacia_planks",burn_time = 1.5 }, { name = "minecraft:dark_oak_planks",burn_time = 1.5 }, { name = "minecraft:mangrove_planks",burn_time = 1.5 }, { name = "minecraft:cherry_planks",burn_time = 1.5 }, { name = "minecraft:stick", burn_time = 0.5 }, } -- Build a set for quick lookup local FUEL_SET = {} for _, f in ipairs(FUEL_LIST) do FUEL_SET[f.name] = true end ------------------------------------------------- -- 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, furnaceCount = 0, furnaceStatus = {}, -- per-furnace { name, type, input, fuel, output, active } } ------------------------------------------------- -- Activity state (shown on monitor) ------------------------------------------------- local activity = { sorting = false, -- barrel sort in progress dispensing = false, -- order in progress scanning = false, -- background scan in progress smelting = false, -- auto-smelt 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 getFurnaces() local furnaces = {} for _, ftype in ipairs(FURNACE_TYPES) do for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == ftype then table.insert(furnaces, name) end end end return furnaces end local function refreshFurnaceStatus() local furnaces = getFurnaces() local status = {} for _, fname in ipairs(furnaces) do local furnace = peripheral.wrap(fname) if furnace then local contents = furnace.list() local entry = { name = fname, type = peripheral.getType(fname), input = contents[SLOT_INPUT] or nil, fuel = contents[SLOT_FUEL] or nil, output = contents[SLOT_OUTPUT] or nil, active = (contents[SLOT_INPUT] ~= nil and contents[SLOT_FUEL] ~= nil), } table.insert(status, entry) end end cache.furnaceStatus = status 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 -- onProgress(current, total, chestName) is called per chest if provided local function refreshCache(onProgress) activity.scanning = true local chests = getChests() local catalogue = {} local totalSlots = 0 local usedSlots = 0 for ci, chest in ipairs(chests) do if onProgress then onProgress(ci, #chests, chest) end 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 -- Count furnaces local furnaceCount = 0 for _, ftype in ipairs(FURNACE_TYPES) do for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == ftype then furnaceCount = furnaceCount + 1 end end end cache.furnaceCount = furnaceCount -- Scan furnace contents for smelter dashboard refreshFurnaceStatus() activity.scanning = false -- Persist cache to disk pcall(function() local data = { catalogue = cache.catalogue, itemList = cache.itemList, grandTotal = cache.grandTotal, chestCount = cache.chestCount, totalSlots = cache.totalSlots, usedSlots = cache.usedSlots, freeSlots = cache.freeSlots, usedRatio = cache.usedRatio, dropperOk = cache.dropperOk, barrelOk = cache.barrelOk, furnaceCount = cache.furnaceCount, furnaceStatus = cache.furnaceStatus, savedAt = os.epoch("utc"), } local f = fs.open(CACHE_FILE, "w") f.write(textutils.serialise(data)) f.close() end) end -- Load cache from disk (returns true if loaded) local function loadCacheFromDisk() if not fs.exists(CACHE_FILE) then return false end local ok, err = pcall(function() local f = fs.open(CACHE_FILE, "r") local raw = f.readAll() f.close() local data = textutils.unserialise(raw) if data and data.catalogue and data.itemList then cache.catalogue = data.catalogue cache.itemList = data.itemList cache.grandTotal = data.grandTotal or 0 cache.chestCount = data.chestCount or 0 cache.totalSlots = data.totalSlots or 0 cache.usedSlots = data.usedSlots or 0 cache.freeSlots = data.freeSlots or 0 cache.usedRatio = data.usedRatio or 0 cache.dropperOk = data.dropperOk or false cache.barrelOk = data.barrelOk or false cache.furnaceCount = data.furnaceCount or 0 cache.furnaceStatus = data.furnaceStatus or {} else error("invalid cache data") end end) if not ok then print("[WARN] Could not load cache: " .. tostring(err)) return false end return true end ------------------------------------------------- -- Monitor setup ------------------------------------------------- local mon = nil local monName = nil local smelterMon = nil local smelterMonName = nil local function setupMonitor() mon = peripheral.wrap(MONITOR_SIDE) if mon and mon.setTextScale then monName = MONITOR_SIDE else mon = nil end if not mon then -- Search for a monitor on the network (skip smelter side) for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "monitor" and name ~= SMELTER_MONITOR_SIDE then mon = peripheral.wrap(name) monName = name break end end end if not mon then return false end mon.setTextScale(0.5) mon.clear() return true end local function setupSmelterMonitor() smelterMon = peripheral.wrap(SMELTER_MONITOR_SIDE) if smelterMon and smelterMon.setTextScale then smelterMonName = SMELTER_MONITOR_SIDE else smelterMon = nil end if not smelterMon then -- Search for a second monitor on the network for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "monitor" and name ~= monName then smelterMon = peripheral.wrap(name) smelterMonName = name break end end end if not smelterMon then return false end smelterMon.setTextScale(0.5) smelterMon.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"}, } ------------------------------------------------- -- Smelter dashboard state ------------------------------------------------- local smelterView = "status" -- "status" or "recipes" local smelterPage = 1 local smelterTotalPages = 1 local smelterTouchZones = {} local smelterPendingZones = {} local smelterNeedsRedraw = true local smeltingPaused = false local disabledRecipes = {} -- { ["minecraft:raw_iron"] = true } local function loadDisabledRecipes() if not fs.exists(DISABLED_RECIPES_FILE) then return end pcall(function() local f = fs.open(DISABLED_RECIPES_FILE, "r") local raw = f.readAll() f.close() local data = textutils.unserialise(raw) if type(data) == "table" then if data.disabled then disabledRecipes = data.disabled end if data.paused ~= nil then smeltingPaused = data.paused end end end) end local function saveDisabledRecipes() pcall(function() local f = fs.open(DISABLED_RECIPES_FILE, "w") f.write(textutils.serialise({ disabled = disabledRecipes, paused = smeltingPaused })) f.close() end) end -- 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 local function addSmelterZone(x1, y1, x2, y2, action, data) table.insert(smelterPendingZones, { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action, data = data }) end local function smelterHitTest(x, y) for _, zone in ipairs(smelterTouchZones) 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: --") if cache.furnaceCount and cache.furnaceCount > 0 then table.insert(statusParts, string.format("Furnaces: %d", cache.furnaceCount)) end -- Activity indicators local actParts = {} if activity.sorting then table.insert(actParts, "SORTING") end if activity.dispensing then table.insert(actParts, "DISPENSING") end if activity.smelting then table.insert(actParts, "SMELTING") 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.smelting then bottomMsg = " SMELTING... " 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 ------------------------------------------------- -- Smelter Dashboard ------------------------------------------------- local function drawSmelterDashboard() if not smelterMon then return end local w, h = smelterMon.getSize() smelterPendingZones = {} -- Offscreen buffer draw = window.create(smelterMon, 1, 1, w, h, false) draw.setBackgroundColor(colors.black) draw.clear() -- ===== Title bar ===== monFill(1, colors.purple) monCenter(1, " ** SMELTER DASHBOARD ** ", colors.white, colors.purple) -- ===== Status bar ===== monFill(2, colors.gray) local activeCount = 0 for _, fs in ipairs(cache.furnaceStatus or {}) do if fs.active then activeCount = activeCount + 1 end end local statusStr = string.format(" Furnaces: %d Active: %d", cache.furnaceCount or 0, activeCount) monWrite(2, 2, statusStr, colors.white, colors.gray) -- Pause/Resume button local pauseLabel = smeltingPaused and " PAUSED " or " ACTIVE " local pauseBg = smeltingPaused and colors.red or colors.lime local pauseFg = smeltingPaused and colors.white or colors.black monWrite(w - #pauseLabel, 2, pauseLabel, pauseFg, pauseBg) addSmelterZone(w - #pauseLabel, 2, w - 1, 2, "toggle_pause", nil) -- ===== Divider ===== monFill(3, colors.magenta) monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.pink, colors.magenta) -- ===== Tab row ===== monFill(4, colors.black) local tabStatusBg = smelterView == "status" and colors.purple or colors.gray local tabRecipesBg = smelterView == "recipes" and colors.purple or colors.gray local bx1, by1, bx2, by2 bx1, by1, bx2, by2 = drawButton(2, 4, "Status", colors.white, tabStatusBg) addSmelterZone(bx1, by1, bx2, by2, "tab", "status") bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Recipes", colors.white, tabRecipesBg) addSmelterZone(bx1, by1, bx2, by2, "tab", "recipes") if smelterView == "status" then -- ===== Furnace Status View ===== -- Column headers monFill(5, colors.gray) local outCol = math.floor(w * 0.40) local fuelCol = math.floor(w * 0.65) local statCol = w - 6 monWrite(2, 5, "#", colors.lightGray, colors.gray) monWrite(4, 5, "T", colors.lightGray, colors.gray) monWrite(6, 5, "Input", colors.lightGray, colors.gray) monWrite(outCol, 5, "Output", colors.lightGray, colors.gray) monWrite(fuelCol, 5, "Fuel", colors.lightGray, colors.gray) monWrite(statCol, 5, "State", colors.lightGray, colors.gray) -- Furnace rows local furnaceList = cache.furnaceStatus or {} local maxRows = h - 8 if maxRows < 1 then maxRows = 1 end smelterTotalPages = math.max(1, math.ceil(#furnaceList / maxRows)) if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end if smelterPage < 1 then smelterPage = 1 end local startIdx = (smelterPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #furnaceList) local row = 6 if #furnaceList == 0 then monFill(7, colors.black) monCenter(7, "No furnaces found on network", colors.gray, colors.black) row = 8 else for i = startIdx, endIdx do local fs = furnaceList[i] local y = row local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) -- Number monWrite(2, y, string.format("%d", i), colors.lightBlue, rowBg) -- Type abbreviation local typeAbbr = "F" local typeColor = colors.orange if fs.type == "minecraft:smoker" then typeAbbr = "S" typeColor = colors.green elseif fs.type == "minecraft:blast_furnace" then typeAbbr = "B" typeColor = colors.cyan end monWrite(4, y, typeAbbr, typeColor, rowBg) -- Input if fs.input then local inName = fs.input.name:gsub("^minecraft:", ""):gsub("_", " ") local maxIn = outCol - 8 if #inName > maxIn then inName = inName:sub(1, maxIn - 2) .. ".." end monWrite(6, y, inName, colors.white, rowBg) monWrite(outCol - 4, y, "x" .. fs.input.count, colors.yellow, rowBg) else monWrite(6, y, "(empty)", colors.lightGray, rowBg) end -- Output if fs.output then local outName = fs.output.name:gsub("^minecraft:", ""):gsub("_", " ") local maxOut = fuelCol - outCol - 5 if #outName > maxOut then outName = outName:sub(1, maxOut - 2) .. ".." end monWrite(outCol, y, outName, colors.white, rowBg) monWrite(fuelCol - 4, y, "x" .. fs.output.count, colors.yellow, rowBg) else monWrite(outCol, y, "-", colors.lightGray, rowBg) end -- Fuel if fs.fuel then local fuelName = fs.fuel.name:gsub("^minecraft:", ""):gsub("_", " ") local maxFuel = statCol - fuelCol - 4 if #fuelName > maxFuel then fuelName = fuelName:sub(1, maxFuel - 2) .. ".." end monWrite(fuelCol, y, fuelName, colors.white, rowBg) monWrite(statCol - 4, y, "x" .. fs.fuel.count, colors.yellow, rowBg) else monWrite(fuelCol, y, "-", colors.lightGray, rowBg) end -- Status indicator if smeltingPaused then monWrite(statCol, y, "PAUSE", colors.red, rowBg) elseif fs.active then monWrite(statCol, y, " COOK", colors.lime, rowBg) elseif fs.input and not fs.fuel then monWrite(statCol, y, "FUEL?", colors.orange, rowBg) else monWrite(statCol, y, " IDLE", colors.lightGray, rowBg) end row = row + 1 end end -- Fill remaining rows while row <= h - 2 do monFill(row, colors.black) row = row + 1 end else -- ===== Recipe Manager View ===== -- Build sorted recipe list local recipeList = {} for inputName, recipe in pairs(SMELTABLE) do local short = inputName:gsub("^minecraft:", ""):gsub("_", " ") short = short:sub(1,1):upper() .. short:sub(2) local resultShort = recipe.result:gsub("^minecraft:", ""):gsub("_", " ") resultShort = resultShort:sub(1,1):upper() .. resultShort:sub(2) local types = "" for _, ft in ipairs(recipe.furnaces) do if ft == "minecraft:furnace" then types = types .. "F" elseif ft == "minecraft:smoker" then types = types .. "S" elseif ft == "minecraft:blast_furnace" then types = types .. "B" end end local enabled = not disabledRecipes[inputName] local inStorage = 0 if cache.catalogue[inputName] then for _, s in ipairs(cache.catalogue[inputName]) do inStorage = inStorage + s.total end end table.insert(recipeList, { inputName = inputName, inputShort = short, resultShort = resultShort, types = types, enabled = enabled, inStorage = inStorage, }) end table.sort(recipeList, function(a, b) return a.inputShort < b.inputShort end) -- Column positions local arrowCol = math.floor(w * 0.30) local typeCol = math.floor(w * 0.60) local stockCol = math.floor(w * 0.72) local toggleCol = w - 5 -- Column headers monFill(5, colors.gray) monWrite(2, 5, "Input", colors.lightGray, colors.gray) monWrite(arrowCol, 5, "Output", colors.lightGray, colors.gray) monWrite(typeCol, 5, "Type", colors.lightGray, colors.gray) monWrite(stockCol, 5, "Stock", colors.lightGray, colors.gray) monWrite(toggleCol, 5, "On?", colors.lightGray, colors.gray) -- Bulk action buttons on tab row local bulkX = w - 22 bx1, by1, bx2, by2 = drawButton(bulkX, 4, "All On", colors.white, colors.green) addSmelterZone(bx1, by1, bx2, by2, "enable_all", nil) bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "All Off", colors.white, colors.red) addSmelterZone(bx1, by1, bx2, by2, "disable_all", nil) -- Recipe rows local maxRows = h - 8 if maxRows < 1 then maxRows = 1 end smelterTotalPages = math.max(1, math.ceil(#recipeList / maxRows)) if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end if smelterPage < 1 then smelterPage = 1 end local startIdx = (smelterPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #recipeList) local row = 6 for i = startIdx, endIdx do local r = recipeList[i] local y = row local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) -- Input name local maxInputLen = arrowCol - 3 local inputDisplay = r.inputShort if #inputDisplay > maxInputLen then inputDisplay = inputDisplay:sub(1, maxInputLen - 2) .. ".." end monWrite(2, y, inputDisplay, colors.white, rowBg) -- Output local maxOutLen = typeCol - arrowCol - 2 local outDisplay = r.resultShort if #outDisplay > maxOutLen then outDisplay = outDisplay:sub(1, maxOutLen - 2) .. ".." end monWrite(arrowCol, y, outDisplay, colors.lightBlue, rowBg) -- Types monWrite(typeCol, y, r.types, colors.orange, rowBg) -- Stock monWrite(stockCol, y, tostring(r.inStorage), colors.yellow, rowBg) -- Toggle button if r.enabled then monWrite(toggleCol, y, " ON ", colors.white, colors.green) else monWrite(toggleCol, y, " OFF", colors.white, colors.red) end addSmelterZone(1, y, w, y, "toggle_recipe", r.inputName) row = row + 1 end -- Fill remaining rows while row <= h - 2 do monFill(row, colors.black) row = row + 1 end end -- ===== Pagination (h - 1) ===== monFill(h - 1, colors.gray) local pageStr = string.format("Pg %d/%d", smelterPage, smelterTotalPages) monCenter(h - 1, pageStr, colors.white, colors.gray) if smelterPage > 1 then monWrite(2, h - 1, " < ", colors.white, colors.lightGray) addSmelterZone(2, h - 1, 4, h - 1, "page_prev", nil) end if smelterPage < smelterTotalPages then monWrite(w - 3, h - 1, " > ", colors.white, colors.lightGray) addSmelterZone(w - 3, h - 1, w - 1, h - 1, "page_next", nil) end -- ===== Bottom accent ===== monFill(h, colors.purple) local enabledCount = 0 local totalRecipes = 0 for _ in pairs(SMELTABLE) do totalRecipes = totalRecipes + 1 end for inputName in pairs(SMELTABLE) do if not disabledRecipes[inputName] then enabledCount = enabledCount + 1 end end local bottomMsg = string.format(" Recipes: %d/%d enabled ", enabledCount, totalRecipes) if activity.smelting then bottomMsg = " SMELTING... " end monCenter(h, bottomMsg, colors.pink, colors.purple) -- Flush to monitor draw.setVisible(true) -- Swap zones smelterTouchZones = smelterPendingZones 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 ------------------------------------------------- -- Auto-smelt ------------------------------------------------- local function autoSmelt() if smeltingPaused then return false end local furnaces = getFurnaces() if #furnaces == 0 then return end local chests = getChests() local catalogue = cache.catalogue local didWork = false for _, fname in ipairs(furnaces) do local furnace = peripheral.wrap(fname) if furnace then local contents = furnace.list() -- 1) Pull finished output (slot 3) back to chests if contents[SLOT_OUTPUT] then local outputItem = contents[SLOT_OUTPUT] local remaining = outputItem.count for _, chest in ipairs(chests) do local n = furnace.pushItems(chest, SLOT_OUTPUT) if n and n > 0 then remaining = remaining - n print(string.format("[SMELT] Output %s x%d -> %s", outputItem.name, n, chest)) didWork = true if remaining <= 0 then break end end end if remaining > 0 then print(string.format("[WARN] Could not move %d %s from %s output (chests full?)", remaining, outputItem.name, fname)) end end -- Also check all slots in case output ended up elsewhere -- Some modded furnaces or CC versions may use different slot indices for slot, item in pairs(contents) do if slot ~= SLOT_INPUT and slot ~= SLOT_FUEL and slot ~= SLOT_OUTPUT then for _, chest in ipairs(chests) do local n = furnace.pushItems(chest, slot) if n and n > 0 then print(string.format("[SMELT] Extra slot %d: %s x%d -> %s", slot, item.name, n, chest)) didWork = true break end end end end -- Re-read after output pull contents = furnace.list() -- 2) Check for incompatible items in input slot and remove them local furnaceType = peripheral.getType(fname) local inputItem = contents[SLOT_INPUT] if inputItem then local recipe = SMELTABLE[inputItem.name] local validHere = false if recipe then for _, ft in ipairs(recipe.furnaces) do if ft == furnaceType then validHere = true break end end end if not validHere then -- This item doesn't belong in this furnace type — pull it out for _, chest in ipairs(chests) do local n = furnace.pushItems(chest, SLOT_INPUT) if n and n > 0 then print(string.format("[SMELT] Removed incompatible %s x%d from %s -> %s", inputItem.name, n, fname, chest)) didWork = true break end end -- Re-read after removal contents = furnace.list() end end -- 3) Refuel if fuel slot is empty or low local fuelItem = contents[SLOT_FUEL] local needFuel = not fuelItem or fuelItem.count < 8 if needFuel then for _, fuel in ipairs(FUEL_LIST) do if catalogue[fuel.name] then for _, source in ipairs(catalogue[fuel.name]) do local chest = peripheral.wrap(source.chest) if chest then for slot, slotItem in pairs(chest.list()) do if slotItem.name == fuel.name then local toMove = math.min(16, slotItem.count) local n = chest.pushItems(fname, slot, toMove, SLOT_FUEL) if n and n > 0 then print(string.format("[SMELT] Fuel %s x%d -> %s", fuel.name, n, fname)) didWork = true needFuel = false break end end end end if not needFuel then break end end end if not needFuel then break end end end -- 4) Load smeltable items into empty input slot inputItem = contents[SLOT_INPUT] if not inputItem then -- Find something smeltable in chests that this furnace type can process for itemName, recipe in pairs(SMELTABLE) do -- Check if this furnace type is compatible local compatible = false for _, ft in ipairs(recipe.furnaces) do if ft == furnaceType then compatible = true break end end if compatible and not disabledRecipes[itemName] and catalogue[itemName] then -- Count total of this item across all chests local totalInStorage = 0 for _, src in ipairs(catalogue[itemName]) do totalInStorage = totalInStorage + src.total end -- Only smelt the excess beyond SMELT_RESERVE local available = totalInStorage - SMELT_RESERVE if available <= 0 then -- Skip: need to keep reserve else local loaded = false local remaining = math.min(available, 64) for _, source in ipairs(catalogue[itemName]) do local chest = peripheral.wrap(source.chest) if chest then for slot, slotItem in pairs(chest.list()) do if slotItem.name == itemName then local toMove = math.min(slotItem.count, remaining) local n = chest.pushItems(fname, slot, toMove, SLOT_INPUT) if n and n > 0 then print(string.format("[SMELT] Input %s x%d -> %s (reserve %d)", itemName, n, fname, math.max(0, totalInStorage - n))) didWork = true remaining = remaining - n if remaining <= 0 then loaded = true break end end end end end if loaded then break end end if loaded or remaining < math.min(available, 64) then break end end end end end end end return didWork 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 ------------------------------------------------- -- Smelter touch handler ------------------------------------------------- local function handleSmelterTouch(x, y) local action, data = smelterHitTest(x, y) if not action then return end if action == "tab" then smelterView = data smelterPage = 1 print("[SMELT-UI] Tab: " .. data) smelterNeedsRedraw = true elseif action == "toggle_pause" then smeltingPaused = not smeltingPaused print("[SMELT-UI] Smelting " .. (smeltingPaused and "PAUSED" or "RESUMED")) saveDisabledRecipes() smelterNeedsRedraw = true needsRedraw = true elseif action == "toggle_recipe" then if disabledRecipes[data] then disabledRecipes[data] = nil else disabledRecipes[data] = true end local short = data:gsub("^minecraft:", ""):gsub("_", " ") print("[SMELT-UI] Recipe " .. short .. ": " .. (disabledRecipes[data] and "OFF" or "ON")) saveDisabledRecipes() smelterNeedsRedraw = true elseif action == "enable_all" then disabledRecipes = {} print("[SMELT-UI] All recipes enabled") saveDisabledRecipes() smelterNeedsRedraw = true elseif action == "disable_all" then for inputName in pairs(SMELTABLE) do disabledRecipes[inputName] = true end print("[SMELT-UI] All recipes disabled") saveDisabledRecipes() smelterNeedsRedraw = true elseif action == "page_prev" then if smelterPage > 1 then smelterPage = smelterPage - 1 end smelterNeedsRedraw = true elseif action == "page_next" then if smelterPage < smelterTotalPages then smelterPage = smelterPage + 1 end smelterNeedsRedraw = 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("") -- Try loading cached inventory from disk for instant startup local cacheLoaded = loadCacheFromDisk() if cacheLoaded then print("[INIT] Loaded cached inventory (" .. #cache.itemList .. " types)") print("[INIT] Background refresh starting...") else -- No cache: do full scan with progress bar print("[INIT] No cache found. Scanning inventories...") if mon then local w, h = mon.getSize() local buf = window.create(mon, 1, 1, w, h, false) local function drawBoot(current, total, chestName) buf.setBackgroundColor(colors.black) buf.clear() -- Title buf.setBackgroundColor(colors.blue) buf.setCursorPos(1, 1) buf.write(string.rep(" ", w)) local title = " INVENTORY MANAGER " buf.setCursorPos(math.floor((w - #title) / 2) + 1, 1) buf.setTextColor(colors.white) buf.write(title) -- Scanning label local midY = math.floor(h / 2) buf.setBackgroundColor(colors.black) buf.setTextColor(colors.lightGray) local label = "Scanning inventories..." buf.setCursorPos(math.floor((w - #label) / 2) + 1, midY - 2) buf.write(label) -- Chest name local short = chestName or "" if #short > w - 4 then short = ".." .. short:sub(-(w - 6)) end buf.setTextColor(colors.gray) buf.setCursorPos(math.floor((w - #short) / 2) + 1, midY - 1) buf.write(short) -- Progress bar local barW = math.min(w - 8, 40) local barX = math.floor((w - barW) / 2) + 1 local ratio = total > 0 and (current / total) or 0 local filled = math.floor(ratio * barW) buf.setCursorPos(barX, midY + 1) buf.setBackgroundColor(colors.lime) buf.write(string.rep(" ", filled)) buf.setBackgroundColor(colors.gray) buf.write(string.rep(" ", barW - filled)) -- Percentage + count buf.setBackgroundColor(colors.black) buf.setTextColor(colors.white) local pct = string.format("%d/%d (%d%%)", current, total, math.floor(ratio * 100)) buf.setCursorPos(math.floor((w - #pct) / 2) + 1, midY + 3) buf.write(pct) -- Bottom accent buf.setCursorPos(1, h) buf.setBackgroundColor(colors.blue) buf.write(string.rep(" ", w)) buf.setVisible(true) buf.setVisible(false) end refreshCache(drawBoot) else refreshCache() end print("[INIT] Done. Found " .. #cache.itemList .. " item types.") end print("") parallel.waitForAny( -- Task 1: Background inventory scanner function() -- If we loaded from disk cache, refresh immediately in background if cacheLoaded then pcall(refreshCache) needsRedraw = true print("[INIT] Background refresh complete. " .. #cache.itemList .. " types.") end 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: Auto-smelt function() while true do activity.smelting = true needsRedraw = true local ok, didWork = pcall(autoSmelt) activity.smelting = false needsRedraw = true -- If work was done, scan sooner to update cache if ok and didWork then pcall(refreshCache) needsRedraw = true end sleep(SMELT_INTERVAL) end end, -- Task 4: 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 5: 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()