My game has been getting several reports of data loss all of a sudden and it seems to be partial. I’m very confused on how this would happen, I’m guessing an issue with not loading/saving data fast enough or smth.
My loadData function looks like this:
--load player data
local function loadData(player)
print(player)
local success = nil
local playerData = nil
local attempt = 1
repeat
success, playerData = pcall(function()
return dataBase:GetAsync(player.UserId)
end)
attempt += 1
if not success then
warn(playerData)
task.wait()
end
until success or attempt == 3
if success then
if not playerData then--give default data if they're new
playerData = {
["Gems"] = 0,
["SelectedTowers"] = {"Cameraguy"},
["OwnedTowers"] = {"Cameraguy"},
["MaxTowers"] = 5,
["RedeemedCodes"] = {},
["Wins"] = 0,
["Snowflakes"] = 0,
}
beamEvent:FireClient(player)
end
data[player.UserId] = playerData
isLoaded[player] = true
Saving data looks like this:
--save player data
local function saveData(player)
if data[player.UserId] and player.UserId ~= 0 and isLoaded[player] then
local success, errorInfo = pcall(function()
dataBase:UpdateAsync(player.UserId, function(oldData)
if oldData then
-- Update only the necessary fields to avoid overwriting
oldData.Gems = data[player.UserId].Gems
oldData.SelectedTowers = data[player.UserId].SelectedTowers
oldData.OwnedTowers = data[player.UserId].OwnedTowers
oldData.MaxTowers = data[player.UserId].MaxTowers
oldData.RedeemedCodes = data[player.UserId].RedeemedCodes
oldData.Wins = data[player.UserId].Wins
oldData.Snowflakes = data[player.UserId].Snowflakes
return oldData
else
-- If no existing data, use the entire data table
return data[player.UserId]
end
end)
end)
if success then
print("Data saved successfully for player:", player.UserId)
else
warn("Unable to save data for player:", player.UserId, errorInfo)
end
else
warn("No session data for player:", player.UserId)
end
isLoaded[player] = nil
end
Players.PlayerRemoving:Connect(saveData)
game:BindToClose(function() --if game shuts down
if not RunService:IsStudio() then
print("Shutting down")
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(function()
saveData(player)
end)
end
else
print("Shutting down inside studio")
end
end)
Basically, sometimes when players join, a lot of their inventory (OwnedTowers) is wiped (not all of it tho) This makes it rlly confusing for me since it rules out that it’s just not doing the saving or loading since it is but not fully for some reason
If anyone has any idea on how this could be caused please respond since I want this fixed asap, any help is appreciated!
Your example looks fine. The only thing that springs to mind would be the issue regarding saving mixed table data. IIrc, data stores share the same serialization limitations as remote events/functions. When you try to save a mixed table in the data store, some keys will be silently removed and will be gone when you try to reload the data.
In your example, OwnedTowers and SelectedTowers look like they’re used as an array. Are they consistently treated as an array throughout the rest of your code?
Please investigate further and figure out the exact problem first. Send us a before and after snapshot of your data (data on load vs data saved and reloaded next game). Based on what you described it’s hard to know if your data is getting ROLLED BACK, PARTIALLY SAVED, or SOMETHING ELSE. Each issue is solved in different ways and has different causes.
ROLLED BACK:
you join game
you have 1x stick, 2x rocks
you play for a bit
you leave the game with 1x pickaxe, 10x sticks, 20x rocks
you rejoin the game later
you find out you have 1x stick, 2x rocks again
PARTIAL SAVE:
you join game
you have 1x stick, 2x rocks
you play for a bit
you leave the game with 1x pickaxe, 10x sticks, 20x rocks
you rejoin the game later
you find out you have 1x pickaxe, 20x rocks, but you’re missing 10x sticks
This only applies to keys being indexed and only for the remote system. It’s fine if you have mixed values, but you can’t have mixed keys. It’s generally good practice to not mix numbers with string keys and people apply it to other systems (ie datastore), but I don’t think roblox’s documentation mentions it for datastore.
Roblox doesn’t mention it in the data stores documentation but they do share the same behavior. With mixed tables, keys will be dropped when you read them back from the data store (or rather, they’re dropped as soon as you save it to the data store).
It seem to happen because of the fast leave > join.
If your datas aren’t saved or didn’t finished to save when leaving and you fastly join a new server next, then it will load the old datas (the one before your last session) instead of the last datas.
You are checking if the current datas are loaded before saving, but you are not checking if the most recent datas was saved before loading.
Ok so I’m just going off what people have been saying as I wasn’t able to replicate the issue myself so I don’t have images either but I can rule out it’s not data getting rolled back because players are losing towers up to many sessions ago, so the data is getting partially wiped, only “other” thing I think could be the issue is this one error I got like
"error: "DataStoreService: InvalidUniverse: The provided universe is not valid. API: GetAsync, Data Store: database "
I save on both of them because PlayerRemoving, when the player leaves obviously, and BindToClose, when the game shuts down. Though what you said about a fallback method sounds good, what would it look like?
You can avoid saving twice by checking if a PlayerRemoving was already runned or not about the selected player, then the BindToClose function will only save unsaved player data.
When server shutdown or get closed, sometimes only BindToClose is running, sometimes both BindToClose and playerRemoving are running at same time, but playerRemoving is always runned at first, so it allow you to skip the save of all already saved players datas.
local PlayerRemoved = {}
PlayerService.PlayerRemoving:Connect(function(Player)
table.insert(PlayerRemoved, Player.UserId)
--save
table.remove(PlayerRemoved, Player.UserId)
end)
game:BindToClose(function()
for _, Player in pairs(PlayerService:GetPlayers()) do
if not table.find(PlayerRemoved, Player.UserId) then
--save
end
end
end)
If your playerbase isn’t complaining about losing gems/wins/snowflakes and only about OwnedTowers:
your data is being partially lost/corrupted
investigate the code for setting and removing OwnedTowers. It’s likely your inventory script isn’t working properly not the datastore.
If your playerbase IS complaining about losing gems/wins/snowflakes:
your data is getting rolled back
It could be that your :GetAsync is failing/erroring. In your code here you give up after 3 super fast tries. You need to rework it to be a bit smarter.
instead of trying it only 3 times and then giving up and letting the players play your game, add extra safety
instead of trying every task.wait(), try task.wait(1), keep trying indefinitely until the player leaves the game
if it’s not working, either kick the player, warn them, or pause gameplay (load screen) until it’s fixed
Same thing with your SAVE function, it’s trying once then giving up. Use the points above to make it better too.**
Fix this too
You almost have the code pattern properly implemented. This is why your code fails:
server shutdowns
BindToClose is called, server waits for this function to finish
there is no while/for loop to stall the BindToClose function
BindToClose ends up spawning a bunch of saveData functions and immediately terminates
server ends all running threads and shutdowns mid save
data loss
Fixed:
game:BindToClose(function() --if game shuts down
if not RunService:IsStudio() then
print("Shutting down")
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(function()
saveData(player)
end)
end
else
print("Shutting down inside studio")
end
while not next(isLoaded) do -- we keep waiting until all player datas are saved and closed by the spawned functions
task.wait(1)
end
end)
In code implementation, there is a check to see if the data has been saved or not yet. Because of this “debounce” check multiple calls to save is safe.
Wow, thanks for the really thorough response, I did some close examination and I think it’s an issue with the trading system, it has the most to do with data and OwnedTowers (it has several callings of loadData if the player isn’t loaded, sets OwnedTowers equal to a new array which could cause problems, etc)
luckily I still have the contacts of the guy (he made it like 6 months ago but since I’m only getting many reports now I think it’s an issue with both the system and roblox DataStore but with the Data part improved, I think this is this is the last thing to solve) who made it and I’m trying to get back in touch with him since he’s probably the most fitted guy to tamper with the code, but yeah, thanks for your help!