Scripting HelpersARCHIVE
A little piece of Roblox history. Preserved for everyone, one question at a time.Read about the archive →
COMMUNITY QUESTION

How do I fix the ID Doubling issue in Matchmaking?

As you may know, roblox's matchmaking script is extremely broken and has bugs all over it. I have fixed most of the problems, but I still don't know how to fix an important one.

When searching for a game, it looks for another player's ID to create a game. If you cancel the matchmaking, your player's ID does not fully remove itself from the queue, resulting in a teleportation with two of your same ID's.

This only happens when you cancel the matchmaking, it works perfectly if you don't. But for a game, I need to to allow players to cancel matchmaking if they feel like it.

Here is the code:

local lookForGameEvent = game.ReplicatedStorage.LookForGameEvent
local rankedList = require(game.ServerStorage.MatchmakingRankedListModule)
local ratingData = game:GetService("DataStoreService"):GetDataStore("PlayerRating")
local coinData = game:GetService("DataStoreService"):GetDataStore("PlayerCoin")
local gemData = game:GetService("DataStoreService"):GetDataStore("PlayerGems")
local teleportService = game:GetService("TeleportService")
local arenaPlaceTemplateId = 1983540262

-- Table to hold the queue of players looking for a game
local matchMakingQueue = {}

-- Add player's id to the queue as well as the time that they entered the queue
local addToMMQueue = function(playerId, enteredTime)
    local data = {}
    data.userId = playerId
    data.EnteredQueue = enteredTime
    table.insert(matchMakingQueue, data)
end

-- Remove player from the queue
local removeFromMMQueue = function(playerId)
    for i, playerData in pairs(matchMakingQueue) do
        if playerData.userId == playerId then
            table.remove(matchMakingQueue, i)
            return
        end
    end
end

-- Handle player joining the game. If they are a new player we need to give them
-- an initial rank. For existing players we need to fetch their rank from our 
-- DataStore. Lastly, we want to display the player's rank in the Leaderboard
game.Players.PlayerAdded:connect(function(player)
    local playerData = {}

    -- Create a default rating of 100 if the player's data is not in the DataStore
    ratingData:UpdateAsync(player.userId, function(oldValue)
        local newValue = oldValue
        if not newValue then
            newValue = {Rating = 100}
        end
        return newValue
    end)
    coinData:UpdateAsync(player.userId, function(oldvalue2)
        local newValue2 = oldvalue2
        if not newValue2 then
            newValue2 = {Coins = 10}
        end
        return newValue2
    end)
    gemData:UpdateAsync(player.userId, function(oldvalue3)
        local newValue3 = oldvalue3
        if not newValue3 then
            newValue3 = {Gems = 0}
        end
        return newValue3
    end)
    playerData.Score = ratingData:GetAsync(tostring(player.userId)).Rating
    playerData.Score2 = coinData:GetAsync(tostring(player.userId)).Coins
    playerData.Score3 = gemData:GetAsync(tostring(player.userId)).Gems

    -- Display rating in Leaderboard
    local stats = Instance.new("Folder", player)
    stats.Name = "leaderstats"

    local displayRating = Instance.new("IntValue", stats)
    displayRating.Name = "Rating"
    displayRating.Value = playerData.Score

    local displayCoins = Instance.new("IntValue", stats)
    displayCoins.Name = "Coins"
    displayCoins.Value = playerData.Score2

    local displayGems = Instance.new("IntValue", stats)
    displayGems.Name = "Gems"
    displayGems.Value = playerData.Score3
end)

-- Adds player both to queue and list to search for match
local function playerSearchingForMatch(userId, rank)
    local now = os.time()
    addToMMQueue(userId, now)
    rankedList:AddPlayer(userId, rank, now)
end

-- Remove player from list and queue when they leave the game
game.Players.PlayerRemoving:connect(function(player)
    removeFromMMQueue(player.userId)
    rankedList:RemovePlayer(player.userId)
end)

-- Handle remote event to either add or remove player from the queue
lookForGameEvent.OnServerEvent:connect(function(player, lookingForGame)
    if lookingForGame then
        print(player.Name .. " now looking for game")
        local enteredTime = os.time()
        playerSearchingForMatch(player.userId, player.leaderstats.Rating.Value)
    else
        print(player.Name .. " has left the queue")
        removeFromMMQueue(player.userId)
        rankedList:RemovePlayer(player.userId)
    end
end)

-- Returns a rank range to search through based on time waiting in the queue.
-- If player has been waiting too long just return math.huge so the player can
-- be matched with anyone.
local getRange = function(timeWaiting)
    if timeWaiting < 10 then
        return 100
    elseif timeWaiting >=10 and timeWaiting < 20 then
        return 200
    elseif timeWaiting >=20 and timeWaiting <= 35 then
        return 300
    end
    return math.huge
end

-- Creates place for game and teleports both players to it
local startGame = function(playerAId, playerBId)
    local message = ""
    print("starting game with " .. playerAId .. " and " .. playerBId)

    --[[if playerAId == playerBId then
        print("same ID!")
        return
    end--]]

    -- Get both player objects
    local playerA = nil
    local playerB = nil
    for _, player in pairs(game.Players:GetPlayers()) do
        if player.userId == playerAId then
            playerA = player
        end
        if player.userId == playerBId then
            playerB = player
        end
    end

    -- Create arena place and get its id
    local arenaPlaceId = game:GetService("AssetService"):CreatePlaceAsync(
        "Match: " .. playerAId .. " VS. " .. playerBId, arenaPlaceTemplateId)

    -- Bind OnTeleport event to playerA (who is teleported first). If that teleport is successful
    -- then we want playerB to be teleported to the same instance
    local connection = playerA.OnTeleport:connect(function(teleportState, placeId)
        if teleportState == Enum.TeleportState.Started then
            local teleportStarted = os.time()
            -- Keep checking if playerA has arrived in other instance.
            while true do
                local success, error, placeId, arenaInstanceId = teleportService:GetPlayerPlaceInstanceAsync(playerAId)
                -- If playerA is in the correct place then we can teleport playerB there as well
                if success then -- placeId == arenaPlaceId
                    teleportService:Teleport(arenaPlaceId, playerB)
                    return
                end
                wait()
            end 
        end
    end)
    wait(1)

    -- Teleport playerA to the arena
    teleportService:Teleport(arenaPlaceId, playerA)
end

-- Matchmaking loop. Cycles about every 5 seconds to match players.
while true do
    local now = os.time()
    -- Cycle through queue, try to find players in range
    for _, mmData in pairs(matchMakingQueue) do
        print("attempting to find match for " .. mmData.userId)
        -- Get rank range to search
        local range = getRange(now - mmData.EnteredQueue)
        -- Use list to find a player in player's rank range
        local otherPlayerId = rankedList:FindPlayerInRange(mmData.userId, range)
        if otherPlayerId then
            -- Another player was found. Remove both players from the queue and list so they
            -- can't be matched with anyone else
            print("found player: " .. otherPlayerId)
            rankedList:RemovePlayer(mmData.userId)
            rankedList:RemovePlayer(otherPlayerId)
            removeFromMMQueue(mmData.userId)
            removeFromMMQueue(otherPlayerId)
            -- Start game with the two players. This function can take some times, so start a
            -- coroutine so the loop can continue.
            local thread = coroutine.create(function() startGame(mmData.userId, otherPlayerId) end)
            coroutine.resume(thread)
        end
    end
    wait(5)
end

If you need the module script, please ask!

3 comments

0

"If you need the module script, please ask!" No 'asking'. It’s needed the second you click the ‘ Ask Question' button to ask this question.

0

Not enough space to provide in the question...

0

Stop lying.

1 answer

The Module Script

-- Module for double linked list for matchmaking
local rankList = {}
rankList.Top = nil
rankList.Bottom = nil

-- Dictionary mapping userIds to the corrosponding linked list node for quick lookup
local keyTable = {}

-- Inserts newNode before oldNode in list. Reassigns rankList.Top if oldNode at top
function rankList:InsertBefore(newNode, oldNode)
	if oldNode.Prev then
		newNode.Prev = oldNode.Prev
		oldNode.Prev.Next = newNode
	end
	newNode.Next = oldNode
	oldNode.Prev = newNode
	if rankList.Top == oldNode then
		rankList.Top = newNode
	end
	return newNode
end

-- Removes node from the linked list. Makes sure Top and Bottom pointing to correct nodes.
-- Also removes node from lookup dictionary
function rankList:RemoveNode(node)
	if node.Prev then
		node.Prev.Next = node.Next
		if rankList.Bottom == node then
			rankList.Bottom = node.Prev
		end
	end
	if node.Next then
		node.Next.Prev = node.Prev
		if rankList.Top == node then
			rankList.Top = node.Next
		end
	end
	keyTable[tostring(node.userId)] = nil
end

-- Adds player to list. Ensures list is in stored order by inserting player in correct spot
-- based on rank. Also adds player to lookup dictionary
function rankList:AddPlayer(userId, rank, startedWaiting)
	local node = {UserId = userId, Rank = rank, Age = startedWaiting, Next = nil, Prev = nil}
	keyTable[tostring(userId)] = node	
	
	-- First check if list is empty. If so just point Top and Bottom to the new node
	if not rankList.Top then
		rankList.Top = node
		rankList.Bottom = node
		return node
	else
		-- If the list is not empty find the place where the new node should go. Start
		-- with the Top node and continue through the list comparing ranks
		local currentNode = rankList.Top
		
		while currentNode do
			if currentNode.Rank	> rank then
				return rankList:InsertBefore(node, currentNode)
			end		
			
			currentNode = currentNode.Next
		end
		
		--if code got here then the node has to be inserted at the end of the list
		rankList.Bottom.Next = node
		node.Prev = rankList.Bottom
		rankList.Bottom = node
		return node
	end
end

-- Removes player from list
function rankList:RemovePlayer(userId)
	local playerNode = keyTable[tostring(userId)]
	rankList:RemoveNode(playerNode)
end

-- Private search function for FindPlayerInRangeWithNode. Can search either towards
-- the top of the list or the bottom based on searchAscending
local function Search(current, rank, searchAscending, range, oldestWait)
	local retNode = nil	
	while current do
		if math.abs(rank - current.Rank) > range then
			break
		end		
		if current.Age < oldestWait then
			oldestWait = current.Age
			retNode = current
		end
		if searchAscending then 
			current = current.Next
		else
			current = current.Prev
		end
	end
	return retNode, oldestWait
end

-- Returns node of a player in the rank range of the player in startNode. The player who
-- is returned is the the player in the rank range who has been waiting the longest.
function rankList:FindPlayerInRangeWithNode(startNode, range)
	local oldestWait = math.huge
	local rank = startNode.Rank
	local current = startNode.Next
	local retNode = nil
	
	retNode, oldestWait = Search(startNode.Next, startNode.Rank, true, range, oldestWait)
	retNode, oldestWait = Search(startNode.Prev, startNode.Rank, false, range, oldestWait)
	
	return retNode
end

-- Returns user id of other player in range of userId's rank. Returns nil if no such player
-- found.
function rankList:FindPlayerInRange(userId, range)
	local playerNode = keyTable[tostring(userId)]
	if playerNode then
		local otherPlayer = rankList:FindPlayerInRangeWithNode(playerNode, range)
		if otherPlayer then 
			return otherPlayer.UserId 
		end
	end
	return nil
end

return rankList

This discussion is preserved as it was. New answers, comments, and votes are closed.