How to create multiple global leaderboards under one datastore key without resetting data?

I released a game about a week ago and when I released the game I thought the global leaderboards were working fine until I soon realized that all the values were the same across each leaderboard. I’ve been looking up how to make multiple leaderboards and I keep hearing that each stat needs to be stored in its own key because of how OrderedDataStores work. The problem is, I think this would reset the data of the players that have already played my game. Any good solution to fix this?

Here’s the DataStore script:

local DS = game:GetService("DataStoreService")

local DSName = DS:GetDataStore("cannonDataStore05")
local leaderStatDS = DS:GetDataStore("cannonLeaderstats05")
local cannonUtils = require(script.Parent.cannonUtils)

function convertTableToInstance(container, tbl)--loads in datastore values
	for name, data in pairs(tbl) do
		local inst = Instance.new(data["ClassName"])
		inst.Name = name
		if data["ClassName"] ~= "Folder" then
			inst.Value = data["Value"]
		end
		inst.Parent = container
		convertTableToInstance(inst, data["Children"])
	end
end

game.Players.PlayerAdded:Connect(function(player) --initiates loading
	local plrdafold = Instance.new("Folder", player)--main folder for storing all values
	plrdafold.Name = "playerData"

	local leaderstats = Instance.new("Folder", player)--main folder for storing all values
	leaderstats.Name = "leaderstats"

	local moneyDS = leaderStatDS:GetAsync(player.UserId)
	local plrdata = DSName:GetAsync(player.UserId)--gets the datastore

	if plrdata == nil or moneyDS == nil then--if this is a new player
		print("New Player - Creating Data")
		local folder 
		local money = Instance.new("NumberValue")
		money.Name = "Coins"
		money.Value = 0
		money.Parent = leaderstats
		
		local rebirth = Instance.new("IntValue")
		rebirth.Name = "Rebirths"
		rebirth.Value = 0
		rebirth.Parent = leaderstats
		
		local score = Instance.new("IntValue")
		score.Name = "Highscore"
		score.Value = 0
		score.Parent = leaderstats

		local AO = Instance.new("StringValue")
		AO.Name = "EquippedCannon"
		AO.Value = "Basic"
		AO.Parent = plrdafold
		
		local AO = Instance.new("StringValue")
		AO.Name = "EquippedTrail"
		AO.Value = ""
		AO.Parent = plrdafold
		
		local A1 = Instance.new("IntValue")
		A1.Name = "CannonPower"
		A1.Value = 150
		A1.Parent = plrdafold
		
		local A3 = Instance.new("IntValue")
		A3.Name = "Multiplier"
		A3.Value = 1
		A3.Parent = plrdafold
		
		local A2 = Instance.new("Folder")
		A2.Name = "Inventory"
		A2.Parent = plrdafold
		
		local A3 = Instance.new("Folder")
		A3.Name = "Trails"
		A3.Parent = plrdafold
		
		
		cannonUtils.giveCannon(player, "Basic") --gives the default cannon
	else --If this is a returning player

		print("Returning Player - Loading Data")
		print(plrdata)
		print(moneyDS)
		convertTableToInstance(plrdafold, plrdata)--starts loading in data
		convertTableToInstance(leaderstats, moneyDS)
		
	end
end)

function convertInstanceToTable(folder) --uploads all of the values into the datastore
	local tbl = {}

	for i, inst in pairs(folder:GetChildren()) do
		if inst.ClassName == "Folder" then
			tbl[inst.Name] = {["ClassName"] = "Folder", ["Children"] = convertInstanceToTable(inst)}
		else
			tbl[inst.Name] = {["ClassName"] = inst.ClassName, ["Value"] = inst.Value, ["Children"] = convertInstanceToTable(inst)}
		end
	end

	return tbl
end

game.Players.PlayerRemoving:Connect(function(player)--when the player leaves

	local dataTbl = convertInstanceToTable(player.playerData)
	local dataTbl2 = convertInstanceToTable(player.leaderstats)

	print(dataTbl)
	print(dataTbl2)

	DSName:SetAsync(player.UserId, dataTbl)
	leaderStatDS:SetAsync(player.UserId, dataTbl2)

end)

game:BindToClose(function() --when the game shuts down
	for i, player in pairs(game.Players:GetPlayers())do --gets the player list
		if player then --if the player exists
			player:kick("The game is shutting down, please rejoin later") --kick message
		end
	end

	wait(4) --wait to make sure the data saves
end)

And here is the script I use for the global leaderboards (I just duplicate this script and change the StatsName for each leaderboard)

local StatsName = "Coins" -- Your stats name
local MaxItems = 20 -- Max number of items to be displayed on the leaderboard
local MinValueDisplay = 1 -- Any numbers lower than this will be excluded
local MaxValueDisplay = 1000000000e1000000 -- (10 ^ 15) Any numbers higher than this will be excluded
local UpdateEvery = 60 -- (in seconds) How often the leaderboard has to update


local suffixes = {"K", "M", "B", "T", "Qd"} -- numbers don't go higher than 'Q' in Lua.

local function toSuffixString(n)
	if n <= 0 then
		return 0
	else
		local i = math.floor(math.log(n, 1e3))
		local v = math.pow(10, i * 3)
		return ("%.1f"):format(n / v):gsub("%.?0+$", "") .. (suffixes[i] or "")
	end
end


local DataStoreService = game:GetService("DataStoreService")
local DataStore = DataStoreService:GetOrderedDataStore("cannonLeaderstats05") --CoinsDataStore
local SurfaceGui = script.Parent
local Sample = script.Sample
local List = SurfaceGui.TopBar
local ItemsFrame = SurfaceGui.ScrollingFrame

local function GetItems()
	local Data = DataStore:GetSortedAsync(false, MaxItems, MinValueDisplay, MaxValueDisplay)
	local TopPage = Data:GetCurrentPage()

	--ItemsFrame.Nothing.Visible = #TopPage == 0 and true or false

	for i, v in ipairs(TopPage) do
		local UserId = v.key
		local Value = v.value
		local Username = "[Not Available]"
		local Color = Color3.fromRGB(38, 50, 56)

		local Success, Error = pcall(function()
			Username = game.Players:GetNameFromUserIdAsync(UserId)
		end)

		if i == 1 then
			Color = Color3.fromRGB(255, 215, 0)
		elseif i == 2 then
			Color = Color3.fromRGB(192, 192, 192)
		elseif i == 3 then
			Color = Color3.fromRGB(205, 127, 50)
		end
		local image = game.Players:GetUserThumbnailAsync(UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150)
		local Item = Sample:Clone()
		Item.Name = Username
		Item.LayoutOrder = i
		Item.Image.Image = image
		Item.Image.Place.TextColor3 = Color
		Item.Image.Place.Text = i
		Item.PName.Text = Username
		Item.PName.TextColor3 = Color
		Item.Value.Text = toSuffixString(Value)
		Item.Parent = ItemsFrame
	end
end

while true do
	for i, v in pairs(game.Players:GetPlayers()) do
		local lstats = v.leaderstats:WaitForChild(StatsName).Value
		if lstats then
			pcall(function()
				DataStore:UpdateAsync(v.UserId, function(Value)
					return tonumber(lstats)
				end)
			end)
		end
	end

	for i, v in pairs(ItemsFrame:GetChildren()) do
		if v:IsA("Frame") then
			v:Destroy()
		end
	end

	GetItems()

	wait()
	--List.Value.Text = StatsName
	--List.CanvasSize = UDim2.new(0, 0, 0, ItemsFrame.UIListLayout.AbsoluteContentSize.Y + 35)
	wait(UpdateEvery)
end

Thanks for any help :slight_smile:

1 Like

You will have to migrate the data to the new OrderedDataStores for each stat. The way to handle this logically would be:

  1. check when the player joins if there is an existing entry in the new OrderedDataStores, if yes just read the data
  2. If no data in new DS, read the old DataStore entries and rewrite them in to the new DS
1 Like