Datastore Version Backup: How to Restore Player Data

Datastore Version Backups



*** THIS IS NOT DATALOSS RECOVERY, but it may help revert ALL players to older data versions easily that are saved and not ‘corrupt’

Sometimes when working on a game, you’ve been working late into the night, and release an update for your popular game and then go to bed. You wake up, and all the players have infinite cash because you updated the game, and forgot to turn off the “infinite money” testing variable in studio. Now your game economy is ruined!

If you had been using the ‘berreza method’ which combines Datastores and Ordered Datastores to save and load data, you can could try to reset a players data to a previous version. This could take forever to look through ordered data store entries to try to find a good entry to use to revert one players data manually to a state before an update came out. This is especially hard if you have to do this for a lot of your players. This is when versioning data can be helpful.

Versioning data allows us to be able to revert to a good data version from the past, and not have to worry so much when publishing game updates that might break.

If we save our data using datastores and ordered datastores (both together), we can easily add versioning support to our data.

How berreza's method generally loads data ...

(I will expand on this some more in next post update)

local DSS = game:GetService("DataStoreService")

local dsKey = player.UserId --something unique
local playerData
local ods = DSS:GetOrderedDataStore("PlayerData",player.UserId).
local dataVersions = orderedDataStore:GetSortedAsync(false, 100)
local dataVersionKeys = {}
local currentPage = dataVersions:GetCurrentPage()

for entry, data in pairs(currentPage) do
	table.insert(dataVersionKeys, data.key)
end

if #dataVersionKeys == 0 then
	--No keys mean no saved data ever, so its a new player. Create new data
	playerData = CreateDefaultData(player)
else
	--They have some data that can be loaded
	local dataStore = game:GetService("DataStoreService"):GetDataStore("PlayerData"..dsKey)
	playerData = dataStore:GetAsync(dataVersionKeys[1])
end

How to support data versioning

1.0.1

First we need to know the structure of what we are loading and saving. Data for me is a dictionary of many keys, with SaveIndex and LastSave at the top. (my simple implementation of berreza’s method)

local currentDataVersion = 2.0
local badDataVersions = { ["1.5"] = true;}  --talked about more later in section 1.0.2
local playerData = {
	SaveIndex = 0; --berreza method has us increments this each time we save data
	LastSave = tick(); --nice to know

	Version = currentDataVersion;
	Versions = {};

	...
	--and more keys of data that you use
}

To add data versioning rollback support, we use the Versions table, that contains previous version keys. We use this to backtrack if needed, and it’s a good idea to start it out with a stable version by default. The entries should also be sorted from newest versions first, to old versions at the end. We will always add new versions to the front.

local playerData = LoadPlayerData(player) -- or wherever you are holding the players Data
playerData["Versions"] = {
	{
		{
			Version = currentDataVersion;
			SaveIndex = playerData["SaveIndex"];
		}	
	}
}

Loading Data

1.0.2

When we are loading data, we have access to the previous data versions that are stored in
playerData["Versions"]. We need to make sure the most recent version is not a bad version. In section 1.0.1, we created a variable that holds a dictionary of ‘bad data versions’.

We can loop through the the versions, and remove the bad versions. You load data as you normally would, and then we verify the data’s version history.

local done = false
if playerData["Versions"] == nil then
	playerData["Versions"]  = {}
end

if #playerData["Versions"] == 0 then
	done = true --nothing to remove
end

while not done do
	local entry = playerData["Versions"][1]
	local version = tostring(entry.Version)
	if badDataVersions[version] then
		table.remove(playerData["Versions"],1)
	else
		done = true
	end

	if #playerData["Versions"] == 0  then
		done = true
	end
end

Now that all the bad versions are removed from the list, we either have no versions that are saved, or we have the most recent allowed version that was saved in the front of the table. We can then finalize the loading data.

if #playerData["Versions"] == 0 then
	--There are no versions to load data from
	--Lets hope that playerData.Version is valid, or else we have to create default data :(

	if playerData.Version then
		local key = tostring(playerData.Version)
		if badDataVersions[key] then
			--Its a bad version, and the player has no other data to load, so we gotta load default data
			playerData = CreateDefailtData();
		else
			--its good data. we don't have to do anything else
		end
	else
		
	end	

	var SaveIndex = playerData.SaveIndex or 0;
	playerData = CreateDefaultData()
	playerData.SaveIndex = SaveIndex
else
	--At this point, we know the most recent version is a valid 
end

Saving Data

1.0.3

Whenever we are saving, we get the players data, and increment SaveIndex and LastSave

playerData["SaveIndex"] = playerData["SaveIndex"] + 1;
playerData["LastSave"] = tick();

We also need to check to see if the current version is needing to be saved into the Versions table. It might not be in the Versions table yet, so we need to check and create if needed.
(If you loaded data using the above implementation, playerData.Versions should at least have one good entry in it, and not be 0)

if #playerData["Versions"] > 0 then
	if playerData["Versions"][1]["Version"]] == currentDataVersion then 
		--We don't have to insert the newest version into playerData.Versions. It's already there
		playerData["Versions"][1]["SaveIndex"]  = SaveIndex
	else
		--We don't have the latest version saved, so we create space for it at the front of playerData.Versions
		if playerData["Versions"][1]["Version"] < currentDataVersion then
			--its an older version that they can revert to later
			--since our new version is higher, we add it to the front of the Versions table
			table.insert(playerData["Versions"],0,
				{Version = currentDataVersion;SaveIndex = SaveIndex}
			)
		else
			--This else should never happen. If loaded correctly
		end
	end
else
	--There are no versions in the table. 
	-- We could add the current version if it wasn't added earlier
	table.insert(playerData["Versions"],0,
		{Version = currentDataVersion;SaveIndex = SaveIndex}
	)
end

and thats it! If done correctly, you should be able to revert data back to older data versions!

Example Place

I made an example place that roughly implements versioning. I might adjust the above code or the place code later on to make it easier to understand.

https://www.roblox.com/games/7055745980/Datastore-Versioning-Example

Need more help?

Feel free to ask questions on this post, and I'll try to get back to you. I will probably be making an open source example place in a few days.
12 Likes

This is an useful tutorial if you’re using the berezaa method already, but I don’t recommend using it anymore, sure it was useful in 2018, 2019… But now, we’re supposed to get versioning in normal datastores soon, and with the berezaa method, you also have some issues with Data Protection Law because you need to use :RemoveAsync on each key, that shouldn’t be the case for when that feature releases. I don’t know exactly how it’ll work, but I can assure you using the berezaa method now, will not let you move away from that easily.

If anything, you should “invest” in session-locking for now.

Anyway this is good information for anyone already using custom versioning, but just feel like that should be clarified.

5 Likes