OOP Server Creator And Tracker Module

This is a module which you can use to store and keep track of active servers

Code (Paste this into a ModuleScript):

local Players = game:GetService("Players")
local TeleportService = game:GetService("TeleportService")
local MemStoreService = game:GetService("MemoryStoreService")
local MessagingService = game:GetService("MessagingService")

local SERVER_REGISTERED_EXPIRATION = 432_000 -- 5 days

local ServerInfoStore = MemStoreService:GetSortedMap("_ReservedServerInformation")
	
local Server = {}
Server.__index = Server

function Server:MessageAsync(topic: string, ...)
	local success = pcall(MessagingService.PublishAsync, MessagingService, self.PrivateServerId..topic, {...})
	return success
end

function Server:BindToMessageAsync(topic: string, fn: (sentTime: number, ...) -> ()): RBXScriptConnection
	local c = MessagingService:SubscribeAsync(self.PrivateServerId..topic, function(data)
		fn(data.Sent, unpack(data.Data))
	end)
	table.insert(self._bound, c)
	return c
end

function Server:TeleportAsync(players: {Player}): (boolean, TeleportAsyncResult | string)
	local success, why = pcall(TeleportService.TeleportAsync, TeleportService, self.PlaceId, players, self._options)
	return success, why
end

--! Should only be called inside <code>DataModel:BindToClose(callback) </code> callback !
function Server:CloseAsync(reason: string?)
	self:MessageAsync("Closing")
end

export type Server = typeof(Server) & {
	UserIds: {number},
	PrivateServerId: string,
	PlaceId: number,
	CreatedTime: number,
	IsClosed: boolean,

	OnPlayerJoined: ((joinTimestamp: number, userId: number) -> ())?,
	OnPlayerLeft: ((leftTimestamp: number, userId: number) -> ())?,
	OnClose: ((closeTimestamp: number, reason: string?) -> ())?
}

local function server_new(teleportToOptions, placeId, userIds, privateServerId, createdTime): Server
	local server = setmetatable({
		_options = teleportToOptions,
		_bound = {},
		
		UserIds = userIds,
		PrivateServerId = privateServerId,
		PlaceId = placeId,
		CreatedTime = createdTime,
		IsClosed = false
	}, Server)
	local onJoin = server:BindToMessageAsync("PlayerJoined", function(sent, userId)
		local index = #server.UserIds + 1
		server.UserIds[index] = userId
		if server.OnPlayerJoined then
			server.OnPlayerJoined(sent, userId)
		end
	end)
	local onLeft = server:BindToMessageAsync("PlayerLeft", function(sent, userId)
		local index = table.find(server.UserIds, userId)
		if not index then return end
		server.UserIds[index] = nil
		if server.OnPlayerLeft then
			server.OnPlayerLeft(sent, userId)
		end
	end)
	local onClose
	onClose = server:BindToMessageAsync("Closing", function(sent, reason)
		onClose:Disconnect()
		onJoin:Disconnect()
		onLeft:Disconnect()
		teleportToOptions:Destroy()
		for _, c in server._bound do
			if not c.Connected then continue end
			c:Disconnect()
		end
		table.clear(server._bound)
		if server.OnClose then
			server.OnClose(sent, reason)
		end
		ServerInfoStore:RemoveAsync(server.PrivateServerId)
		setmetatable(server, nil)
		table.clear(server)
		server.IsClosed = true
	end) 
	return server
end

local function create(placeId: number, players: {Player}): (boolean, Server?)
	local createdTime = DateTime.now().UnixTimestampMillis
	local userIds = {}
	for _, player in players do
		 if not player then continue end
		table.insert(userIds, player.UserId)
	end
	local options = Instance.new("TeleportOptions")
	options.ShouldReserveServer = true

	local success, result = pcall(TeleportService.TeleportAsync, TeleportService, placeId, players, options)
	if not success then 
		warn(`Server creation for place id {placeId} failed: {result}`)
		return false, nil
	end
	ServerInfoStore:SetAsync(result.PrivateServerId, {
		userIds = userIds,
		placeId = placeId,
		accessCode = result.ReservedServerAccessCode,
	}, SERVER_REGISTERED_EXPIRATION, createdTime)
	local teleportToOptions = Instance.new("TeleportOptions")
	teleportToOptions.ReservedServerAccessCode = result.ReservedServerAccessCode 
	
	return true, server_new(teleportToOptions, placeId, userIds, result.PrivateServerId, createdTime)
end

local function retrieve(sortDirection: Enum.SortDirection, minCreatedTimeMillis: number?, maxCreatedTimeMillis: number?): {{any}}
	local lowerBound = if minCreatedTimeMillis then {sortKey = minCreatedTimeMillis} else nil
	local upperBound = if maxCreatedTimeMillis then {sortKey = maxCreatedTimeMillis} else nil
	
	local servers = {}
	while true do
		local entries = ServerInfoStore:GetRangeAsync(sortDirection, 100, lowerBound, upperBound)
		for _, pair in entries do
			local createdTime = pair.sortKey
			local privateServerId = pair.key
			local info = pair.value
			local accessCode = info.accessCode
			local options = Instance.new("TeleportOptions")
			options.ReservedServerAccessCode = accessCode
			table.insert(servers, {options, info.placeId, info.userIds, privateServerId, createdTime})
		end
		if #entries < 100 then break end
		local last = entries[#entries]
		lowerBound = {key = last.key, sortKey = last.sortKey}
	end
	return servers
end

local runningServerInstance: Server?
if game.PrivateServerId ~= "" then
	local info, createdTime = ServerInfoStore:GetAsync(game.PrivateServerId)
	if not info or not createdTime then
		error("Exception: Info or CreatedTime not found for running server (Maybe server was not created with this module?)")
	end
	local options = Instance.new("TeleportOptions")
	options.ReservedServerAccessCode = info.accessCode
	runningServerInstance = setmetatable({
		_options = options,
		_bound = {},
		
		UserIds = info.userIds,
		PrivateServerId = game.PrivateServerId,
		PlaceId = game.PlaceId,
		CreatedTime = createdTime,
		IsClosed = false
	}, Server)
	Players.PlayerAdded:Connect(function(player)
		local index = #runningServerInstance.UserIds + 1
		runningServerInstance.UserIds[index] = player.UserId
		if runningServerInstance.OnPlayerJoined then
			runningServerInstance.OnPlayerJoined(player.UserId)
		end
		runningServerInstance:MessageAsync("PlayerJoined", player.UserId)
		ServerInfoStore:UpdateAsync(game.PrivateServerId, function(value, sortKey)
			value.userIds[index] = player.UserId
			return value, sortKey
		end, SERVER_REGISTERED_EXPIRATION - ((DateTime.now().UnixTimestampMillis - createdTime) / 1000))
	end)
	Players.PlayerRemoving:Connect(function(player)
		local index = table.find(runningServerInstance.UserIds, player.UserId)
		if not index then return end
		table.remove(runningServerInstance.UserIds, index)
		if runningServerInstance.OnPlayerLeft then
			runningServerInstance.OnPlayerLeft(player.UserId)
		end
		runningServerInstance:MessageAsync("PlayerLeft", player.UserId)
		ServerInfoStore:UpdateAsync(game.PrivateServerId, function(value, sortKey)
			table.remove(value.userIds, index)
			return value, sortKey
		end, SERVER_REGISTERED_EXPIRATION - ((DateTime.now().UnixTimestampMillis - createdTime) / 1000)) 
	end)
end

return {
	create = create,
	retrieve = retrieve,
	running = runningServerInstance,

	fromInfo = server_new,
	ServerInfoStore = ServerInfoStore
}

Usage:

local Server = require(path.to.this.module)

--....
-- Maybe this happens after a certain time has passed after waiting in a lobby

local success, server = Server.create(123456, players)
if not success then return end

-- Later, when another lobby tries to join the same game maybe

server:TeleportAsync(players)
-- To get all currently running Servers
local Server = require(path.to.this.module)

local serverInfo = Server.retrieve(Enum.SortDirection.Descending)

-- Have to instantiate a new Server because .retrieve only gets the informations
local firstServer = Server.fromInfo(unpack(serverInfo[1]))
-- Maybe you want the server to do some event
firstServer:MessageAsync("DoEvent", 123)
-- Maybe you also wanna see when a player is added to the server
function firstServer.OnPlayerJoined(userId)
	print(`Player {userId} has successfully joined server {firstServer.PrivateServerId}!`)
end
-- Now lets say you wanna recieve messages from a reserved server
local Server = require(path.to.this.module)
local running = Server.running

running:BindToMessageAsync("DoEvent", ...)
	print("Done event: ", ...)
end)

I wrote this bored out of my mind while at school so uh don’t expect it to work perfectly xd Just wanted to share something I thought would be useful

Let me know down in the replies if you encounter any issues

13 Likes