Is this decent for player data storage?

Hello! I’ve rewritten my datastore code to incorporate UpdateAsync and IncementAsync. I’m trying to understand data storage better because this code works but it’s tied to an autosave. My requests pile up quickly and they get added to the queue frequently. I want to make sure that I get this right because I’m about to release my game, and can’t have players losing data. Especially if they buy some Coins.

-- Frriend's Improved Datastore Service --

local DSS = game:GetService("DataStoreService")
local plrDataStore = DSS:GetDataStore(workspace.Constants.Datastores.DataStoreID.Value)

local saveNotifyRE = game.ReplicatedStorage.RemoteEvent.SaveNotify

local currentPatch = game.Workspace.Constants.GLOBALS.CurrentPatch.Value
local TS = game:GetService("TestService")

-- Billboard Constants --
local billboardStats = {"Bricks", "Level", "Wins"}
local billboardStores = {Bricks = workspace.Constants.Datastores.ODSBricksID.Value,
						 Level = workspace.Constants.Datastores.ODSLevelID.Value,
						 Wins = workspace.Constants.Datastores.ODSWinsID.Value}

-- Save Notification for Players --
function notifySave(plr, state)
	saveNotifyRE:FireClient(plr, state)
end

-- Billboard Functions --
function isBillboardStat(tag)
	for _, stat in ipairs(billboardStats) do
		if stat == tag then
			return true
		end
	end
	
	return false
end

-- Not as Important, So its Simplified --
function updateOrderedBillboardStats(plr, store, data)
	local orderedDataStore = DSS:GetOrderedDataStore(billboardStores[store])
		
	local success, err = pcall(function()
		return orderedDataStore:SetAsync(plr.UserId, data)
	end)
end

-- Debug --
function writeToConsole(ln, warning, message)
	warning = warning or false
	message = message or false
	
	local prefix = "[Player Data Handler] "
	
	if warning then
		warn(prefix .. ln)
	elseif message then
		TS:Message(prefix .. ln)
	else
		print(prefix .. ln)
	end
end

-- Only for Checking if store Exists / Gettings its Value if so --
function getAsync(plr, key)
	local success, value = pcall(function()
		return plrDataStore:GetAsync(plr.UserId .. "_" .. key)
	end)
		
	if success then return value else return nil end
end

-- Only for Creating Stores --
function setAsync(plr, key, value)
	local success, value = pcall(function()
		return plrDataStore:SetAsync(plr.UserId .. "_" .. key, value)
	end)
		
	if success then return true else return false end
end

-- Only for Updating Stores --
function updateAsync(plr, key, newValue) 
	if isBillboardStat(key) then
		updateOrderedBillboardStats(plr, key, newValue)
	end
	
	local success, err = pcall(function()
		plrDataStore:UpdateAsync(plr.UserId .. "_" .. key, function(oldData)
			local newData = oldData or -1
			
			if newData ~= -1 then
				-- writeToConsole(plr.Name .. " : Updated ['" .. key .. "' : (SWAPPED " .. oldData .. " FOR " .. newValue .. ") Successfully]", false, true)
				return newValue
			else
				writeToConsole(plr.Name .. " : UPDATE ERROR ['" .. key .. "' Not Found] Skipping to Prevent Data Loss..", true)
				return nil
			end
		end)
	end)
		
	if success then
		return true
	else
		writeToConsole("[LN 45] " .. err, true)
		return false
	end
end

-- Checks if user's data was saved on current patch --
function checkPatch(plr)
	local patch = getAsync(plr, "Patch")
	
	if patch then
		return patch
	else
		writeToConsole("NOTE : Patch Data not Found, Validating Data..", true)
		setAsync(plr, "Patch", currentPatch)
		
		return false
	end
end

function checkIfPlayerDataUpdated(plr)
	local plrData = plr.Stats:GetChildren()
	local tally = 0
	
	for _, data in ipairs(plrData) do
		if getAsync(plr, data.Name) then
			tally = tally + 1
		else
			writeToConsole(plr.Name .. " : FLAG ['" .. data.Name .. "' Not Found] Creating Store..", true)
			setAsync(plr, data.Name, data.Value)
		end
	end
	
	writeToConsole(plr.Name .. " : CHECK PATCH Integrity Check: (" .. tally .. "/" .. #plrData .. ") Stores Loaded Properly")
end

-- For use when saving data. We make sure the data is there before we update it --
function savePlayerData(plr)
	notifySave(plr, true)	

	local plrData = plr.Stats:GetChildren()
	local tally = 0
	
	for _, data in ipairs(plrData) do
		local info = updateAsync(plr, data.Name, data.Value)
		
		if info then
			tally = tally + 1
		end
	end
	
	spawn(function() wait(2) notifySave(plr, false)	end)
	
	-- writeToConsole(plr.Name .. " : SAVE Integrity Check: (" .. tally .. "/" .. #plrData .. ") Stores Updated Properly")
end

-- For Loading player Data. Only Load if they are now on the current patch --
function loadPlayerData(plr)
	local plrData = plr.Stats:GetChildren()
	local tally = 0
	
	for _, data in ipairs(plrData) do
		local loadData = getAsync(plr, data.Name)
		
		if loadData then
			tally = tally + 1
			data.Value = loadData
		else
			writeToConsole(plr.Name .. " : LOAD ERROR ['" .. data.Name .. "' Not Found] Skipping to Avoid Data Loss..", true)
		end
	end
	
	writeToConsole(plr.Name .. " : LOAD Integrity Check: (" .. tally .. "/" .. #plrData .. ") Stores Loaded Properly")
end

function incrementStat(plr, stat, value)
	local key = plr.UserId .. "_" .. stat
	
	local coins
	
	local success, err = pcall(function()
		coins = plrDataStore:IncrementAsync(key, value)
	end)	
		
	if success then 
		writeToConsole('Incremented Successfully')
	else
		writeToConsole(err)
	end
end

_G.savePlayerData = function(plr)
	print 'SAVING'
	savePlayerData(plr)
end

_G.incrementSave = function(plr, stat, value)
	writeToConsole("Increment Request Recieve")
	incrementStat(plr, stat, value)
end

_G.loadPlayerData = function(plr)
	local getPatch = checkPatch(plr)
	
	if getPatch ~= currentPatch then
		setAsync(plr, "Patch", currentPatch)
		checkIfPlayerDataUpdated(plr)
		
		spawn(function()
			wait(1)
			loadPlayerData(plr)
		end)
	else
		loadPlayerData(plr)
	end
end

Thanks in advance :slight_smile:

1 Like

Hello, may I ask, are you using three seperate keys for each player?

If you are you are in risk of throttling.

Actually yes. I’m basically taking a folder full of stats and putting the UID before the stat.

But the ordered datastore is for the leaderboards in spawn.

plrDataStore:SetAsync(plr.UserId .. "_" .. key, value)

-- and --

UpdateAsync(plr.UserId .. "_" .. key, function(oldData) 
    ...
end)
--etc.

What would be a better way of doing this?

It is recommended to store all your data as a table, and then to save it under one key. However, I see that you’re using an orderdatastore, so if you were planning to use GetSortedAsync it will not work with tables.

To circumvent this you can use a key that holds the value you want to order. You must be careful not to use this datastore as frequently as the main one.

Global Leaderstats Help This is a thread that is relevant to creating two keys for your ordered datastore.

1 Like

You can do this quite easily by just iterating over the folder and adding it to the table with the form -

table[name] = value

1 Like

Personally, I would recommend using DataStore2 to handle your DataStores, it would save a lot of time and it would be secured if done properly.

Here’s a little example:

local players = game:GetService("Players")
local dataStore2 = require(1936396537)

local function onPlayerAdded(player)
    if player then
        local levelStore = dataStore2("LevelStore", player)
        local cashStore = dataStore2("CashStore", player)
        
        local leaderstats = Instance.new("Folder")
        leaderstats.Name = "leaderstats"
        leaderstats.Parent = player
        
        local level = Instance.new("IntValue")
        level.Name = "Level"
        level.Value = levelStore:Get(1) -- Argument is the default value, if no data is found the default is given
        level.Parent = leaderstats
        
        local cash = Instance.new("IntValue")
        cash.Name = "Cash"
        cash.Value = cashStore:Get(0) -- Same here
        cash.Parent = leaderstats
        
        levelStore:OnUpdate(function()
            level.Value = levelStore:Get()
        end)
        
        cashStore:OnUpdate(function()
            cash.Value = cashStore:Get()
        end)
    end
end

local function onPlayerRemoving(player)
    if player then
        local levelStore = dataStore2("LevelStore", player)
        local cashStore = dataStore2("CashStore", player)
        levelStore:Save()
        cashStore:Save()
    end
end

players.PlayerAdded:Connect(onPlayerAdded)
players.PlayerRemoving:Connect(onPlayerRemoving)

Take this reply with a grain of salt, it’s just an example, if you wanted to dive further into DataStore2, here’s a YouTube tutorial made by the man himself AlvinBlox:

2 Likes

There is no point on saving data on player removing since datastore2 already does it for you.

1 Like

I implemented DataStore2 into our game. Let me just say WOW. Amazing. Thanks for the awesome suggestion! I shortened the code to 85 lines and incorporated inventory storage as well. I’ll be using DS2 in all my games from here on out.

I did some researching, and apparently you do not have to call :Save() at all during PlayerRemoving, DS2 should do this for you. With that being said, here’s an edited example of my previous code:

local players = game:GetService("Players")
local dataStore2 = require(1936396537)

local function onPlayerAdded(player)
    if player then
        local levelStore = dataStore2("LevelStore", player)
        local cashStore = dataStore2("CashStore", player)
        
        local leaderstats = Instance.new("Folder")
        leaderstats.Name = "leaderstats"
        leaderstats.Parent = player
        
        local level = Instance.new("IntValue")
        level.Name = "Level"
        level.Value = levelStore:Get(1) -- Argument is the default value, if no data is found the default is given
        level.Parent = leaderstats
        
        local cash = Instance.new("IntValue")
        cash.Name = "Cash"
        cash.Value = cashStore:Get(0) -- Same here
        cash.Parent = leaderstats
        
        levelStore:OnUpdate(function(newLevel)
            level.Value = newLevel
        end)
        
        cashStore:OnUpdate(function(newCash)
            cash.Value = newCash
        end)
    end
end
players.PlayerAdded:Connect(onPlayerAdded)
for _, p in next, players:GetPlayers() do
    onPlayerAdded(p)
end

Another thing you may have noticed is that :OnUpdate returns the new value as a parameter, something I also just now learned.