Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local TeleportService = game:GetService("TeleportService")
- local Players = game:GetService("Players")
- local RunService = game:GetService("RunService")
- -- Remote events for client-server communication
- local QueueRequest = ReplicatedStorage.Remotes.QueueRequest
- local QueueUpdate = ReplicatedStorage.Remotes.QueueUpdate
- local data = require(ReplicatedStorage.Modules.Data.GameDefinitions)
- -- Regional server configuration for optimal matchmaking
- -- Each region has its own queue to reduce latency
- local REGIONS = {
- ["US-East"] = "us-east",
- ["US-West"] = "us-west",
- ["Europe"] = "eu",
- ["Asia"] = "asia"
- }
- -- Place IDs for different game modes
- -- SOD appears to be the main game with different PvP modes
- local PLACE_IDS = {
- sod = {
- pvp_1v1 = data.SOD.PlaceId, -- 1v1 PvP matches
- pvp_3player = data.SOD.PlaceId, -- 3-player free-for-all
- }
- }
- -- Configuration for each queue type
- -- Defines how many players are needed and timeout settings
- local QUEUE_SETTINGS = {
- sod = {
- pvp_1v1 = {
- playersNeeded = 2, -- Need exactly 2 players for 1v1
- matchTimeout = 30, -- Wait 30 seconds before cross-region matching
- },
- pvp_3player = {
- playersNeeded = 3, -- Need exactly 3 players for FFA
- matchTimeout = 45, -- Wait 45 seconds before cross-region matching
- }
- }
- }
- -- Multi-dimensional queue structure: queues[game][mode][region] = {players}
- local queues = {}
- -- Parallel structure to track when each player joined: queueTimestamps[game][mode][region] = {timestamps}
- local queueTimestamps = {}
- -- Initialize the queue data structures
- -- Creates nested tables for each game, mode, and region combination
- for gameKey, modes in pairs(QUEUE_SETTINGS) do
- queues[gameKey] = {}
- queueTimestamps[gameKey] = {}
- for mode, _ in pairs(modes) do
- queues[gameKey][mode] = {}
- queueTimestamps[gameKey][mode] = {}
- for region, _ in pairs(REGIONS) do
- queues[gameKey][mode][region] = {}
- queueTimestamps[gameKey][mode][region] = {}
- end
- end
- end
- -- Track which players are currently in any queue to prevent double-queuing
- local playersInQueue = {}
- -- Rate limiting to prevent spam requests
- local lastQueueRequest = {}
- -- Rate limit configuration: max 3 requests per 10 seconds per player
- local RATE_LIMIT = {
- requests = 3,
- window = 10,
- }
- --[[
- Rate limiting function to prevent queue spam
- Maintains a sliding window of recent requests per player
- @param player: The player making the request
- @return success: boolean, error message if failed
- ]]
- local function checkRateLimit(player)
- local now = tick()
- local playerRequests = lastQueueRequest[player] or {}
- -- Remove requests outside the time window (sliding window approach)
- for i = #playerRequests, 1, -1 do
- if now - playerRequests[i] > RATE_LIMIT.window then
- table.remove(playerRequests, i)
- end
- end
- -- Check if player has exceeded rate limit
- if #playerRequests >= RATE_LIMIT.requests then
- return false, "Too many queue requests. Please wait before trying again."
- end
- -- Add current request to tracking
- table.insert(playerRequests, now)
- lastQueueRequest[player] = playerRequests
- return true, nil
- end
- --[[
- Get player's optimal region based on their location
- Currently returns US-East as default, but should be implemented
- to use actual geolocation or ping testing
- @param player: The player to get region for
- @return region: String identifier for the player's region
- ]]
- local function getPlayerRegion(player)
- -- TODO: Implement actual region detection based on:
- -- - Player's locale/country
- -- - Ping testing to different regions
- -- - Manual region selection preference
- return "US-East"
- end
- --[[
- Remove a player from all queues they might be in
- Used when player leaves game or cancels queue
- @param player: The player to remove from queues
- ]]
- local function removePlayerFromQueues(player)
- -- Search through all possible queue locations
- for gameKey, gameModes in pairs(queues) do
- for mode, regions in pairs(gameModes) do
- for region, queue in pairs(regions) do
- -- Search backwards to avoid index issues when removing
- for i = #queue, 1, -1 do
- if queue[i] == player then
- table.remove(queue, i)
- table.remove(queueTimestamps[gameKey][mode][region], i)
- break -- Player can only be in one position per queue
- end
- end
- end
- end
- end
- playersInQueue[player] = nil
- end
- --[[
- Attempt to create a match for a specific game mode and region
- Will try cross-region matching if timeout is exceeded
- @param gameKey: Game identifier (e.g. "sod")
- @param mode: Game mode (e.g. "pvp_1v1")
- @param region: Region identifier (e.g. "US-East")
- ]]
- local function tryMatch(gameKey, mode, region)
- local queue = queues[gameKey][mode][region]
- local settings = QUEUE_SETTINGS[gameKey][mode]
- local timestamps = queueTimestamps[gameKey][mode][region]
- -- Check if we have enough players for a match
- if #queue < settings.playersNeeded then
- -- If queue has players but not enough, check if oldest player has waited too long
- if #queue > 0 then
- local oldestWaitTime = tick() - timestamps[1]
- if oldestWaitTime >= settings.matchTimeout then
- -- Try cross-region matching due to timeout
- return tryMatchCrossRegion(gameKey, mode, region)
- end
- end
- return -- Not enough players and no timeout yet
- end
- -- We have enough players! Remove them from queue (FIFO - first in, first out)
- local playersToMatch = {}
- for i = 1, settings.playersNeeded do
- table.insert(playersToMatch, table.remove(queue, 1))
- table.remove(timestamps, 1)
- end
- -- Remove players from tracking since they're about to be teleported
- for _, player in pairs(playersToMatch) do
- playersInQueue[player] = nil
- end
- -- Reserve a private server for the match
- local placeId = PLACE_IDS[gameKey][mode]
- local success, code = pcall(function()
- return TeleportService:ReserveServer(placeId)
- end)
- if success then
- -- Prepare teleport data to send to the game server
- local teleportData = {
- gameKey = gameKey,
- mode = mode,
- region = region,
- playerCount = #playersToMatch
- }
- -- Log match creation for debugging
- local modeDesc = mode == "pvp_1v1" and "1v1" or "3-player FFA"
- print("Starting", modeDesc, "match for", gameKey, "in", region, "with", #playersToMatch, "players")
- -- Attempt to teleport players to the reserved server
- local teleportSuccess, teleportError = pcall(function()
- TeleportService:TeleportToPrivateServer(placeId, code, playersToMatch, nil, teleportData)
- end)
- -- If teleport fails, put players back in queue
- if not teleportSuccess then
- warn("Failed to teleport players:", teleportError)
- -- Re-add players to front of queue since the failure wasn't their fault
- for i = #playersToMatch, 1, -1 do -- Reverse order to maintain original queue position
- local player = playersToMatch[i]
- if player and player.Parent then -- Verify player is still in game
- table.insert(queue, 1, player)
- table.insert(timestamps, 1, tick())
- playersInQueue[player] = {gameKey = gameKey, mode = mode, region = region}
- end
- end
- end
- else
- -- Server reservation failed, put players back in queue
- warn("Failed to reserve server:", code)
- for i = #playersToMatch, 1, -1 do
- local player = playersToMatch[i]
- if player and player.Parent then
- table.insert(queue, 1, player)
- table.insert(timestamps, 1, tick())
- playersInQueue[player] = {gameKey = gameKey, mode = mode, region = region}
- end
- end
- end
- -- Update all remaining players in queue about their new positions
- updateQueueStatus(gameKey, mode, region)
- end
- --[[
- Attempt cross-region matching when regional queues don't have enough players
- Combines players from all regions for the same game mode
- @param gameKey: Game identifier
- @param mode: Game mode
- @param primaryRegion: Preferred region for the match server
- @return success: Boolean indicating if a match was created
- ]]
- local function tryMatchCrossRegion(gameKey, mode, primaryRegion)
- local settings = QUEUE_SETTINGS[gameKey][mode]
- local allPlayers = {}
- local playerRegions = {}
- -- Collect all players from all regions for this game mode
- for region, queue in pairs(queues[gameKey][mode]) do
- for _, player in pairs(queue) do
- table.insert(allPlayers, player)
- playerRegions[player] = region -- Remember which region each player came from
- end
- end
- -- Check if we have enough players across all regions
- if #allPlayers >= settings.playersNeeded then
- local playersToMatch = {}
- -- Take the required number of players (FIFO across all regions)
- for i = 1, settings.playersNeeded do
- local player = allPlayers[i]
- local playerRegion = playerRegions[player]
- table.insert(playersToMatch, player)
- -- Remove player from their original regional queue
- local queue = queues[gameKey][mode][playerRegion]
- local timestamps = queueTimestamps[gameKey][mode][playerRegion]
- for j = #queue, 1, -1 do
- if queue[j] == player then
- table.remove(queue, j)
- table.remove(timestamps, j)
- break
- end
- end
- playersInQueue[player] = nil
- end
- -- Reserve server and attempt teleport
- local placeId = PLACE_IDS[gameKey][mode]
- local success, code = pcall(function()
- return TeleportService:ReserveServer(placeId)
- end)
- if success then
- local teleportData = {
- gameKey = gameKey,
- mode = mode,
- region = primaryRegion,
- crossRegion = true, -- Flag to indicate this is a cross-region match
- playerCount = #playersToMatch
- }
- local modeDesc = mode == "pvp_1v1" and "1v1" or "3-player FFA"
- print("Starting cross-region", modeDesc, "match for", gameKey, "with", #playersToMatch, "players")
- -- Note: No error handling here - could be improved
- pcall(function()
- TeleportService:TeleportToPrivateServer(placeId, code, playersToMatch, nil, teleportData)
- end)
- end
- return true -- Match was attempted
- end
- return false -- Not enough players even across regions
- end
- --[[
- Update all players in a specific queue about their current status
- Sends position in queue, estimated wait time, etc.
- @param gameKey: Game identifier
- @param mode: Game mode
- @param region: Region identifier
- ]]
- function updateQueueStatus(gameKey, mode, region)
- local queue = queues[gameKey][mode][region]
- local queueSize = #queue
- -- Send update to each player in the queue
- for i, player in pairs(queue) do
- if player and player.Parent then -- Verify player is still in game
- pcall(function() -- Wrap in pcall to handle disconnected players
- QueueUpdate:FireClient(player, {
- gameKey = gameKey,
- mode = mode,
- region = region,
- position = i, -- Player's position in queue (1st, 2nd, etc.)
- totalInQueue = queueSize, -- Total players in this specific queue
- playersNeeded = QUEUE_SETTINGS[gameKey][mode].playersNeeded,
- estimatedWait = calculateEstimatedWait(gameKey, mode, region)
- })
- end)
- end
- end
- end
- --[[
- Calculate estimated wait time message for players in queue
- @param gameKey: Game identifier
- @param mode: Game mode
- @param region: Region identifier
- @return waitMessage: String describing expected wait time
- ]]
- function calculateEstimatedWait(gameKey, mode, region)
- local queueSize = #queues[gameKey][mode][region]
- local settings = QUEUE_SETTINGS[gameKey][mode]
- if queueSize >= settings.playersNeeded then
- return "Starting match..." -- Enough players, match should start soon
- else
- local playersNeeded = settings.playersNeeded - queueSize
- return "Waiting for " .. playersNeeded .. " more player(s)"
- end
- end
- -- Main event handler for queue requests from clients
- QueueRequest.OnServerEvent:Connect(function(player, requestData)
- -- Validate request data exists
- if not requestData then
- warn("Invalid queue request from", player.Name, ": No request data")
- return
- end
- local gameKey = requestData.gameKey
- local mode = requestData.mode
- local action = requestData.action or "join" -- Default to join action
- -- Check rate limiting first
- local rateLimitOk, rateLimitMsg = checkRateLimit(player)
- if not rateLimitOk then
- warn("Rate limit exceeded for", player.Name, ":", rateLimitMsg)
- return
- end
- -- Validate that the requested game and mode exist
- if not gameKey or not mode or not queues[gameKey] or not queues[gameKey][mode] then
- warn("Invalid queue request from", player.Name, ":", gameKey, mode)
- return
- end
- -- Handle PvE mode separately (single player, immediate start)
- if mode == "pve" then
- print(player.Name, "is starting PvE mode for", gameKey)
- -- TODO: Implement PvE server creation and teleport
- return
- end
- -- Handle leaving queue
- if action == "leave" then
- removePlayerFromQueues(player)
- print(player.Name, "left the queue for", gameKey, mode)
- return
- end
- -- Check if player is already in a queue (prevent double-queuing)
- if playersInQueue[player] then
- local current = playersInQueue[player]
- print(player.Name, "is already in queue for", current.gameKey, current.mode, "in", current.region)
- return -- Could send error message to client here
- end
- -- Determine player's optimal region
- local region = getPlayerRegion(player)
- -- Add player to appropriate queue
- local queue = queues[gameKey][mode][region]
- local timestamps = queueTimestamps[gameKey][mode][region]
- table.insert(queue, player) -- Add to end of queue (FIFO)
- table.insert(timestamps, tick()) -- Record join time
- playersInQueue[player] = { -- Track that player is now in queue
- gameKey = gameKey,
- mode = mode,
- region = region
- }
- print(player.Name, "joined", mode, "queue for", gameKey, "in", region, "- Queue size:", #queue)
- -- Update all players in this queue about the new status
- updateQueueStatus(gameKey, mode, region)
- -- Try to create a match now that we have a new player
- tryMatch(gameKey, mode, region)
- end)
- -- Clean up when players leave the game
- Players.PlayerRemoving:Connect(function(player)
- removePlayerFromQueues(player) -- Remove from any queues
- lastQueueRequest[player] = nil -- Clear rate limit tracking
- end)
- -- Periodic maintenance and matching attempts
- local lastQueueCheck = 0
- RunService.Heartbeat:Connect(function()
- local now = tick()
- if now - lastQueueCheck >= 3 then -- Run maintenance every 3 seconds
- lastQueueCheck = now
- -- Check all queues for disconnected players and matching opportunities
- for gameKey, gameModes in pairs(queues) do
- for mode, regions in pairs(gameModes) do
- for region, queue in pairs(regions) do
- if #queue > 0 then
- local timestamps = queueTimestamps[gameKey][mode][region]
- -- Remove disconnected players (cleanup)
- for i = #queue, 1, -1 do
- if not queue[i] or not queue[i].Parent then
- table.remove(queue, i)
- table.remove(timestamps, i)
- end
- end
- -- Try to create matches with remaining players
- if #queue > 0 then
- tryMatch(gameKey, mode, region)
- end
- end
- end
- end
- end
- end
- end)
Advertisement
Add Comment
Please, Sign In to add comment