Matchmaking having big issues

I want to make it so 2 players from different servers can connect with eachoder and chat. When a player starts he is added to a queue where he can connect with another player and chat. If the players skip, they are both added to the queue. If one stops, he just stops and the other one is added to the queue. Same with leaving. They can chat with eachoder.

Im not sure why, spent a long time trying to debug this, there are many bugs. One bug is that if 2 players are connected and there is a third one in the queue, if one of the 2 players skip then they will both be connected to the third player. Leaving does not clear the queue as it should (you can still match with someone who left, no idea how), sometimes you can connect with players who stopped. I think its because both players try to connect at the same time.

I tried to make the queue be cleared in multiple places, but it didnt help. Spent a few hours spamming prints and deleting them from places but didnt find anything. Should I delete everything and remake the script? I put my whole script bc I have no idea where this happens. Is there a better way to do the matchmaking? Maybe im doing this completely wrong.

local MESSAGE_SIZE_LIMIT = 100
local MESSAGE_COOLDOWN = 1
local DURATION = 60*60*24 -- How long the queue will last for

-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MemoryService = game:GetService("MemoryStoreService")
local MessagingService = game:GetService("MessagingService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local TextService = game:GetService("TextService")
local MarketplaceService = game:GetService("MarketplaceService")

-- Instances
local QueueStore = MemoryService:GetSortedMap("SessionQueue")

local Remotes = ReplicatedStorage.Remotes
local StartSessionRf = Remotes.StartSession
local StopSessionEvent = Remotes.StopSession
local SendMessageRf = Remotes.SendMessage
local ReceiveMessageEvent = Remotes.ReceiveMessage
local LoadAvatarEvent = Remotes.LoadAvatar
local SkipPlayerEvent = Remotes.SkipPlayer
local MatchChangedEvent = Remotes.MatchChanged

-- Constants
local SESSION_QUEUE_KEY = "ChatSessions"
local CONNECTION_TOPIC = "ConnectionFound"
local MESSAGE_PREFIX = "Session"  -- Prefix for message topics

-- Variables
local Connections = {} -- [Player] = MessagingService subscription
local Sessions = {}    -- [Player] = MatchedPlayerId
local MessageCooldowns = {} -- [Player] = last message time


-- Helper Functions

-- Gets the animation id
local function GetAnimationId(MarketplaceId)
	return MarketplaceService:GetProductInfo(MarketplaceId).ProductId
end

-- Filters text using Roblox text filtering.
local function FilterText(Text, Player)
	if not Text or Text == "" then return "" end
	local Success, Result = pcall(function()
		local Filtered = TextService:FilterStringAsync(Text, Player.UserId)
		return Filtered:GetNonChatStringForUserAsync(Player.UserId)
	end)
	if Success then
		return Result
	else
		warn("Error filtering text: " .. Result)
		return "##########"
	end
end

-- Gets the player in the server, their Id, and their match's Id.
local function GetPlayerAndMatch(Player1Id, Player2Id)
	for _, Plr in Players:GetPlayers() do
		if Plr.UserId == Player1Id or Plr.UserId == Player2Id then
			local MatchId = (Plr.UserId == Player1Id) and Player2Id or Player1Id
			return {Player = Plr, PlayerId = Plr.UserId, MatchId = MatchId}
		end
	end
	return nil
end

-- Updates the queue with the data.
local function UpdateQueue(QueueData)
	local Success, ErrorMsg = pcall(function()
		QueueStore:SetAsync(SESSION_QUEUE_KEY, QueueData, DURATION)
	end)
	if not Success then
		warn("Error updating queue: " .. tostring(ErrorMsg))
	end
end

-- Connects the player and their match.
local function ConnectPlayers(Player1Id, Player2Id)
	local MatchData = GetPlayerAndMatch(tonumber(Player1Id), tonumber(Player2Id))
	if not MatchData then print("Could not connect players: no match data found!") return end

	local Player = MatchData.Player
	Sessions[Player] = MatchData.MatchId
	
	-- May not be necessary but if a previous one fails for some reason this will help
	local Success, QueueData = pcall(function()
		return QueueStore:GetAsync(SESSION_QUEUE_KEY)
	end)
	if Success then
		QueueData[tostring(Player1Id)] = nil
		QueueData[tostring(Player2Id)] = nil
		UpdateQueue(QueueData)
	end

	Connections[Player] = MessagingService:SubscribeAsync(MESSAGE_PREFIX .. Player.UserId, function(Message)
		local FilteredMessage = FilterText(Message.Data, Player)
		ReceiveMessageEvent:FireClient(Player, FilteredMessage)
		print("MESSAGE RECEIVED")
	end)
	MatchChangedEvent:FireClient(Player, "Connected", MatchData.MatchId)
end


-- Main Functions

-- Adds player to queue or matches them with another player if it finds one.
local function OnStart(Player)
	local PlayerId = Player.UserId
	print("got player id")
	local Success, QueueData = pcall(function()
		return QueueStore:GetAsync(SESSION_QUEUE_KEY)
	end)

	if not Success then
		warn("Failed to get queue")
		return false
	end
		
	QueueData = QueueData or {}
	print("did this")
	-- Try to find a match
	local MatchedPlayerId
	for Id, _ in QueueData do
		MatchedPlayerId = Id
		QueueData[tostring(Id)] = nil
		break
	end
	print("everything good so far")
	if QueueData[tostring(PlayerId)] then
		QueueData[tostring(PlayerId)] = nil
	end
	print("hmmm")
	if MatchedPlayerId == tostring(PlayerId) then print("only found me") return false end
	
	if MatchedPlayerId then
		print("player found")
		UpdateQueue(QueueData)
		
		print("updated queue")
		
		local MatchedPlayers = {MatchedPlayerId, PlayerId, true}
		local EncodedData = HttpService:JSONEncode(MatchedPlayers)

		local SuccessPublish, Error = pcall(function()
			MessagingService:PublishAsync(CONNECTION_TOPIC, EncodedData)
		end)
		
		print("async published")
		
		if not SuccessPublish then
			warn("Error publishing match: " .. tostring(Error))
			return false
		end
	else
		print("no queue found")
		QueueData[tostring(PlayerId)] = PlayerId
		UpdateQueue(QueueData)
	end
	print("everything is good")
	return true
end

-- Stops the session.
local function StopSession(Player)
	-- Dosent seem possible but just to be safe
	if not Player then warn("Player not found ... How?") return end
	local PlayerId = Player.UserId

	local Success, QueueData = pcall(function()
		return QueueStore:GetAsync(SESSION_QUEUE_KEY)
	end)
	if Success and QueueData then
		QueueData[tostring(PlayerId)] = nil
		UpdateQueue(QueueData)
	else
		warn("There was an error while stopping!!!!")
	end

	-- Cleanup stuff
	if Connections[Player] then
		Connections[Player]:Disconnect()
		Connections[Player] = nil
	end
	if Sessions[Player] then
		local Data = {PlayerId, Sessions[Player], false}
		local EncodedData = HttpService:JSONEncode(Data)
		local Success, Error = pcall(function()
			MessagingService:PublishAsync(CONNECTION_TOPIC, EncodedData)
		end)
		if not Success then
			warn("Error while stopping session: " .. tostring(Error))
		end
		Sessions[Player] = nil
	end
end

-- Sends the message to the match.
local function SendMessage(Player, Message)
	-- Cooldown and size check
	if MessageCooldowns[Player] and tick() - MessageCooldowns[Player] < MESSAGE_COOLDOWN or #Message > MESSAGE_SIZE_LIMIT then
		return false
	end
	
	local MatchId = Sessions[Player]
	if not MatchId then warn("No match found!") return false end

	local FilteredMessage = FilterText(Message, Player)
	local Success, Error = pcall(function()
		MessagingService:PublishAsync(MESSAGE_PREFIX .. MatchId, FilteredMessage)
	end)
	if not Success then
		warn("Error sending message: " .. tostring(Error))
		return false
	end
	print("MESSAGE SENT!!!!!!!!!!!")
	MessageCooldowns[Player] = tick()
	return FilteredMessage
end

-- Loads the avatar on the dummy
local function LoadAvatar(Player, UserId)
	local PlayerGui = Player.PlayerGui
	local Avatars = PlayerGui.Avatars
	local YourDummy = Avatars.YourAvatar.WorldModel.Dummy
	local MatchDummy = Avatars.MatchedAvatar.WorldModel.Dummy
	
	local DummyToApplyThisTo = nil
	
	if not UserId then
		UserId = Player.UserId
		DummyToApplyThisTo = YourDummy
	else
		DummyToApplyThisTo = MatchDummy
	end
	
	local Animator = DummyToApplyThisTo.Humanoid.Animator
	for _, animation in Animator:GetPlayingAnimationTracks() do
		animation:Stop()
	end
	
	local Success, Result = pcall(function()
		return Players:GetHumanoidDescriptionFromUserId(UserId)
	end)

	if not Success then
		warn("Error while getting avatar description for " .. UserId .. ": " .. Result)
		return
	end

	local NewHumanoidDescription = Result
	DummyToApplyThisTo.Humanoid:ApplyDescription(NewHumanoidDescription)
	
	local Animation = Instance.new("Animation")
	Animation.AnimationId = "rbxassetid://10921259953"

	Animation.Parent = Animator
	Animator:LoadAnimation(Animation):Play()
end

-- Disconnects 2 players and starts a new session
local function DisconnectPlayers(Player)
	StopSession(Player)
	--[[
	if Connections[Player] then
		Connections[Player]:Disconnect()
		Connections[Player] = nil
	end
	if Sessions[Player] then
		Sessions[Player] = nil
		MatchChangedEvent:FireClient(Player, "Disconnected")
	end
	local Success, QueueData = pcall(function()
		return QueueStore:GetAsync(SESSION_QUEUE_KEY)
	end)
	QueueData[tostring(Player.UserId)] = nil
	UpdateQueue(QueueData)
	]]
	print("Starting onstart function")
	
	OnStart(Player)
end

-- Disconnects the players when one of them leaves
local function OnPlayerRemoving(Player)
	if Sessions[Player] then
		local Data = {Player.UserId, Sessions[Player], false}
		local EncodedData = HttpService:JSONEncode(Data)
		local Success, Error = pcall(function()
			MessagingService:PublishAsync(CONNECTION_TOPIC, EncodedData)
		end)
		
		local QueueSuccess, QueueData = pcall(function()
			return QueueStore:GetAsync(SESSION_QUEUE_KEY)
		end)
		if QueueSuccess then
			QueueData[tostring(Player.UserId)] = nil
			UpdateQueue(QueueData)
		end
		
		if not Success then
			warn("Error disconnecting player: " .. tostring(Error))
		end
	end
end

-- Skips the player
local function SkipPlayer(Player)
	print("skipping!")
	if not Sessions[Player] then return false end -- no session
	local Data = {Player.UserId, Sessions[Player], false}
	local EncodedData = HttpService:JSONEncode(Data)
	local Success, Error = pcall(function()
		MessagingService:PublishAsync(CONNECTION_TOPIC, EncodedData)
	end)
	print("things are success")
	if not Success then
		warn("Error skipping player: " .. tostring(Error))
		return false
	end
	print("before disconnect call in skip")
	DisconnectPlayers(Player)
	print("after disconnect call in skip")
	return true
end

-- Runtime
MessagingService:SubscribeAsync(CONNECTION_TOPIC, function(SessionData)
	local DecodedData = HttpService:JSONDecode(SessionData.Data)
	local MatchData = GetPlayerAndMatch(tonumber(DecodedData[1]), tonumber(DecodedData[2]))
	if not MatchData then return end 
	local PlayerInServer = MatchData.Player
	if not PlayerInServer then return end
	
	if not Sessions[PlayerInServer] and not Connections[PlayerInServer] then return end
	
	if DecodedData[3] == true then
		ConnectPlayers(DecodedData[1], DecodedData[2])
	else
		DisconnectPlayers(PlayerInServer)
	end
end)

SendMessageRf.OnServerInvoke = SendMessage
StartSessionRf.OnServerInvoke = OnStart
SkipPlayerEvent.OnServerInvoke = SkipPlayer
StopSessionEvent.OnServerEvent:Connect(StopSession)
LoadAvatarEvent.OnServerEvent:Connect(LoadAvatar)
Players.PlayerRemoving:Connect(OnPlayerRemoving)
1 Like

Hey! I’ve debugged your script and found the root causes of the bugs. No need to delete everything and start over – the core idea (queue + MessagingService for cross-server chat) is solid, but there are a few critical issues with races, inverted logic, and non-atomic queue ops. Here’s the full fixed script (drop-in replacement) + explanations.

Quick Bug Summary & Fixes:

  1. CONNECTION_TOPIC handler skips connects: Inverted condition – fixed with safe guards.

  2. Skip/stop double-publishes: Split cleanup from publish, no cascades.

  3. Queue races/ghosts: Switched to MemoryStoreQueue (atomic Dequeue/Put, TTL=5min auto-expires leavers).

  4. Text filter yields + wrong filtering: Sender filters for receiver (GetChatForUserAsync), use RemoteEvent (no timeout).

  5. Missing client notifies: Added “Disconnected” event.

Full Fixed Script (ServerScript in ServerScriptService):


local MESSAGE_SIZE_LIMIT = 100
local MESSAGE_COOLDOWN = 1
local QUEUE_TTL = 300  -- 5 minutes, auto-expires ghosts/leavers

-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MemoryService = game:GetService("MemoryStoreService")
local MessagingService = game:GetService("MessagingService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local TextService = game:GetService("TextService")
local MarketplaceService = game:GetService("MarketplaceService")

-- Instances
local QueueStore = MemoryService:GetQueue("ChatQueue")
local Remotes = ReplicatedStorage.Remotes
local StartSessionRf = Remotes.StartSession
local StopSessionEvent = Remotes.StopSession
local SendMessageRf = Remotes.SendMessage  -- Now treated as RemoteEvent
local ReceiveMessageEvent = Remotes.ReceiveMessage
local LoadAvatarEvent = Remotes.LoadAvatar
local SkipPlayerEvent = Remotes.SkipPlayer
local MatchChangedEvent = Remotes.MatchChanged

-- Constants
local CONNECTION_TOPIC = "ConnectionFound"
local MESSAGE_PREFIX = "Session"  -- Prefix for message topics
local QUEUE_KEY = "ChatQueue"

-- Variables
local Connections = {} -- [Player] = MessagingService subscription
local Sessions = {}    -- [Player] = MatchedPlayerId
local MessageCooldowns = {} -- [Player] = last message time

-- Helper Functions

-- Gets the animation id (unused in LoadAvatar, but kept)
local function GetAnimationId(MarketplaceId)
	return MarketplaceService:GetProductInfo(MarketplaceId).ProductId
end

-- Gets the player in the server, their Id, and their match's Id.
local function GetPlayerAndMatch(Player1Id, Player2Id)
	for _, Plr in Players:GetPlayers() do
		if Plr.UserId == Player1Id or Plr.UserId == Player2Id then
			local MatchId = (Plr.UserId == Player1Id) and Player2Id or Player1Id
			return {Player = Plr, PlayerId = Plr.UserId, MatchId = MatchId}
		end
	end
	return nil
end

-- Connects the player and their match.
local function ConnectPlayers(Player1Id, Player2Id)
	local MatchData = GetPlayerAndMatch(tonumber(Player1Id), tonumber(Player2Id))
	if not MatchData then 
		warn("Could not connect players: no match data found!")
		return 
	end

	local Player = MatchData.Player
	if Sessions[Player] then return end  -- Already connected, ignore

	Sessions[Player] = MatchData.MatchId
	
	Connections[Player] = MessagingService:SubscribeAsync(MESSAGE_PREFIX .. Player.UserId, function(Message)
		ReceiveMessageEvent:FireClient(Player, Message.Data)
		print("MESSAGE RECEIVED")
	end)
	MatchChangedEvent:FireClient(Player, "Connected", MatchData.MatchId)
end

-- Cleans up local session state only (no publish!)
local function CleanupSession(Player)
	if Connections[Player] then
		Connections[Player]:Disconnect()
		Connections[Player] = nil
	end
	Sessions[Player] = nil
end

-- Main Functions

-- Adds player to queue or matches them with another player if it finds one. (Atomic with MemoryStoreQueue)
local function OnStart(Player)
	local playerStr = tostring(Player.UserId)
	
	-- Try to dequeue a match atomically
	local dequeued = QueueStore:DequeueAsync(1)
	local matchedStr = dequeued[1]
	if matchedStr then
		local matchId = tonumber(matchedStr)
		if matchId and matchId ~= Player.UserId then  -- Safety check
			local data = {matchId, Player.UserId, true}
			pcall(function()
				MessagingService:PublishAsync(CONNECTION_TOPIC, HttpService:JSONEncode(data))
			end)
			return true
		end
	end
	
	-- No match: enqueue self
	pcall(function()
		QueueStore:PutAsync(playerStr, QUEUE_TTL)
	end)
	return true
end

-- Manual stop (no requeue)
local function OnStopSession(Player)
	local matchId = Sessions[Player]
	if matchId then
		local data = {Player.UserId, matchId, false}
		pcall(function()
			MessagingService:PublishAsync(CONNECTION_TOPIC, HttpService:JSONEncode(data))
		end)
	end
	CleanupSession(Player)
end

-- Sends the message to the match. (Filters for receiver, no yield in Invoke)
local function OnSendMessage(Player, Message)
	if MessageCooldowns[Player] and tick() - MessageCooldowns[Player] < MESSAGE_COOLDOWN or #Message > MESSAGE_SIZE_LIMIT then
		return
	end
	
	local matchId = Sessions[Player]
	if not matchId then return end

	local success, safeMsg = pcall(function()
		local filterResult = TextService:FilterStringAsync(Message, Player.UserId)
		return filterResult:GetChatForUserAsync(matchId)  -- Safe for match
	end)
	
	if success then
		pcall(function()
			MessagingService:PublishAsync(MESSAGE_PREFIX .. matchId, safeMsg)
		end)
		print("MESSAGE SENT!!!!!!!!!!!")
		MessageCooldowns[Player] = tick()
	end
end

-- Loads the avatar on the dummy
local function LoadAvatar(Player, UserId)
	local PlayerGui = Player.PlayerGui
	local Avatars = PlayerGui.Avatars
	local YourDummy = Avatars.YourAvatar.WorldModel.Dummy
	local MatchDummy = Avatars.MatchedAvatar.WorldModel.Dummy
	
	local DummyToApplyThisTo = nil
	
	if not UserId then
		UserId = Player.UserId
		DummyToApplyThisTo = YourDummy
	else
		DummyToApplyThisTo = MatchDummy
	end
	
	local Animator = DummyToApplyThisTo.Humanoid.Animator
	for _, animation in Animator:GetPlayingAnimationTracks() do
		animation:Stop()
	end
	
	local Success, Result = pcall(function()
		return Players:GetHumanoidDescriptionFromUserId(UserId)
	end)

	if not Success then
		warn("Error while getting avatar description for " .. UserId .. ": " .. Result)
		return
	end

	local NewHumanoidDescription = Result
	DummyToApplyThisTo.Humanoid:ApplyDescription(NewHumanoidDescription)
	
	local Animation = Instance.new("Animation")
	Animation.AnimationId = "rbxassetid://10921259953"

	Animation.Parent = Animator
	Animator:LoadAnimation(Animation):Play()
end

-- Disconnects (called on "false" receive): notify client, cleanup, requeue
local function DisconnectPlayers(Player)
	MatchChangedEvent:FireClient(Player, "Disconnected")
	CleanupSession(Player)
	OnStart(Player)
end

-- Disconnects the players when one of them leaves
local function OnPlayerRemoving(Player)
	local matchId = Sessions[Player]
	if matchId then
		local data = {Player.UserId, matchId, false}
		pcall(function()
			MessagingService:PublishAsync(CONNECTION_TOPIC, HttpService:JSONEncode(data))
		end)
	end
	CleanupSession(Player)
	-- Queue auto-expires via TTL, no manual clear needed
end

-- Skips the player: publish disconnect, cleanup, requeue
local function SkipPlayer(Player)
	local matchId = Sessions[Player]
	if not matchId then return false end
	
	local data = {Player.UserId, matchId, false}
	pcall(function()
		MessagingService:PublishAsync(CONNECTION_TOPIC, HttpService:JSONEncode(data))
	end)
	
	CleanupSession(Player)
	OnStart(Player)
	return true
end

-- Runtime
MessagingService:SubscribeAsync(CONNECTION_TOPIC, function(SessionData)
	local DecodedData = HttpService:JSONDecode(SessionData.Data)
	local p1id, p2id, isConnect = tonumber(DecodedData[1]), tonumber(DecodedData[2]), DecodedData[3]
	if not (p1id and p2id) then return end

	local MatchData = GetPlayerAndMatch(p1id, p2id)
	if not MatchData then return end
	local player = MatchData.Player

	if isConnect then
		if Sessions[player] then return end  -- Already connected, ignore
		ConnectPlayers(p1id, p2id)
	else
		if not Sessions[player] then return end  -- Not connected, ignore
		DisconnectPlayers(player)
	end
end)

-- Remotes (SendMessage now OnServerEvent, not Invoke)
SendMessageRf.OnServerEvent:Connect(OnSendMessage)
StartSessionRf.OnServerInvoke = OnStart
SkipPlayerEvent.OnServerInvoke = SkipPlayer
StopSessionEvent.OnServerEvent:Connect(OnStopSession)
LoadAvatarEvent.OnServerEvent:Connect(LoadAvatar)
Players.PlayerRemoving:Connect(OnPlayerRemoving)

And please tell me you don’t want to make a game line Tinder.

Dw, im not making Tinder. Im making like a phone players can use in the game. Also, DequeueAsync dosent exist and GetChatForUserAsync should not be used as it is deprecated.

1 Like