gelatine87

AE2 Pattern Checker

Oct 24th, 2025 (edited)
1,069
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.76 KB | Gaming | 0 0
  1. --[[
  2. ================================================================================
  3. PATTERN DETECTOR — ME Bridge Crafting Watcher (CC:Tweaked / ComputerCraft)
  4. ================================================================================
  5.  
  6. Overview:
  7.   Monitors Applied Energistics crafting CPUs via an ME Bridge peripheral and
  8.   checks whether any *watchlisted* items are currently being crafted. If so,
  9.   it flips a redstone output (e.g., a lamp) and shows a status UI in color
  10.   terminals. It also logs to a file if enabled.
  11.  
  12. Key Features:
  13.   • Reads a watchlist from "watchlist.txt" (auto-creates a default if missing).
  14.   • Polls ME Bridge CPUs to detect active jobs for watchlisted items.
  15.   • Toggles a redstone line (CONFIG.redstoneSide) when any watchlisted item is
  16.     being crafted.
  17.   • Color terminal UI:
  18.       - Header banner
  19.       - Status bar showing redstone state
  20.       - Watchlist line (truncates if too wide)
  21.       - Crafting status list (with item, quantity, and CPU ID)
  22.   • Optional logging to a file (CONFIG.logFile).
  23.  
  24. Requirements:
  25.   • CC:Tweaked / ComputerCraft environment with a color-capable terminal (for UI).
  26.   • A connected ME Bridge peripheral (from Advanced Peripherals) on any side.
  27.   • Redstone output wired on the configured side (CONFIG.redstoneSide).
  28.  
  29. Usage:
  30.   • Place a computer next to or wired to an ME Bridge peripheral.
  31.   • Adjust CONFIG values below if needed.
  32.   • Create "watchlist.txt" with one item id per line (e.g. minecraft:stone).
  33.   • Run the program; it will show the UI and control redstone automatically.
  34.  
  35. Notes:
  36.   • If "watchlist.txt" is missing or empty, a default file with a few items
  37.     is created on first run.
  38.   • Logging is appended to CONFIG.logFile when CONFIG.enableLogging is true.
  39.   • Pressing Enter switches to a simple input mode placeholder (returns immediately).
  40.  
  41. ================================================================================
  42. ]]
  43.  
  44. -- Configuration for polling rate, I/O files, redstone side, etc.
  45. local CONFIG = {
  46.     pollInterval = 1.0,           -- seconds between CPU checks when idle
  47.     redstoneSide = "top",          -- side used for redstone output (e.g., "top", "left")
  48.     watchlistFile = "watchlist.txt", -- file with one item id per line (e.g., minecraft:stone)
  49.     logFile = "patters.log",       -- log file path (appends); note: name kept as provided
  50.     enableLogging = true,          -- toggle logging on/off
  51. }
  52.  
  53. -- Last snapshot of CPU states to compare changes between iterations
  54. local lastCpuSnapshot = nil
  55.  
  56. -- Loaded from watchlist file; entries are { name, displayName }
  57. local watchlist = {}
  58.  
  59. --------------------------------------------------------------------------------
  60. -- Logging helpers
  61. --------------------------------------------------------------------------------
  62.  
  63. -- Writes a line to the screen and (optionally) to the log file.
  64. local function log(msg)
  65.     print(msg)
  66.  
  67.     if CONFIG.enableLogging then
  68.         local file = fs.open(CONFIG.logFile, "a")
  69.         if file then
  70.             file.write(msg .. "\n")
  71.             file.close()
  72.         end
  73.     end
  74. end
  75.  
  76. -- Writes a colored line to screen (if color is available) and to log file.
  77. -- Falls back to plain print on non-color terminals.
  78. local function logColored(msg, color)
  79.     if term.isColor() then
  80.         term.setTextColor(color)
  81.         print(msg)
  82.         term.setTextColor(colors.white)
  83.     else
  84.         print(msg)
  85.     end
  86.  
  87.     if CONFIG.enableLogging then
  88.         local file = fs.open(CONFIG.logFile, "a")
  89.         if file then
  90.             file.write(msg .. "\n")
  91.             file.close()
  92.         end
  93.     end
  94. end
  95.  
  96. --------------------------------------------------------------------------------
  97. -- Drawing helpers (UI)
  98. --------------------------------------------------------------------------------
  99.  
  100. -- Draws a horizontal line (full terminal width) at row y in the given color.
  101. local function drawLine(y, color)
  102.     if term.isColor() then
  103.         term.setTextColor(color)
  104.         term.setCursorPos(1, y)
  105.         term.write(string.rep("=", term.getSize()))
  106.         term.setTextColor(colors.white)
  107.     end
  108. end
  109.  
  110. -- Draws a simple bordered box with optional title.
  111. -- (Currently unused in the main loop but handy for future UI expansion.)
  112. local function drawBox(x, y, width, height, title, color)
  113.     if term.isColor() then
  114.         term.setTextColor(color)
  115.  
  116.         -- Top border
  117.         term.setCursorPos(x, y)
  118.         term.write("+" .. string.rep("-", width - 2) .. "+")
  119.  
  120.         -- Title row (if provided)
  121.         if title then
  122.             term.setCursorPos(x, y + 1)
  123.             term.write("| " .. title .. string.rep(" ", width - #title - 3) .. "|")
  124.         end
  125.  
  126.         -- Middle rows (skip title’s body line if present)
  127.         for i = 2, height - 1 do
  128.             if not title or i > 1 then
  129.                 term.setCursorPos(x, y + i)
  130.                 term.write("|" .. string.rep(" ", width - 2) .. "|")
  131.             end
  132.         end
  133.  
  134.         -- Bottom border
  135.         term.setCursorPos(x, y + height)
  136.         term.write("+" .. string.rep("-", width - 2) .. "+")
  137.  
  138.         term.setTextColor(colors.white)
  139.     end
  140. end
  141.  
  142. -- Clears the screen and prints the top header banner.
  143. local function showHeader()
  144.     if term.isColor() then
  145.         term.clear()
  146.         local termWidth, _ = term.getSize()
  147.         local title = "PATTERN DETECTOR"
  148.         local titleX = math.floor((termWidth - #title) / 2) + 1
  149.  
  150.         term.setCursorPos(1, 1)
  151.         term.setBackgroundColor(colors.blue)
  152.         term.setTextColor(colors.white)
  153.         term.write(string.rep(" ", termWidth))
  154.         term.setCursorPos(titleX, 1)
  155.         term.write(title)
  156.         term.setBackgroundColor(colors.black)
  157.         term.setTextColor(colors.white)
  158.     end
  159. end
  160.  
  161. -- Shows a centered status bar indicating whether the redstone output is ON/OFF.
  162. local function showStatusBar(redstoneOn)
  163.     if term.isColor() then
  164.         local termWidth, _ = term.getSize()
  165.         local statusOn = "Redstone Lamp: ON"
  166.         local statusOff = "Redstone Lamp: OFF"
  167.         local status = redstoneOn and statusOn or statusOff
  168.         local color = redstoneOn and colors.green or colors.gray
  169.  
  170.         local barY = 5
  171.         term.setCursorPos(1, barY)
  172.         term.setBackgroundColor(colors.black)
  173.         term.write(string.rep(" ", termWidth))
  174.  
  175.         -- Centered text
  176.         local barX = math.floor((termWidth - #status) / 2) + 1
  177.         term.setCursorPos(barX, barY)
  178.         term.setTextColor(color)
  179.         term.write(status)
  180.         term.setTextColor(colors.white)
  181.     end
  182. end
  183.  
  184. -- Renders the "Watchlist:" line (names joined by ", ", truncated if needed).
  185. -- Falls back to logging in non-color terminals.
  186. local function showWatchlist()
  187.     if term.isColor() then
  188.         local y = 7
  189.         local termWidth, _ = term.getSize()
  190.         local label = "Watchlist:"
  191.         local names = {}
  192.         for i, item in ipairs(watchlist) do
  193.             table.insert(names, item.displayName)
  194.         end
  195.         local namesStr = (#watchlist == 0) and "(empty)" or table.concat(names, ", ")
  196.  
  197.         -- Compose and maybe truncate to terminal width
  198.         local line = label .. " " .. namesStr
  199.         if #line > termWidth then
  200.             local maxNamesLen = termWidth - #label - 4
  201.             if maxNamesLen < 0 then
  202.                 maxNamesLen = 0
  203.             end
  204.             namesStr = namesStr:sub(1, maxNamesLen) .. "..."
  205.             line = label .. " " .. namesStr
  206.         end
  207.  
  208.         -- Clear line then draw colored label + content
  209.         term.setCursorPos(1, y)
  210.         term.setBackgroundColor(colors.black)
  211.         term.write(string.rep(" ", termWidth))
  212.  
  213.         term.setCursorPos(1, y)
  214.         term.setTextColor(colors.cyan)
  215.         term.write(label)
  216.         term.setTextColor(#watchlist == 0 and colors.orange or colors.white)
  217.         term.write(" " .. namesStr)
  218.         term.setTextColor(colors.white)
  219.     else
  220.         -- Non-color: print to log
  221.         if #watchlist == 0 then
  222.             log("Watchlist: (empty)")
  223.         else
  224.             local names = {}
  225.             for i, item in ipairs(watchlist) do
  226.                 table.insert(names, item.displayName)
  227.             end
  228.             log("Watchlist: " .. table.concat(names, ", "))
  229.         end
  230.     end
  231. end
  232.  
  233. -- Displays the crafting status block.
  234. -- Lists each watchlisted item currently being crafted with quantity and CPU id.
  235. local function showCraftingStatus(craftingPatterns)
  236.     if term.isColor() then
  237.         local termWidth, _ = term.getSize()
  238.         local title = "CRAFTING STATUS"
  239.         local y = 9
  240.  
  241.         -- Clear an area that can fit title + up to N lines of jobs
  242.         local linesNeeded = 1 + math.max(1, #craftingPatterns)
  243.         for i = 0, linesNeeded do
  244.             term.setCursorPos(1, y + i)
  245.             term.setBackgroundColor(colors.black)
  246.             term.write(string.rep(" ", termWidth))
  247.         end
  248.  
  249.         -- Title centered
  250.         local titleX = math.floor((termWidth - #title) / 2) + 1
  251.         term.setCursorPos(titleX, y)
  252.         term.setTextColor(colors.cyan)
  253.         term.write(title)
  254.         term.setTextColor(colors.white)
  255.  
  256.         -- Lines with content or a single "no items" message
  257.         if #craftingPatterns > 0 then
  258.             local craftingLines = {}
  259.             local maxLineLen = 0
  260.             for i, crafting in ipairs(craftingPatterns) do
  261.                 local line = "+ "
  262.                     .. crafting.displayName
  263.                     .. " x"
  264.                     .. crafting.quantity
  265.                     .. " (CPU "
  266.                     .. crafting.cpuId
  267.                     .. ")"
  268.                 if #line > termWidth then
  269.                     line = line:sub(1, termWidth - 3) .. "..."
  270.                 end
  271.                 craftingLines[i] = line
  272.                 if #line > maxLineLen then
  273.                     maxLineLen = #line
  274.                 end
  275.             end
  276.  
  277.             local blockWidth = maxLineLen
  278.             local blockX = math.floor((termWidth - blockWidth) / 2) + 1
  279.             for i, line in ipairs(craftingLines) do
  280.                 term.setCursorPos(blockX, y + i)
  281.                 term.setTextColor(colors.green)
  282.                 term.write(line)
  283.                 term.setTextColor(colors.white)
  284.             end
  285.         else
  286.             local msg = "- No watchlisted items being crafted"
  287.             if #msg > termWidth then
  288.                 msg = msg:sub(1, termWidth - 3) .. "..."
  289.             end
  290.             local msgX = math.floor((termWidth - #msg) / 2) + 1
  291.             term.setCursorPos(msgX, y + 1)
  292.             term.setTextColor(colors.orange)
  293.             term.write(msg)
  294.             term.setTextColor(colors.white)
  295.         end
  296.     else
  297.         -- Non-color fallback to log output
  298.         log("CRAFTING STATUS:")
  299.         if #craftingPatterns > 0 then
  300.             for _, crafting in ipairs(craftingPatterns) do
  301.                 log(" " .. crafting.displayName .. " x" .. crafting.quantity .. " (CPU " .. crafting.cpuId .. ")")
  302.             end
  303.         else
  304.             log("- No watchlisted items being crafted")
  305.         end
  306.     end
  307. end
  308.  
  309. --------------------------------------------------------------------------------
  310. -- Data loading / input
  311. --------------------------------------------------------------------------------
  312.  
  313. -- Loads the watchlist file. If missing/empty, writes a default file and returns it.
  314. local function loadWatchlist()
  315.     local items = {}
  316.  
  317.     if fs.exists(CONFIG.watchlistFile) then
  318.         local file = fs.open(CONFIG.watchlistFile, "r")
  319.         if file then
  320.             local line = file.readLine()
  321.             while line do
  322.                 -- Trim whitespace
  323.                 local trimmed = line:gsub("^%s*(.-)%s*$", "%1")
  324.                 -- Skip empty lines and comments starting with '#'
  325.                 if trimmed ~= "" and not trimmed:match("^#") then
  326.                     table.insert(items, {
  327.                         name = trimmed,
  328.                         -- Tidy display: drop common namespaces
  329.                         displayName = trimmed:gsub("minecraft:", ""):gsub("ftbmaterials:", ""),
  330.                     })
  331.                 end
  332.                 line = file.readLine()
  333.             end
  334.             file.close()
  335.         end
  336.     end
  337.  
  338.     -- Provide a default list if nothing loaded, and create the file for convenience
  339.     if #items == 0 then
  340.         items = {
  341.             { name = "minecraft:stone",       displayName = "Stone" },
  342.             { name = "minecraft:iron_ingot",  displayName = "Iron Ingot" },
  343.         }
  344.  
  345.         local file = fs.open(CONFIG.watchlistFile, "w")
  346.         if file then
  347.             for _, item in ipairs(items) do
  348.                 file.write(item.name .. "\n")
  349.             end
  350.             file.close()
  351.         end
  352.     end
  353.  
  354.     return items
  355. end
  356.  
  357. -- Placeholder for handling typed user input when in "control mode".
  358. -- Currently normalizes input but does nothing and exits control mode immediately.
  359. local function handleUserInput()
  360.     local input = read()
  361.     if not input then
  362.         return false
  363.     end
  364.  
  365.     -- Normalize
  366.     input = input:lower():gsub("^%s*(.-)%s*$", "%1")
  367.     -- No commands implemented; return false to exit control mode.
  368. end
  369.  
  370. --------------------------------------------------------------------------------
  371. -- ME Bridge integration
  372. --------------------------------------------------------------------------------
  373.  
  374. -- Finds and returns the first wrapped peripheral that looks like an ME Bridge
  375. -- (i.e., has getCraftingCPUs). Tries "bottom" first, then other sides.
  376. local function findMeBridge()
  377.     if peripheral.isPresent("bottom") then
  378.         local bridge = peripheral.wrap("bottom")
  379.         if bridge and bridge.getCraftingCPUs then
  380.             return bridge
  381.         end
  382.     end
  383.  
  384.     -- Try other sides if not at "bottom"
  385.     for _, side in ipairs({ "left", "right", "front", "back", "top" }) do
  386.         if peripheral.isPresent(side) then
  387.             local bridge = peripheral.wrap(side)
  388.             if bridge and bridge.getCraftingCPUs then
  389.                 return bridge
  390.             end
  391.         end
  392.     end
  393.  
  394.     return nil
  395. end
  396.  
  397. -- Takes a snapshot of crafting CPUs with minimal fields needed for display/compare.
  398. local function takeCpuSnapshot(bridge)
  399.     local snapshot = {}
  400.  
  401.     if bridge.getCraftingCPUs then
  402.         -- Use pcall to be resilient against peripheral/API errors
  403.         local success, cpus = pcall(bridge.getCraftingCPUs, bridge)
  404.         if success and cpus then
  405.             for i, cpu in ipairs(cpus) do
  406.                 snapshot[i] = {
  407.                     isBusy = cpu.isBusy,               -- boolean
  408.                     name = cpu.name,                   -- string
  409.                     storage = cpu.storage,             -- number or details (depends on API)
  410.                     craftingJob = cpu.craftingJob,     -- table with job info
  411.                 }
  412.             end
  413.         end
  414.     end
  415.  
  416.     return snapshot
  417. end
  418.  
  419. -- Inspects current CPU snapshot and collects jobs matching the watchlist.
  420. -- Returns a list of crafting entries for the UI/log: { pattern, cpuId, cpuName, quantity, displayName }
  421. local function checkCraftingPatterns(bridge)
  422.     local craftingPatterns = {}
  423.  
  424.     -- Capture the current CPU states
  425.     local currentCpuSnapshot = takeCpuSnapshot(bridge)
  426.  
  427.     -- Quick check: if no CPUs are busy, we can skip deeper analysis
  428.     local anyBusyCPUs = false
  429.     for cpuId, cpu in pairs(currentCpuSnapshot) do
  430.         if cpu.isBusy then
  431.             anyBusyCPUs = true
  432.         end
  433.     end
  434.  
  435.     if not anyBusyCPUs then
  436.         lastCpuSnapshot = currentCpuSnapshot
  437.         return craftingPatterns
  438.     end
  439.  
  440.     -- If we have a previous snapshot, scan active jobs for watchlist matches
  441.     if lastCpuSnapshot then
  442.         for cpuId, currentCpu in pairs(currentCpuSnapshot) do
  443.             if currentCpu.isBusy and currentCpu.craftingJob then
  444.                 local job = currentCpu.craftingJob
  445.                 if job.resource and job.resource.name then
  446.                     -- Exact id match against the watchlist entry names
  447.                     for _, watchlistItem in ipairs(watchlist) do
  448.                         if job.resource.name == watchlistItem.name then
  449.                             table.insert(craftingPatterns, {
  450.                                 pattern = watchlistItem,
  451.                                 cpuId = cpuId,
  452.                                 cpuName = currentCpu.name,
  453.                                 quantity = job.quantity,
  454.                                 displayName = job.resource.displayName or job.resource.name,
  455.                             })
  456.                             break
  457.                         end
  458.                     end
  459.                 end
  460.             end
  461.         end
  462.     end
  463.  
  464.     -- Persist snapshot for next compare
  465.     lastCpuSnapshot = currentCpuSnapshot
  466.  
  467.     return craftingPatterns
  468. end
  469.  
  470. --------------------------------------------------------------------------------
  471. -- Redstone control
  472. --------------------------------------------------------------------------------
  473.  
  474. -- Sets the redstone output to ON/OFF on the configured side.
  475. local function setRedstoneLamp(on)
  476.     if on then
  477.         redstone.setOutput(CONFIG.redstoneSide, true)
  478.     else
  479.         redstone.setOutput(CONFIG.redstoneSide, false)
  480.     end
  481. end
  482.  
  483. --------------------------------------------------------------------------------
  484. -- Main loop
  485. --------------------------------------------------------------------------------
  486.  
  487. -- Initializes UI, loads watchlist, finds the ME Bridge, and starts the monitor loop.
  488. local function runMonitor()
  489.     showHeader()
  490.     watchlist = loadWatchlist()
  491.  
  492.     local bridge = findMeBridge()
  493.     if not bridge then
  494.         logColored("ERROR: No ME Bridge found!", colors.red)
  495.         return
  496.     end
  497.  
  498.     local lastState = false     -- last redstone state we set
  499.     local controlMode = false   -- toggled when user presses Enter (char "\r")
  500.  
  501.     while true do
  502.         if controlMode then
  503.             -- If we ever implement commands, this is where they'd be handled
  504.             if handleUserInput() then
  505.                 sleep(0.1)
  506.             else
  507.                 -- Exit control mode and redraw the header
  508.                 controlMode = false
  509.                 showHeader()
  510.             end
  511.         else
  512.             -- Event polling for simple key/timer handling in color terminals
  513.             if term.isColor() then
  514.                 local timer = os.startTimer(2)
  515.                 local event, param = os.pullEvent()
  516.  
  517.                 if event == "timer" and param == timer then
  518.                     -- Timer triggered; continue to work loop
  519.                 elseif event == "char" and param == "\r" then
  520.                     -- Enter pressed: enter control mode
  521.                     controlMode = true
  522.                 end
  523.             end
  524.  
  525.             -- Check for watchlisted crafting jobs
  526.             local craftingPatterns = checkCraftingPatterns(bridge)
  527.  
  528.             -- Redstone is ON iff we have any matching jobs
  529.             local currentState = #craftingPatterns > 0
  530.  
  531.             -- Update redstone output only when it changes
  532.             if currentState ~= lastState then
  533.                 setRedstoneLamp(currentState)
  534.                 lastState = currentState
  535.             end
  536.  
  537.             -- Refresh UI sections
  538.             showStatusBar(currentState)
  539.             showWatchlist()
  540.             showCraftingStatus(craftingPatterns)
  541.  
  542.             -- Throttle loop to the configured poll interval
  543.             sleep(CONFIG.pollInterval)
  544.         end
  545.     end
  546. end
  547.  
  548. -- Entry point
  549. runMonitor()
Advertisement
Add Comment
Please, Sign In to add comment