Need help preventing data loss / possibly saving in a way better way

Hello, I’ve been working on an ‘‘AFK FOR UGC’’ game. The drop went well, but I’ve noticed a couple people experiencing data loss.

I’d like some recommendations on how to update my current data module and possibly save data in a better way in the future.

ModuleScript in ServerStorage:

local DataStoreService: DataStoreService = game:GetService("DataStoreService")
local MarketplaceService: MarketplaceService = game:GetService("MarketplaceService")
local Players: Players = game:GetService("Players")

local dataStores = {
	-- Misc
	["Time"] = DataStoreService:GetDataStore("PlayerTime");
	["Redeemed"] = DataStoreService:GetDataStore("PlayerRedeemed");
	["Vip Bonus"] = DataStoreService:GetDataStore("ReceivedVipBonus");
	
	-- Character Position
	["Last Position X"] = DataStoreService:GetDataStore("PlayerPositionX");
	["Last Position Y"] = DataStoreService:GetDataStore("PlayerPositionY");
	["Last Position Z"] = DataStoreService:GetDataStore("PlayerPositionZ");
	
	-- Boost Potions
	["Red Boost"] = DataStoreService:GetDataStore("PlayerRedBoost");
	["Blue Boost"] = DataStoreService:GetDataStore("PlayerBlueBoost");
	["Green Boost"] = DataStoreService:GetDataStore("PlayerGreenBoost");
	["Pink Boost"] = DataStoreService:GetDataStore("PlayerPinkBoost");
	["Purple Boost"] = DataStoreService:GetDataStore("PlayerPurpleBoost");
	["Golden Boost"] = DataStoreService:GetDataStore("PlayerGoldenBoost");
}

local dataModule = {}

function dataModule.getData(player: Player, dataName: string)
	local data
	
	local success, errorMessage = pcall(function()
		local dataKey: string = "Player_"..player.UserId
		local dataIndex = dataStores[dataName]
		
		data = dataIndex:GetAsync(dataKey)
		
		if not data and dataName == "Time" then -- Defaults new player time to 0
			data = 0
		elseif not data and dataName == "Redeemed" then -- Defaults new player redeemed to 0
			data = 0
			
		elseif not data and dataName == "Last Position X" then
			data = 0
		elseif not data and dataName == "Last Position Y" then
			data = 0
		elseif not data and dataName == "Last Position Z" then
			data = 0
			
		elseif not data and dataName == "Red Boost" then -- Defaults new player Red Boost time to 0
			data = 0
		elseif not data and dataName == "Blue Boost" then -- Defaults new player Blue Boost time to 0
			data = 0
		elseif not data and dataName == "Green Boost" then -- Defaults new player Green Boost time to 0
			data = 0
		elseif not data and dataName == "Pink Boost" then -- Defaults new player Pink Boost time to 0
			data = 0
		elseif not data and dataName == "Purple Boost" then -- Defaults new player Purple Boost time to 0
			data = 0
		elseif not data and dataName == "Golden Boost" then -- Defaults new player Golden Boost time to 0
			data = 0
		end
		
		return data
	end)
	
	if success then
		return data
	else
		warn("Error while getting data: " .. errorMessage)
	end
end

function dataModule.saveData(player: Player, dataName: string, newValue: any)
	local data

	local success, errorMessage = pcall(function()
		local dataKey: string = "Player_"..player.UserId
		
		local dataIndex = dataStores[dataName]
		
		data = dataIndex:SetAsync(dataKey, newValue)
		return data
	end)

	if success then
		return
	else
		warn("Error while saving data: " .. errorMessage)
	end
end

return dataModule

Script in ServerScriptService:

local Players: Players = game:GetService("Players")
local RunService: RunService = game:GetService("RunService")
local ServerStorage: ServerStorage = game:GetService("ServerStorage")

local dataModule = require(ServerStorage:WaitForChild("DataModule"))

local function onPlayerAdded(player: Player)
	player:SetAttribute("IsLoaded", false)
	
	local playerStats: Folder = Instance.new("Folder")
	playerStats.Name = "leaderstats"
	playerStats.Parent = player
	
	local playerTime: IntValue = Instance.new("IntValue")
	playerTime.Name = "Time"
	playerTime.Parent = playerStats
	
	local playerRedeemed: IntValue = Instance.new("IntValue")
	playerRedeemed.Name = "Redeemed"
	playerRedeemed.Parent = playerStats
	
	-- Boost Potions
	player:SetAttribute("RedBoostTime", 0)
	player:SetAttribute("BlueBoostTime", 0)
	player:SetAttribute("GreenBoostTime", 0)
	player:SetAttribute("PinkBoostTime", 0)
	player:SetAttribute("PurpleBoostTime", 0)
	player:SetAttribute("GoldenBoostTime", 0)
	
	local success, errorMessage = pcall(function()
		playerTime.Value = dataModule.getData(player, "Time")
		playerRedeemed.Value = dataModule.getData(player, "Redeemed")
		player:SetAttribute("IsLoaded", true)
		
		player:SetAttribute("RedBoostTime", dataModule.getData(player, "Red Boost"))
		player:SetAttribute("BlueBoostTime", dataModule.getData(player, "Blue Boost"))
		player:SetAttribute("GreenBoostTime", dataModule.getData(player, "Green Boost"))
		player:SetAttribute("PinkBoostTime", dataModule.getData(player, "Pink Boost"))
		player:SetAttribute("PurpleBoostTime", dataModule.getData(player, "Purple Boost"))
		player:SetAttribute("GoldenBoostTime", dataModule.getData(player, "Golden Boost"))
	end)
	
	if not success then
		warn("Error while loading player's stats: " .. errorMessage)
		player:Kick("Unable to load data, to avoid data corruption, your data was not saved.")
	end
	
	local function autoSaveTime()
		while task.wait(60) do
			dataModule.saveData(player, "Time", playerTime.Value)
			
			-- Boost Potions
			dataModule.saveData(player, "Red Boost", player:GetAttribute("RedBoostTime"))
			dataModule.saveData(player, "Blue Boost", player:GetAttribute("BlueBoostTime"))
			dataModule.saveData(player, "Green Boost", player:GetAttribute("GreenBoostTime"))
			dataModule.saveData(player, "Pink Boost", player:GetAttribute("PinkBoostTime"))
			dataModule.saveData(player, "Purple Boost", player:GetAttribute("PurpleBoostTime"))
			dataModule.saveData(player, "Golden Boost", player:GetAttribute("GoldenBoostTime"))
		end
	end
	
	local autoSaveCoroutine = coroutine.create(autoSaveTime)
	coroutine.resume(autoSaveCoroutine)
	
	local function onPlayerRemoving(playerRemoved: Player)
		if playerRemoved ~= player then return end
		coroutine.close(autoSaveCoroutine)
		
		dataModule.saveData(player, "Time", playerTime.Value)
		dataModule.saveData(player, "Redeemed", playerRedeemed.Value)
		
		-- Boost Potions
		dataModule.saveData(player, "Red Boost", player:GetAttribute("RedBoostTime"))
		dataModule.saveData(player, "Blue Boost", player:GetAttribute("BlueBoostTime"))
		dataModule.saveData(player, "Green Boost", player:GetAttribute("GreenBoostTime"))
		dataModule.saveData(player, "Pink Boost", player:GetAttribute("PinkBoostTime"))
		dataModule.saveData(player, "Purple Boost", player:GetAttribute("PurpleBoostTime"))
		dataModule.saveData(player, "Golden Boost", player:GetAttribute("GoldenBoostTime"))
	end
	
	Players.PlayerRemoving:Connect(onPlayerRemoving)
end

Players.PlayerAdded:Connect(onPlayerAdded)

game:BindToClose(function()
	if RunService:IsStudio() then return end
	
	for _, player: Player in Players:GetPlayers() do
		local playerStats: Folder? = player:FindFirstChild("leaderstats") :: Folder
		local playerTime: IntValue? = if playerStats and playerStats:FindFirstChild("Time") then playerStats:FindFirstChild("Time") :: IntValue else nil
		local playerRedeemed: IntValue? = if playerStats and playerStats:FindFirstChild("Redeemed") then playerStats:FindFirstChild("Redeemed") :: IntValue else nil
		if not playerTime or not playerRedeemed then return end
		
		dataModule.saveData(player, "Time", playerTime.Value)
		dataModule.saveData(player, "Redeemed", playerRedeemed.Value)
		
		-- Boost Potions
		dataModule.saveData(player, "Red Boost", player:GetAttribute("RedBoostTime"))
		dataModule.saveData(player, "Blue Boost", player:GetAttribute("BlueBoostTime"))
		dataModule.saveData(player, "Green Boost", player:GetAttribute("GreenBoostTime"))
		dataModule.saveData(player, "Pink Boost", player:GetAttribute("PinkBoostTime"))
		dataModule.saveData(player, "Purple Boost", player:GetAttribute("PurpleBoostTime"))
		dataModule.saveData(player, "Golden Boost", player:GetAttribute("GoldenBoostTime"))
	end
end)

Thanks in advance!

Hi! I suggest you use ProfileService, which is a great DataStore Module that implements data-loss prevention measures, and session-locking, etc.

This will require you to use a new DataStore (I think), and you will have to implement a way to port legacy data to the new one using ProfileService.

1 Like

Hello, sorry but I’m not into ProfileService. I’ve been trying to use it, but I went with doing it my own way (which should be perfectly possible). I’m currently trying to fix these data loss issues / improve my current code and make sure everything works correctly.

It’s because when u save, the function will yield and because the player is leaving the player parameter will eventually be nil causing the player to lose data. Also, these are a lot of SetAsyncs you’re doing why not save it to one table?

1 Like

Well, if that’s what you want then I recommend storing all data in one table, and then saving it into one DataStore instead of having a separate DataStore for each data entry.

For example:

playerData = {
    Time = 29,
    Redeemed = 2,
    Boosts = {
        ["Red Boost"] = 2939,
        ["Green Boost"] = 9484,
        ...
    }
}

In addition, you can implement auto-save every X minutes in case the PlayerRemoving one fails.

1 Like

We’re using autosave. This was not included in the code samples I’ve sent. I will now implement this, thanks!

If you want, you can use extrenal databases like firebase, it gives some features that DataStoreService doesn’t have.

1 Like