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.1First 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.2When 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.3Whenever 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