--[[ ================================================================================ PATTERN DETECTOR — ME Bridge Crafting Watcher (CC:Tweaked / ComputerCraft) ================================================================================ Overview: Monitors Applied Energistics crafting CPUs via an ME Bridge peripheral and checks whether any *watchlisted* items are currently being crafted. If so, it flips a redstone output (e.g., a lamp) and shows a status UI in color terminals. It also logs to a file if enabled. Key Features: • Reads a watchlist from "watchlist.txt" (auto-creates a default if missing). • Polls ME Bridge CPUs to detect active jobs for watchlisted items. • Toggles a redstone line (CONFIG.redstoneSide) when any watchlisted item is being crafted. • Color terminal UI: - Header banner - Status bar showing redstone state - Watchlist line (truncates if too wide) - Crafting status list (with item, quantity, and CPU ID) • Optional logging to a file (CONFIG.logFile). Requirements: • CC:Tweaked / ComputerCraft environment with a color-capable terminal (for UI). • A connected ME Bridge peripheral (from Advanced Peripherals) on any side. • Redstone output wired on the configured side (CONFIG.redstoneSide). Usage: • Place a computer next to or wired to an ME Bridge peripheral. • Adjust CONFIG values below if needed. • Create "watchlist.txt" with one item id per line (e.g. minecraft:stone). • Run the program; it will show the UI and control redstone automatically. Notes: • If "watchlist.txt" is missing or empty, a default file with a few items is created on first run. • Logging is appended to CONFIG.logFile when CONFIG.enableLogging is true. • Pressing Enter switches to a simple input mode placeholder (returns immediately). ================================================================================ ]] -- Configuration for polling rate, I/O files, redstone side, etc. local CONFIG = { pollInterval = 1.0, -- seconds between CPU checks when idle redstoneSide = "top", -- side used for redstone output (e.g., "top", "left") watchlistFile = "watchlist.txt", -- file with one item id per line (e.g., minecraft:stone) logFile = "patters.log", -- log file path (appends); note: name kept as provided enableLogging = true, -- toggle logging on/off } -- Last snapshot of CPU states to compare changes between iterations local lastCpuSnapshot = nil -- Loaded from watchlist file; entries are { name, displayName } local watchlist = {} -------------------------------------------------------------------------------- -- Logging helpers -------------------------------------------------------------------------------- -- Writes a line to the screen and (optionally) to the log file. local function log(msg) print(msg) if CONFIG.enableLogging then local file = fs.open(CONFIG.logFile, "a") if file then file.write(msg .. "\n") file.close() end end end -- Writes a colored line to screen (if color is available) and to log file. -- Falls back to plain print on non-color terminals. local function logColored(msg, color) if term.isColor() then term.setTextColor(color) print(msg) term.setTextColor(colors.white) else print(msg) end if CONFIG.enableLogging then local file = fs.open(CONFIG.logFile, "a") if file then file.write(msg .. "\n") file.close() end end end -------------------------------------------------------------------------------- -- Drawing helpers (UI) -------------------------------------------------------------------------------- -- Draws a horizontal line (full terminal width) at row y in the given color. local function drawLine(y, color) if term.isColor() then term.setTextColor(color) term.setCursorPos(1, y) term.write(string.rep("=", term.getSize())) term.setTextColor(colors.white) end end -- Draws a simple bordered box with optional title. -- (Currently unused in the main loop but handy for future UI expansion.) local function drawBox(x, y, width, height, title, color) if term.isColor() then term.setTextColor(color) -- Top border term.setCursorPos(x, y) term.write("+" .. string.rep("-", width - 2) .. "+") -- Title row (if provided) if title then term.setCursorPos(x, y + 1) term.write("| " .. title .. string.rep(" ", width - #title - 3) .. "|") end -- Middle rows (skip title’s body line if present) for i = 2, height - 1 do if not title or i > 1 then term.setCursorPos(x, y + i) term.write("|" .. string.rep(" ", width - 2) .. "|") end end -- Bottom border term.setCursorPos(x, y + height) term.write("+" .. string.rep("-", width - 2) .. "+") term.setTextColor(colors.white) end end -- Clears the screen and prints the top header banner. local function showHeader() if term.isColor() then term.clear() local termWidth, _ = term.getSize() local title = "PATTERN DETECTOR" local titleX = math.floor((termWidth - #title) / 2) + 1 term.setCursorPos(1, 1) term.setBackgroundColor(colors.blue) term.setTextColor(colors.white) term.write(string.rep(" ", termWidth)) term.setCursorPos(titleX, 1) term.write(title) term.setBackgroundColor(colors.black) term.setTextColor(colors.white) end end -- Shows a centered status bar indicating whether the redstone output is ON/OFF. local function showStatusBar(redstoneOn) if term.isColor() then local termWidth, _ = term.getSize() local statusOn = "Redstone Lamp: ON" local statusOff = "Redstone Lamp: OFF" local status = redstoneOn and statusOn or statusOff local color = redstoneOn and colors.green or colors.gray local barY = 5 term.setCursorPos(1, barY) term.setBackgroundColor(colors.black) term.write(string.rep(" ", termWidth)) -- Centered text local barX = math.floor((termWidth - #status) / 2) + 1 term.setCursorPos(barX, barY) term.setTextColor(color) term.write(status) term.setTextColor(colors.white) end end -- Renders the "Watchlist:" line (names joined by ", ", truncated if needed). -- Falls back to logging in non-color terminals. local function showWatchlist() if term.isColor() then local y = 7 local termWidth, _ = term.getSize() local label = "Watchlist:" local names = {} for i, item in ipairs(watchlist) do table.insert(names, item.displayName) end local namesStr = (#watchlist == 0) and "(empty)" or table.concat(names, ", ") -- Compose and maybe truncate to terminal width local line = label .. " " .. namesStr if #line > termWidth then local maxNamesLen = termWidth - #label - 4 if maxNamesLen < 0 then maxNamesLen = 0 end namesStr = namesStr:sub(1, maxNamesLen) .. "..." line = label .. " " .. namesStr end -- Clear line then draw colored label + content term.setCursorPos(1, y) term.setBackgroundColor(colors.black) term.write(string.rep(" ", termWidth)) term.setCursorPos(1, y) term.setTextColor(colors.cyan) term.write(label) term.setTextColor(#watchlist == 0 and colors.orange or colors.white) term.write(" " .. namesStr) term.setTextColor(colors.white) else -- Non-color: print to log if #watchlist == 0 then log("Watchlist: (empty)") else local names = {} for i, item in ipairs(watchlist) do table.insert(names, item.displayName) end log("Watchlist: " .. table.concat(names, ", ")) end end end -- Displays the crafting status block. -- Lists each watchlisted item currently being crafted with quantity and CPU id. local function showCraftingStatus(craftingPatterns) if term.isColor() then local termWidth, _ = term.getSize() local title = "CRAFTING STATUS" local y = 9 -- Clear an area that can fit title + up to N lines of jobs local linesNeeded = 1 + math.max(1, #craftingPatterns) for i = 0, linesNeeded do term.setCursorPos(1, y + i) term.setBackgroundColor(colors.black) term.write(string.rep(" ", termWidth)) end -- Title centered local titleX = math.floor((termWidth - #title) / 2) + 1 term.setCursorPos(titleX, y) term.setTextColor(colors.cyan) term.write(title) term.setTextColor(colors.white) -- Lines with content or a single "no items" message if #craftingPatterns > 0 then local craftingLines = {} local maxLineLen = 0 for i, crafting in ipairs(craftingPatterns) do local line = "+ " .. crafting.displayName .. " x" .. crafting.quantity .. " (CPU " .. crafting.cpuId .. ")" if #line > termWidth then line = line:sub(1, termWidth - 3) .. "..." end craftingLines[i] = line if #line > maxLineLen then maxLineLen = #line end end local blockWidth = maxLineLen local blockX = math.floor((termWidth - blockWidth) / 2) + 1 for i, line in ipairs(craftingLines) do term.setCursorPos(blockX, y + i) term.setTextColor(colors.green) term.write(line) term.setTextColor(colors.white) end else local msg = "- No watchlisted items being crafted" if #msg > termWidth then msg = msg:sub(1, termWidth - 3) .. "..." end local msgX = math.floor((termWidth - #msg) / 2) + 1 term.setCursorPos(msgX, y + 1) term.setTextColor(colors.orange) term.write(msg) term.setTextColor(colors.white) end else -- Non-color fallback to log output log("CRAFTING STATUS:") if #craftingPatterns > 0 then for _, crafting in ipairs(craftingPatterns) do log(" " .. crafting.displayName .. " x" .. crafting.quantity .. " (CPU " .. crafting.cpuId .. ")") end else log("- No watchlisted items being crafted") end end end -------------------------------------------------------------------------------- -- Data loading / input -------------------------------------------------------------------------------- -- Loads the watchlist file. If missing/empty, writes a default file and returns it. local function loadWatchlist() local items = {} if fs.exists(CONFIG.watchlistFile) then local file = fs.open(CONFIG.watchlistFile, "r") if file then local line = file.readLine() while line do -- Trim whitespace local trimmed = line:gsub("^%s*(.-)%s*$", "%1") -- Skip empty lines and comments starting with '#' if trimmed ~= "" and not trimmed:match("^#") then table.insert(items, { name = trimmed, -- Tidy display: drop common namespaces displayName = trimmed:gsub("minecraft:", ""):gsub("ftbmaterials:", ""), }) end line = file.readLine() end file.close() end end -- Provide a default list if nothing loaded, and create the file for convenience if #items == 0 then items = { { name = "minecraft:stone", displayName = "Stone" }, { name = "minecraft:iron_ingot", displayName = "Iron Ingot" }, } local file = fs.open(CONFIG.watchlistFile, "w") if file then for _, item in ipairs(items) do file.write(item.name .. "\n") end file.close() end end return items end -- Placeholder for handling typed user input when in "control mode". -- Currently normalizes input but does nothing and exits control mode immediately. local function handleUserInput() local input = read() if not input then return false end -- Normalize input = input:lower():gsub("^%s*(.-)%s*$", "%1") -- No commands implemented; return false to exit control mode. end -------------------------------------------------------------------------------- -- ME Bridge integration -------------------------------------------------------------------------------- -- Finds and returns the first wrapped peripheral that looks like an ME Bridge -- (i.e., has getCraftingCPUs). Tries "bottom" first, then other sides. local function findMeBridge() if peripheral.isPresent("bottom") then local bridge = peripheral.wrap("bottom") if bridge and bridge.getCraftingCPUs then return bridge end end -- Try other sides if not at "bottom" for _, side in ipairs({ "left", "right", "front", "back", "top" }) do if peripheral.isPresent(side) then local bridge = peripheral.wrap(side) if bridge and bridge.getCraftingCPUs then return bridge end end end return nil end -- Takes a snapshot of crafting CPUs with minimal fields needed for display/compare. local function takeCpuSnapshot(bridge) local snapshot = {} if bridge.getCraftingCPUs then -- Use pcall to be resilient against peripheral/API errors local success, cpus = pcall(bridge.getCraftingCPUs, bridge) if success and cpus then for i, cpu in ipairs(cpus) do snapshot[i] = { isBusy = cpu.isBusy, -- boolean name = cpu.name, -- string storage = cpu.storage, -- number or details (depends on API) craftingJob = cpu.craftingJob, -- table with job info } end end end return snapshot end -- Inspects current CPU snapshot and collects jobs matching the watchlist. -- Returns a list of crafting entries for the UI/log: { pattern, cpuId, cpuName, quantity, displayName } local function checkCraftingPatterns(bridge) local craftingPatterns = {} -- Capture the current CPU states local currentCpuSnapshot = takeCpuSnapshot(bridge) -- Quick check: if no CPUs are busy, we can skip deeper analysis local anyBusyCPUs = false for cpuId, cpu in pairs(currentCpuSnapshot) do if cpu.isBusy then anyBusyCPUs = true end end if not anyBusyCPUs then lastCpuSnapshot = currentCpuSnapshot return craftingPatterns end -- If we have a previous snapshot, scan active jobs for watchlist matches if lastCpuSnapshot then for cpuId, currentCpu in pairs(currentCpuSnapshot) do if currentCpu.isBusy and currentCpu.craftingJob then local job = currentCpu.craftingJob if job.resource and job.resource.name then -- Exact id match against the watchlist entry names for _, watchlistItem in ipairs(watchlist) do if job.resource.name == watchlistItem.name then table.insert(craftingPatterns, { pattern = watchlistItem, cpuId = cpuId, cpuName = currentCpu.name, quantity = job.quantity, displayName = job.resource.displayName or job.resource.name, }) break end end end end end end -- Persist snapshot for next compare lastCpuSnapshot = currentCpuSnapshot return craftingPatterns end -------------------------------------------------------------------------------- -- Redstone control -------------------------------------------------------------------------------- -- Sets the redstone output to ON/OFF on the configured side. local function setRedstoneLamp(on) if on then redstone.setOutput(CONFIG.redstoneSide, true) else redstone.setOutput(CONFIG.redstoneSide, false) end end -------------------------------------------------------------------------------- -- Main loop -------------------------------------------------------------------------------- -- Initializes UI, loads watchlist, finds the ME Bridge, and starts the monitor loop. local function runMonitor() showHeader() watchlist = loadWatchlist() local bridge = findMeBridge() if not bridge then logColored("ERROR: No ME Bridge found!", colors.red) return end local lastState = false -- last redstone state we set local controlMode = false -- toggled when user presses Enter (char "\r") while true do if controlMode then -- If we ever implement commands, this is where they'd be handled if handleUserInput() then sleep(0.1) else -- Exit control mode and redraw the header controlMode = false showHeader() end else -- Event polling for simple key/timer handling in color terminals if term.isColor() then local timer = os.startTimer(2) local event, param = os.pullEvent() if event == "timer" and param == timer then -- Timer triggered; continue to work loop elseif event == "char" and param == "\r" then -- Enter pressed: enter control mode controlMode = true end end -- Check for watchlisted crafting jobs local craftingPatterns = checkCraftingPatterns(bridge) -- Redstone is ON iff we have any matching jobs local currentState = #craftingPatterns > 0 -- Update redstone output only when it changes if currentState ~= lastState then setRedstoneLamp(currentState) lastState = currentState end -- Refresh UI sections showStatusBar(currentState) showWatchlist() showCraftingStatus(craftingPatterns) -- Throttle loop to the configured poll interval sleep(CONFIG.pollInterval) end end end -- Entry point runMonitor()