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)
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.
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?
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.
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)
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.
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.
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.
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.