1480 lines
53 KiB
Lua
1480 lines
53 KiB
Lua
-- Inventory Client: Display-only dashboard
|
|
-- Connects to the master inventoryManager via wired modem.
|
|
-- Shows the same dashboard but runs NO automation tasks.
|
|
-- Orders and smelter controls are sent to the master.
|
|
|
|
-------------------------------------------------
|
|
-- Configuration
|
|
-------------------------------------------------
|
|
|
|
local BROADCAST_CHANNEL = 4200 -- master sends state on this channel
|
|
local ORDER_CHANNEL = 4201 -- master listens for commands here
|
|
local CLIENT_CHANNEL = 4202 -- client listens for replies here
|
|
local MONITOR_SIDE = "left"
|
|
local SMELTER_MONITOR_SIDE = "top"
|
|
|
|
-- Remote station: set these to use a different dropper/barrel than the master.
|
|
-- Leave empty ("") to use the master's default dropper/barrel.
|
|
local CLIENT_DROPPER_NAME = "" -- e.g. "minecraft:dropper_5"
|
|
local CLIENT_BARREL_NAME = "" -- e.g. "minecraft:barrel_3"
|
|
|
|
local DROPPER_ANNOUNCE_INTERVAL = 30 -- seconds between dropper announcements
|
|
|
|
-------------------------------------------------
|
|
-- State (received from master)
|
|
-------------------------------------------------
|
|
|
|
local cache = {
|
|
itemList = {},
|
|
grandTotal = 0,
|
|
chestCount = 0,
|
|
totalSlots = 0,
|
|
usedSlots = 0,
|
|
freeSlots = 0,
|
|
usedRatio = 0,
|
|
dropperOk = false,
|
|
barrelOk = false,
|
|
furnaceCount = 0,
|
|
furnaceStatus = {},
|
|
}
|
|
|
|
local activity = {
|
|
sorting = false,
|
|
dispensing = false,
|
|
scanning = false,
|
|
smelting = false,
|
|
defragging = false,
|
|
composting = false,
|
|
crafting = false,
|
|
}
|
|
|
|
local activeAlerts = {}
|
|
local smeltingPaused = false
|
|
local disabledRecipes = {}
|
|
local SMELTABLE = {} -- populated from master broadcast
|
|
local CRAFTABLE = {} -- populated from master broadcast
|
|
local craftTurtleOk = false
|
|
local connected = false -- true once first state received
|
|
|
|
-------------------------------------------------
|
|
-- UI State (local to client)
|
|
-------------------------------------------------
|
|
|
|
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
|
|
|
|
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"
|
|
local smelterPage = 1
|
|
local smelterTotalPages = 1
|
|
local smelterTouchZones = {}
|
|
local smelterPendingZones = {}
|
|
local smelterNeedsRedraw = true
|
|
|
|
-------------------------------------------------
|
|
-- Monitor setup
|
|
-------------------------------------------------
|
|
|
|
local mon = nil
|
|
local monName = nil
|
|
local smelterMon = nil
|
|
local smelterMonName = nil
|
|
local networkModem = 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
|
|
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
|
|
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
|
|
|
|
-------------------------------------------------
|
|
-- Filtered items helper
|
|
-------------------------------------------------
|
|
|
|
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
|
|
|
|
-------------------------------------------------
|
|
-- Touch zone helpers
|
|
-------------------------------------------------
|
|
|
|
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
|
|
|
|
-------------------------------------------------
|
|
-- Crafting helpers (display-only, no peripheral calls)
|
|
-------------------------------------------------
|
|
|
|
local function getRecipeIngredients(recipe)
|
|
local ingredients = {}
|
|
for _, item in ipairs(recipe.grid) do
|
|
if item then
|
|
ingredients[item] = (ingredients[item] or 0) + 1
|
|
end
|
|
end
|
|
return ingredients
|
|
end
|
|
|
|
local function canCraftRecipe(recipe)
|
|
local ingredients = getRecipeIngredients(recipe)
|
|
local itemTotals = {}
|
|
for _, item in ipairs(cache.itemList) do
|
|
itemTotals[item.name] = item.total
|
|
end
|
|
for itemName, needed in pairs(ingredients) do
|
|
if (itemTotals[itemName] or 0) < needed then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function maxCraftBatches(recipe)
|
|
local ingredients = getRecipeIngredients(recipe)
|
|
local itemTotals = {}
|
|
for _, item in ipairs(cache.itemList) do
|
|
itemTotals[item.name] = item.total
|
|
end
|
|
local minBatches = math.huge
|
|
for itemName, needed in pairs(ingredients) do
|
|
local batches = math.floor((itemTotals[itemName] or 0) / needed)
|
|
if batches < minBatches then minBatches = batches end
|
|
end
|
|
if minBatches == math.huge then return 0 end
|
|
return minBatches
|
|
end
|
|
|
|
local function getMissingIngredients(recipe)
|
|
local ingredients = getRecipeIngredients(recipe)
|
|
local itemTotals = {}
|
|
for _, item in ipairs(cache.itemList) do
|
|
itemTotals[item.name] = item.total
|
|
end
|
|
local missing = {}
|
|
for itemName, needed in pairs(ingredients) do
|
|
local have = itemTotals[itemName] or 0
|
|
if have < needed then
|
|
table.insert(missing, { name = itemName, have = have, need = needed })
|
|
end
|
|
end
|
|
return missing
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Touch zone helpers
|
|
-------------------------------------------------
|
|
|
|
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
|
|
-------------------------------------------------
|
|
|
|
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
|
|
|
|
-------------------------------------------------
|
|
-- Send command to master
|
|
-------------------------------------------------
|
|
|
|
local function sendToMaster(message)
|
|
if not networkModem then return end
|
|
networkModem.transmit(ORDER_CHANNEL, CLIENT_CHANNEL, message)
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Client dropper discovery
|
|
-------------------------------------------------
|
|
|
|
local function discoverLocalDroppers()
|
|
local droppers = {}
|
|
-- Check configured dropper first
|
|
if CLIENT_DROPPER_NAME ~= "" then
|
|
local exists = peripheral.wrap(CLIENT_DROPPER_NAME) ~= nil
|
|
if exists then
|
|
table.insert(droppers, { name = CLIENT_DROPPER_NAME, isDefault = false, clientId = os.getComputerID() })
|
|
end
|
|
end
|
|
-- Also discover any other droppers on client's local network
|
|
for _, name in ipairs(peripheral.getNames()) do
|
|
local isDropper = name:match("^minecraft:dropper_")
|
|
if not isDropper and peripheral.hasType then
|
|
isDropper = peripheral.hasType(name, "minecraft:dropper")
|
|
end
|
|
if not isDropper then
|
|
isDropper = peripheral.getType(name) == "minecraft:dropper"
|
|
end
|
|
if isDropper then
|
|
-- Avoid duplicates with configured dropper
|
|
local isDuplicate = false
|
|
for _, d in ipairs(droppers) do
|
|
if d.name == name then isDuplicate = true; break end
|
|
end
|
|
if not isDuplicate then
|
|
table.insert(droppers, { name = name, isDefault = false, clientId = os.getComputerID() })
|
|
end
|
|
end
|
|
end
|
|
return droppers
|
|
end
|
|
|
|
local function announceDroppers()
|
|
local droppers = discoverLocalDroppers()
|
|
if #droppers > 0 then
|
|
sendToMaster({
|
|
type = "register_droppers",
|
|
clientId = os.getComputerID(),
|
|
droppers = droppers,
|
|
})
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Inventory Dashboard (identical to master)
|
|
-------------------------------------------------
|
|
|
|
local function drawDashboard()
|
|
if not mon then return end
|
|
|
|
local w, h = mon.getSize()
|
|
pendingZones = {}
|
|
|
|
draw = window.create(mon, 1, 1, w, h, false)
|
|
draw.setBackgroundColor(colors.black)
|
|
draw.clear()
|
|
|
|
if not connected then
|
|
monFill(1, colors.blue)
|
|
monCenter(1, " ** INVENTORY CLIENT ** ", colors.white, colors.blue)
|
|
local midY = math.floor(h / 2)
|
|
monCenter(midY - 1, "Waiting for master...", colors.lightGray, colors.black)
|
|
monCenter(midY + 1, "Make sure the master is running", colors.gray, colors.black)
|
|
monCenter(midY + 2, "and connected via wired modem.", colors.gray, colors.black)
|
|
monFill(h, colors.blue)
|
|
draw.setVisible(true)
|
|
touchZones = pendingZones
|
|
return
|
|
end
|
|
|
|
-- ===== 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
|
|
|
|
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
|
|
if activity.defragging then table.insert(actParts, "DEFRAG") end
|
|
if activity.composting then table.insert(actParts, "COMPOST") 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
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
local filteredItems = getFilteredItems()
|
|
|
|
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
|
|
|
|
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 =====
|
|
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
|
|
|
|
local lastItemRow = h - 3
|
|
while row <= lastItemRow do
|
|
monFill(row, colors.black)
|
|
row = row + 1
|
|
end
|
|
|
|
if showKeyboard then
|
|
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)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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 #activeAlerts > 0 then
|
|
local alertIdx = math.floor(os.epoch("utc") / 2000) % #activeAlerts + 1
|
|
local a = activeAlerts[alertIdx]
|
|
local alertMsg = string.format(" LOW STOCK: %s (%d/%d) ", a.label, a.current, a.min)
|
|
monCenter(h - 2, alertMsg, colors.white, colors.red)
|
|
elseif 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... "
|
|
elseif activity.defragging then
|
|
bottomMsg = " DEFRAGMENTING... "
|
|
elseif activity.composting then
|
|
bottomMsg = " COMPOSTING... "
|
|
end
|
|
monCenter(h, bottomMsg, colors.lightBlue, colors.blue)
|
|
end
|
|
|
|
draw.setVisible(true)
|
|
touchZones = pendingZones
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Smelter Dashboard (identical to master)
|
|
-------------------------------------------------
|
|
|
|
local function drawSmelterDashboard()
|
|
if not smelterMon then return end
|
|
|
|
local w, h = smelterMon.getSize()
|
|
smelterPendingZones = {}
|
|
|
|
draw = window.create(smelterMon, 1, 1, w, h, false)
|
|
draw.setBackgroundColor(colors.black)
|
|
draw.clear()
|
|
|
|
if not connected then
|
|
monFill(1, colors.purple)
|
|
monCenter(1, " ** SMELTER CLIENT ** ", colors.white, colors.purple)
|
|
monCenter(math.floor(h / 2), "Waiting for master...", colors.gray, colors.black)
|
|
monFill(h, colors.purple)
|
|
draw.setVisible(true)
|
|
smelterTouchZones = smelterPendingZones
|
|
return
|
|
end
|
|
|
|
-- ===== 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)
|
|
|
|
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 tabSmeltBg = smelterView == "smelt" and colors.purple or colors.gray
|
|
local tabCraftBg = smelterView == "craft" and colors.purple or colors.gray
|
|
local tabMissingBg = smelterView == "missing" 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, "Smelt", colors.white, tabSmeltBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "smelt")
|
|
|
|
bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Craft", colors.white, tabCraftBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "craft")
|
|
|
|
bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Missing", colors.white, tabMissingBg)
|
|
addSmelterZone(bx1, by1, bx2, by2, "tab", "missing")
|
|
|
|
if smelterView == "status" then
|
|
-- ===== Furnace Status View =====
|
|
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)
|
|
|
|
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)
|
|
|
|
monWrite(2, y, string.format("%d", i), colors.lightBlue, rowBg)
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
while row <= h - 2 do
|
|
monFill(row, colors.black)
|
|
row = row + 1
|
|
end
|
|
|
|
elseif smelterView == "smelt" then
|
|
-- ===== Smelt Recipe Manager View =====
|
|
-- Build item totals lookup from itemList
|
|
local itemTotals = {}
|
|
for _, item in ipairs(cache.itemList) do
|
|
itemTotals[item.name] = item.total
|
|
end
|
|
|
|
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 = itemTotals[inputName] or 0
|
|
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)
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
monWrite(typeCol, y, r.types, colors.orange, rowBg)
|
|
monWrite(stockCol, y, tostring(r.inStorage), colors.yellow, rowBg)
|
|
|
|
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
|
|
|
|
while row <= h - 2 do
|
|
monFill(row, colors.black)
|
|
row = row + 1
|
|
end
|
|
|
|
elseif smelterView == "craft" then
|
|
-- ===== Available Crafting Recipes =====
|
|
|
|
-- Turtle status on tab row
|
|
local tLabel = craftTurtleOk and " Turtle OK " or " No Turtle "
|
|
local tBg = craftTurtleOk and colors.lime or colors.red
|
|
local tFg = craftTurtleOk and colors.black or colors.white
|
|
monWrite(w - #tLabel, 4, tLabel, tFg, tBg)
|
|
|
|
local availList = {}
|
|
for idx, recipe in ipairs(CRAFTABLE) do
|
|
if canCraftRecipe(recipe) then
|
|
local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ")
|
|
short = short:sub(1,1):upper() .. short:sub(2)
|
|
local batches = maxCraftBatches(recipe)
|
|
table.insert(availList, {
|
|
idx = idx,
|
|
short = short,
|
|
count = recipe.count,
|
|
batches = batches,
|
|
})
|
|
end
|
|
end
|
|
|
|
monFill(5, colors.gray)
|
|
local makeCol = w - 6
|
|
monWrite(2, 5, "#", colors.lightGray, colors.gray)
|
|
monWrite(4, 5, "Output", colors.lightGray, colors.gray)
|
|
monWrite(math.floor(w * 0.45), 5, "Yield", colors.lightGray, colors.gray)
|
|
monWrite(math.floor(w * 0.60), 5, "Can Make", colors.lightGray, colors.gray)
|
|
monWrite(makeCol, 5, "Go", colors.lightGray, colors.gray)
|
|
|
|
local maxRows = h - 8
|
|
if maxRows < 1 then maxRows = 1 end
|
|
smelterTotalPages = math.max(1, math.ceil(#availList / 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, #availList)
|
|
|
|
local row = 6
|
|
if #availList == 0 then
|
|
monFill(7, colors.black)
|
|
monCenter(7, "No recipes available to craft", colors.gray, colors.black)
|
|
row = 8
|
|
else
|
|
for i = startIdx, endIdx do
|
|
local r = availList[i]
|
|
local y = row
|
|
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)
|
|
|
|
local maxNameLen = math.floor(w * 0.40)
|
|
local nameDisplay = r.short
|
|
if #nameDisplay > maxNameLen then
|
|
nameDisplay = nameDisplay:sub(1, maxNameLen - 2) .. ".."
|
|
end
|
|
monWrite(4, y, nameDisplay, colors.white, rowBg)
|
|
|
|
monWrite(math.floor(w * 0.45), y, "x" .. r.count, colors.yellow, rowBg)
|
|
monWrite(math.floor(w * 0.60), y,
|
|
string.format("x%d", r.batches), colors.lime, rowBg)
|
|
|
|
if craftTurtleOk then
|
|
monWrite(makeCol, y, " MAKE ", colors.white, colors.green)
|
|
addSmelterZone(makeCol, y, makeCol + 5, y, "craft", r.idx)
|
|
else
|
|
monWrite(makeCol, y, " ---- ", colors.gray, colors.black)
|
|
end
|
|
|
|
row = row + 1
|
|
end
|
|
end
|
|
|
|
while row <= h - 2 do
|
|
monFill(row, colors.black)
|
|
row = row + 1
|
|
end
|
|
|
|
elseif smelterView == "missing" then
|
|
-- ===== Unavailable Crafting Recipes =====
|
|
|
|
local missList = {}
|
|
for idx, recipe in ipairs(CRAFTABLE) do
|
|
if not canCraftRecipe(recipe) then
|
|
local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ")
|
|
short = short:sub(1,1):upper() .. short:sub(2)
|
|
local missing = getMissingIngredients(recipe)
|
|
local parts = {}
|
|
for _, m in ipairs(missing) do
|
|
local mShort = m.name:gsub("^minecraft:", ""):gsub("_", " ")
|
|
table.insert(parts, string.format("%s %d/%d", mShort, m.have, m.need))
|
|
end
|
|
table.insert(missList, {
|
|
idx = idx,
|
|
short = short,
|
|
count = recipe.count,
|
|
summary = table.concat(parts, ", "),
|
|
})
|
|
end
|
|
end
|
|
|
|
monFill(5, colors.gray)
|
|
monWrite(2, 5, "#", colors.lightGray, colors.gray)
|
|
monWrite(4, 5, "Output", colors.lightGray, colors.gray)
|
|
monWrite(math.floor(w * 0.35), 5, "Missing (have/need)", colors.lightGray, colors.gray)
|
|
|
|
local maxRows = h - 8
|
|
if maxRows < 1 then maxRows = 1 end
|
|
smelterTotalPages = math.max(1, math.ceil(#missList / 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, #missList)
|
|
|
|
local row = 6
|
|
if #missList == 0 then
|
|
monFill(7, colors.black)
|
|
monCenter(7, "All recipes can be crafted!", colors.lime, colors.black)
|
|
row = 8
|
|
else
|
|
for i = startIdx, endIdx do
|
|
local r = missList[i]
|
|
local y = row
|
|
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)
|
|
|
|
local nameCol = math.floor(w * 0.35) - 5
|
|
local nameDisplay = r.short .. " x" .. r.count
|
|
if #nameDisplay > nameCol then
|
|
nameDisplay = nameDisplay:sub(1, nameCol - 2) .. ".."
|
|
end
|
|
monWrite(4, y, nameDisplay, colors.white, rowBg)
|
|
|
|
local missCol = math.floor(w * 0.35)
|
|
local missW = w - missCol - 1
|
|
local summaryDisplay = r.summary
|
|
if #summaryDisplay > missW then
|
|
summaryDisplay = summaryDisplay:sub(1, missW - 2) .. ".."
|
|
end
|
|
monWrite(missCol, y, summaryDisplay, colors.red, rowBg)
|
|
|
|
row = row + 1
|
|
end
|
|
end
|
|
|
|
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 bottomMsg = ""
|
|
if smelterView == "status" or smelterView == "smelt" then
|
|
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
|
|
bottomMsg = string.format(" Smelt: %d/%d enabled ", enabledCount, totalRecipes)
|
|
if activity.smelting then bottomMsg = " SMELTING... " end
|
|
elseif smelterView == "craft" then
|
|
bottomMsg = " Tap MAKE to craft "
|
|
if activity.crafting then bottomMsg = " CRAFTING... " end
|
|
elseif smelterView == "missing" then
|
|
local totalC = #CRAFTABLE
|
|
local availC = 0
|
|
for _, r in ipairs(CRAFTABLE) do
|
|
if canCraftRecipe(r) then availC = availC + 1 end
|
|
end
|
|
bottomMsg = string.format(" Available: %d/%d recipes ", availC, totalC)
|
|
end
|
|
monCenter(h, bottomMsg, colors.pink, colors.purple)
|
|
|
|
draw.setVisible(true)
|
|
smelterTouchZones = smelterPendingZones
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Touch handler (sends orders to master via modem)
|
|
-------------------------------------------------
|
|
|
|
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)
|
|
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
|
|
needsRedraw = true
|
|
-- Send order to master
|
|
sendToMaster({
|
|
type = "order",
|
|
itemName = itemName,
|
|
amount = selectedAmount,
|
|
dropperName = CLIENT_DROPPER_NAME ~= "" and CLIENT_DROPPER_NAME or nil,
|
|
})
|
|
print(string.format("[ORDER] Sent to master: %s x%d", itemName, selectedAmount))
|
|
end
|
|
|
|
elseif action == "scan" then
|
|
statusMessage = "Requesting refresh..."
|
|
statusColor = colors.cyan
|
|
statusTimer = 3
|
|
needsRedraw = true
|
|
sendToMaster({ type = "scan" })
|
|
print("[UI] Scan request sent to master")
|
|
|
|
elseif action == "kb_toggle" then
|
|
showKeyboard = not showKeyboard
|
|
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
|
|
needsRedraw = true
|
|
|
|
elseif action == "kb_clear" then
|
|
searchQuery = ""
|
|
currentPage = 1
|
|
needsRedraw = true
|
|
|
|
elseif action == "page_prev" then
|
|
if currentPage > 1 then
|
|
currentPage = currentPage - 1
|
|
end
|
|
needsRedraw = true
|
|
|
|
elseif action == "page_next" then
|
|
if currentPage < totalPages then
|
|
currentPage = currentPage + 1
|
|
end
|
|
needsRedraw = true
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Smelter touch handler (sends commands to master)
|
|
-------------------------------------------------
|
|
|
|
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
|
|
smelterNeedsRedraw = true
|
|
|
|
elseif action == "toggle_pause" then
|
|
sendToMaster({ type = "toggle_pause" })
|
|
print("[SMELT-UI] Toggle pause sent to master")
|
|
smelterNeedsRedraw = true
|
|
|
|
elseif action == "toggle_recipe" then
|
|
sendToMaster({ type = "toggle_recipe", recipe = data })
|
|
print("[SMELT-UI] Toggle recipe sent: " .. data)
|
|
smelterNeedsRedraw = true
|
|
|
|
elseif action == "enable_all" then
|
|
sendToMaster({ type = "enable_all" })
|
|
print("[SMELT-UI] Enable all sent to master")
|
|
smelterNeedsRedraw = true
|
|
|
|
elseif action == "disable_all" then
|
|
sendToMaster({ type = "disable_all" })
|
|
print("[SMELT-UI] Disable all sent to master")
|
|
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
|
|
|
|
elseif action == "craft" then
|
|
sendToMaster({ type = "craft", recipeIdx = data })
|
|
local recipe = CRAFTABLE[data]
|
|
if recipe then
|
|
local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ")
|
|
print("[CRAFT-UI] Craft request sent: " .. short)
|
|
end
|
|
smelterNeedsRedraw = true
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Main
|
|
-------------------------------------------------
|
|
|
|
local function main()
|
|
print("=================================")
|
|
print(" Inventory Client (Display Only)")
|
|
print("=================================")
|
|
print("")
|
|
|
|
if setupMonitor() then
|
|
print("[OK] Monitor: " .. (monName or MONITOR_SIDE))
|
|
else
|
|
print("[WARN] No monitor on " .. MONITOR_SIDE)
|
|
end
|
|
|
|
if setupSmelterMonitor() then
|
|
print("[OK] Smelter monitor: " .. (smelterMonName or SMELTER_MONITOR_SIDE))
|
|
else
|
|
print("[WARN] No smelter monitor on " .. SMELTER_MONITOR_SIDE)
|
|
end
|
|
|
|
-- Find modem
|
|
for _, name in ipairs(peripheral.getNames()) do
|
|
if peripheral.getType(name) == "modem" then
|
|
networkModem = peripheral.wrap(name)
|
|
networkModem.open(BROADCAST_CHANNEL)
|
|
networkModem.open(CLIENT_CHANNEL)
|
|
print("[OK] Modem: " .. name)
|
|
break
|
|
end
|
|
end
|
|
if not networkModem then
|
|
print("[ERR] No modem found! Cannot receive data from master.")
|
|
print(" Attach a wired modem and restart.")
|
|
return
|
|
end
|
|
|
|
print("")
|
|
print("Listening for master broadcasts on channel " .. BROADCAST_CHANNEL)
|
|
print("Console shows log. Use the monitors to interact.")
|
|
print("")
|
|
|
|
-- Draw initial "waiting" screen
|
|
needsRedraw = true
|
|
smelterNeedsRedraw = true
|
|
|
|
parallel.waitForAny(
|
|
-- Task 1: Modem receiver (state updates + order replies)
|
|
function()
|
|
while true do
|
|
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
|
|
if channel == BROADCAST_CHANNEL and type(message) == "table" and message.type == "state" then
|
|
-- Update all state from master
|
|
if message.cache then
|
|
cache.itemList = message.cache.itemList or cache.itemList
|
|
cache.grandTotal = message.cache.grandTotal or cache.grandTotal
|
|
cache.chestCount = message.cache.chestCount or cache.chestCount
|
|
cache.totalSlots = message.cache.totalSlots or cache.totalSlots
|
|
cache.usedSlots = message.cache.usedSlots or cache.usedSlots
|
|
cache.freeSlots = message.cache.freeSlots or cache.freeSlots
|
|
cache.usedRatio = message.cache.usedRatio or cache.usedRatio
|
|
cache.dropperOk = message.cache.dropperOk
|
|
cache.barrelOk = message.cache.barrelOk
|
|
cache.furnaceCount = message.cache.furnaceCount or cache.furnaceCount
|
|
cache.furnaceStatus = message.cache.furnaceStatus or cache.furnaceStatus
|
|
end
|
|
if message.activity then
|
|
activity = message.activity
|
|
end
|
|
if message.alerts then
|
|
activeAlerts = message.alerts
|
|
end
|
|
if message.smeltingPaused ~= nil then
|
|
smeltingPaused = message.smeltingPaused
|
|
end
|
|
if message.disabledRecipes then
|
|
disabledRecipes = message.disabledRecipes
|
|
end
|
|
if message.smeltable then
|
|
SMELTABLE = message.smeltable
|
|
end
|
|
if message.craftable then
|
|
CRAFTABLE = message.craftable
|
|
end
|
|
if message.craftTurtleOk ~= nil then
|
|
craftTurtleOk = message.craftTurtleOk
|
|
end
|
|
|
|
if not connected then
|
|
connected = true
|
|
print("[OK] Connected to master!")
|
|
end
|
|
|
|
needsRedraw = true
|
|
smelterNeedsRedraw = true
|
|
|
|
elseif channel == CLIENT_CHANNEL and type(message) == "table" and message.type == "order_result" then
|
|
-- Order result from master
|
|
statusMessage = message.message or ""
|
|
statusColor = message.color or (message.success and colors.lime or colors.red)
|
|
statusTimer = 5
|
|
needsRedraw = true
|
|
if message.success then
|
|
print("[OK] " .. statusMessage)
|
|
else
|
|
print("[WARN] " .. statusMessage)
|
|
end
|
|
|
|
elseif channel == CLIENT_CHANNEL and type(message) == "table" and message.type == "craft_result" then
|
|
-- Craft result from master
|
|
statusMessage = message.message or ""
|
|
statusColor = message.success and colors.lime or colors.red
|
|
statusTimer = 5
|
|
smelterNeedsRedraw = true
|
|
if message.success then
|
|
print("[OK] " .. statusMessage)
|
|
else
|
|
print("[WARN] " .. statusMessage)
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
|
|
-- Task 2: Inventory dashboard redraw
|
|
function()
|
|
needsRedraw = true
|
|
while true do
|
|
if needsRedraw then
|
|
needsRedraw = false
|
|
pcall(drawDashboard)
|
|
end
|
|
if statusTimer > 0 then
|
|
statusTimer = statusTimer - 0.1
|
|
if statusTimer <= 0 then
|
|
statusTimer = 0
|
|
needsRedraw = true
|
|
end
|
|
end
|
|
if #activeAlerts > 0 then
|
|
needsRedraw = true
|
|
end
|
|
sleep(0.1)
|
|
end
|
|
end,
|
|
|
|
-- Task 3: Smelter dashboard redraw
|
|
function()
|
|
smelterNeedsRedraw = true
|
|
while true do
|
|
if smelterNeedsRedraw then
|
|
smelterNeedsRedraw = false
|
|
pcall(drawSmelterDashboard)
|
|
end
|
|
sleep(0.1)
|
|
end
|
|
end,
|
|
|
|
-- Task 4: Announce local droppers to master periodically
|
|
function()
|
|
-- Initial announcement after short delay
|
|
sleep(3)
|
|
pcall(announceDroppers)
|
|
while true do
|
|
sleep(DROPPER_ANNOUNCE_INTERVAL)
|
|
pcall(announceDroppers)
|
|
end
|
|
end,
|
|
|
|
-- Task 5: Client barrel auto-sort (sends to master only when barrel has items)
|
|
function()
|
|
if CLIENT_BARREL_NAME == "" then return end
|
|
print("[OK] Client barrel: " .. CLIENT_BARREL_NAME)
|
|
while true do
|
|
local barrel = peripheral.wrap(CLIENT_BARREL_NAME)
|
|
if barrel then
|
|
local contents = barrel.list()
|
|
if contents and next(contents) then
|
|
sendToMaster({ type = "sort_barrel", barrelName = CLIENT_BARREL_NAME })
|
|
end
|
|
end
|
|
sleep(2)
|
|
end
|
|
end,
|
|
|
|
-- Task 6: Touch event listener (both monitors)
|
|
function()
|
|
while true do
|
|
local event, side, x, y = os.pullEvent("monitor_touch")
|
|
if smelterMonName and side == smelterMonName then
|
|
print(string.format("[SMELT-TOUCH] x=%d y=%d", x, y))
|
|
handleSmelterTouch(x, y)
|
|
else
|
|
print(string.format("[TOUCH] x=%d y=%d", x, y))
|
|
handleTouch(x, y)
|
|
end
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
main()
|