Global Leaderboard using regular datastore

What I want to achieve is basically making sure this is a secure way of handling a leaderboard. I want to know if multiple servers running this function will cause trouble!

This isn’t my first time making a global leaderboard but this time I’m doing it with one key on a normal datastore, the way it gets updated is by taking previous data and comparing it live every few minutes to see if any of the previous top ten players need to be replaced. It works well for me but in theory I can see issues happening if multiple servers call getasync or setasync for the key at the same time.

Here is the function, I gets called once when the first player gets added to a server then again every 200 seconds.

local function UpdateLeaderboard(Signal, key, tab)
	local ColorByRank = Module.ColorByRank
	local NewValues = {
		["Player1"] = {
			Name = "None",
			Level = "None"
		},
		["Player2"] = {
			Name = "None",
			Level = "None"
		},
		["Player3"] = {
			Name = "None",
			Level = "None"
		},
		["Player4"] = {
			Name = "None",
			Level = "None"
		},
		["Player5"] = {
			Name = "None",
			Level = "None"
		},
		["Player6"] = {
			Name = "None",
			Level = "None"
		},
		["Player7"] = {
			Name = "None",
			Level = "None"
		},
		["Player8"] = {
			Name = "None",
			Level = "None"
		},
		["Player9"] = {
			Name = "None",
			Level = "None"
		},
		["Player10"] = {
			Name = "None",
			Level = "None"
		}
	}
	if Signal == "New" then
		local NewTopTen = {}
		for i, v in pairs(game.Players:GetPlayers()) do
			v:WaitForChild("leaderstats")
			v.leaderstats:WaitForChild("Level")
			if v.leaderstats.Level.Value < 1 then
				v.leaderstats.Level.Changed:Wait()
			else
				-- player is ready to be inserted into table
			end
			table.insert(NewTopTen, v)
		end
		if #NewTopTen > 10 then
			for i = 11, ((#NewTopTen - 10) + 10) do
				table.remove(NewTopTen, i)
			end
		end
		table.sort(NewTopTen, function(p, p2)
			return p.leaderstats.Level.Value > p2.leadertats.Level.Value
		end)
		for x = 1, 10 do
			if NewTopTen[x] ~= nil then
				NewValues["Player"..x].Name = NewTopTen[x].UserId
				NewValues["Player"..x].Level = NewTopTen[x].leaderstats.Level.Value
			end
		end
		local Leaderboard = workspace.Lobby.Top10Players.LB.SurfaceGui
		for z = 1, 10 do
			if NewValues["Player"..z].Name == "None" and NewValues["Player"..z].Level == "None" then
				Leaderboard[tostring(z)].Player.Text = "(To Be Decided)"
				Leaderboard[tostring(z)].Level.Text = "Lvl - -"
				Leaderboard[tostring(z)].Level.TextColor3 = ColorByRank.Bronze
				Leaderboard[tostring(z)].Thumbnail.Image = ""
			else
				Leaderboard[tostring(z)].Player.Text = game.Players:GetNameFromUserIdAsync(NewValues["Player"..z].Name)
				Leaderboard[tostring(z)].Level.Text = "Lvl "..NewValues["Player"..z].Level
				Leaderboard[tostring(z)].Level.TextColor3 = Module.GetRankColor(NewValues["Player"..z].Level)
				Leaderboard[tostring(z)].Thumbnail.Image = game.Players:GetUserThumbnailAsync(NewValues["Player"..z].Name, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size420x420)
				-- Leaves info for applicable users
			end
		end
		print(NewValues.Player1.Level)
		LeaderBoardDS:SetAsync(key, NewValues)
	elseif Signal == "Update" then
		local NewTopTen = {}
		local OldTab = {tab.Player1, tab.Player2, tab.Player3, tab.Player4, tab.Player5, tab.Player6, tab.Player7, tab.Player8, tab.Player9, tab.Player10}
		for x, PreviousPlayer in pairs(OldTab) do
			if PreviousPlayer.Level ~= "None" then 
				local PlayerModel = Instance.new("Model")
				PlayerModel.Name = game.Players:GetNameFromUserIdAsync(PreviousPlayer.Name)
				PlayerModel.Parent = game.ServerStorage.LeadeboardStation
				local UserId = Instance.new("StringValue")
				UserId.Name = "UserId"
				UserId.Value = PreviousPlayer.Name
				UserId.Parent = PlayerModel
				local leaderstats = Instance.new("Folder")
				leaderstats.Name = "leaderstats"
				leaderstats.Parent = PlayerModel
				local Level = Instance.new("IntValue")
				Level.Name = "Level"
				Level.Value = PreviousPlayer.Level
				Level.Parent = leaderstats
				table.insert(NewTopTen, PlayerModel)
			end
		end 
		table.sort(NewTopTen, function(p, p1)
			return p.leaderstats.Level.Value > p1.leaderstats.Level.Value
		end)
		for i, v in pairs(game.Players:GetPlayers()) do
			local leaderstats = v:WaitForChild("leaderstats")
			local Level = leaderstats:WaitForChild("Level")
			if not game.ServerStorage.LeadeboardStation:FindFirstChild(v.Name) then
				if Level.Value < 1 then
					Level.Changed:Wait()
				else
					-- ready to be inseted to table
				end
				table.insert(NewTopTen, v)
			end
		end
		table.sort(NewTopTen, function(p, p1)
			return p.leaderstats.Level.Value > p1.leaderstats.Level.Value
		end)
		if #NewTopTen > 10 then
			for i = 11, ((#NewTopTen - 10) + 10) do
				table.remove(NewTopTen, i)
			end
		end
		for x = 1, 10 do
			if NewTopTen[x] ~= nil then
				if NewTopTen[x]:IsA("Player") then
					NewValues["Player"..x].Name = NewTopTen[x].UserId
				elseif NewTopTen[x]:IsA("Model") then
					NewValues["Player"..x].Name = NewTopTen[x].UserId.Value
				end
				NewValues["Player"..x].Level = NewTopTen[x].leaderstats.Level.Value
			end
		end
		local Leaderboard = workspace.Lobby.Top10Players.LB.SurfaceGui
		for z = 1, 10 do
			if NewValues["Player"..z].Name == "None" and NewValues["Player"..z].Level == "None" then
				Leaderboard[tostring(z)].Player.Text = "(To Be Decided)"
				Leaderboard[tostring(z)].Level.Text = "Lvl - -"
				Leaderboard[tostring(z)].Level.TextColor3 = ColorByRank.Bronze
				Leaderboard[tostring(z)].Thumbnail.Image = ""
			else
				Leaderboard[tostring(z)].Player.Text = game.Players:GetNameFromUserIdAsync(NewValues["Player"..z].Name)
				Leaderboard[tostring(z)].Level.Text = "Lvl "..NewValues["Player"..z].Level
				Leaderboard[tostring(z)].Level.TextColor3 = Module.GetRankColor(NewValues["Player"..z].Level)
				Leaderboard[tostring(z)].Thumbnail.Image = game.Players:GetUserThumbnailAsync(NewValues["Player"..z].Name, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size420x420)
				-- Leaves info for applicable users
			end
		end
		LeaderBoardDS:UpdateAsync(key, function()
			return NewValues
		end)
		game.ServerStorage.LeadeboardStation:ClearAllChildren()
	end
end

Then this is the loop after the first call, new is the signal that only gets called when there is no previous data on the key, then it’s update for every call after.

while wait(200) do
	local LBData = nil
	local Loaded, Fail = pcall(function()
		 LBData = LeaderBoardDS:GetAsync(LeaderboardKey)
	end)
	if Loaded then
		if LBData ~= nil then
			UpdateLeaderboard("Update", LeaderboardKey, LBData)
		else
			UpdateLeaderboard("New", LeaderboardKey, LBData)
		end
	else
		warn("There was an issue trying to receive data for the global leaderboard, trying again next call")
	end
end

–This is called when a player joins
local function PlayerJoined(Player)
local Leaderboard = workspace.Lobby.Top10Players.LB.SurfaceGui
for x = 1, 10 do
Leaderboard[tostring(x)].Player.Text = “(To Be Decided)”
Leaderboard[tostring(x)].Level.Text = “Lvl - -”
Leaderboard[tostring(x)].Level.TextColor3 = ColorByRank.Bronze
Leaderboard[tostring(x)].Thumbnail.Image = “”
end
end

game.Players.PlayerAdded:Connect(PlayerJoined)
game.Players.PlayerRemoving:Connect(PlayerJoined)

game.Players.PlayerAdded:Connect(function(Player)
NewPlayer(Player)
end)

This is setup with one key, I wanted to know if my approach is something that needs to be changed.

I think you can make the leaderboard update independent of the system that keeps track of the leaderboard. If you just want a leaderboard, Roblox should have one built-in.
If you want to create your own, I think you should take a look at Roblox’s leaderboard example and see how they accomplish this there.

1 Like

The leaderboard actually works, I understood how to make it, the thing is is that it’s not for the server it’s for everyone who has played the game. The key checks every few minutes for new competitors with the previous 10 on the key to swap and replace. This has worked great but I just wanted to know if multiple servers can make changes to the key without causing issues with the datastore. I have it in a protected state where if get async fails it won’t mess with the leaderboard and try again next time it is scheduled to.

I learned that ordered datastores take away the pain of ordering data myself, I realize it is way better and makes sense to use. Here’s the code if you want to know how I did it.

local function UpdateLeaderboard()
	local Leaderboard = workspace.Lobby.Top10Players.LB.SurfaceGui
	local ColorByRank = Module.ColorByRank
	local LevelDataToCompare = "null"
	local GotData, IssueComparing = pcall(function()
		LevelDataToCompare = GlobalLeaderboard:GetSortedAsync(false, 10, 1)
	end)
	if GotData then
		print("Comparing Level Data, and Updating Leaderboard")
		for z, FoundData in ipairs(LevelDataToCompare:GetCurrentPage()) do
			if FoundData ~= nil then
				local PlayerId = (string.sub(FoundData.key, string.len("User-#"), string.len(FoundData.key)))
				local PlayerName = game.Players:GetNameFromUserIdAsync(PlayerId)
				local PlayerLevel = FoundData.value
				Leaderboard[tostring(z)].Player.Text = PlayerName
				Leaderboard[tostring(z)].Level.Text = "Lvl "..PlayerLevel
				Leaderboard[tostring(z)].Level.TextColor3 = Module.GetRankColor(PlayerLevel)
				Leaderboard[tostring(z)].Thumbnail.Image = game.Players:GetUserThumbnailAsync(PlayerId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size420x420)
				-- Leaves info for applicable users
			else
				Leaderboard[tostring(z)].Player.Text = "(To Be Decided)"
				Leaderboard[tostring(z)].Level.Text = "Lvl - -"
				Leaderboard[tostring(z)].Level.TextColor3 = ColorByRank.Bronze
				Leaderboard[tostring(z)].Thumbnail.Image = ""
				-- leaves these sections to bet set if the corresponding data doesn't exist
			end
		end
	elseif IssueComparing then
		warn("There was an issue comparing data for the leaderboard, will retry next request!")
	end
end