Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Controller Script
- -- Variables
- local PROTOCOL = "mobFarm"
- local LOCAL_ID = "controller"
- -- Mapping between hostName and computerID
- local nodeIDs = {}
- --Mapping buttons
- local buttonMap = {}
- local messageBuffer = {} -- Stores incoming messages
- local updateAcknowledgments = {}
- local stateFile = "stayOnlineStates.json"
- local version = "1.0.2" -- Version of the startup.lua
- -- Ensure update.lua exists with version and Pastebin checks
- local pastebinID = "vMG1SPAL" -- Pastebin ID for update.lua
- local pastebinURL = "https://pastebin.com/raw/" .. pastebinID
- local updateScriptName = "update.lua"
- local localVersionFile = "update_version.txt" -- File to store the local version
- -- Function to check if the Pastebin file exists
- local function checkPastebinFileExists()
- print("Checking if Pastebin file exists...")
- local response = http.get(pastebinURL)
- if response then
- response.close()
- print("Pastebin file exists.")
- return true
- else
- print("Pastebin file does not exist or cannot be accessed.")
- return false
- end
- end
- -- Function to read the local version of update.lua
- local function readLocalVersion()
- if fs.exists(localVersionFile) then
- local file = fs.open(localVersionFile, "r")
- local version = file.readLine()
- file.close()
- return version
- else
- return nil -- No local version exists
- end
- end
- -- Function to write the local version of update.lua
- local function writeLocalVersion(version)
- local file = fs.open(localVersionFile, "w")
- file.writeLine(version)
- file.close()
- end
- -- Function to fetch the remote version from Pastebin
- local function fetchRemoteVersion()
- print("Fetching remote version from Pastebin...")
- local response = http.get(pastebinURL)
- if response then
- local content = response.readAll()
- response.close()
- -- Extract the version from the script content
- local remoteVersion = content:match('local version%s*=%s*"(.-)"')
- if remoteVersion then
- print("Remote version found: " .. remoteVersion)
- return remoteVersion
- else
- print("Failed to extract version from remote script.")
- return nil
- end
- else
- print("Failed to fetch remote script. Check network or Pastebin ID.")
- return nil
- end
- end
- -- Function to compare versions
- local function isUpdateRequired(localVersion, remoteVersion)
- if not localVersion then
- print("No local version found. Update required.")
- return true
- end
- if localVersion ~= remoteVersion then
- print("Version mismatch: Local (" .. localVersion .. ") vs Remote (" .. remoteVersion .. "). Update required.")
- return true
- else
- print("Local version (" .. localVersion .. ") is up-to-date.")
- return false
- end
- end
- -- Main logic to ensure update.lua exists and is up-to-date
- print("Checking update.lua...")
- -- Check if the Pastebin file exists
- if not checkPastebinFileExists() then
- print("Pastebin file does not exist. Aborting check.")
- return
- end
- -- Read local version
- local localVersion = readLocalVersion()
- -- Fetch remote version
- local remoteVersion = fetchRemoteVersion()
- -- Check if an update is required
- if remoteVersion and isUpdateRequired(localVersion, remoteVersion) then
- -- Remove outdated update.lua
- if fs.exists(updateScriptName) then
- print("Removing outdated " .. updateScriptName)
- fs.delete(updateScriptName)
- end
- -- Download the new update.lua
- print("Downloading updated " .. updateScriptName .. " from Pastebin...")
- local success = shell.run("pastebin get " .. pastebinID .. " " .. updateScriptName)
- if success then
- print(updateScriptName .. " downloaded successfully.")
- -- Write the new version to the local version file
- writeLocalVersion(remoteVersion)
- else
- print("Failed to download " .. updateScriptName .. ". Please check the Pastebin ID or your connection.")
- end
- else
- print(updateScriptName .. " is up-to-date. No action required.")
- end
- -- Peripherals
- peripheral.find("modem", rednet.open)
- local monitor = peripheral.find("monitor")
- -- Host controller as 'controller' on network
- rednet.host(PROTOCOL, LOCAL_ID)
- if not monitor then
- error("Monitor not found!")
- end
- if not rednet.isOpen() then
- error("Rednet is not open! Check the modem.")
- end
- -- Configuration
- monitor.setTextScale(0.5)
- monitor.clear()
- local screenHeight = monitor.getSize()
- itemsPerPage = 6
- local nodes = {}
- local messageBuffer = {}
- local currentPage = 1
- -- Utility Functions
- -- Updated addNode Function
- local function addNode(hostName, computerID)
- if not nodes[hostName] then
- nodes[hostName] = {
- type = hostName:match("^(%a+)_"),
- id = tonumber(hostName:match("_(%d+)$")),
- status = "UNKNOWN",
- stayOnline = false, -- Default to not required to stay online
- storage = 0
- }
- nodeIDs[hostName] = computerID -- Map hostName to computerID
- end
- end
- local function getNodeLabel(hostName)
- local node = nodes[hostName]
- return string.format("%s FARM %d", node.type, node.id)
- end
- -- UI Functions
- local function countTableItems(tbl)
- local count = 0
- for _ in pairs(tbl) do
- count = count + 1
- end
- return count
- end
- -- Save stayOnline states and statuses to a file
- local function saveNodeStates()
- local data = {}
- for hostName, node in pairs(nodes) do
- data[hostName] = {
- stayOnline = node.stayOnline,
- status = node.status,
- }
- end
- local file = fs.open(stateFile, "w")
- if file then
- file.write(textutils.serializeJSON(data))
- file.close()
- print("Node states saved to file.")
- else
- print("Error saving node states!")
- end
- end
- -- Regenerate the state file with default values
- local function regenerateNodeStates()
- local defaultStates = {}
- for hostName, node in pairs(nodes) do
- defaultStates[hostName] = {
- stayOnline = node.stayOnline or false,
- storage = node.storage or 0
- }
- end
- local file = fs.open(stateFile, "w")
- if file then
- file.write(textutils.serializeJSON(defaultStates))
- file.close()
- print("Regenerated state file with default values.")
- else
- print("Error regenerating state file!")
- end
- end
- -- Load stayOnline states and other node data from the state file
- local function loadNodeStates()
- if not fs.exists(stateFile) then
- print("No state file found. Skipping load.")
- regenerateNodeStates()
- return
- end
- local file = fs.open(stateFile, "r")
- if not file then
- print("Error opening state file! Regenerating...")
- regenerateNodeStates()
- return
- end
- local data = textutils.unserializeJSON(file.readAll())
- file.close()
- if type(data) ~= "table" then
- print("Invalid state file format. Regenerating...")
- regenerateNodeStates()
- return
- end
- for hostName, nodeData in pairs(data) do
- if type(nodeData) == "table" then
- if not nodes[hostName] then
- -- Add the node if it doesn't already exist
- addNode(hostName, nodeData.computerID or nil)
- end
- -- Update node properties
- nodes[hostName].stayOnline = nodeData.stayOnline or false
- nodes[hostName].storage = nodeData.storage or 0
- print("Loaded state for node " .. hostName)
- else
- print("Skipping invalid data for node:", hostName)
- end
- end
- -- Ensure the file is valid even if there are partial issues
- regenerateNodeStates()
- end
- --Sort Node buttons in asc order
- local function getSortedNodeKeys()
- local keys = {}
- for hostName in pairs(nodes) do
- table.insert(keys, hostName)
- end
- table.sort(keys, function(a, b)
- local aID = tonumber(a:match("_(%d+)$")) or 0
- local bID = tonumber(b:match("_(%d+)$")) or 0
- return aID < bID
- end)
- return keys
- end
- -- Toggle a node's state
- local function toggleNode(hostName)
- if not nodes[hostName] then return end
- local currentStatus = nodes[hostName].status
- local newStatus = (currentStatus == "RUNNING") and "STOPPED" or "RUNNING"
- local computerID = nodeIDs[hostName] -- Get the computerID for the node
- if not computerID then
- print("No computer ID found for node: " .. hostName)
- return
- end
- -- Send the toggle message to the computerID
- rednet.send(computerID, { type = "toggle" }, PROTOCOL)
- nodes[hostName].status = "PENDING" -- Update the status to "PENDING"
- saveNodeStates() -- Save state after toggling
- print("Toggled node " .. hostName .. " to " .. newStatus)
- end
- -- Define drawStorageBar BEFORE calling it in drawButtons
- local function drawStorageBar(x, y, storagePercent)
- x = x + 2
- -- Determine the color for the filled portion of the bar
- local barColor = colors.green
- if storagePercent >= 90 then
- barColor = colors.red
- elseif storagePercent >= 75 then
- barColor = colors.orange
- elseif storagePercent >= 50 then
- barColor = colors.yellow
- end
- -- Calculate the filled and unfilled width
- local totalWidth = 10 -- Width of the storage bar
- local filledWidth = math.floor(totalWidth * (storagePercent / 100))
- local unfilledWidth = totalWidth - filledWidth
- -- Draw the filled portion of the bar
- monitor.setCursorPos(x, y)
- monitor.setBackgroundColor(barColor)
- monitor.write(string.rep(" ", filledWidth))
- -- Draw the unfilled portion of the bar in gray
- monitor.setBackgroundColor(colors.gray)
- monitor.write(string.rep(" ", unfilledWidth))
- -- Reset the background color
- monitor.setBackgroundColor(colors.black)
- -- Write the percentage text in the middle of the bar
- local percentText = storagePercent .. "%"
- local foreground = string.rep(colors.toBlit(colors.white), #tostring(percentText))
- local color = storagePercent <= 40 and colors.gray or barColor
- local background = string.rep(colors.toBlit(color), #tostring(percentText))
- monitor.setCursorPos(x + math.floor((totalWidth - #percentText) / 2), y)
- monitor.blit(tostring(percentText), foreground, background )
- end
- -- Draw Buttons
- local function drawButtons()
- monitor.clear()
- monitor.setCursorPos(1, 1)
- monitor.write("Mob Farm Controller")
- buttonMap = {}
- local screenWidth, screenHeight = monitor.getSize()
- local sortedNodeKeys = getSortedNodeKeys()
- local nodeCount = #sortedNodeKeys
- if nodeCount == 0 then
- monitor.setCursorPos(2, 3)
- monitor.write("No nodes available")
- return
- end
- local x, y = 2, 2 -- Initial position for buttons
- local progressBarWidth = 10 -- Storage bar width
- local startIndex = (currentPage - 1) * itemsPerPage + 1
- local endIndex = math.min(startIndex + itemsPerPage - 1, nodeCount)
- for i = startIndex, endIndex do
- local hostName = sortedNodeKeys[i]
- local node = nodes[hostName]
- local label = getNodeLabel(hostName)
- local status = node.status or "UNKNOWN"
- local storagePercent = node.storage or 0
- local color = (status == "RUNNING") and colors.red or colors.green
- -- Draw Node Label and Status
- local labelText = label .. " (" .. status .. ")"
- local textLength = #labelText
- local foreground = string.rep(colors.toBlit(colors.white), textLength)
- local background = string.rep(colors.toBlit(color), textLength)
- monitor.setCursorPos(x, y)
- monitor.blit(labelText, foreground, background)
- -- Draw Persistence Toggle Button
- local stayOnline = node.stayOnline or false
- local toggleText = stayOnline and "ON" or "OFF"
- local toggleColor = stayOnline and colors.green or colors.red
- local toggleStartX = x + textLength + 2
- local toggleEndX = toggleStartX + 3 -- Button width is 4 characters
- monitor.setCursorPos(toggleStartX, y)
- monitor.setBackgroundColor(toggleColor)
- monitor.write(" " .. toggleText .. " ")
- monitor.setBackgroundColor(colors.black)
- -- Add Toggle Button to Button Map
- table.insert(buttonMap, {
- name = "StayOnline_" .. hostName,
- x1 = toggleStartX,
- y1 = y,
- x2 = toggleEndX,
- y2 = y,
- action = function()
- print("Toggling stayOnline for " .. hostName)
- node.stayOnline = not node.stayOnline
- saveNodeStates() -- Save state after toggle
- drawButtons() -- Refresh the display
- end
- })
- -- Draw Storage Progress Bar
- local storageBarStartX = toggleEndX + 2 -- Space after the toggle button
- drawStorageBar(storageBarStartX, y, storagePercent)
- -- Add Node Toggle Button to Button Map
- table.insert(buttonMap, {
- name = "NodeToggle_" .. hostName,
- x1 = x,
- y1 = y,
- x2 = x + textLength - 1,
- y2 = y,
- action = function()
- print("Toggling node:", hostName)
- toggleNode(hostName)
- end
- })
- y = y + 2 -- Move to the next row
- end
- -- Draw Pagination
- local totalPages = math.ceil(nodeCount / itemsPerPage)
- local paginationText = "Page " .. currentPage .. " of " .. totalPages
- monitor.setCursorPos(math.floor((screenWidth - #paginationText) / 2), 17)
- monitor.write(paginationText)
- local xPos = math.floor((screenWidth - #paginationText) / 2)
- local prevPos = xPos - 3
- local nextPos = xPos + #paginationText + 2
- -- Add Pagination Buttons
- if currentPage > 1 then
- monitor.setCursorPos(prevPos, 17)
- monitor.write("<")
- table.insert(buttonMap, {
- name = "PreviousPage",
- x1 = prevPos,
- y1 = 17,
- x2 = prevPos,
- y2 = 17,
- action = function()
- currentPage = currentPage - 1
- drawButtons()
- end
- })
- end
- if currentPage < totalPages then
- monitor.setCursorPos(nextPos, 17)
- monitor.write(">")
- table.insert(buttonMap, {
- name = "NextPage",
- x1 = nextPos,
- y1 = 17,
- x2 = nextPos,
- y2 = 17,
- action = function()
- currentPage = currentPage + 1
- drawButtons()
- end
- })
- end
- end
- -- Validate nodes and retry toggling states if needed
- local function validateNodesAgainstStates()
- for hostName, node in pairs(nodes) do
- if node.stayOnline and node.status ~= "RUNNING" then
- print("Node " .. hostName .. " should be running. Toggling...")
- toggleNode(hostName) -- Attempt to start the node
- elseif not node.stayOnline and node.status == "RUNNING" then
- print("Node " .. hostName .. " should be stopped. Toggling...")
- toggleNode(hostName) -- Attempt to stop the node
- end
- end
- end
- -- Force status update requests from all nodes immediately after startup
- local function requestInitialStatuses()
- print("Requesting initial statuses from all nodes...")
- for hostName, computerID in pairs(nodeIDs) do
- if nodes[hostName] then
- rednet.send(computerID, { type = "requestStatus" }, PROTOCOL)
- nodes[hostName].status = "PENDING" -- Temporarily mark as PENDING until the response arrives
- end
- end
- end
- -- Network Functions
- -- Discover Nodes with Debug Logs and Retries
- local function discoverNodes()
- print("Discovering nodes...")
- local computers = { rednet.lookup(PROTOCOL) }
- print("Discovered computers:", textutils.serialize(computers))
- if #computers == 0 then
- print("No nodes discovered, awaiting contact from nodes as they come online.")
- else
- for _, computerID in ipairs(computers) do
- if computerID ~= os.getComputerID() then
- print("Requesting CLIENT_ID from computer ID:", computerID)
- rednet.send(computerID, { type = "requestId", controllerId = LOCAL_ID }, PROTOCOL)
- -- Retry receiving response a few times
- local attempts = 3
- while attempts > 0 do
- local senderId, response = rednet.receive(PROTOCOL, 3) -- 3-second timeout
- if senderId == computerID and response and response.type == "responseId" then
- local clientId = response.CLIENT_ID
- print("Received CLIENT_ID:", clientId, "from computer ID:", computerID)
- addNode(clientId, computerID)
- drawButtons() -- Update UI after adding node
- break
- else
- print("No response or invalid response from computer ID:", computerID, "Retrying...")
- attempts = attempts - 1
- if attempts == 0 then
- print("Failed to get CLIENT_ID from computer ID:", computerID)
- end
- end
- end
- end
- end
- end
- end
- -- Request Status Update Function
- local function requestStatusUpdates()
- while true do
- for hostName, node in pairs(nodes) do
- if node.status == "UNKNOWN" then
- local computerID = nodeIDs[hostName]
- if computerID then
- print("Requesting status update from node: " .. hostName)
- rednet.send(computerID, { type = "requestStatus" }, PROTOCOL)
- else
- print("No computer ID found for node: " .. hostName)
- end
- end
- end
- sleep(5) -- Wait 5 seconds before rechecking
- end
- end
- local function handleControllerCommands()
- while true do
- write("> ")
- local input = read()
- if input == "update" then
- print("Initiating update process...")
- -- Clear previous acknowledgments
- updateAcknowledgments = {}
- -- Notify all nodes to update
- for hostName, computerID in pairs(nodeIDs) do
- rednet.send(computerID, { type = "update" }, PROTOCOL)
- print("Sent update command to node:", hostName)
- end
- -- Wait for acknowledgments from all nodes
- print("Waiting for all nodes to acknowledge update...")
- while true do
- local allAcknowledged = true
- for hostName, _ in pairs(nodeIDs) do
- if not updateAcknowledgments[hostName] then
- allAcknowledged = false
- break
- end
- end
- if allAcknowledged then
- print("All nodes acknowledged update.")
- break
- end
- sleep(1) -- Wait briefly before checking again
- end
- -- Perform controller update
- print("Updating controller...")
- shell.run("rm startup.lua") -- Delete existing startup.lua
- shell.run("pastebin get RkmNxEg1 startup.lua") -- Download new script
- os.reboot() -- Reboot controller
- else
- print("Unknown command. Available commands: update")
- end
- end
- end
- -- Handle Incoming Messages
- local function processMessageBuffer()
- while true do
- if #messageBuffer > 0 then
- local messageData = table.remove(messageBuffer, 1)
- local senderId = messageData.senderId
- local message = messageData.message
- if message.type == "storageUpdate" then
- -- Update storage percentage for the node
- local hostName = message.hostName
- local storagePercent = message.storagePercent
- if nodes[hostName] then
- nodes[hostName].storage = storagePercent
- print("Updated storage for node " .. hostName .. " to " .. storagePercent .. "%")
- drawButtons() -- Refresh UI with updated storage
- else
- print("Received storage update for unknown node: " .. hostName)
- end
- elseif message.type == "status" then
- -- Handle status updates
- local hostName = message.hostName
- if nodes[hostName] then
- nodes[hostName].status = message.status
- print("Updated status for node: " .. hostName .. " to " .. message.status)
- drawButtons()
- else
- print("Unknown node status received: " .. hostName)
- end
- end
- else
- sleep(0.1) -- Avoid busy-waiting
- end
- end
- end
- local function rediscoverUnknownNodes()
- print("Checking for nodes with unknown statuses...")
- local rediscovered = false
- for hostName, node in pairs(nodes) do
- if node.status == "UNKNOWN" then
- print("Node " .. hostName .. " has an unknown status. Attempting rediscovery...")
- -- Get the node's computer ID from rednet lookup
- local computerID = rednet.lookup(PROTOCOL, hostName)
- if computerID then
- print("Rediscovered node:", hostName)
- nodeIDs[hostName] = computerID -- Map hostName to computerID
- rednet.send(computerID, { type = "requestStatus" }, PROTOCOL) -- Request status
- rediscovered = true
- else
- print("Failed to rediscover node:", hostName)
- end
- end
- end
- if not rediscovered then
- print("No nodes required rediscovery.")
- else
- print("Rediscovery completed. Waiting for status updates...")
- sleep(5) -- Allow time for responses
- validateNodesAgainstStates() -- Ensure node states are valid
- end
- end
- local function monitorNodeStatuses()
- while true do
- -- Check for nodes with "UNKNOWN" statuses
- local hasUnknown = false
- for _, node in pairs(nodes) do
- if node.status == "UNKNOWN" then
- hasUnknown = true
- break
- end
- end
- -- If any node has an unknown status, trigger rediscovery
- if hasUnknown then
- print("Unknown statuses detected. Initiating rediscovery...")
- rediscoverUnknownNodes()
- end
- sleep(10) -- Check every 10 seconds
- end
- end
- -- Handle Incoming Messages (Modified to process status responses)
- local function handleIncomingMessage()
- while true do
- local senderId, message = rednet.receive(PROTOCOL)
- print("Received message from ID:", senderId, "Type:", message.type)
- -- Add the message to the buffer
- table.insert(messageBuffer, { senderId = senderId, message = message })
- end
- end
- -- Function to check for mouse click
- local function handleMouseClick()
- while true do
- local _, _, x, y = os.pullEvent("monitor_touch") -- Capture monitor touch event
- print("Monitor clicked at x:", x, "y:", y)
- for _, button in ipairs(buttonMap) do
- if x >= button.x1 and x <= button.x2 and y >= button.y1 and y <= button.y2 then
- print("Button clicked:", button.name)
- if button.action then
- button.action() -- Call the action associated with the button
- else
- print("No action assigned to button:", button.name)
- end
- break
- end
- end
- end
- end
- -- Main Loop
- discoverNodes() -- Discover nodes on startup
- loadNodeStates() -- Load saved states (stayOnline and status)
- requestInitialStatuses() -- Request updated statuses immediately
- validateNodesAgainstStates() -- Validate and restore node states
- parallel.waitForAll(
- handleIncomingMessage, -- Receive and buffer messages from nodes
- processMessageBuffer, -- Process messages from the buffer
- requestStatusUpdates, -- Periodic status updates for nodes
- handleMouseClick, -- Handle monitor clicks for buttons
- handleControllerCommands, -- Listen for user input commands
- monitorNodeStatuses, -- Periodically check for unknown statuses
- function()
- -- Monitor update loop
- while true do
- drawButtons() -- Periodically refresh the monitor interface
- sleep(1) -- Update every second
- end
- end
- )
Comments
-
- This is the lua script to designate a CC:Tweaked computer as the controller module for the farm. It will receive updates from various nodes (No limit atm) and display them to a monitor. From the monitor, you can see the storage used, toggle on or off various farms, and toggle persistence so you do not have to chunckload areas the computers are in for the nodes to remember if they were previously running or not.
Add Comment
Please, Sign In to add comment