Data sometimes doesn't save when afking for too long

So I’ve recently some feedbacks from my community, and some players are complaining about their data not saving when afking for too long. I don’t know what is causing this, and I don’t know what I’ve done wrong too
(perhaps the :BindToClose event not coded right?)

This is my script :

-------------------- CONFIG --------------------

local DataStore_Name = "Data V2"

local Default_Luck = 2
local Default_Speed = 1
local Default_Coins = 1

local Default_SlotsAmount = 5

-------------------- VARIABLES --------------------

local DSS = game:GetService("DataStoreService")
local DS = DSS:GetDataStore(DataStore_Name)
local SessionData = {}

local Modules = script.Parent.Parent:WaitForChild("Modules")
local PlayerFunctions = require(Modules.PlayerFunctions)

local GameShutdown = false

-------------------- FUNCTIONS --------------------

local function GiveQuest(Player, QuestInfos)
	local Quests = Player:WaitForChild("Quests")
	task.wait(1.1)
	if not Quests:FindFirstChild(QuestInfos.Title) then
		local Done = false
		local NewQuest = PlayerFunctions.CreateFolder(Quests, QuestInfos.Title)
		local QuestTitle = PlayerFunctions.CreateValue(NewQuest, "StringValue", "Title", QuestInfos.Title)
		local QuestTask = PlayerFunctions.CreateValue(NewQuest, "StringValue", "Task", QuestInfos.Task)
		local QuestGoal = PlayerFunctions.CreateValue(NewQuest, "IntValue", "Goal", QuestInfos.Goal)
		local QuestGoalCurrency = PlayerFunctions.CreateValue(NewQuest, "StringValue", "GoalCurrency", QuestInfos.GoalCurrency)
		local Reward_Gold = PlayerFunctions.CreateValue(NewQuest, "IntValue", "Reward_Gold", QuestInfos.Reward_Gold)
		local QuestProgress = PlayerFunctions.CreateValue(NewQuest, "IntValue", "Progress", 0)
		for i, Table in pairs(SessionData[Player.UserId]) do -- "Quests"
			if tostring(type(Table)) == "table" then
				for i, SubTable in pairs(Table) do -- "QuestFolder"
					if tostring(type(SubTable)) == "table" then
						if SubTable[1] == QuestTitle.Value then
							for i, ValueTable in pairs(SubTable) do -- "Title", etc
								if tostring(type(ValueTable)) == "table" then
									if ValueTable[1] == "Progress" then
										QuestProgress.Value = ValueTable[2]
									end
								end
							end
						end
					end
				end
			end
		end
		if QuestInfos.Reward_Aura then
			if QuestInfos.Reward_Aura ~= "" then
				local Reward_Aura = PlayerFunctions.CreateValue(NewQuest, "StringValue", "Reward_Aura", QuestInfos.Reward_Aura)
			end
		end
		if Quests:FindFirstChild(NewQuest.Name) then
			Player:WaitForChild("leaderstats"):WaitForChild(QuestInfos.GoalCurrency):GetPropertyChangedSignal("Value"):Connect(function()
				if not Done then
					QuestProgress.Value += 1
					if QuestProgress.Value >= QuestGoal.Value then
						Done = true
						Player:WaitForChild("leaderstats"):WaitForChild("Gold").Value += Reward_Gold.Value
						if QuestInfos.Reward_Aura then
							PlayerFunctions.GiveAura(Player, QuestInfos.Reward_Aura, 1)
						end
						NewQuest:Destroy()
					end
				end
			end)
		end
	end
end

local function SaveValues(Player, Value, Folder)
	task.spawn(function()
		Value:GetPropertyChangedSignal("Value"):Connect(function() -- Update the value when it changes
			for i, Table in pairs(SessionData[Player.UserId]) do -- Loop trough the Data
				if type(Table == "table") then -- If v is a table
					if tostring(Table[1]) == Folder.Name then
						if type(Table[2] == "table") then
							for i, v in pairs(Table[2]) do
								if tostring(v[1]) == Value.Name then
									if v[2] then
										v[2] = Value.Value
										print(v[1].." is now "..v[2])
									end
									break
								end
							end
						end
						break
					end
				end
			end
		end)
	end)
end

local function SaveFolder(Player, Folder)
	task.spawn(function()
		for i, Table in pairs(SessionData[Player.UserId]) do -- Loop trough the Data
			local Success, Error = pcall(function()
				if type(Table == "table") then
					if type(Table[1] == "string") then -- If the name of the table corresponds with the Folder
						if tostring(Table[1]) == Folder.Name then
							for i, v in pairs(Folder:GetDescendants()) do -- Get everything inside the folder
								if v:IsA("ValueBase") then -- If it's a value
									task.spawn(function()
										local AlreadyExists = false
										for i, ExistingValue in pairs(Table[2]) do
											if ExistingValue[1] == v.Name then
												AlreadyExists = true
											end
										end
										if not AlreadyExists then
											table.insert(Table[2], {v.Name, v.Value}) -- Insert the value into the data
										end
									end)
									task.spawn(function()
										v:GetPropertyChangedSignal("Value"):Connect(function()
											for i, ValueTable in pairs(Table[2]) do
												if tostring(ValueTable[1]) == v.Name then
													ValueTable[2] = v.Value
													break
												end
											end
										end)
									end)
								end
							end
						end
					end
				end
			end)
		end
	end)
end

local function SetData(Player, Table)
	local Potions = Player:WaitForChild("Potions")
	local Gears = Player:WaitForChild("Gears")
	local Collection = Player:WaitForChild("Collection")
	local QuestInfos = {
		["Title"] = "",
		["Task"] = "",
		["Goal"] = 0,
		["GoalCurrency"] = "",
		["Reward_Gold"] = 0,
		["Reward_Aura"] = "",
	}
	for i, v in pairs(Table[2]) do
		if v[1] ~= nil then
			print("Loaded "..v[1].." ("..v[2]..")")
			if Table[1] == "Auras" then
				if not Player:WaitForChild("Auras"):FindFirstChild(v[1]) then
					local Aura = Instance.new("IntValue", Player:WaitForChild("Auras"))
					Aura.Name = v[1]
					Aura.Value = 1
					PlayerFunctions.GiveAura(Player, v[1], v[2] - 1)
				end
			elseif Table[1] == "Potions" then
				if not Player:WaitForChild("Potions"):FindFirstChild(v[1]) then
					PlayerFunctions.CreateValue(Potions, "IntValue", v[1], v[2])
				end
			elseif Table[1] == "Gears" then
				if not Player:WaitForChild("Gears"):FindFirstChild(v[1]) then
					PlayerFunctions.CreateValue(Gears, "StringValue", v[1], v[2])
				end
			elseif Table[1] == "Collection" then
				if not Collection:FindFirstChild(v[1]) then
					PlayerFunctions.CreateValue(Collection, "StringValue", v[1], v[2])
				end
			elseif Table[1] == "Quests" then
				local Info = v[1]
				QuestInfos[Info] = v[2]
			end
		end
	end
	warn("QuestInfos: ", QuestInfos.Title, QuestInfos.Task, QuestInfos.Goal, QuestInfos.Progress, QuestInfos.GoalCurrency, QuestInfos.GoldReward, QuestInfos.AuraReward)
	if QuestInfos.Title ~= "" then
		task.spawn(function()
			task.wait(1)
			GiveQuest(Player, QuestInfos)
		end)
	end
end

local function RemoveFromData(Player : Player, ItemName : string)
	for i, Table in pairs(SessionData[Player.UserId]) do
		local Succ, Err = pcall(function()
			if type(Table == "table") then
				if type(Table[2] == "table") then
					for i, v in pairs(Table[2]) do
						if type(v == "table") and v[1] == ItemName then
							print(v[1].." removed from Data")
							v[1] = nil
							print(v[1])
						end
					end
				elseif type(Table[2] ~= "table") then
					print(Table[1].." removed from Data")
					Table = nil
				end
			end
		end)
	end
end

-------------------- EVENTS --------------------

game.Players.PlayerAdded:Connect(function(Player)

	local Success = nil
	local PlayerData = nil
	local Attempt = 1

	local DefaultData = {
		{"Gold", 0},
		{"Rolls", 0},
		{"Aura", ""},
		{"Gear", ""},

		{"Speed", Default_Speed},
		{"Luck", Default_Luck},
		{"Coins", Default_Coins},

		{"Auras", {}},
		{"Gears", {}},
		{"Potions", {}},
		{"Quests", {}},
		{"Titles", {}},

		{"Storage", 0},
		{"MaxStorage", Default_SlotsAmount},
		{"LuckBoostDuration", 0},
		{"SpeedBoostDuration", 0},
		{"CoinsBoostDuration", 0},
		{"Banned", false},
		{"Title", "PLAYER"},

		{"AutoSkip", 0},
		{"AutoEquip", 0},

		{"Collection", {}},
	}

	-------------------- GETTING DATA --------------------

	repeat
		Success, PlayerData = pcall(function()
			return DS:GetAsync(Player.UserId)
		end)
		Attempt += 1
		if not Success then
			warn(PlayerData)
			task.wait(2)
		end
	until Success or Attempt == 5

	if Success then
		print("Connected to DataBase")
		if not PlayerData then
			print("Assigning default Data")
			PlayerData = DefaultData
		end
		SessionData[Player.UserId] = PlayerData
	else
		Player:Kick("Unable to load your data. Please try again later.")
	end

	local function UpdateData(Val)
		if Val:IsA("Folder") then

			-------------------- SAVE VALUES --------------------

			task.spawn(function()

				Val.ChildRemoved:Connect(function(Child)
					RemoveFromData(Player, Child.Name)
				end)

				Val.ChildAdded:Connect(function(Child) -- When a value is added into a folder
					if Child:IsA("ValueBase") then
						SaveValues(Player, Child, Val)
						for i, Table in pairs(SessionData[Player.UserId]) do -- Loop trough the Data
							if type(Table == "table") then -- If v is a table
								local Error, Success = pcall(function()
									if tostring(Table[1]) == Val.Name then -- Auras
										if type(Table[2] == "table") then
											task.spawn(function()
												local AlreadyExists = false
												for i, ExistingValue in pairs(Table[2]) do
													if ExistingValue[1] == Child.Name then
														AlreadyExists = true
													end
												end
												if not AlreadyExists then
													table.insert(Table[2], {Child.Name, Child.Value}) -- Insert the value into the data
													print(Child.Name, Child.Value)
												end
											end)
										end
									end
								end)
							end
						end

						-------------------- SAVE FOLDERS --------------------

					elseif Child:IsA("Folder") then
						SaveFolder(Player, Val)
					end
				end)

			end)

			-------------------- LOAD FOLDERS --------------------

			task.spawn(function()
				for i, Table in pairs(SessionData[Player.UserId]) do
					if tostring(type(Table)) == "table" then
						if tostring(Table[1]) == Val.Name then -- Auras
							if type(Table[2] == "table") then
								SetData(Player, Table)
							end
							break
						end
					end
				end
			end)

		else

			-------------------- LOADING VALUES --------------------

			local Success, ErrorMessage = pcall(function()
				for i, Table in pairs(SessionData[Player.UserId]) do
					if tostring(type(Table)) == "table" then
						if Table[1] == Val.Name then -- Auras
							if type(Table[2] == "number") then
								Val.Value = Table[2]
							end
							break
						end
					end
				end
			end)

			if ErrorMessage then
				print(Val, ":", ErrorMessage)
			end

			Val:GetPropertyChangedSignal("Value"):Connect(function()
				for i, v in pairs(SessionData[Player.UserId]) do
					if tostring(type(v)) == "table" then
						if v[1] == Val.Name then
							if type(v[2] == "number") then
								v[2] = Val.Value
							elseif type(v[2] == "table") then
								for _, e in pairs(v[2]) do
									e[2] = Val.Value
								end
							end
							break
						end
					end
				end
			end)

		end
	end

	-------------------- STATS --------------------

	local Leaderstats = Player:WaitForChild("leaderstats")
	local Gold = Leaderstats:WaitForChild("Gold")
	local Rolls = Leaderstats:WaitForChild("Rolls")
	local Aura = Leaderstats:WaitForChild("Aura")
	local Gear = Leaderstats:WaitForChild("Gear")
	local Upgrades = Player:WaitForChild("Upgrades")
	local Autoroll = Upgrades:WaitForChild("Autoroll")
	local Quickroll = Upgrades:WaitForChild("Quickroll")
	local Speed = Upgrades:WaitForChild("Speed")
	local Luck = Upgrades:WaitForChild("Luck")
	local Coins = Upgrades:WaitForChild("Coins")
	local Auras = Player:WaitForChild("Auras")
	local Gears = Player:WaitForChild("Gears")
	local Potions = Player:WaitForChild("Potions")
	local Quests = Player:WaitForChild("Quests")
	local Titles = Player:WaitForChild("ChatTitles")
	local Values = Player:WaitForChild("Values")
	local Storage = Values:WaitForChild("Storage")
	local MaxStorage = Values:WaitForChild("MaxStorage")
	local IsDialoguing = Values:WaitForChild("IsDialoguing")
	local CanRoll = Values:WaitForChild("CanRoll")
	local LuckBoostDuration = Values:WaitForChild("LuckBoostDuration")
	local SpeedBoostDuration = Values:WaitForChild("SpeedBoostDuration")
	local CoinsBoostDuration = Values:WaitForChild("CoinsBoostDuration")
	local IsInArena = Values:WaitForChild("IsInArena")
	local AutoEquip = Values:WaitForChild("AutoEquip")
	local Banned = Values:WaitForChild("Banned")
	local AutoSkip = Values:WaitForChild("AutoSkip")
	local AutoEquip = Values:WaitForChild("AutoEquip")
	local Title = Values:WaitForChild("Title")
	local Collection = Player:WaitForChild("Collection")

	-------------------- UPDATE DATA --------------------

	task.spawn(function()
		UpdateData(Gold)
		UpdateData(Rolls)
		UpdateData(Aura)
		UpdateData(Gear)
		UpdateData(Luck)
		UpdateData(Speed)
		UpdateData(Coins)
		UpdateData(Auras)
		UpdateData(Gears)
		UpdateData(Potions)
		UpdateData(Titles)
		UpdateData(Quests)
		UpdateData(Storage)
		UpdateData(MaxStorage)
		UpdateData(LuckBoostDuration)
		UpdateData(SpeedBoostDuration)
		UpdateData(CoinsBoostDuration)
		UpdateData(Banned)
		UpdateData(AutoSkip)
		UpdateData(AutoEquip)
		UpdateData(Title)
		UpdateData(Collection)
		
	end)

	-------------------- STORAGE --------------------

	Storage.Value = #Auras:GetChildren()
	SessionData[Player.UserId]["Storage"] = Storage.Value

	Auras.ChildAdded:Connect(function(Child)
		Storage.Value = #Auras:GetChildren()
		SessionData[Player.UserId]["Storage"] = Storage.Value
	end)

	Auras.ChildRemoved:Connect(function(Child)
		Storage.Value = #Auras:GetChildren()
		SessionData[Player.UserId]["Storage"] = Storage.Value
	end)

end)

-------------------- SAVE DATA --------------------

game.Players.PlayerRemoving:Connect(function(Player)
	if not GameShutdown then
		if SessionData[Player.UserId] then
			local Success = nil
			local ErrorMessage = nil
			local Attempt = 1

			for i, Table in pairs(SessionData[Player.UserId]) do -- "Quests"
				if tostring(type(Table)) == "table" then
					for i, SubTable in pairs(Table) do -- "QuestFolder"
						if tostring(type(SubTable)) == "table" then
							for i, ValueTable in pairs(SubTable) do -- "Title", etc
								if tostring(type(ValueTable)) == "table" then
									print("Saved", tostring(ValueTable[1]), "("..tostring(ValueTable[2])..")")
								end
								break
							end
						end
						break
					end
				end
			end

			repeat
				Success, ErrorMessage = pcall(function()
					DS:SetAsync(Player.UserId, SessionData[Player.UserId])
				end)
				Attempt += 1
				if not Success then
					warn(ErrorMessage)
					task.wait(2)
				end
			until Success or Attempt == 5
		end
	end
end)

-------------------- GIVE QUEST --------------------

game:GetService("ReplicatedStorage"):WaitForChild("RemoteEvents"):WaitForChild("GiveQuest").OnServerEvent:Connect(function(Player, QuestInfos)
	local QuestTable = {
		["Title"] = "",
		["Task"] = "",
		["Goal"] = 0,
		["GoalCurrency"] = "",
		["Reward_Gold"] = 0,
		["Reward_Aura"] = "",
	}
	QuestTable.Title = QuestInfos[1]
	QuestTable.Task = QuestInfos[2]
	QuestTable.Goal = QuestInfos[3]
	QuestTable.GoalCurrency = QuestInfos[4]
	QuestTable.Reward_Gold = QuestInfos[5]
	if QuestInfos[6] then
		QuestTable.Reward_Aura = QuestInfos[6]
	end
	GiveQuest(Player, QuestTable)
end)

-------------------- SAVE ON SHUTDOWN --------------------

game:BindToClose(function()
	GameShutdown = true
	task.wait(5)
end)

Thank you for any help!

2 Likes

Please use or in if statements

Loop through the table of things with for i, v do loop instead

for _, item in ipairs({item1, item2, item3}) do
    UpdateData(item)
end

Put save function inside of bind to close, it will properly wait until everything is saved, do not use spawn() there

2 Likes

Should I save the Data before the GameShutdown = true or after?
And how would I get the player in order to do DS:SetAsync(Player.UserId, Key?

You need to save data for all players in BindToClose, just like @LvieReal said.
You can iterate over an array of them.

local players = game:GetService("Players")
local runService = game:GetService("RunService")

game:BindToClose(function()
    --for example purposes, I called the saving procedure "save"
    if runService:IsStudio() or #players:GetPlayers() <= 1 then task.wait(3) return nil end

    for _, player in ipairs(players:GetPlayers()) do
        save(player)
    end
    task.wait(3)
end)

Also, you should use UpdateAsync instead of SetAsync. SetAsync should be used with caution, when overwriting data, or writing to stores that mimic data (leaderboards, etc.). With UpdateAsync, you can compare data and more efficiently check for missing data elements to help prevent data loss.

Even if you don’t insert this extra code, UpdateAsync has some safety features which SetAsync doesn’t.

1 Like

Seems to work fine with the new changes. Hopefully the Data Loss won’t happen anymore.

This is my current code using your help just to make sure I understood correctly :

game:BindToClose(function()
	if game:GetService("RunService"):IsStudio() or #game:GetService("Players"):GetPlayers() <= 1 then task.wait(3) return nil end
	GameShutdown = true
	for _, Player in ipairs(game:GetService("Players"):GetPlayers()) do
		DS:UpdateAsync(Player.UserId, SessionData[Player.UserId])
	end
	task.wait(5)
end)
1 Like

Not quite. UpdateAsync takes the second parameter as a transform function, and also still needs a pcall.

Also, try to get the services before going into the function.

local players = game:GetService("Players")
local runService = game:GetService("RunService")

game:BindToClose(function()
    if runService:IsStudio() or #players:GetPlayers() <= 1 then task.wait(3) return nil end

    for _, player in ipairs(players:GetPlayers()) do
        local success, result = pcall(DS.UpdateAsync, DS, Player.UserId, function(old)
            return SessionData[Player.UserId]
        end)
    end
    task.wait(3)
end)
1 Like

The bug still persist, even after he updated his script. I was wondering if eventually ProfileService could resolve the problem.
I heard that the module is really powerful and data loss will not happens anymore. Any thoughts ?

I think this might solve my datasaving issues in my game