Files
StargateControl/startup.lua

945 lines
34 KiB
Lua

--[[
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",
"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, 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()
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)
utils.debug("Sent: IRIS_OPEN")
return true
else
utils.log("Incorrect password received")
utils.sendPasswordResponse(false)
utils.debug("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(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()
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)
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
-- DEBUG: Print all messages received
print("DEBUG: Message received: " .. tostring(message))
utils.log("DEBUG: Message received: " .. tostring(message))
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
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 result = HandlePasswordEntry()
-- Check if iris opened during password entry (via GDO)
if result == "IRIS_OPENED" then
utils.log("Iris opened during password entry (via GDO or message)")
connectionSafe = true
-- Show success message briefly
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) -- Wait up to 3 seconds for response
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 iris state after password/GDO attempt
if transceiver then
remoteIrisState = transceiver.checkConnectedShielding()
utils.log("Re-checked remote iris state: " .. tostring(remoteIrisState))
if not remoteIrisState or remoteIrisState == 0 then
connectionSafe = true
utils.log("Remote iris confirmed open")
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
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()