--[[ 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 ]] --------------------------------------------- -- PROGRAM VERSION --------------------------------------------- local PROGRAM_VERSION = "2.0" --------------------------------------------- -- 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", "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 -- Update addresses.lua from server-specific folder if config.addressBook then local addressUrl = config.repoBaseUrl .. "addresses/" .. config.addressBook .. "/addresses.lua" local response = http.get(addressUrl) if response then local newContent = response.readAll() response.close() local needsUpdate = true if fs.exists("addresses.lua") then local file = fs.open("addresses.lua", "r") if file then local currentContent = file.readAll() file.close() if currentContent == newContent then needsUpdate = false print(" [SKIP] addresses.lua (unchanged)") end end end if needsUpdate then if fs.exists("addresses.lua") then fs.delete("addresses.lua") end local file = fs.open("addresses.lua", "w") if file then file.write(newContent) file.close() print(" [UPDATE] addresses.lua (" .. config.addressBook .. ")") updated = true else print(" [FAIL] Could not write addresses.lua") end end else print(" [SKIP] Could not download addresses.lua for " .. config.addressBook) 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 gate type and iris availability local gateType = gate.getStargateType() -- Tollan gates don't have iris support and will crash if checked if gateType == "sgjourney:tollan_stargate" then print("Tollan Stargate detected - iris functionality disabled") config.irisEnabled = false elseif 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, chatBox) display.init(mon, config, addresses, utils, localGateAddress) handlers.init(config, gate, mon, utils, display, events, transceiver) -- 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 -- 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() local result = handlers.handlePasswordInput() return result 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) return true else utils.log("Incorrect password received") utils.sendPasswordResponse(false) return false end end local function MonitorRemoteIris() -- Continuously monitor remote iris state while connected -- Password handling is done during initial connection only 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 during active connection if remoteIrisState == 100 then utils.log("ALERT: Remote iris fully closed during connection!") else utils.log("ALERT: Remote iris moving: " .. remoteIrisState .. "%") end -- Show warning screen 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 -- 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(PROGRAM_VERSION) utils.debug("Sent: SGCS_V" .. PROGRAM_VERSION) -- 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() utils.debug("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 utils.debug("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 events.pullEvent("stargate_chevron_engaged") end else if gateType == "sgjourney:universe_stargate" then events.pullEvent("stargate_chevron_engaged") redstone.setOutput("top", true) elseif (gateType == "sgjourney:pegasus_stargate") then events.pullEvent("stargate_chevron_engaged") end end end end end local function selectGateFromList(isHazardGate) local state = getState() local selecting = true while dialing == false and selecting == true do selx, sely = GetClick() local buttonXY, computerAddresses, computerNames, hazardReasons = display.getButtonData() for i = 1, #buttonXY do if (sely == buttonXY[i][3]) and ((selx >= buttonXY[i][1]) and (selx <= buttonXY[i][2])) then local shouldDial = true -- If this is a hazard gate, show confirmation prompt if isHazardGate and hazardReasons and hazardReasons[i] then mon.setBackgroundColor(colors.black) mon.clear() mon.setBackgroundColor(colors.red) mon.setTextScale(1) mon.setCursorPos(5, 5) mon.write("WARNING: HAZARD GATE") mon.setBackgroundColor(colors.black) mon.setCursorPos(2, 8) mon.write("Destination:") mon.setCursorPos(2, 9) mon.write(computerNames[i]) mon.setCursorPos(2, 11) mon.write("Hazard:") mon.setCursorPos(2, 12) mon.write(hazardReasons[i]) mon.setCursorPos(2, 15) mon.write("Proceed with dialing?") -- Draw YES and NO buttons mon.setBackgroundColor(colors.green) mon.setCursorPos(5, 17) mon.write(" YES ") mon.setBackgroundColor(colors.red) mon.setCursorPos(18, 17) mon.write(" NO ") -- Wait for confirmation (use direct os.pullEvent to bypass handlers) local confirmed = false while true do local event, side, cx, cy = os.pullEvent("monitor_touch") if cy == 17 then if cx >= 5 and cx <= 10 then -- YES clicked confirmed = true break elseif cx >= 17 and cx <= 21 then -- NO clicked break end end end shouldDial = confirmed end if shouldDial then dialGate(computerAddresses[i]) state.destAddressname = computerNames[i] state.destAddress = computerAddresses[i] dialing = true end 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(false) 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(false) 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(true) 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) events.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 utils.debug("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 elseif message == "GDO_IRIS_OPEN" then utils.log("Remote iris opened via GDO") state.remotePasswordRequired = false lastMessageTime = os.clock() break -- Iris opened, no password needed 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 break -- No more messages coming elseif not state.remoteHasComputer and (os.clock() - startTime) > 1 then break -- No computer at remote gate end end end -- Check if remote iris is closed (for safety, not password prompting) local remoteIrisState = nil if transceiver then remoteIrisState = transceiver.checkConnectedShielding() end -- Close local iris if remote iris is closed (unsafe for travel) if remoteIrisState and remoteIrisState > 0 then 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 end local connectionSafe = false -- If remote computer requested a password, show the prompt if state.remotePasswordRequired then utils.log("Password required by remote gate") local result = HandlePasswordEntry() -- Check if iris opened during password entry (via GDO) if result == "IRIS_OPENED" then connectionSafe = true display.showPasswordResult(true) sleep(1) elseif result then -- Password was entered, send it utils.debug("Sent: IRIS_PASSWORD:" .. result) -- Wait for response local function WaitForResponse() sleep(3) return nil end local waitResult = parallel.waitForAny(GetMessage, WaitForResponse) if waitResult == 1 then local response = state.lastReceivedMessage state.lastReceivedMessage = nil if response == "IRIS_OPEN" or response == "GDO_IRIS_OPEN" then display.showPasswordResult(true) utils.log("Password accepted - iris opened") sleep(1) connectionSafe = true elseif response == "IRIS_DENIED" then display.showPasswordResult(false) utils.log("Password rejected") sleep(2) end end end -- Re-check remote iris state after password/GDO attempt if transceiver then remoteIrisState = transceiver.checkConnectedShielding() if not remoteIrisState or remoteIrisState == 0 then connectionSafe = true end end -- If still not safe, show warning if not connectionSafe then mon.setBackgroundColor(colors.red) mon.clear() mon.setTextScale(1) mon.setCursorPos(6, 5) mon.write("REMOTE IRIS") mon.setCursorPos(8, 7) mon.write("CLOSED!") mon.setCursorPos(3, 10) mon.write("Connection unsafe") display.drawIrisStatus() display.drawDisconnectButton() end else -- No password required - check if remote iris is open if transceiver then remoteIrisState = transceiver.checkConnectedShielding() end if not remoteIrisState or remoteIrisState == 0 then -- Remote iris is open - connection is safe connectionSafe = true else -- Remote iris is closed but no password system - show warning utils.log("WARNING: Remote iris closed but no password system available") 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 end utils.log("DEBUG: Before iris opening check - connectionSafe=" .. tostring(connectionSafe) .. ", config.irisEnabled=" .. tostring(config.irisEnabled)) -- Only open local iris if connection is safe if connectionSafe and config.irisEnabled then utils.debug("Opening local iris NOW") utils.log("Connection safe - opening local iris") -- Wait 2 seconds to avoid voiding the iris sleep(2) utils.openIris() display.showConnected(state.destAddressname, state.destAddress) elseif connectionSafe then utils.log("Connection safe - no iris to open") display.showConnected(state.destAddressname, state.destAddress) else utils.log("Connection NOT safe - iris remains closed") 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()