--[[ Stargate Control System - Main Entry Point For Stargate Journey Mod - AllTheMods 9 Comprehensive control program featuring: - Full dialing system with touch screen interface - Automatic iris control with security features - Incoming/outgoing wormhole management - Address book with categories - Entity tracking and logging - Whitelist/blacklist security ]] --------------------------------------------- -- LOAD MODULES --------------------------------------------- local config = require("config") --------------------------------------------- -- AUTO-UPDATE SYSTEM --------------------------------------------- local function autoUpdate() if not (config.autoUpdate and http) then return false end print("Checking for program updates...") local filesToUpdate = { "startup.lua", "addresses.lua", "utils.lua", "display.lua", "events.lua", "handlers.lua" } local updated = false for _, filename in ipairs(filesToUpdate) do local url = config.repoBaseUrl .. filename local response = http.get(url) if response then local newContent = response.readAll() response.close() -- Check if file exists and compare content local needsUpdate = true if fs.exists(filename) then local file = fs.open(filename, "r") if file then local currentContent = file.readAll() file.close() -- Only update if content is different if currentContent == newContent then needsUpdate = false print(" [SKIP] " .. filename .. " (unchanged)") end end end if needsUpdate then -- Delete old file if it exists if fs.exists(filename) then fs.delete(filename) end local file = fs.open(filename, "w") if file then file.write(newContent) file.close() print(" [UPDATE] " .. filename) updated = true else print(" [FAIL] Could not write " .. filename) end end else print(" [SKIP] Could not download " .. filename) end end return updated end -- Perform auto-update if autoUpdate() then print("Program updated! Restarting...") sleep(2) os.reboot() end local addresses = require("addresses") local utils = require("utils") local display = require("display") local events = require("events") local handlers = require("handlers") --------------------------------------------- -- INITIALIZATION --------------------------------------------- -- Find and initialize peripherals local mon = peripheral.find("monitor") local gate = peripheral.find("advanced_crystal_interface") or peripheral.find("crystal_interface") or peripheral.find("basic_interface") local transceiver = peripheral.find("transceiver") local chatBox = peripheral.find("chatBox") if gate == nil then error("Stargate interface not found! Please connect an interface.") end if mon == nil then error("Monitor not found! Please connect a monitor.") end if transceiver == nil then print("WARNING: No transceiver found. Remote iris detection and GDO support disabled.") end if chatBox == nil and config.enableChatboxDebug then print("WARNING: No chatbox found. Debug messages disabled.") config.enableChatboxDebug = false end -- Check iris availability if gate.getIris() == nil then print("Config has Iris enabled, but there is no iris! Disabling Iris") config.irisEnabled = false end -- Get local gate address local localGateAddress = gate.getLocalAddress() if localGateAddress then print("Local gate address: " .. gate.addressToString(localGateAddress)) else print("WARNING: Could not read local gate address") end -- Initialize modules utils.init(config, gate) display.init(mon, config, addresses, utils, localGateAddress) handlers.init(config, gate, mon, utils, display, events) -- Configure transceiver IDC after utils is initialized (so we can use utils.log) if transceiver and config.enableGDO and config.irisPassword then local idcCode = config.irisPassword if idcCode then transceiver.setCurrentCode(idcCode) utils.log("Transceiver IDC configured to: " .. idcCode) else print("WARNING: irisPassword must be a number for GDO support") end end -- Helper function for chatbox debugging local function debugChat(message) if config.enableChatboxDebug and chatBox then chatBox.sendMessageToPlayer("[SGC] " .. message, config.chatboxDebugPlayer) end end -- Ensure gate starts disconnected gate.disconnectStargate() -- Set iris to default state if config.irisEnabled then if config.irisClosedByDefault then gate.closeIris() else gate.openIris() end end --------------------------------------------- -- GLOBAL STATE --------------------------------------------- local dialing = false local totalstate = nil local disconnect = false local selx, sely = 0, 0 local y = 0 -- Get shared state from handlers local function getState() return handlers.getState() end --------------------------------------------- -- EVENT HANDLERS (Using centralized event system) --------------------------------------------- local function GetClick() mon.setTextScale(1) local result, eventType, eventData = events.pullEvent("monitor_touch") -- eventData is {eventType, side, x, y} if result == "disconnect" then return 0, 0 elseif type(result) == "table" and result.x and result.y then return result.x, result.y else -- Fallback: extract from eventData return eventData[3] or 0, eventData[4] or 0 end end local function GetActivation() local result = events.pullEvent("stargate_incoming_wormhole") return 1 end local function ParaDisconnect() -- Now handled by event system with priority while true do local result = events.pullEvent("monitor_touch") if result == "disconnect" then return 1 end end end local function EntityRead() sleep(0.1) local result = events.pullEvent("stargate_reconstructing_entity") return 1 end local function DisconnectCheck() local result = events.pullEvent("stargate_disconnected") return 2 end local function Paratimeout() sleep(300) return 2 end local function GetMessage() local result = events.pullEvent("stargate_message_received") return 1 end local function GetGDOTransmission() local result = events.pullEvent("transceiver_transmission_received") return 2 end local function HandlePasswordEntry() return handlers.handlePasswordInput() end local function HandleIncomingPasswordRequest(password) -- Check if password matches if config.irisPassword and password == config.irisPassword then utils.log("Correct password received, opening iris") utils.openIris() utils.sendPasswordResponse(true) debugChat("Sent: IRIS_OPEN") return true else utils.log("Incorrect password received") utils.sendPasswordResponse(false) debugChat("Sent: IRIS_DENIED") return false end end local function MonitorRemoteIris() -- Continuously monitor remote iris state while connected local lastIrisState = nil local state = getState() while gate.isStargateConnected() do sleep(0.5) -- Check every half second if transceiver then local remoteIrisState = transceiver.checkConnectedShielding() -- Only update display if state changed if remoteIrisState ~= lastIrisState then if remoteIrisState and remoteIrisState > 0 then -- Remote iris closed or closing if remoteIrisState == 100 then utils.log("ALERT: Remote iris fully closed during connection!") else utils.log("ALERT: Remote iris moving: " .. remoteIrisState .. "%") end -- If remote has computer and iris just became fully closed, check for password request if state.remoteHasComputer and remoteIrisState == 100 and (not lastIrisState or lastIrisState < 100) then utils.log("Remote iris closed and computer detected - checking for password request") -- Check if password was already requested during initial connection if state.remotePasswordRequired then utils.log("Password already requested - showing password prompt") local password = HandlePasswordEntry() -- Wait for response local function WaitForResponse() sleep(3) -- Wait up to 3 seconds for response return nil end local result = parallel.waitForAny(GetMessage, WaitForResponse) if result == 1 then local response = state.lastReceivedMessage state.lastReceivedMessage = nil if response == "IRIS_OPEN" then display.showPasswordResult(true) utils.log("Password accepted - iris opened") -- Continue monitoring, iris state will update elseif response == "IRIS_DENIED" then display.showPasswordResult(false) utils.log("Password rejected") end end else -- Wait briefly for password request message if not already received utils.log("Waiting for password request message") local function WaitForPasswordRequest() sleep(2) -- Wait up to 2 seconds for password request return nil end local result = parallel.waitForAny(GetMessage, WaitForPasswordRequest) if result == 1 then local message = state.lastReceivedMessage state.lastReceivedMessage = nil -- Only show password panel if explicitly requested if message == "IRIS_PASSWORD_REQUIRED" then utils.log("Password requested by remote gate - showing password prompt") local password = HandlePasswordEntry() -- Wait for response local function WaitForResponse() sleep(3) -- Wait up to 3 seconds for response return nil end result = parallel.waitForAny(GetMessage, WaitForResponse) if result == 1 then local response = state.lastReceivedMessage state.lastReceivedMessage = nil if response == "IRIS_OPEN" then display.showPasswordResult(true) utils.log("Password accepted - iris opened") -- Continue monitoring, iris state will update elseif response == "IRIS_DENIED" then display.showPasswordResult(false) utils.log("Password rejected") end end end end end -- Re-check iris state after password attempt remoteIrisState = transceiver.checkConnectedShielding() end -- Show warning screen if iris still closed if remoteIrisState and remoteIrisState > 0 then mon.setBackgroundColor(colors.red) mon.clear() mon.setTextScale(1) mon.setCursorPos(6, 5) mon.write("REMOTE IRIS") mon.setCursorPos(8, 7) if remoteIrisState == 100 then mon.write("CLOSED!") else mon.write(remoteIrisState .. "% CLOSED") end mon.setCursorPos(3, 10) mon.write("Connection unsafe") display.drawIrisStatus() display.drawDisconnectButton() else display.showConnected(state.destAddressname, state.destAddress) end else -- Remote iris is open (0 or nil) if lastIrisState and lastIrisState > 0 then -- Iris just opened utils.log("Remote iris opened - connection safe") -- Open local iris now that connection is safe if config.irisEnabled then utils.openIris() end end display.showConnected(state.destAddressname, state.destAddress) end lastIrisState = remoteIrisState end end end return 3 -- Return unique value to indicate iris monitoring ended end --------------------------------------------- -- INCOMING WORMHOLE HANDLER --------------------------------------------- local function findAddressName(address) -- Search all address categories for matching address local categories = { addresses.MainGates, addresses.playerGates, addresses.hazardGates } for _, category in ipairs(categories) do for _, entry in ipairs(category) do local name, storedAddress = entry[1], entry[2] -- Compare addresses (excluding point of origin) if #address == #storedAddress then local match = true for i = 1, #address do if address[i] ~= storedAddress[i] then match = false break end end if match then return name end end end end return nil -- Not found in address book end local function handleIncomingWormhole() local state = getState() -- Setup event handlers for this connection handlers.setupConnectionHandlers() mon.setBackgroundColor(colors.black) mon.clear() mon.setBackgroundColor(colors.red) mon.setTextScale(1) mon.setCursorPos(9, 4) mon.write("INCOMING") state.incomingAddress = utils.addressToTable(state.incomingAddress) -- Check security local allowed, reason = utils.isAddressAllowed(state.incomingAddress) local addressString = gate.addressToString(state.incomingAddress) or "Unknown" -- Look up address name in address book local addressName = findAddressName(state.incomingAddress) utils.log("Incoming wormhole from: " .. (addressName or addressString) .. " " .. reason) -- Show incoming connection status display.showIncoming(addressName, addressString, allowed, reason) -- Send version message to remote gate sleep(0.5) -- Brief delay to ensure connection is stable utils.sendVersionMessage() debugChat("Sent: SGCS_V" .. config.programVersion) -- Handle iris if config.autoCloseIrisOnIncoming then sleep(config.irisCloseDelay) if allowed then -- Wait 2 seconds after connection established before opening iris sleep(2) utils.openIris() else utils.closeIris() -- Send password request if iris is closed and password system is enabled if config.irisPassword and config.enableMessaging then utils.sendPasswordRequest() debugChat("Sent: IRIS_PASSWORD_REQUIRED") end end end -- Monitor for entities disconnect = false while (disconnect == false) do -- parallel.waitForAny runs multiple functions at the same time and returns which one finished first -- Here we're watching for things simultaneously: -- 1 = EntityRead (someone/something came through the gate) -- 2 = DisconnectCheck (gate disconnected on its own) -- 3 = GetMessage (received a message from remote gate) -- 4 = GetGDOTransmission (received GDO code) -- ParaDisconnect (user clicked screen to manually disconnect) -- Whichever happens first, that function returns and we handle it local result = parallel.waitForAny(EntityRead, DisconnectCheck, GetMessage, GetGDOTransmission, ParaDisconnect) if (result == 1) then display.showEntity(state.incomingEntityType, state.incomingEntityName, allowed) state.incomingEntityType = "" state.incomingEntityName = "" elseif (result == 3) then -- Received a message local message = state.lastReceivedMessage debugChat("Received: " .. tostring(message)) -- Check if it's a password attempt (don't log passwords in plaintext) if message:sub(1, 14) == "IRIS_PASSWORD:" then utils.log("Received password attempt") local password = message:sub(15) local passwordAccepted = HandleIncomingPasswordRequest(password) if passwordAccepted then -- Update allowed status so entities don't show as impacts allowed = true end else utils.log("Received message: " .. message) end state.lastReceivedMessage = nil elseif (result == 4) then -- GDO transmission received (already handled by event system) utils.log("GDO transmission processed") else disconnect = true end end disconnect = false end --------------------------------------------- -- DIALING FUNCTIONS --------------------------------------------- local function dialGate(address) tmp = utils.deepcopy(address) table.remove(tmp) utils.log("Dialing: " .. gate.addressToString(tmp)) local gateType = gate.getStargateType() -- Manual Milky Way dialing with ring rotation if gateType == "sgjourney:milky_way_stargate" and config.manualDial == true then local addressLength = #address if addressLength == 8 then gate.setChevronConfiguration({ 1, 2, 3, 4, 6, 7, 8, 5 }) elseif addressLength == 9 then gate.setChevronConfiguration({ 1, 2, 3, 4, 5, 6, 7, 8 }) end local start = gate.getChevronsEngaged() + 1 for chevron = start, addressLength, 1 do local symbol = address[chevron] if chevron % 2 == 0 then gate.rotateClockwise(symbol) else gate.rotateAntiClockwise(symbol) end while (not gate.isCurrentSymbol(symbol)) do sleep(0) end gate.openChevron() sleep(1) gate.closeChevron() sleep(0.5) display.showDialing(chevron, symbol, gateType) end else -- Automatic dialing for other gate types if gateType ~= "sgjourney:universe_stargate" then local addressLength = #address if addressLength == 8 then gate.setChevronConfiguration({ 1, 2, 3, 4, 6, 7, 8, 5 }) elseif addressLength == 9 then gate.setChevronConfiguration({ 1, 2, 3, 4, 5, 6, 7, 8 }) end end local start = gate.getChevronsEngaged() + 1 for chevron = start, #address, 1 do local symbol = address[chevron] gate.engageSymbol(symbol) sleep(config.gatespeed) display.showDialing(chevron, symbol, gateType) if (symbol) ~= 0 then if (gateType == "sgjourney:universe_stargate") or (gateType == "sgjourney:pegasus_stargate") then os.pullEvent("stargate_chevron_engaged") end else if gateType == "sgjourney:universe_stargate" then os.pullEvent("stargate_chevron_engaged") redstone.setOutput("top", true) elseif (gateType == "sgjourney:pegasus_stargate") then os.pullEvent("stargate_chevron_engaged") end end end end end local function selectGateFromList() local state = getState() local selecting = true while dialing == false and selecting == true do selx, sely = GetClick() local buttonXY, computerAddresses, computerNames = display.getButtonData() for i = 1, #buttonXY do if (sely == buttonXY[i][3]) and ((selx >= buttonXY[i][1]) and (selx <= buttonXY[i][2])) then dialGate(computerAddresses[i]) state.destAddressname = computerNames[i] state.destAddress = computerAddresses[i] dialing = true sely = 0 selx = 0 break end end -- Check back button (x: 23-28, y: 17-19) if not dialing and sely >= 17 and sely <= 19 and selx >= 23 and selx <= 28 then selecting = false sely = 0 selx = 0 end end display.clearButtonData() return dialing end local function selectCategory() local state = true while state == true do display.selectionTabs() local tabx, taby = GetClick() y = 2 local count = 0 if (taby >= 2) and (taby <= 6) and ((tabx >= 2) and (tabx <= 13)) then if #addresses.MainGates ~= 0 then mon.setBackgroundColor(colors.black) mon.clear() mon.setBackgroundColor(colors.purple) count, y = display.screenWrite(addresses.MainGates, count, y) local returnstate = selectGateFromList() if returnstate == true then state = false end display.clearButtonData() else mon.setCursorPos(9, 7) mon.write("no gates available") sleep(5) end elseif (taby >= 2) and (taby <= 6) and ((tabx >= 16) and (tabx <= 27)) then if #addresses.playerGates ~= 0 then mon.setBackgroundColor(colors.black) mon.clear() mon.setBackgroundColor(colors.green) count, y = display.screenWrite(addresses.playerGates, count, y) local returnstate = selectGateFromList() if returnstate == true then state = false end display.clearButtonData() else mon.setCursorPos(9, 7) mon.write("no gates available") sleep(5) end elseif (((taby >= 8) and (taby <= 12)) and ((tabx >= 2) and (tabx <= 13))) and (config.canAccessHazardGates == true) then if (#addresses.hazardGates ~= 0) and (config.canAccessHazardGates == true) then mon.setBackgroundColor(colors.black) mon.clear() mon.setBackgroundColor(colors.red) count, y = display.screenWrite(addresses.hazardGates, count, y) local returnstate = selectGateFromList() if returnstate == true then state = false end display.clearButtonData() else mon.setCursorPos(9, 7) mon.write("no gates available") sleep(5) end elseif (taby >= 17) and (tabx >= 23) then state = false totalstate = false end end return 1 end local function handleOutgoingDial() local state = getState() -- Setup event handlers for this connection handlers.setupConnectionHandlers() totalstate = true local PDO = 0 PDO = parallel.waitForAny(selectCategory, Paratimeout) if (PDO == 1) and totalstate == true then sleep(1) os.pullEvent("stargate_outgoing_wormhole") -- Wait briefly for version message and password request from remote gate state.remoteHasComputer = false state.remotePasswordRequired = false -- Collect messages for up to 2 seconds local startTime = os.clock() local lastMessageTime = startTime while (os.clock() - startTime) < 2 and gate.isStargateConnected() do local result = parallel.waitForAny(GetMessage, GetGDOTransmission, function() sleep(0.3); return -1 end) if result == 1 then local message = state.lastReceivedMessage state.lastReceivedMessage = nil -- DEBUG: Print all messages received print("DEBUG: Message received: " .. tostring(message)) utils.log("DEBUG: Message received: " .. tostring(message)) debugChat("Received: " .. tostring(message)) if message and message:sub(1, 6) == "SGCS_V" then state.remoteHasComputer = true local version = message:sub(7) utils.log("Remote gate has control system version " .. version) lastMessageTime = os.clock() elseif message == "IRIS_PASSWORD_REQUIRED" then state.remotePasswordRequired = true utils.log("Remote gate requires password for entry") lastMessageTime = os.clock() break -- Got password request, that's all we need end elseif result == 2 then -- GDO transmission received (handled by event system) utils.log("GDO transmission detected") lastMessageTime = os.clock() else -- Timeout - if we got a version message, assume no password required if state.remoteHasComputer and (os.clock() - lastMessageTime) > 0.5 then print("DEBUG: Breaking - got version, no password request after 0.5s") break -- No more messages coming elseif not state.remoteHasComputer and (os.clock() - startTime) > 1 then print("DEBUG: Breaking - no version message after 1s") break -- No computer at remote gate end end end print("DEBUG: Message collection complete. remoteHasComputer=" .. tostring(state.remoteHasComputer) .. ", remotePasswordRequired=" .. tostring(state.remotePasswordRequired)) -- Check if remote iris is closed using transceiver local remoteIrisState = nil if transceiver then remoteIrisState = transceiver.checkConnectedShielding() end local connectionSafe = false if remoteIrisState and remoteIrisState > 0 then -- Remote iris is closed (partially or fully) - UNSAFE -- Close local iris to protect travelers if config.irisEnabled then utils.closeIris() end if remoteIrisState == 100 then utils.log("WARNING: Remote iris is fully closed!") else utils.log("WARNING: Remote iris is " .. remoteIrisState .. "% closed!") end -- If password is required and remote has computer, show prompt now if state.remotePasswordRequired and remoteIrisState == 100 then utils.log("Showing password prompt for remote gate") local password = HandlePasswordEntry() utils.sendPasswordAttempt(password) debugChat("Sent: IRIS_PASSWORD:" .. password) -- Wait for response local function WaitForResponse() sleep(3) -- Wait up to 3 seconds for response return nil end local result = parallel.waitForAny(GetMessage, WaitForResponse) if result == 1 then local response = state.lastReceivedMessage state.lastReceivedMessage = nil if response == "IRIS_OPEN" then display.showPasswordResult(true) utils.log("Password accepted - iris opened") sleep(1) -- Re-check iris state if transceiver then remoteIrisState = transceiver.checkConnectedShielding() end if not remoteIrisState or remoteIrisState == 0 then connectionSafe = true end elseif response == "IRIS_DENIED" then display.showPasswordResult(false) utils.log("Password rejected") sleep(2) end end end -- Show warning screen if iris still closed if not connectionSafe and remoteIrisState and remoteIrisState > 0 then mon.setBackgroundColor(colors.red) mon.clear() mon.setTextScale(1) mon.setCursorPos(6, 5) mon.write("REMOTE IRIS") mon.setCursorPos(8, 7) if remoteIrisState == 100 then mon.write("CLOSED!") else mon.write(remoteIrisState .. "% CLOSED") end mon.setCursorPos(3, 10) mon.write("Connection unsafe") display.drawIrisStatus() display.drawDisconnectButton() end else -- Remote iris is open or no iris present connectionSafe = true end -- Only open local iris if connection is safe if connectionSafe and config.irisEnabled then -- Wait 2 seconds to avoid voiding the iris sleep(2) utils.openIris() display.showConnected(state.destAddressname, state.destAddress) elseif connectionSafe then display.showConnected(state.destAddressname, state.destAddress) end if (gate.isStargateConnected() == true) then -- parallel.waitForAny runs functions at the same time, returns which finished first -- While the wormhole is open, we wait for: -- DisconnectCheck = gate disconnects naturally (timeout or remote disconnect) -- ParaDisconnect = user manually clicks screen to disconnect -- Paratimeout = safety timeout (5 minutes) -- MonitorRemoteIris = continuously checks remote iris state -- Whichever happens first ends the connection PDO = parallel.waitForAny(DisconnectCheck, ParaDisconnect, Paratimeout, MonitorRemoteIris) dialing = false end end display.clearButtonData() state.destAddress = {} state.destAddressname = "" state.remoteHasComputer = false end --------------------------------------------- -- MAIN MENU --------------------------------------------- local function mainMenu() while true do -- Setup basic handlers for main menu handlers.setupConnectionHandlers() display.showMainMenu() local answer = parallel.waitForAny(GetClick, GetActivation) if (answer == 1) then handleOutgoingDial() else handleIncomingWormhole() end end end --------------------------------------------- -- STARTUP --------------------------------------------- utils.log("=== Stargate Control System Starting ===") utils.log("Gate Type: " .. gate.getStargateType()) utils.log("Iris Available: " .. tostring(config.irisEnabled)) -- Start the main menu mainMenu()