Having problems with DataStore

Hello there!

Recently I discovered a bug regarding my DataStore in a game I just released. It seems to reset all saved values once in a while when I join a server. I have never had any problems with this system before, so I really want to fix this as soon as possible. Here is my code for reference:
(Keep in mind that the “Tokens” value was added after this saving system was made. The Tokens stat is therefore displayed first in the leaderboard, and last in DataStore)

local DataStoreService = game:GetService("DataStoreService")
local stats = DataStoreService:GetDataStore("Stats")

game.Players.PlayerAdded:Connect(function(player)
	local leaderstatsFolder = Instance.new("Folder",player)
	leaderstatsFolder.Name = "leaderstats"
	local tokens = Instance.new("NumberValue",leaderstatsFolder)
	tokens.Name = "Tokens"
	local points = Instance.new("NumberValue",leaderstatsFolder)
	points.Name = "Points"
	-- Uploading saves:
	local key = "player-" .. player.userId
	local savedPlayerStats = stats:GetAsync(key)
	if savedPlayerStats ~= nil then -- player have been here before
		-- MAIN STATS:
		points.Value = savedPlayerStats[1]
		tokens.Value = savedPlayerStats[2]
	else -- new player
		points.Value = 0
		tokens.Value = 0
	end
	while true do
		-- Saving:
		local playerStatsToSave = {points.Value,tokens.Value}
		stats:SetAsync(key,playerStatsToSave)
		wait(60*5)
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	-- MAIN STATS:
	local points = player:WaitForChild("leaderstats"):WaitForChild("Points")
	local tokens = player:WaitForChild("leaderstats"):WaitForChild("Tokens")
	-- Saving:
	local key = "player-" .. player.userId
	local playerStatsToSave = {points.Value,tokens.Value}
	stats:SetAsync(key,playerStatsToSave)
end)

According to the wiki: Data Stores you really should wrap requests in a pcall to handle potential connection errors. Per the wiki:

Other than that your code looks fine to me. I’m guessing that maybe the savedPlayerStats is somehow nil from a failed request (not sure if/why it doesn’t just throw an error and cause your code to fail). It might actually be throwing an error and it looks like your save is lost since those values would be 0 by default and then when you leave it resets your stats. Since it would error after your points/tokens values are created I would guess it’s a possible issue. In any case you really don’t want to leave it open to risk.

On that note, I’m not sure why these functions even call themselves “Async” since they clearly aren’t.

2 Likes

I see! Thank you, I have never really used pcalls, and therefore I don’t understand them quite well. Could you help me wrap them into my code? And how does it handle any errors?

you have an inf loop in playerAdded ? this will run all the time even afte the player leaves the game.

1 Like

The example on the wiki isn’t bad to be honest.

local success, currentExperience = pcall(function()
	return experienceStore:GetAsync("Player_1234")
end);

if (success) then
    -- Do some fun stuff here.
else
    -- Retry the request.
end

You could set this up as a function and just rerun the function if the pcall fails. You’ll want to be careful with this approach as it could cap your request limit pretty quickly depending on why it failed. If you do retries I would make sure you have a maximum. Although, I’m not certain if a failed request counts toward your limit.

1 Like

Aaaah but won’t this loop break once the player leaves?

No, it will only break if break is called or the initial condition is not met.

You can save data even if a player isn’t it the game, just be sure to store their userid in a variable.

1 Like

He could also check for the player’s parent as a condition of the while loop.

1 Like

Just to be sure - how does this look?

local DataStoreService = game:GetService("DataStoreService")
local stats = DataStoreService:GetDataStore("Stats")

game.Players.PlayerAdded:Connect(function(player)
	local leaderstatsFolder = Instance.new("Folder",player)
	leaderstatsFolder.Name = "leaderstats"
	local tokens = Instance.new("NumberValue",leaderstatsFolder)
	tokens.Name = "Tokens"
	local points = Instance.new("NumberValue",leaderstatsFolder)
	points.Name = "Points"
	-- Uploading saves:
	local key = "player-" .. player.userId
	local success, savedPlayerStats = pcall(function()
		return stats:GetAsync(key)
	end)
	if (success) then
	    points.Value = savedPlayerStats[1]
		tokens.Value = savedPlayerStats[2]
	else
	    points.Value = 0
		tokens.Value = 0
	end
	while game.Players:FindFirstChild(player.Name) ~= nil do
		-- Saving:
		local playerStatsToSave = {points.Value,tokens.Value}
		local success, err = pcall(function()
			stats:SetAsync(key,playerStatsToSave)
		end)
		if success then
			print("Success!")
		end
		wait(60*5)
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	-- MAIN STATS:
	local points= player:WaitForChild("leaderstats"):WaitForChild("Points")
	local tokens = player:WaitForChild("leaderstats"):WaitForChild("Tokens")
	-- Saving:
	local key = "player-" .. player.userId
	local playerStatsToSave = {points.Value,tokens.Value}
	local success, err = pcall(function()
		stats:SetAsync(key,playerStatsToSave)
	end)
	if success then
		print("Success!")
	end
end)

That’s pretty much doing the same thing unfortunately. If your initial get request was unsuccessful you want to retry it. Otherwise you’ll just overwrite the data in the while loop.

Also, on the while loops condition you can simplify it to:

while player.Parent do
...

Consider moving your request logic into its own function:

local function attemptRequest(request, retries)
  local retries = retries or 0;
  local success, data = pcall(function()
    return request();
  end);

  if (success) then
    return data;
  elseif (not success and retries != 0) then
    return attemptRequest(request, retries - 1);
  else
    error('Unable to get requested data!');
  end
end

Then your code would look something like:

local DataStoreService = game:GetService("DataStoreService")
local stats = DataStoreService:GetDataStore("Stats")

local function attemptRequest(request, retries)
  local retries = retries or 0;
  local success, data = pcall(function()
    return request();
  end);

  if (success) then
    return data;
  elseif (not success and retries != 0) then
    return attemptRequest(request, retries - 1);
  else
    error('Unable to get requested data!');
  end
end

game.Players.PlayerAdded:Connect(function(player)
	-- Get saves:
	local key = "player-" .. player.userId
	local savedPlayerStats = attemptRequest(function()
		return stats:GetAsync(key);
	end, 5); -- Retries 5 times maximum.

  local leaderstatsFolder = Instance.new("Folder",player)
	leaderstatsFolder.Name = "leaderstats"

  local tokens, points;

  if (savedPlayerStats) then
    tokens = Instance.new("NumberValue",leaderstatsFolder)
    tokens.Name = "Tokens"
    points = Instance.new("NumberValue",leaderstatsFolder)
    points.Name = "Points"

	  points.Value = savedPlayerStats[1]
		tokens.Value = savedPlayerStats[2]
	else
    return
	end

	while (player.Parent) do
		-- Saving:
		local playerStatsToSave = {points.Value, tokens.Value}
		local success = attemptRequest(function()
			return stats:SetAsync(key, playerStatsToSave)
		end, 0) -- Doesn't retry.
		if success then
			print('Successfully saved stats for', player.Name);
    else
      print('Failed to save stats for', player.Name);
		end
		wait(60*5)
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	-- MAIN STATS:
	local points= player:WaitForChild("leaderstats"):FindFirstChild('Points')
	local tokens = player:WaitForChild("leaderstats"):FindFirstChild('Tokens')
	-- Saving:
  if (points and tokens) then
    local key = "player-" .. player.userId
    local playerStatsToSave = {points.Value, tokens.Value}
    local success, err = attemptRequest(function()
      return stats:SetAsync(key, playerStatsToSave)
    end, 2) -- Retries twice.
    if success then
      print('Successfully saved stats for', player.Name, 'upon leaving.');
    else
      warn('Failed to save stats for', player.Name, 'upon leaving.')
    end
  end
end)
1 Like

Yeah, but as you said before, the requests may pile up and reach the limit. How can I prevent this? With a counter? And I guess I don’t need to set the stats to 0 since they already have this value once they are made.

Accidentally updated my previous post. Hopefully it’s helpful. Gosh the tabbing in here is weird :joy:

1 Like

Thanks a ton! It’s much easier to understand the whole picture now :slight_smile:

I tried testing it but there are no stats being made if the player is new to the game, so I tried changing this:

if (savedPlayerStats) then
	tokens = Instance.new("NumberValue",leaderstatsFolder)
    tokens.Name = "Tokens"
    points = Instance.new("NumberValue",leaderstatsFolder)
    points.Name = "Points"
	points.Value = savedPlayerStats[1]
	tokens.Value = savedPlayerStats[2]
else
    tokens = Instance.new("NumberValue",leaderstatsFolder)
    tokens.Name = "Tokens"
    points = Instance.new("NumberValue",leaderstatsFolder)
    points.Name = "Points"
    points.Value = 0
	tokens.Value = 0
end

Does this look good? The else-block were changed from return into this

Nevermind, this didn’t change anything at all. The leaderboard won’t show up, hmmm, but it got rid of the error

Now it works! Studio didn’t save my changes to the Script, lol… However does my change to the script look good? Would it interfere with anything? It seems to work perfect now.

Alright, so to start off, I’m seeing many different problems with your current setup. First of all, when creating a loop to auto save data, you should probably use a pcall and have checks to make sure that you aren’t overwriting their data if they haven’t loaded in.

local function autoSave()
    while wait(60) do
        if player and player:FindFirstChild("DataLoaded") then --Make sure that the player's data is loaded
            --Save data
        else
            break
        end
    end
end

spawn(autoSave)

Second of all, I recommend using tables rather than values for many reasons like, better organization and more functionality.

2 Likes

Interesting, I just tried implementing this check for DataLoaded, but it won’t save. Also, using tables to store the stats seems interesting, how is this being done? (with minimal change to the code I already have)

Current code so far:

local DataStoreService = game:GetService("DataStoreService")
local stats = DataStoreService:GetDataStore("Stats")

local function attemptRequest(request, retries)
	local retries = retries or 0;
	local success, data = pcall(function()
		return request();
	end);
	if (success) then
		return data;
	elseif (not success and retries ~= 0) then
		return attemptRequest(request, retries - 1);
	else
		error('Unable to get requested data!');
	end
end

game.Players.PlayerAdded:Connect(function(player)
	-- Get saves:
	local key = "player-" .. player.userId
	local savedPlayerStats = attemptRequest(function()
		return stats:GetAsync(key);
	end, 5); -- Retries 5 times maximum.

	local leaderstatsFolder = Instance.new("Folder",player)
	leaderstatsFolder.Name = "leaderstats"

	local tokens, points;

	if (savedPlayerStats) then -- Player has been here before:
		tokens = Instance.new("NumberValue",leaderstatsFolder)
    	tokens.Name = "Tokens"
    	points = Instance.new("NumberValue",leaderstatsFolder)
    	points.Name = "Points"
		points.Value = savedPlayerStats[1]
		tokens.Value = savedPlayerStats[2]
	else -- New player:
    	tokens = Instance.new("NumberValue",leaderstatsFolder)
    	tokens.Name = "Tokens"
    	points = Instance.new("NumberValue",leaderstatsFolder)
    	points.Name = "Points"
		points.Value = 0
		tokens.Value = 0
	end

	while (player.Parent) do
		if player:FindFirstChild("DataLoaded") then
			-- Saving:
			local playerStatsToSave = {points.Value, tokens.Value}
			local success = attemptRequest(function()
				return stats:SetAsync(key, playerStatsToSave)
			end, 0) -- Doesn't retry.
			if success then
				print('Successfully saved stats for', player.Name);
	    	else
	      		print('Failed to save stats for', player.Name);
			end
		end
		wait(60*5)
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	-- MAIN STATS:
	local points= player:WaitForChild("leaderstats"):FindFirstChild('Points')
	local tokens = player:WaitForChild("leaderstats"):FindFirstChild('Tokens')
	-- Saving:
	if (points and tokens) then
	    local key = "player-" .. player.userId
	    local playerStatsToSave = {points.Value, tokens.Value}
	    local success, err = attemptRequest(function()
	      return stats:SetAsync(key, playerStatsToSave)
	    end, 2) -- Retries twice.
	    if success then
	      print('Successfully saved stats for', player.Name, 'upon leaving.');
	    else
	      warn('Failed to save stats for', player.Name, 'upon leaving.')
	    end
	end
end)

Alright, this is a good start. First of all, you shouldn’t check the player’s parent as this could error if the player is nil, you can just do while player do. Now, with DataLoaded, I meant you have to create a value and insert it into the player.

local loaded = Instance.new("StringValue")
loaded.Parent = player

--autosave

Tables can really help with organization and functionality, which you can do like so (customize it to your needs)

local data = {}

local function playerAdded(player)
    local success, response = pcall(function()
        return dataStore:GetAsync("player-"..player.UserId)
    end)
    
    if success then --If it's a success
        local playerData = response or {} --If there's not any data, then create a new table.
        
        local currency = Instance.new("NumberValue")
        currency.Name = "Currency"
        currency.Value = playerData.currency or 0 --If there's currency then use that. Else use default, 0
        currency.Parent = player
        
        local loaded = Instance.new("StringValue")
        loaded.Name = "DataLoaded"
        loaded.Parent = player
        
        data[player.UserId] = playerData --store the data in a table
        
        spawn(function()
             while wait(60) do
                 if player and player:FindFirstChild("DataLoaded") then
                     save(player)
                end
             end
        end)
    end
end

function save(player)
    if player:FindFirstChild("DataLoaded") then
        local playerData = data[player.UserId]
        
        playerData.currency = playerData.currency or 0 --set the value of the currency for next time if data does not already exist
        
        local success, response = pcall(function()
            return dataStore:SetAsync("player-"..player.UserId, playerData) --save the table
        end)
    end
end

If you’d like to learn more about tables, you can go here for documentation on tables and here for how to use tables in data stores.

1 Like

Please use the condition of the while loop properly.

local dataReady = {} -- Example; format is [UserId] = true

local function autoSave()
    -- Don't create an instance for this, use a table @ original implementation
    while player and dataReady[player.UserId] == true then
        -- Save data
    end
end

spawn(autoSave)

Using wait to implicitly pass the condition of a while loop is bad code. Check out my resource thread, The While-Wait-Do Idiom for more information.

As for your second response, you’re contradicting yourself a little here.

Your first post mentioned that ValueObjects should be avoided, but your second response is very ValueObject heavy. I’m not sure whether you’re posting a modification to OP’s current code or whether that’s something you came up with as you were replying.

A little someone once upon a time pointed out to me (though I was only half-aware at the time) that arrays are faster than relying on ValueObjects. In that respect, you can also have all your code directly in your code environment rather than relying on external values. I would definitely recommend a table over ValueObjects for the sake of simplicity, organisation and control.

Moving over to your code, a little protip to wrap the actual method calls whenever possible over creating an anonymous function and wrapping that.

-- You could just do tostring(player.UserId). Kept old key for consistency's sake.
local success, response = pcall(dataStore.GetAsync, dataStore, "player-"..player.UserId)

Also, playerData never gets any entries put into it…? This code seems more reliant on the ValueObjects than the table.

2 Likes