CCCPaste

AE2 Pattern Checker

--[[
================================================================================
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()