Trying to Optimize my Datastore script, several players are reporting Data Loss

A few months ago I created a Datastore script that saves several bool values for the player under one Key. People were reporting to me quite frequently that they had lost their save, so my solution was to add a pcall function to catch an error if the datastore script fails for any reason, and retry the code to make sure the player’s data saves for any reason, whether it be a crash or the server goes down.

I thought this fixed the problem, but I still have several players reporting that they lost their entire save. I looked up solutions to this, and I learned that there were problems on Roblox’s end with datastores, and I followed the thread closely, and when it was confirmed fixed, I was relieved, but still weeks after the fix players report data loss. It might have something to do with my script then.

This is the entire script that handles the data. There is one key for everything, and it loads when the player joins, and saves when the player leaves.

The context for the data loss is that the entire player’s save file is not there, and I get a print saying that the player has no data yet, so I’m thinking it has something to do with an error happening with GetAsync(). I added a print to warn me if an error occurred, and when I see data loss on myself, (It happens to me maybe once out of every 20(approximate) times I join, which is not good) and I see no print errors about an error getting a save, I simply get an error saying I have no data yet.

This is a Server Script inside of ServerScriptService. This is what the ancestry looks like:
image

Help would be so appreciated!!

local PlayerSavedData = game:GetService('DataStoreService'):GetDataStore("PLAYERSAVEFILE")
local RunService = game:GetService("RunService")

local moneygain = 120
local errordebounce = 6

game.Players.PlayerAdded:connect(function(player)
	repeat wait() until script:FindFirstChild("PlayerSave")
	local save = script.PlayerSave:Clone()
	save.Parent = player
	local amount = save:WaitForChild("Amount")
	while wait(moneygain) do
		if amount.Value < 5000 then
		amount.Value = amount.Value+5
		end
	end
end)

function BirdsDataTable(player)
    local BirdInventory = {
		Amount = player.PlayerSave.Amount.Value,
        Peafowl = player.PlayerSave.Peafowl.Value,
        Parrot = player.PlayerSave.Parrot.Value,
		Ostrich = player.PlayerSave.Ostrich.Value,
		Penguin = player.PlayerSave.Penguin.Value,
		Puffin = player.PlayerSave.Puffin.Value,
		Toucan = player.PlayerSave.Toucan.Value,
		Flamingo = player.PlayerSave.Flamingo.Value,
		Falcon = player.PlayerSave.Falcon.Value,
		Owl = player.PlayerSave.Owl.Value,
		Vulture = player.PlayerSave.Vulture.Value,
		Eagle = player.PlayerSave.Eagle.Value
    }
	return BirdInventory
end

function SaveData(player)
	local Success,Error = pcall(function()
		local playerkey = "player-"..player.userId
		local data = BirdsDataTable(player)
		PlayerSavedData:SetAsync(playerkey,data)
	end)
	if Error then
		print("***ERROR SAVING DATA FOR "..player.Name)
		local attempts = 0
		while wait (errordebounce) do
			local retries = attempts + 1
			attempts = retries
			local pass,fail = pcall(function()
				local playerkey = "player-"..player.userId
				local data = BirdsDataTable(player)
				PlayerSavedData:SetAsync(playerkey,data)
				print("Successfully saved data for "..player.Name)
			end)
			if pass or attempts == 10 then break end
		end
	end
end

game.Players.PlayerAdded:connect(function(player)
	repeat wait() until player.Character
	repeat wait() until player:FindFirstChild("PlayerSave")
	local PS = player.PlayerSave
	local data = BirdsDataTable(player)
	local playerkey = "player-"..player.userId
	local Passed, Error = pcall(function() 
		local SaveFiles = PlayerSavedData:GetAsync(playerkey)
			if SaveFiles == nil then
				print(player.Name.." has no data yet.")
			else
				PS.Amount.Value = SaveFiles.Amount
				PS.Peafowl.Value = SaveFiles.Peafowl
				PS.Parrot.Value = SaveFiles.Parrot
				PS.Ostrich.Value = SaveFiles.Ostrich
				PS.Penguin.Value = SaveFiles.Penguin
				PS.Puffin.Value = SaveFiles.Puffin
				PS.Toucan.Value = SaveFiles.Toucan
				PS.Flamingo.Value = SaveFiles.Flamingo
				PS.Falcon.Value = SaveFiles.Falcon
				PS.Owl.Value = SaveFiles.Owl
				PS.Vulture.Value = SaveFiles.Vulture
				PS.Eagle.Value = SaveFiles.Eagle
			end
		end)
	if Error then
		print("***ERROR GETTING SAVE FILE FOR "..player.Name)
		local attempts = 0
		while wait(errordebounce) do
			local retries = attempts + 1
			attempts = retries
			local pass,err = pcall(function()
				local SaveFiles = PlayerSavedData:GetAsync(playerkey)
					if SaveFiles == nil then
						print(player.Name.." has no save data yet")
					else
						PS.Amount.Value = SaveFiles.Amount
						PS.Peafowl.Value = SaveFiles.Peafowl
						PS.Parrot.Value = SaveFiles.Parrot
						PS.Ostrich.Value = SaveFiles.Ostrich
						PS.Penguin.Value = SaveFiles.Penguin
						PS.Puffin.Value = SaveFiles.Puffin
						PS.Toucan.Value = SaveFiles.Toucan
						PS.Flamingo.Value = SaveFiles.Flamingo
						PS.Falcon.Value = SaveFiles.Falcon
						PS.Owl.Value = SaveFiles.Owl
						PS.Vulture.Value = SaveFiles.Vulture
						PS.Eagle.Value = SaveFiles.Eagle
						print("Successfully got saved data for "..player.Name)
					end
			end)
			if pass or attempts == 10 then break end
		end
	end
	if PS.Amount.Value < 0 then
		PS.Amount.Value = 100
	end
end)

game.Players.PlayerRemoving:Connect(SaveData)

game:BindToClose(function()
	if RunService:IsStudio() then return end
	local Players = game:GetService("Players")
	for _,player in pairs(Players:GetPlayers()) do
		SaveData(player)
		print("Saved data for "..player.Name.." on shutdown.")
	end
end)
2 Likes

Go through your code’s execution path for these cases:

  • What happens if the player leaves before their data loads? Do you overwrite their data with default data?
  • What happens if the player stays in-game long enough for your retry on the data get to be completely exhausted? (= 10 attempts judging from code) Do you then save default data for that player when they leave / the server shuts down?
4 Likes

That’s a good idea. I’m not too familiar with datastore scripting, so how could I create a code that makes sure the player’s save doesn’t overwrite with default data?

Also, yes, after 10 attempts i break the error code so the server doesn’t keep retrying on an endless loop. In my experience, I have never seen it ever go past 2 attempts before successfully recovering a save, but just to be safe, should I raise the attempts to a higher number or infinite?

Your SaveData function doesn’t care if player default data was filled with the DataStore data. Players who happen to leave your game (or get kicked / server shutdown / lose connection) before :GetAsync() finishes will lose their data every time and no error will be thrown.

After you fix that, everything else should work fine.

Protip is to do an infinite, but slow loop of trying to fetch data of a player when :GetAsync throws errors (every 5 - 10 seconds). Another protip is to completely ditch :GetAsync and :SetAsync for data structures you load once and then only upload while the player is playing the game, but it’s best to understand the DataStore better before making this switch. :UpdateAsync doesn’t return cached results and respects the order of calls, can be used both for writting and loading or both at the same time.

I recommend you take a look at my method of Datastore saving/loading

1 Like

This was a huge help, thank you! I used this method and now SetAsync won’t fire if the player’s data hasn’t loaded. I also made a print to show me how often this was happening. At least 1 person every 2-3 hours was losing data before this fix, and now everything works great.

After I fixed the issue, there are understandably several players who have contacted me asking for their lost save files back. Is there a way I could give it back to them? I’m not too familiar with Datastores. Is it possible to rollback a datastore for a specific user?

You would have to use the previous key you were using(unless you editted my script to use yours).

However it is most likely that their data is gone for good(if it was lost before you used my method). I might release a module using the method I created above.

I’m continuing to use the same previous datastore key name with the new script so no one loses their save file.

Also, even though I can’t rollback save files, I created a command to edit an offline user’s data:

local PlayerSavedData = game:GetService('DataStoreService'):GetDataStore("PLAYERSAVEFILE");
local playerkey = "player-"..131574961;
local SaveFiles = PlayerSavedData:GetAsync(playerkey)
function BirdsDataTable(player)
	local BirdInventory = {
	Amount = 2000,
	}
	return BirdInventory
end
local data = BirdsDataTable(player)
PlayerSavedData:SetAsync(playerkey,data)

So far I’ve been using this command in the command prompt on online servers to manually give money back to people who have told me they lost their save files.