- Implemented a client-side dashboard that connects to the master inventoryManager via wired modem. - The dashboard displays inventory status without performing any automation tasks. - Added configuration for communication channels and monitor setup. - Created UI elements for displaying item lists, status messages, and touch interactions. - Included functionality for handling touch events and sending commands to the master. - Integrated smelter dashboard with similar display capabilities.
1129 lines
40 KiB
Lua
1129 lines
40 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"
|
|
|
|
-------------------------------------------------
|
|
-- 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,
|
|
}
|
|
|
|
local activeAlerts = {}
|
|
local smeltingPaused = false
|
|
local disabledRecipes = {}
|
|
local SMELTABLE = {} -- populated from master broadcast
|
|
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
|
|
|
|
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
|
|
|
|
-------------------------------------------------
|
|
-- 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 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 =====
|
|
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
|
|
|
|
else
|
|
-- ===== 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
|
|
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)
|
|
|
|
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,
|
|
})
|
|
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
|
|
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 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
|
|
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: 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()
|