Datatstore very rarely not saving for players

some players in my discord server are reporting their entire data wiping, everything, obviously this is bad… not sure what’s going on and why it saves about 99% of the time, but is there anything wrong with my datastore?

local dss = game:GetService("DataStoreService")
local ds = dss:GetDataStore("PlayerData_1.2")
local yenLeaderboard = dss:GetOrderedDataStore("YenLeaderboard")

game.Players.PlayerAdded:Connect(function(plr)
	local plot = game.Workspace.Plots:FindFirstChild("Unclaimed")
	plot.Name = plr.Name
	plot.NameTag.BillboardGui.TextLabel.Text = plr.Name.."'s Base"
	
	plr.CharacterAdded:Connect(function()
		coroutine.wrap(function()
			print("pivoted character")
			plr.Character:PivotTo(plot.Lasers.Block.CFrame*CFrame.new(0,0,-10))

			for _,v in pairs(plr.Data:WaitForChild("ToolPasses"):GetChildren()) do
				if v.Value == true then
					local tool = game.ReplicatedStorage.ToolGamepasses[v.Name]:Clone()
					tool.Parent = plr.Backpack
				end
				
				v.Changed:Connect(function()
					if v.Value == true then
						local tool = game.ReplicatedStorage.ToolGamepasses[v.Name]:Clone()
						tool.Parent = plr.Backpack
					end
				end)
			end
		end)()
	end)
	
	local data = Instance.new("Folder",plr)
	data.Name = "Data"
	
	local bannedbool = Instance.new("BoolValue")
	bannedbool.Parent = data
	bannedbool.Name = "Banned"
	
	local codes = Instance.new("Folder",data)
	codes.Name = "Codes"
	
	for _,v in pairs(game.ReplicatedStorage.Codes:GetChildren()) do
		local codebool = Instance.new("BoolValue",codes)
		codebool.Parent = codes
		codebool.Name = v.Name
	end
	
	local leaderstats = Instance.new("Folder")
	leaderstats.Parent = plr
	leaderstats.Name = "leaderstats"
	local yenleaderstat = Instance.new("NumberValue")
	yenleaderstat.Parent = leaderstats
	yenleaderstat.Name = "Yen"
	local rebirthsleaderstat = Instance.new("NumberValue")
	rebirthsleaderstat.Parent = leaderstats
	rebirthsleaderstat.Name = "Rebirths"
	
	local yen = Instance.new("NumberValue",data)
	yen.Name = "Yen"
	yen.Value = 50
	yenleaderstat.Value = 50
	
	local timeleft = Instance.new("NumberValue",data)
	timeleft.Name = "TimeLeft"
	
	local rebirths = Instance.new("NumberValue",data)
	rebirths.Name = "Rebirths"
	
	local ownedcharacters = Instance.new("Folder",data)
	ownedcharacters.Name = "Characters"
	
	local items = Instance.new("Folder",data)
	items.Name = "Items"
	
	local yenmulti = Instance.new("NumberValue",data)
	yenmulti.Name = "YenMultiplier"
	yenmulti.Value = 1
	
	local quests = Instance.new("Folder",data)
	quests.Name = "Quests"
	
	local toolgamepasses = Instance.new("Folder",data)
	toolgamepasses.Name = "ToolPasses"
	for _,v in pairs(game.ReplicatedStorage.ToolGamepasses:GetChildren()) do
		local toolbool = Instance.new("BoolValue",toolgamepasses)
		toolbool.Name = v.Name
	end
	
	for _,v in pairs(game.ReplicatedStorage.Quests:GetChildren()) do
		local questbool = Instance.new("BoolValue",quests)
		questbool.Name = v.Name
		questbool.Value = false
		
		local claimed = Instance.new("BoolValue",questbool)
		claimed.Name = "Claimed"..v.Name
		claimed.Value = false
	end
	
	local statsfolder = Instance.new("Folder",data)
	statsfolder.Name = "Stats"
	local CharactersSold = Instance.new("NumberValue",statsfolder) CharactersSold.Name = "CharactersSold"
	local Steals = Instance.new("NumberValue",statsfolder) Steals.Name = "Steals"
	
	local index = Instance.new("Folder",data)
	index.Name = "Index"
	for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
		if v:IsA("Model") then
			local indexed = Instance.new("BoolValue",index)
			indexed.Name = v.Name
		end
	end
	
	for i=1,10 do
		local character = Instance.new("StringValue",ownedcharacters)
		character.Name = "Character"..i
		
		local amounttoclaim = Instance.new("NumberValue",character)
		amounttoclaim.Name = "CashGenerated".."Character"..i
		
		local mutation = Instance.new("StringValue",character)
		mutation.Name = "Mutation".."Character"..i
		
		--ignore this its just to load the characters when they're changed
		character.Changed:Connect(function(val)
			if val == "" then
				if workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name):FindFirstChildWhichIsA("Model") then
					workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name):FindFirstChildWhichIsA("Model"):Destroy()
				end
				mutation.Value = ""
				amounttoclaim.Value = 0
			else
				index:FindFirstChild(val).Value = true
				local characterbody
				local rarity
				local raritycolor
				local mutationtype

				for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
					if v.Name == val then
						characterbody = v:Clone()
						rarity = v.Parent.Name
					end
				end

				if rarity == "Secret" then
					raritycolor = Color3.new(0, 1, 0.584314)
				elseif rarity == "Godly" then
					raritycolor = Color3.new(0.533333, 0, 1)
				elseif rarity == "Mythic" then
					raritycolor = Color3.new(1, 0.0980392, 0.85098)
				elseif rarity == "Legendary" then
					raritycolor = Color3.new(1, 0.745098, 0.0941176)
				elseif rarity == "Rare" then
					raritycolor = Color3.new(0.0901961, 0.486275, 1)
				elseif rarity == "Common" then
					raritycolor = Color3.new(0.6, 0.6, 0.6)
				end

				characterbody.HumanoidRootPart.Anchored = true
				characterbody.Parent = game.Workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name)
				characterbody.PrimaryPart.CFrame = game.Workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name).CFrame*CFrame.new(0,3,0)
				
				local idle = characterbody.Humanoid.Animator:LoadAnimation(characterbody.Idle)
				idle:Play()

				local label = game.ReplicatedStorage.CharacterLabel:Clone()
				label.Parent = characterbody
				label.Position = characterbody.Head.Position

				label.BillboardGui.CharName.Text = characterbody.Name
				label.BillboardGui.Rarity.Text = rarity
				if raritycolor then
					label.BillboardGui.Rarity.TextColor3 = raritycolor
				else
					label.BillboardGui.Rarity.TextColor3 = Color3.new(1,1,1)
					local gradient = game.ReplicatedStorage.CelestialGradient:Clone()
					gradient.Parent = label.BillboardGui.Rarity
				end

				label.BillboardGui.Income.Text = characterbody:GetAttribute("Income").."/s"
				label.BillboardGui.Price.Text = "¥"..characterbody:GetAttribute("Price")

				if mutation.Value ~= "" then
					mutationtype = mutation.Value
					label.BillboardGui.Mutations.Text = mutationtype
					characterbody:FindFirstChildWhichIsA("Shirt"):Destroy()
					characterbody:FindFirstChildWhichIsA("Pants"):Destroy()
					local mutationinstance = Instance.new("StringValue",characterbody)
					mutationinstance.Value = mutationtype
					mutationinstance.Name = "Mutation"
				end

				if mutationtype == "Golden" then
					local oldincome
					for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
						if v.Name == characterbody.Name then
							oldincome = v:GetAttribute("Income")
						end
					end

					characterbody:SetAttribute("Income",oldincome*3)
					label.BillboardGui.Income.Text = (characterbody:GetAttribute("Income")).."/s"
					
					local particles = game.ReplicatedStorage.GemMutation:Clone()
					particles.Parent = characterbody.Torso
					particles.Stars.Color = ColorSequence.new(Color3.new(1, 0.737255, 0.0745098))
					particles.Cross.Color = ColorSequence.new(Color3.new(1, 0.737255, 0.0745098))
					label.BillboardGui.Mutations.TextColor3 = Color3.new(1, 0.737255, 0.0745098)
					for _,v in pairs(characterbody:GetDescendants()) do
						if v:IsA("SpecialMesh") then
							v.TextureId = ""
						end

						if v:IsA("BasePart") then
							v.Color = Color3.new(1, 0.737255, 0.0745098)
						end
					end
				elseif mutationtype == "Diamond" then
					local oldincome
					for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
						if v.Name == characterbody.Name then
							oldincome = v:GetAttribute("Income")
						end
					end
					
					characterbody:SetAttribute("Income",oldincome*5)
					label.BillboardGui.Income.Text = (characterbody:GetAttribute("Income")).."/s"
					local particles = game.ReplicatedStorage.GemMutation:Clone()
					particles.Parent = characterbody.Torso
					particles.Stars.Color = ColorSequence.new(Color3.new(0.101961, 0.654902, 1))
					particles.Cross.Color = ColorSequence.new(Color3.new(0.101961, 0.654902, 1))
					label.BillboardGui.Mutations.TextColor3 = Color3.new(0.101961, 0.654902, 1)
					for _,v in pairs(characterbody:GetDescendants()) do
						if v:IsA("SpecialMesh") then
							v.TextureId = ""
						end

						if v:IsA("BasePart") then
							v.Color = Color3.new(0.101961, 0.654902, 1)
						end
					end
				elseif mutationtype == "Emerald" then
					local oldincome
					for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
						if v.Name == characterbody.Name then
							oldincome = v:GetAttribute("Income")
						end
					end

					characterbody:SetAttribute("Income",oldincome*7)
					label.BillboardGui.Income.Text = (characterbody:GetAttribute("Income")).."/s"
					local particles = game.ReplicatedStorage.GemMutation:Clone()
					particles.Parent = characterbody.Torso
					particles.Stars.Color = ColorSequence.new(Color3.new(0.0980392, 1, 0.231373))
					particles.Cross.Color = ColorSequence.new(Color3.new(0.0980392, 1, 0.231373))
					label.BillboardGui.Mutations.TextColor3 = Color3.new(0.0980392, 1, 0.231373)
					for _,v in pairs(characterbody:GetDescendants()) do
						if v:IsA("SpecialMesh") then
							v.TextureId = ""
						end

						if v:IsA("BasePart") then
							v.Color = Color3.new(0.0980392, 1, 0.231373)
						end
					end
				end

				local wc = Instance.new("WeldConstraint",label)
				wc.Part0 = characterbody.Head
				wc.Part1 = label
				
				local stealpromptpart = Instance.new("Part")
				stealpromptpart.CanCollide = false
				stealpromptpart.Size = Vector3.new(0.1, 0.1, 0.1)
				stealpromptpart.Anchored = true
				stealpromptpart.CFrame = characterbody.HumanoidRootPart.CFrame*CFrame.new(0,3.5,0)
				stealpromptpart.Transparency = 1
				stealpromptpart.Name = "StealPrompt"
				stealpromptpart.Parent = characterbody
				
				local stealprompt = game.ReplicatedStorage.StealPrompt:Clone()
				stealprompt.Parent = stealpromptpart
				
				stealprompt.Triggered:Connect(function(plr)
					if not plr.Character:FindFirstChild("Stealing") then
						game.ReplicatedStorage.Notify:FireClient(plr,"Someone is stealing your "..val)
						game.ReplicatedStorage.StealingFrom:FireClient(plr)
					end
				end)
				
				wait(1)
				game.ReplicatedStorage.ChangeToSell:FireClient(plr,characterbody)
				print("changed to sell")
			end	
		end)
	end
	
	pcall(function()
		yenLeaderboard:SetAsync(plr.UserId, math.round(plr.Data.Yen.Value))
	end)
	
	yen.Changed:Connect(function(val)
		yenleaderstat.Value = val
		pcall(function()
			yenLeaderboard:SetAsync(plr.UserId, math.round(plr.Data.Yen.Value))
		end)
	end)
	
	rebirths.Changed:Connect(function(val)
		rebirthsleaderstat.Value = val
	end)
	
	local data = ds:GetAsync(plr.UserId)

	if data then
		print("got savedata")
		local savedValues = data[1]
		local savedItems = data[2]

		-- Cache Data descendants by name for fast lookup
		local dataDescendants = {}
		for _, valObj in pairs(plr.Data:GetDescendants()) do
			dataDescendants[valObj.Name] = valObj
		end

		-- Apply saved values using the cache
		for _, savedVal in pairs(savedValues) do
			local name = savedVal[1]
			local value = savedVal[2]

			local valObj = dataDescendants[name]

			if valObj then
				valObj.Value = value
			end
		end

		-- Recreate saved items
		for _, itemName in pairs(savedItems) do
			local item = Instance.new("StringValue")
			item.Name = itemName
			item.Parent = items
		end
		
		wait(1)
		for _,v in pairs(plot.Spots:GetChildren()) do
			if v:FindFirstChildWhichIsA("Model") then
				local income = v:FindFirstChildWhichIsA("Model"):GetAttribute("Income")
				local secondsgone = os.time() - timeleft.Value
				local profit = math.round((income*secondsgone)/2)
				
				plr.Data.Characters[v.Name]["CashGenerated"..v.Name].Value+=profit
			end
		end
	end
	
	if bannedbool.Value == true then
		plr:Kick("You are banned from the game.")
	end
	
	bannedbool.Changed:Connect(function(val)
		if val == true then
			plr:Kick("You are banned from the game.")
		end
	end)
end)

local function savedata(plr)
	plr.Data.TimeLeft.Value = os.time()

	local savedata = {}
	local basevalues = {}
	for _,v in pairs(plr.Data:GetDescendants()) do
		if v:IsA("ValueBase") and v.Parent.Name ~= "Items" then
			table.insert(basevalues,{v.Name,v.Value})
		end
	end
	table.insert(savedata,basevalues)
	local items = {}
	for _,v in pairs(plr.Data.Items:GetChildren()) do
		table.insert(items,v.Name)
	end
	table.insert(savedata,items)

	print("set savedata")
	
	pcall(function()
		yenLeaderboard:SetAsync(plr.UserId, math.round(plr.Data.Yen.Value))
	end)
	
	local success, err = pcall(function()
		ds:SetAsync(plr.UserId, savedata)
	end)

	if not success then
		warn("Failed to save data for "..plr.Name..": "..err)
	end
end

game.Players.PlayerRemoving:Connect(function(plr)
	local plot = game.Workspace.Plots:FindFirstChild(plr.Name)
	plot.Name = "Unclaimed"
	plot.NameTag.BillboardGui.TextLabel.Text = "Empty Base"
	
	for _,v in pairs(plot.Spots:GetChildren()) do
		if v:FindFirstChildWhichIsA("Model") then
			v:FindFirstChildWhichIsA("Model"):Destroy()
			v.Claim.BillboardGui.TextLabel.Text = ""
		end
	end
	
	savedata(plr)
end)

game:BindToClose(function()
	-- Wait for all PlayerRemoving connections to finish
	for _, plr in pairs(game.Players:GetPlayers()) do
		-- Save data for all players still in game
		local success, err = pcall(function()
			savedata(plr)
		end)

		if not success then
			warn("Failed to save data for "..plr.Name..": "..err)
		end
	end

	-- Give time for datastore requests to complete
	wait(2)
end)
3 Likes

I didnt read your code but i recommend you add checks to make sure the data your saving is not empty so atleast they are not losing their past data.

4 Likes

alright thanks good idea, i just added that

5 Likes

Hey, just wanted to chime in on why your data might only be saving like 1% of the time — I ran into the same issue before and figured out a few things that could be going wrong:


1. Too many DataStore writes (rate limits)

Roblox limits how often you can save data (like 6 times per minute per player). If you’re calling SetAsync() too often — especially inside something like Value.Changed — it can get throttled or even silently dropped.

In your code, you update the leaderboard every time Yen.Changed fires. If Yen changes quickly (say during farming or combat), that could hit the limit really fast.

Add a cooldown so it only saves every few seconds, not instantly on every value change.


2. Saving too much / nested values

If you’re saving everything under plr.Data:GetDescendants(), that could include hundreds of values (like nested BoolValues, folders, etc). Too much data or duplicate names in the table can break saving or cause unexpected behavior.

Try cleaning up your save format or using unique keys so it’s smaller and flatter.


3. Saving too late on shutdown

Even though you’re using BindToClose, Roblox might not give your code enough time to finish all the saves before shutting down the server. You’re using wait(2), which sometimes isn’t enough if the request takes too long.

You can try saving player data one by one, with retries, and keep it short to make sure it completes in time.

:heavy_check_mark: Example for Yen.Changed:

local lastUpdate = 0
yen.Changed:Connect(function(val)
	yenleaderstat.Value = val
	if tick() - lastUpdate > 10 then
		lastUpdate = tick()
		pcall(function()
			yenLeaderboard:SetAsync(plr.UserId, math.round(val))
		end)
	end
end)

But the most probable reason their games aren’t saving is hitting the save limits from Yen.Changed events and saving them every time.

2 Likes

What do you think of something like this…
I think the over saving is probably the problem.

ServerScript
local dss = game:GetService("DataStoreService")
local ds = dss:GetDataStore("PlayerData_1.2")
local yenLeaderboard = dss:GetOrderedDataStore("YenLeaderboard")

game.Players.PlayerAdded:Connect(function(plr)
	local data = ds:GetAsync(plr.UserId)
	if not plr:FindFirstChild("Data") then
		local folder = Instance.new("Folder", plr)
		folder.Name = "Data"
	end

	local yen = Instance.new("NumberValue", plr.Data)
	yen.Name = "Yen" yen.Value = 50

	local rebirths = Instance.new("NumberValue", plr.Data)
	rebirths.Name = "Rebirths"

	if data then
		for _, v in pairs(data[1]) do
			local valObj = plr.Data:FindFirstChild(v[1])
			if valObj then valObj.Value = v[2]
			end
		end
	end

	local leaderstats = Instance.new("Folder", plr)
	leaderstats.Name = "leaderstats"
	local yenStat = Instance.new("NumberValue", leaderstats)
	yenStat.Name = "Yen" yenStat.Value = yen.Value
	local rebirthStat = Instance.new("NumberValue", leaderstats)
	rebirthStat.Name = "Rebirths"
	rebirthStat.Value = rebirths.Value

	yen.Changed:Connect(function(val)
		yenStat.Value = val
	end)
	rebirths.Changed:Connect(function(val)
		rebirthStat.Value = val
	end)
end)

local function savePlayerData(plr)
	if not plr:FindFirstChild("Data") then return end

	local values = {}
	local saveData = {}
	for _, v in pairs(plr.Data:GetChildren()) do
		if v:IsA("ValueBase") then
			table.insert(values, {v.Name, v.Value})
		end
	end
	table.insert(saveData, values)

	pcall(function()
		ds:SetAsync(plr.UserId, saveData)
	end)

	pcall(function()
		yenLeaderboard:SetAsync(plr.UserId, math.round(plr.Data.Yen.Value))
	end)
end

game.Players.PlayerRemoving:Connect(savePlayerData)

game:BindToClose(function()
	for _, plr in pairs(game.Players:GetPlayers()) do
		savePlayerData(plr)
	end task.wait(5)
end)

This may not be for you… I don’t like saving the data every time it changes.
I try to load and save only once, or at least minimally. I’ll go out of my way to do this.
Working with the data, saving and loading are all different things. Always found it best to not mix them. If needed I’ll work with copies of whatever data I need to.

For something like a global high score board I’ll update every 60 seconds, but that would be on its own datastore.

1 Like

Your main issue is right here

image

GetAsync() can fail (error), and if it fails, the player’s data wont load. Your savedata function doesn’t care if loading failed or not, it just saves the values. So if the data never loaded, it will overwrite the player’s data with the default values

To fix this, I would recommend adding a check like this

local PlayerLoaded = {}

local function LoadData(Player)
	local Success, Result = pcall(function()
		return ds:GetAsync(plr.UserId)
	end)

	if not Success then
		warn("Datastore Error: "..Result) -- In case of an error, Result is the error message
		return
	end

	if Result then -- If there are no errors, Result is the data
		-- Your existing code

		PlayerLoaded[Player.UserId] = true
	end
end

local function SaveData(Player)
	
	-- If the player's data didn't load, do not save
	if not PlayerLoaded[Player.UserId] then return end
	PlayerLoaded[Player.UserId] = nil

	-- Your existing saving code
end

You might also want to add a retry mechanism when loading the data, and kicking players when it fails to load, as players would be playing without their data

Note that your datastore script will still be vulnerable to overwriting if a player joins a server right after leaving another (if the player joins a new server before the old server saved the data). However, this should fix the complete wiping of data


The limit is 4mb, which is a lot. OP is definitely very far from this limit

OP’s code is safe from that. It yields until the data is saved for every player, then waits for 2 seconds. The 2 second wait is actually not even needed

That is not the cause of the data wiping, but the leaderboard OrderedDatastore should definitely be updated less often

4 Likes

if the data doesn’t load would the players data be reset? is there a way I could keep attempting to load until it loads successfully?

1 Like

With the way you’ve structured it, yes. The solution I provided should fix that issue. Other people also store a “version” value alongside the main data, that is incremented every time the data is saved. Then, when updating the player’s data, they check the version from the old data with the version from the new data, and if the version from the old data is higher than the new data, that means the new data is actually outdated. This partially fixes the issue of players joining right after leaving another server (but players will still lose the progress from the new session)

You can absolutely attempt to load it until it is successful, it would look something like this

local function GetData(Key, MaxRetries)

	local Retries = 0

	while true do 

		local Success, Result = pcall(function()
			return Datastore:GetAsync(Key)
		end)

		if Success then 
			return true, Result
		else 
			warn("[GetData]: "..Result)

			Retries += 1
			if Retries > MaxRetries then -- Give up :(
				return false, nil
			end
			
			-- Exponential backoff (increases the wait, to alleviate the load on roblox's servers if they go down)
			task.wait(2^Retries)

			continue
		end

	end
end

You can also look into ProfileStore, which has session locking and all the features to keep data safe. I am not sure if it is easy to convert to it though

2 Likes

How would I implement this with the last addition you told me to include: Here’s my script after the last improvement you recommended, just in case I did something wrong or in case you would like a reference:

local dss = game:GetService("DataStoreService")
local ds = dss:GetDataStore("PlayerData_1.2")
local yenLeaderboard = dss:GetOrderedDataStore("YenLeaderboard")
local PlayerLoaded = {}



local function LoadData(plr,plot,items,timeleft)
	local Success, Result = pcall(function()
		return ds:GetAsync(plr.UserId)
	end)

	if not Success then
		warn("Datastore Error: "..Result) -- In case of an error, Result is the error message
		return
	end

	if Result then -- If there are no errors, Result is the data
		PlayerLoaded[plr.UserId] = true
		print("got savedata")
		local savedValues = Result[1]
		local savedItems = Result[2]

		-- Cache Data descendants by name for fast lookup
		local dataDescendants = {}
		for _, valObj in pairs(plr.Data:GetDescendants()) do
			dataDescendants[valObj.Name] = valObj
		end

		-- Apply saved values using the cache
		for _, savedVal in pairs(savedValues) do
			local name = savedVal[1]
			local value = savedVal[2]

			local valObj = dataDescendants[name]

			if valObj then
				valObj.Value = value
			end
		end

		-- Recreate saved items
		for _, itemName in pairs(savedItems) do
			local item = Instance.new("StringValue")
			item.Name = itemName
			item.Parent = items
		end

		wait(1)
		for _,v in pairs(plot.Spots:GetChildren()) do
			if v:FindFirstChildWhichIsA("Model") then
				local income = v:FindFirstChildWhichIsA("Model"):GetAttribute("Income")
				local secondsgone = os.time() - timeleft.Value
				local profit = math.round((income*secondsgone)/2)

				plr.Data.Characters[v.Name]["CashGenerated"..v.Name].Value+=profit
			end
		end
	end
end

game.Players.PlayerAdded:Connect(function(plr)
	local plot = game.Workspace.Plots:FindFirstChild("Unclaimed")
	plot.Name = plr.Name
	plot.NameTag.BillboardGui.TextLabel.Text = plr.Name.."'s Base"

	plr.CharacterAdded:Connect(function()
		coroutine.wrap(function()
			print("pivoted character")
			plr.Character:PivotTo(plot.Lasers.Block.CFrame*CFrame.new(0,0,-10))

			for _,v in pairs(plr.Data:WaitForChild("ToolPasses"):GetChildren()) do
				if v.Value == true then
					local tool = game.ReplicatedStorage.ToolGamepasses[v.Name]:Clone()
					tool.Parent = plr.Backpack
				end

				v.Changed:Connect(function()
					if v.Value == true then
						local tool = game.ReplicatedStorage.ToolGamepasses[v.Name]:Clone()
						tool.Parent = plr.Backpack
					end
				end)
			end
		end)()
	end)

	local data = Instance.new("Folder",plr)
	data.Name = "Data"

	local bannedbool = Instance.new("BoolValue")
	bannedbool.Parent = data
	bannedbool.Name = "Banned"

	local codes = Instance.new("Folder",data)
	codes.Name = "Codes"

	for _,v in pairs(game.ReplicatedStorage.Codes:GetChildren()) do
		local codebool = Instance.new("BoolValue",codes)
		codebool.Parent = codes
		codebool.Name = v.Name
	end

	local leaderstats = Instance.new("Folder")
	leaderstats.Parent = plr
	leaderstats.Name = "leaderstats"
	local yenleaderstat = Instance.new("NumberValue")
	yenleaderstat.Parent = leaderstats
	yenleaderstat.Name = "Yen"
	local rebirthsleaderstat = Instance.new("NumberValue")
	rebirthsleaderstat.Parent = leaderstats
	rebirthsleaderstat.Name = "Rebirths"

	local yen = Instance.new("NumberValue",data)
	yen.Name = "Yen"
	yen.Value = 50
	yenleaderstat.Value = 50

	local timeleft = Instance.new("NumberValue",data)
	timeleft.Name = "TimeLeft"

	local rebirths = Instance.new("NumberValue",data)
	rebirths.Name = "Rebirths"

	local ownedcharacters = Instance.new("Folder",data)
	ownedcharacters.Name = "Characters"

	local items = Instance.new("Folder",data)
	items.Name = "Items"

	local yenmulti = Instance.new("NumberValue",data)
	yenmulti.Name = "YenMultiplier"
	yenmulti.Value = 1

	local quests = Instance.new("Folder",data)
	quests.Name = "Quests"

	local toolgamepasses = Instance.new("Folder",data)
	toolgamepasses.Name = "ToolPasses"
	for _,v in pairs(game.ReplicatedStorage.ToolGamepasses:GetChildren()) do
		local toolbool = Instance.new("BoolValue",toolgamepasses)
		toolbool.Name = v.Name
	end

	for _,v in pairs(game.ReplicatedStorage.Quests:GetChildren()) do
		local questbool = Instance.new("BoolValue",quests)
		questbool.Name = v.Name
		questbool.Value = false

		local claimed = Instance.new("BoolValue",questbool)
		claimed.Name = "Claimed"..v.Name
		claimed.Value = false
	end

	local statsfolder = Instance.new("Folder",data)
	statsfolder.Name = "Stats"
	local CharactersSold = Instance.new("NumberValue",statsfolder) CharactersSold.Name = "CharactersSold"
	local Steals = Instance.new("NumberValue",statsfolder) Steals.Name = "Steals"

	local index = Instance.new("Folder",data)
	index.Name = "Index"
	for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
		if v:IsA("Model") then
			local indexed = Instance.new("BoolValue",index)
			indexed.Name = v.Name
		end
	end

	for i=1,10 do
		local character = Instance.new("StringValue",ownedcharacters)
		character.Name = "Character"..i

		local amounttoclaim = Instance.new("NumberValue",character)
		amounttoclaim.Name = "CashGenerated".."Character"..i

		local mutation = Instance.new("StringValue",character)
		mutation.Name = "Mutation".."Character"..i

		--ignore this its just to load the characters when they're changed
		character.Changed:Connect(function(val)
			if val == "" then
				if workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name):FindFirstChildWhichIsA("Model") then
					workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name):FindFirstChildWhichIsA("Model"):Destroy()
				end
				mutation.Value = ""
				amounttoclaim.Value = 0
			else
				index:FindFirstChild(val).Value = true
				local characterbody
				local rarity
				local raritycolor
				local mutationtype

				for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
					if v.Name == val then
						characterbody = v:Clone()
						rarity = v.Parent.Name
					end
				end

				if rarity == "Secret" then
					raritycolor = Color3.new(0, 1, 0.584314)
				elseif rarity == "Godly" then
					raritycolor = Color3.new(0.533333, 0, 1)
				elseif rarity == "Mythic" then
					raritycolor = Color3.new(1, 0.0980392, 0.85098)
				elseif rarity == "Legendary" then
					raritycolor = Color3.new(1, 0.745098, 0.0941176)
				elseif rarity == "Rare" then
					raritycolor = Color3.new(0.0901961, 0.486275, 1)
				elseif rarity == "Common" then
					raritycolor = Color3.new(0.6, 0.6, 0.6)
				end

				characterbody.HumanoidRootPart.Anchored = true
				characterbody.Parent = game.Workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name)
				characterbody.PrimaryPart.CFrame = game.Workspace.Plots:FindFirstChild(plr.Name).Spots:FindFirstChild(character.Name).CFrame*CFrame.new(0,3,0)

				local idle = characterbody.Humanoid.Animator:LoadAnimation(characterbody.Idle)
				idle:Play()

				local label = game.ReplicatedStorage.CharacterLabel:Clone()
				label.Parent = characterbody
				label.Position = characterbody.Head.Position

				label.BillboardGui.CharName.Text = characterbody.Name
				label.BillboardGui.Rarity.Text = rarity
				if raritycolor then
					label.BillboardGui.Rarity.TextColor3 = raritycolor
				else
					label.BillboardGui.Rarity.TextColor3 = Color3.new(1,1,1)
					local gradient = game.ReplicatedStorage.CelestialGradient:Clone()
					gradient.Parent = label.BillboardGui.Rarity
				end

				label.BillboardGui.Income.Text = characterbody:GetAttribute("Income").."/s"
				label.BillboardGui.Price.Text = "¥"..characterbody:GetAttribute("Price")

				if mutation.Value ~= "" then
					mutationtype = mutation.Value
					label.BillboardGui.Mutations.Text = mutationtype
					characterbody:FindFirstChildWhichIsA("Shirt"):Destroy()
					characterbody:FindFirstChildWhichIsA("Pants"):Destroy()
					local mutationinstance = Instance.new("StringValue",characterbody)
					mutationinstance.Value = mutationtype
					mutationinstance.Name = "Mutation"
				end

				if mutationtype == "Golden" then
					local oldincome
					for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
						if v.Name == characterbody.Name then
							oldincome = v:GetAttribute("Income")
						end
					end

					characterbody:SetAttribute("Income",oldincome*3)
					label.BillboardGui.Income.Text = (characterbody:GetAttribute("Income")).."/s"

					local particles = game.ReplicatedStorage.GemMutation:Clone()
					particles.Parent = characterbody.Torso
					particles.Stars.Color = ColorSequence.new(Color3.new(1, 0.737255, 0.0745098))
					particles.Cross.Color = ColorSequence.new(Color3.new(1, 0.737255, 0.0745098))
					label.BillboardGui.Mutations.TextColor3 = Color3.new(1, 0.737255, 0.0745098)
					for _,v in pairs(characterbody:GetDescendants()) do
						if v:IsA("SpecialMesh") then
							v.TextureId = ""
						end

						if v:IsA("BasePart") then
							v.Color = Color3.new(1, 0.737255, 0.0745098)
						end
					end
				elseif mutationtype == "Diamond" then
					local oldincome
					for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
						if v.Name == characterbody.Name then
							oldincome = v:GetAttribute("Income")
						end
					end

					characterbody:SetAttribute("Income",oldincome*5)
					label.BillboardGui.Income.Text = (characterbody:GetAttribute("Income")).."/s"
					local particles = game.ReplicatedStorage.GemMutation:Clone()
					particles.Parent = characterbody.Torso
					particles.Stars.Color = ColorSequence.new(Color3.new(0.101961, 0.654902, 1))
					particles.Cross.Color = ColorSequence.new(Color3.new(0.101961, 0.654902, 1))
					label.BillboardGui.Mutations.TextColor3 = Color3.new(0.101961, 0.654902, 1)
					for _,v in pairs(characterbody:GetDescendants()) do
						if v:IsA("SpecialMesh") then
							v.TextureId = ""
						end

						if v:IsA("BasePart") then
							v.Color = Color3.new(0.101961, 0.654902, 1)
						end
					end
				elseif mutationtype == "Emerald" then
					local oldincome
					for _,v in pairs(game.ReplicatedStorage.Characters:GetDescendants()) do
						if v.Name == characterbody.Name then
							oldincome = v:GetAttribute("Income")
						end
					end

					characterbody:SetAttribute("Income",oldincome*7)
					label.BillboardGui.Income.Text = (characterbody:GetAttribute("Income")).."/s"
					local particles = game.ReplicatedStorage.GemMutation:Clone()
					particles.Parent = characterbody.Torso
					particles.Stars.Color = ColorSequence.new(Color3.new(0.0980392, 1, 0.231373))
					particles.Cross.Color = ColorSequence.new(Color3.new(0.0980392, 1, 0.231373))
					label.BillboardGui.Mutations.TextColor3 = Color3.new(0.0980392, 1, 0.231373)
					for _,v in pairs(characterbody:GetDescendants()) do
						if v:IsA("SpecialMesh") then
							v.TextureId = ""
						end

						if v:IsA("BasePart") then
							v.Color = Color3.new(0.0980392, 1, 0.231373)
						end
					end
				end

				local wc = Instance.new("WeldConstraint",label)
				wc.Part0 = characterbody.Head
				wc.Part1 = label

				local stealpromptpart = Instance.new("Part")
				stealpromptpart.CanCollide = false
				stealpromptpart.Size = Vector3.new(0.1, 0.1, 0.1)
				stealpromptpart.Anchored = true
				stealpromptpart.CFrame = characterbody.HumanoidRootPart.CFrame*CFrame.new(0,3.5,0)
				stealpromptpart.Transparency = 1
				stealpromptpart.Name = "StealPrompt"
				stealpromptpart.Parent = characterbody

				local stealprompt = game.ReplicatedStorage.StealPrompt:Clone()
				stealprompt.Parent = stealpromptpart

				stealprompt.Triggered:Connect(function(plr)
					if not plr.Character:FindFirstChild("Stealing") then
						game.ReplicatedStorage.Notify:FireClient(plr,"Someone is stealing your "..val)
						game.ReplicatedStorage.StealingFrom:FireClient(plr)
					end
				end)

				wait(1)
				game.ReplicatedStorage.ChangeToSell:FireClient(plr,characterbody)
				print("changed to sell")
			end	
		end)
	end

	pcall(function()
		yenLeaderboard:SetAsync(plr.UserId, math.round(plr.Data.Yen.Value))
	end)

	yen.Changed:Connect(function(val)
		yenleaderstat.Value = val
	end)

	rebirths.Changed:Connect(function(val)
		rebirthsleaderstat.Value = val
	end)

	LoadData(plr,plot,items,timeleft)

	if bannedbool.Value == true then
		plr:Kick("You are banned from the game.")
	end

	bannedbool.Changed:Connect(function(val)
		if val == true then
			plr:Kick("You are banned from the game.")
		end
	end)
end)

local function savedata(plr)
	if not PlayerLoaded[plr.UserId] then return end
	PlayerLoaded[plr.UserId] = nil
	
	plr.Data.TimeLeft.Value = os.time()

	local savedata = {}
	local basevalues = {}
	for _,v in pairs(plr.Data:GetDescendants()) do
		if v:IsA("ValueBase") and v.Parent.Name ~= "Items" then
			table.insert(basevalues,{v.Name,v.Value})
		end
	end
	table.insert(savedata,basevalues)
	local items = {}
	for _,v in pairs(plr.Data.Items:GetChildren()) do
		table.insert(items,v.Name)
	end
	table.insert(savedata,items)

	print("set savedata")

	pcall(function()
		yenLeaderboard:SetAsync(plr.UserId, math.round(plr.Data.Yen.Value))
	end)

	local success, err = pcall(function()
		ds:SetAsync(plr.UserId, savedata)
	end)

	if not success then
		warn("Failed to save data for "..plr.Name..": "..err)
	end
end

game.Players.PlayerRemoving:Connect(function(plr)
	local plot = game.Workspace.Plots:FindFirstChild(plr.Name)
	plot.Name = "Unclaimed"
	plot.NameTag.BillboardGui.TextLabel.Text = "Empty Base"

	for _,v in pairs(plot.Spots:GetChildren()) do
		if v:FindFirstChildWhichIsA("Model") then
			v:FindFirstChildWhichIsA("Model"):Destroy()
			v.Claim.BillboardGui.TextLabel.Text = ""
		end
	end

	savedata(plr)
end)

game:BindToClose(function()
	-- Wait for all PlayerRemoving connections to finish
	for _, plr in pairs(game.Players:GetPlayers()) do
		-- Save data for all players still in game
		local success, err = pcall(function()
			savedata(plr)
		end)

		if not success then
			warn("Failed to save data for "..plr.Name..": "..err)
		end
	end

	-- Give time for datastore requests to complete
	wait(2)
end)
1 Like

Nevermind I found a way to implement it, hopefully the datastore works now, thank you.

1 Like

That should do it. Note that this only fixes complete data wipes, and not data overwrites I’ve mentioned before

2 Likes

It’s been about a day by now, I’ve gotten no reports by players saying there’s data been wiped. the games at about 1300 ccu so I think if it was still a problem someone would report it. a few people have reported certain items missing but they’re probably just lying to try n get free stuff, so thank you.

1 Like

That could be because of this

If it does happen, anything a player did during the session that got overwritten will be lost, so it would be multiple items rather than one item here or there missing

1 Like

so should I make it kick the player when their data doesnt load? I wouldnt be surprised if someone left and joined a new one before it saved, what would be the fix for something like that?

That could be a good idea

The most common solution is session locking. the UpdateAsync Datastore method is central to how session locking works. UpdateAsync takes a TransformFunction, a function like this

Datastore:UpdateAsync(Key, function(OldData) 
	
	print(OldData)
	
	-- NewData is equivalent to savedata in your ds:SetAsync(plr.UserId, savedata)
	return NewData
	
	-- If you return nil instead, it will cancel the update, and keep the old data
	-- return nil
end)

So, how session locking works is, we store which server “owns” the “session”, and prevent any other server from loading or saving the player’s data. This can be achieved by storing a unique identifier of the server directly within the data (game.JobId is perfect for that, every server has a unique JobId), and when the data was last updated (reason being, if the server crashes, the server wont be able to “release” the session to allow other servers to access it, so we need a timeout mechanism, so, if [5] minutes passed since the last update, the session lock is disregarded. Because of this, the lock has to be updated every so often, meaning you kind of have to make an autosave system)

The datastore structure could look something like this

First join, GetAsync equivalent
local Result = nil

local Success, Error = pcall(function()
	
	-- We don't use GetAsync, because we need to establish the session lock
	Datastore:UpdateAsync(Key, function(OldData)
		
		-- Do not have to check for session lock if the data is nil (it's a new player)
		if OldData then
			
			-- Checking if the SessionLock is another server, and if LastUpdate was done less than 5 minutes ago
			if OldData.SessionLock and OldData.SessionLock ~= game.JobId and (os.time() - OldData.LastUpdate) < 60*5 then
				-- The session belongs to another server, we should error
				-- You could also kick the player
				error("Failed to load the data: The session is locked")
				-- If you don't use the error function, return nil, and set some variable to let the script know it failed because of the session lock
			end
			
			Result = OldData
		else
			
			-- New player, we must set OldData to a table manually
			-- Don't need to set it as the default data, since it'll get overwritten on the first save
			OldData = {}
			
			Result = nil
		end
		
		--
		
		-- Establish the session lock
		OldData.SessionLock = game.JobId
		OldData.LastUpdate = os.time()
		
		return OldData
	end)
end)

if Success then 
	-- Result is the data. For a new player, Result is nil
	-- Error will be nil
else 
	-- Result is nil
	-- Error is the error message
	-- You can do your retry mechanism here, and kick the player if it fails multiple times
end
Autosave and session lock update

Autosaves should be performed at an interval shorter than the timeout (in my example code, the timeout is 5 minutes), so I’d say an autosave every minute should be good

local NewData = nil -- DEFINED IN YOUR SAVEDATA CODE

local Success, Error = pcall(function()
	
	-- We don't use GetAsync, because we need to establish the session lock
	Datastore:UpdateAsync(Key, function(OldData)
		
		-- Do not have to check for session lock if the data is nil (it's a new player)
		if OldData then
			
			-- Checking if the SessionLock is another server, and if LastUpdate was done less than 5 minutes ago
			if OldData.SessionLock and OldData.SessionLock ~= game.JobId and (os.time() - OldData.LastUpdate) < 60*5 then
				-- The session belongs to another server, we should error
				-- You could also kick the player
				error("Failed to autosave the data: The session is locked")
			end
		end
		
		--
		
		-- Establish the session lock, but on the NewData this time
		NewData.SessionLock = game.JobId
		NewData.LastUpdate = os.time()
		
		return NewData
	end)
end)

if Success then 
	-- yay
else 
	-- If an error occurs during an autosave, there isn't really a need for a retry mechanism as the data will be saved again later
end
The last save, when the player leaves
local NewData = nil -- DEFINED IN YOUR SAVEDATA CODE

local Success, Error = pcall(function()
	
	-- We don't use GetAsync, because we need to establish the session lock
	Datastore:UpdateAsync(Key, function(OldData)
		
		-- Do not have to check for session lock if the data is nil (it's a new player)
		if OldData then
			
			-- Checking if the SessionLock is another server, and if LastUpdate was done less than 5 minutes ago
			if OldData.SessionLock and OldData.SessionLock ~= game.JobId and (os.time() - OldData.LastUpdate) < 60*5 then
				-- The session belongs to another server, we should error
				-- You could also kick the player
				error("Failed to save the data: The session is locked")
			end
		end
		
		--
		
		-- Free the session lock, by setting SessionLock to nil
		NewData.SessionLock = nil
		NewData.LastUpdate = os.time()
		
		return NewData
	end)
end)

if Success then 
	-- yay
else 
	-- This is the last save! Retrying is much more important
end

Note that the code I wrote is untested. You should first test it in a test copy of your game to ensure it at least works and doesn’t erase player data or something

I’ve actually never used session locking for my own datastores. I’ve opted for a different approach, explained here. Note that the linked thread is complex, but Section 1 is the most accessible, and is about the approach rather than my framework


Somehow didn’t think about it, but you should also add a retry mechanism (like the one in the GetData function), but to the SetAsync. If the SetAsync fails, it will cause the player to lose the data from the session. This is actually probably more likely to happen then players joining before their data saved on another server