1189 lines
44 KiB
Lua
1189 lines
44 KiB
Lua
-- Inventory Manager: Touch UI on monitor
|
|
-- Main computer (networked). Computer 1 sits next to dropper_0 and auto-dispenses.
|
|
|
|
local DROPPER_NAME = "minecraft:dropper_9"
|
|
local BARREL_NAME = "minecraft:barrel_0"
|
|
local POLL_INTERVAL = 2 -- seconds between barrel checks
|
|
local MONITOR_SIDE = "left"
|
|
local SCAN_INTERVAL = 3 -- seconds between background scans
|
|
local SMELT_INTERVAL = 3 -- seconds between furnace checks
|
|
local SMELT_RESERVE = 64 -- keep at least 1 stack of each raw material
|
|
|
|
-------------------------------------------------
|
|
-- 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,
|
|
}
|
|
|
|
-------------------------------------------------
|
|
-- 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 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
|
|
|
|
activity.scanning = false
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Monitor setup
|
|
-------------------------------------------------
|
|
|
|
local mon = nil
|
|
|
|
local function setupMonitor()
|
|
mon = peripheral.wrap(MONITOR_SIDE)
|
|
-- If the side has a modem instead of a direct monitor, find it by type
|
|
if mon and not mon.setTextScale then
|
|
mon = nil
|
|
end
|
|
if not mon then
|
|
-- Search for a monitor on the network
|
|
local name = peripheral.find("monitor")
|
|
if name then
|
|
mon = name
|
|
end
|
|
end
|
|
if not mon then return false end
|
|
mon.setTextScale(0.5)
|
|
mon.clear()
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- UI State
|
|
-------------------------------------------------
|
|
|
|
local selectedAmount = 1
|
|
local amountOptions = {1, 4, 8, 16, 32, 64}
|
|
local statusMessage = ""
|
|
local statusColor = colors.white
|
|
local statusTimer = 0
|
|
|
|
local touchZones = {}
|
|
local pendingZones = {}
|
|
local needsRedraw = true
|
|
|
|
local currentPage = 1
|
|
local totalPages = 1
|
|
local searchQuery = ""
|
|
local showKeyboard = false
|
|
|
|
-- Keyboard layout
|
|
local kbRows = {
|
|
{"Q","W","E","R","T","Y","U","I","O","P"},
|
|
{"A","S","D","F","G","H","J","K","L"},
|
|
{"Z","X","C","V","B","N","M"},
|
|
}
|
|
|
|
-- Get items filtered by search query
|
|
local function getFilteredItems()
|
|
local filtered = {}
|
|
for _, item in ipairs(cache.itemList) do
|
|
if searchQuery == "" then
|
|
table.insert(filtered, item)
|
|
else
|
|
local lower = item.name:lower():gsub("minecraft:", ""):gsub("_", " ")
|
|
if lower:find(searchQuery:lower(), 1, true) then
|
|
table.insert(filtered, item)
|
|
end
|
|
end
|
|
end
|
|
return filtered
|
|
end
|
|
|
|
local function addZone(x1, y1, x2, y2, action, data)
|
|
table.insert(pendingZones, {
|
|
x1 = x1, y1 = y1, x2 = x2, y2 = y2,
|
|
action = action, data = data
|
|
})
|
|
end
|
|
|
|
local function hitTest(x, y)
|
|
for _, zone in ipairs(touchZones) do
|
|
if x >= zone.x1 and x <= zone.x2 and y >= zone.y1 and y <= zone.y2 then
|
|
return zone.action, zone.data
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Drawing helpers (write to draw target)
|
|
-------------------------------------------------
|
|
|
|
local draw = nil
|
|
|
|
local function monWrite(x, y, text, fg, bg)
|
|
draw.setCursorPos(x, y)
|
|
if fg then draw.setTextColor(fg) end
|
|
if bg then draw.setBackgroundColor(bg) end
|
|
draw.write(text)
|
|
end
|
|
|
|
local function monFill(y, color)
|
|
local w, _ = draw.getSize()
|
|
draw.setCursorPos(1, y)
|
|
draw.setBackgroundColor(color)
|
|
draw.write(string.rep(" ", w))
|
|
end
|
|
|
|
local function monCenter(y, text, fg, bg)
|
|
local w, _ = draw.getSize()
|
|
local x = math.floor((w - #text) / 2) + 1
|
|
monWrite(x, y, text, fg, bg)
|
|
end
|
|
|
|
local function monBar(x, y, width, ratio, barColor, bgColor)
|
|
local filled = math.floor(ratio * width)
|
|
draw.setCursorPos(x, y)
|
|
draw.setBackgroundColor(barColor)
|
|
draw.write(string.rep(" ", filled))
|
|
draw.setBackgroundColor(bgColor)
|
|
draw.write(string.rep(" ", width - filled))
|
|
end
|
|
|
|
local function drawButton(x, y, text, fg, bg, padLeft, padRight)
|
|
padLeft = padLeft or 1
|
|
padRight = padRight or 1
|
|
local full = string.rep(" ", padLeft) .. text .. string.rep(" ", padRight)
|
|
monWrite(x, y, full, fg, bg)
|
|
return x, y, x + #full - 1, y
|
|
end
|
|
|
|
-------------------------------------------------
|
|
-- Dashboard (reads ONLY from cache — no peripheral calls — instant)
|
|
-------------------------------------------------
|
|
|
|
local function drawDashboard()
|
|
if not mon then return end
|
|
|
|
local w, h = mon.getSize()
|
|
pendingZones = {}
|
|
|
|
-- Offscreen buffer
|
|
draw = window.create(mon, 1, 1, w, h, false)
|
|
draw.setBackgroundColor(colors.black)
|
|
draw.clear()
|
|
|
|
-- ===== Title bar =====
|
|
monFill(1, colors.blue)
|
|
monCenter(1, " ** INVENTORY MANAGER ** ", colors.white, colors.blue)
|
|
|
|
-- ===== Status bar =====
|
|
monFill(2, colors.gray)
|
|
local statusParts = {}
|
|
table.insert(statusParts, string.format(" Chests: %d", cache.chestCount))
|
|
table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --")
|
|
table.insert(statusParts, cache.barrelOk and "Barrel: OK" or "Barrel: --")
|
|
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
|
|
|
|
-------------------------------------------------
|
|
-- 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()
|
|
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) 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
|
|
|
|
-- 3) Load smeltable items into empty input slot
|
|
local inputItem = contents[SLOT_INPUT]
|
|
local furnaceType = peripheral.getType(fname)
|
|
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 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
|
|
|
|
-------------------------------------------------
|
|
-- Main
|
|
-------------------------------------------------
|
|
|
|
local function main()
|
|
print("=================================")
|
|
print(" Inventory Manager v2 (Touch)")
|
|
print("=================================")
|
|
print("")
|
|
|
|
if peripheral.wrap(DROPPER_NAME) then
|
|
print("[OK] Dropper: " .. DROPPER_NAME)
|
|
else
|
|
print("[WARN] Dropper not found: " .. DROPPER_NAME)
|
|
end
|
|
|
|
if peripheral.wrap(BARREL_NAME) then
|
|
print("[OK] Barrel: " .. BARREL_NAME)
|
|
else
|
|
print("[WARN] Barrel not found: " .. BARREL_NAME)
|
|
end
|
|
|
|
if setupMonitor() then
|
|
print("[OK] Monitor: " .. MONITOR_SIDE)
|
|
else
|
|
print("[WARN] No monitor on " .. MONITOR_SIDE)
|
|
end
|
|
|
|
print("")
|
|
print("Console shows log. Use the monitor to interact.")
|
|
print("")
|
|
|
|
-- Initial scan with progress bar on monitor
|
|
print("[INIT] 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.")
|
|
print("")
|
|
|
|
parallel.waitForAny(
|
|
-- Task 1: Background inventory scanner
|
|
function()
|
|
while true do
|
|
sleep(SCAN_INTERVAL)
|
|
pcall(refreshCache)
|
|
needsRedraw = true
|
|
end
|
|
end,
|
|
|
|
-- Task 2: Barrel auto-sort
|
|
function()
|
|
while true do
|
|
pcall(sortBarrel)
|
|
sleep(POLL_INTERVAL)
|
|
end
|
|
end,
|
|
|
|
-- Task 3: 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()
|