Project Sekai Matchmaking

What is this?
Hello there, this is my matchmaking code to replicate how Project Sekai does their matchmaking. This is useful for making games where a game with multiple players have little to no data being sent (like a rhythm game, only points and players are stored).

How does Project Sekai do their matchmaking? What data is sent?
When a player joins a multiplayer match, the player either makes a room or joins one. Stickers (emotes replicated to all clients) and votes (for what song to play) can be sent as data. The code has been made so either can be added easily.

How does this code work?

  1. Requests
    Requests are sent from a client. The client can be the host itself. When a client sends a message, it has an identifier attached to it from the AddIdentifier function. The identifier shows what client sent the message. The recipient shows where it should go to. For example, stickers can be sent to individual (except the host) or all players by using the recipient function. It could go to the host which then replicates it to all players. Scores can be sent using requests too.

  2. Events
    Events can only be sent by the server. It just sends an event to all clients or just 1 client.

Important note: Clients are on the behalf of the server. The host is not really a host because it can host itself, more like it is a primary client that happens to respond to others. That’s why the IsHost identifier tag can be set to false, even if the UserId is the UserId of the host. The host will treat it as another request and will not treat it like a host response if IsHost is set to false.

Now, here is the code for people who want to modify it for their own use or just want to look at it for fun:

Module in Libraries.WebSocketCreator

local WebSocketCreator = {}

local HttpService = game:GetService("HttpService")

local MessagingService = game:GetService("MessagingService")

function WebSocketCreator.Create(Name)
	local WebSocket = {}
	
	function WebSocket.GetReciever()
		local Reciever = {}
		
		Reciever.CallbackFunctions = {}
		
		function Reciever.Initialize(self, Functions)
			self.CallbackFunctions = Functions
			MessagingService:SubscribeAsync(Name, function(Message)
				for _, Function in pairs(self.CallbackFunctions) do
					local DataTable = HttpService:JSONDecode(Message.Data)
					Function(DataTable)
				end
			end)
		end
		
		function Reciever.GetCallbackFunctions(self)
			return self.CallbackFunctions
		end
		
		function Reciever.SetCallbackFunctions(self, Functions)
			self.CallbackFunctions = Functions
		end
		
		return Reciever
	end
	
	function WebSocket.GetSender()
		local Sender = {}
		
		function Sender.Initialize(self)
			--Sender Initialization Is Not Required It Is Just There To Prevent Confusion
		end
		
		function Sender.SendDataTable(self, Data)
			local Serialized = HttpService:JSONEncode(Data)
			while true do
				local A, B = pcall(function()
					MessagingService:PublishAsync(Name, Serialized)
				end)
				if A == true then
					print("Request sent.")
					break
				else
					print("Request failed. Retrying...")
				end
				task.wait(1)
			end
		end
		
		return Sender
	end
	
	return WebSocket
end

return WebSocketCreator

Matchmaking Server Runner

local WebSocketCreator = require(game:GetService("ServerScriptService").Libraries.WebSocketCreator)

function AddIdentifier(Identification, Target, IsHost, OriginalData)
	local NewData = {}
	if IsHost then
		NewData = {
			Identity = Identification,
			Recipient = Target,
			IsHost = IsHost,
			Response = OriginalData,
		}
	else
		NewData = {
			Identity = Identification,
			Recipient = Target,
			IsHost = IsHost,
			Request = OriginalData,
		}
	end
	return NewData
end

function MatchNewReserve(Amount, UserIDRequester)
	--Matchmaking
	--Initialization
	local WebSocket = WebSocketCreator.Create(tostring(UserIDRequester))

	local Sender = WebSocket.GetSender()

	local Reciever = WebSocket.GetReciever()
	
	function CurrentlyNotRecieving(Data)
		if Data.Identity == UserIDRequester then
			return
		end
		if not (Data.Recipient == UserIDRequester) then
			return
		end
		Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
			Type = "Soft-Failure",
			Response = "Currently not recieving."
		}))
	end

	Reciever:Initialize({CurrentlyNotRecieving})
	
	--Wait for [Amount] of players
	local Players = {UserIDRequester}
	
	function RecievePlayer(Data)
		if Data.Identity == UserIDRequester then
			return
		end
		if not (Data.Recipient == UserIDRequester) then
			return
		end
		if not (Data.Request.Type == "JoinRequest") then
			return
		end
		if #Players >= Amount then
			Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
				Type = "Failure",
				Response = "Too many players!"
			}))
			return
		end
		local AlreadyInGame = table.find(Players, Data.Identity, 1)
		if not AlreadyInGame then
			table.insert(Players, Data.Identity)
			Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
				Type = "Success",
				Response = "Match joined."
			}))
			--Join Signal
			Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
				Type = "Event",
				Response = {
					Event = "Join",
					Data = {
						Identity = tostring(Data.Identity)
					}
				}
			}))
			return
		else
			Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
				Type = "Failure",
				Response = "Already in game!"
			}))
			return
		end
	end
	
	Reciever:SetCallbackFunctions({RecievePlayer, print})
	
	repeat task.wait() until #Players >= Amount
	
	Reciever:SetCallbackFunctions({CurrentlyNotRecieving, print})
	
	local PlayerCounter = 0
	
	local Temp = {}
	
	for _, UserID in pairs(Players) do
		PlayerCounter = PlayerCounter + 1
		if PlayerCounter <= Amount then
			table.insert(Temp, UserID)
		else
			Sender:SendDataTable(AddIdentifier(UserIDRequester, UserID, true, {
				Type = "Failure",
				Response = "Too many players!"
			}))
		end
	end
	
	Players = Temp
	
	print("Reserved Players: "..tostring(#Players))
	print(Players)
	
	task.wait(3)
	
	Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
		Type = "Event",
		Response = {
			Event = "AllReady",
			Data = {}
		}
	}))
	
	task.wait(3)
		
	Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
		Type = "Event",
		Response = {
			Event = "VoteStart",
			Data = {}
		}
	}))
	
	--Voting
	
	local Votes = {}
	
	function RecieveVote(Data)
		if not (Data.Recipient == UserIDRequester) then
			return
		end
		if Data.IsHost == true then
			return
		end
		if not (Data.Request.Type == "Vote") then
			return
		end
		Votes[Data.Identity] = Data.Request.Data.Vote
		Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
			Type = "Success",
			Response = "Vote recieved."
		}))
	end
	
	local TimerVote = 20
	
	Reciever:SetCallbackFunctions({RecieveVote, print})
	
	repeat task.wait(1) TimerVote = TimerVote - 1 
		
	until ((#Votes == Amount) or (TimerVote <= 0))
	
	Reciever:SetCallbackFunctions({CurrentlyNotRecieving, print})
	
	print("Voting completed. Votes: "..tostring(#Votes))
	print(Votes)
	
	Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
		Type = "Event",
		Response = {
			Event = "VoteEnded",
			Data = {
				Winner = Votes[Players[math.random(1,Amount)]]
			}
		}
	}))
	
	-- NonVisualReadyUp
		
	Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
		Type = "Event",
		Response = {
			Event = "NonVisualReady",
			Data = {}
		}
	}))
	
	local ReadyList = {}
	
	function RecieveReadySignalNonVisual(Data)
		if Data.IsHost == true then
			return
		end
		if not (Data.Recipient == UserIDRequester) then
			return
		end
		if not (Data.Request.Type == "NonVisualReadyUp") then
			return
		end
		ReadyList[Data.Identity] = true
		Sender:SendDataTable(AddIdentifier(UserIDRequester, Data.Identity, true, {
			Type = "Success",
			Response = "Ready signal recieved."
		}))
	end
	
	Reciever:SetCallbackFunctions({RecieveReadySignalNonVisual, print})

	local TimeForNonVisualReady = 20

	repeat task.wait(1) TimeForNonVisualReady = TimeForNonVisualReady - 1 until ((TimeForNonVisualReady <= 0) or (#ReadyList == Amount))
	
	if not (#ReadyList == Amount) then
		Sender:SendDataTable(AddIdentifier(UserIDRequester, -100, true, {
			Type = "Event",
			Response = {
				Event = "DisbandedRoom",
				Data = {}
			}
		}))
		print("Room disbanded.")
		return
	end
	
	print("Minigame started.")
end

MatchNewReserve(10, 1)

Test Matchmaking Script

local WebSocketCreator = require(game:GetService("ServerScriptService").Libraries.WebSocketCreator)

task.wait(5)

function AddIdentifier(Identification, Target, IsHost, OriginalData)
	local NewData = {}
	if IsHost then
		NewData = {
			Identity = Identification,
			Recipient = Target,
			IsHost = IsHost,
			Response = OriginalData,
		}
	else
		NewData = {
			Identity = Identification,
			Recipient = Target,
			IsHost = IsHost,
			Request = OriginalData,
		}
	end
	return NewData
end

local WebSocket = WebSocketCreator.Create(tostring(1))

function CreateClient(ID)
	local ClientReturned = {}
	local TestSend = WebSocket.GetSender()

	local TestRecieve = WebSocket.GetReciever()
	TestRecieve:Initialize({})
	
	ClientReturned.SendRequest = function(Type1, Data1, WaitTime)
		local Response = nil
		local TimeResponse = WaitTime*10
		TestRecieve:SetCallbackFunctions({
			function(Data)
				if (Data.IsHost == false) then
					return
				end
				if (Data.Response.Type == "Failure") or (Data.Response.Type == "Success") then
					Response = Data.Response
				end
			end,
		})
		TestSend:SendDataTable(AddIdentifier(ID, 1, false, {
			Type = Type1,
			Data = Data1
		}))
		repeat 
			task.wait(0.1) 
			TimeResponse = TimeResponse - 1
		until (TimeResponse <= 0) or Response
		return Response
	end
	ClientReturned.ConnectEvent = function(Event, Function)
		TestRecieve:SetCallbackFunctions({
			function(Data)
				if (Data.IsHost == false) then
					return
				end
				if not ((Data.Recipient == ID) or (Data.Recipient == -100))then
					return
				end
				if (Data.Response.Type == "Event") then
					if Data.Response.Response.Event == Event then
						Function(Data.Response.Response.Data)
					end
				end
			end,
		})
	end
	return ClientReturned
end

local Clients = {
	CreateClient(1),
	CreateClient(2),
	CreateClient(3),
	CreateClient(4),
	CreateClient(5),
	CreateClient(6),
	CreateClient(7),
	CreateClient(8),
	CreateClient(9),
	CreateClient(10),
}
task.wait(5)
for i, Client in pairs(Clients) do
	spawn(function()
		Client.SendRequest("JoinRequest", {}, 10)
	end)
	task.wait(0.1)
end
Clients[1].ConnectEvent("VoteStart", function()
	task.wait(3)
	local Votes = {"A", "B", "C", "D", "E"}
	for i, Client in pairs(Clients) do
		spawn(function()
			Client.SendRequest("Vote", {Vote = Votes[math.random(1,5)]}, 10)
		end)
		task.wait(0.1)
	end
	Clients[1].ConnectEvent("NonVisualReady", function()
		task.wait(3)
		for i, Client in pairs(Clients) do
			spawn(function()
				Client.SendRequest("NonVisualReadyUp", {}, 10)
			end)
			task.wait(0.1)
		end
	end)
end)
9 Likes