DataStore is yielding when leaving and autosaving

--|| Services ||--
local Players 				= game:GetService("Players")
local RunService 			= game:GetService("RunService")
local ServerStorage			= game:GetService("ServerStorage")
local DataStoreService		= game:GetService("DataStoreService")
local ReplicatedStorage		= game:GetService("ReplicatedStorage")
local ServerScriptService	= game:GetService("ServerScriptService")

local dataStore 			= DataStoreService:GetDataStore("Testing")
local TycoonCloneHolder 	= ServerStorage.TycoonCloneHolder
local TycoonsToDestroy		= ServerStorage.TycoonsToDestroy
local TycoonSetup			= ServerScriptService.TycoonSetup

local UpdateDeaths = ReplicatedStorage.RemoteEvents.UpdateDeath
local SetColorTheme = ServerScriptService.SetColorTheme

--|| Variables ||--
local requestAmount = 1
local TycoonsHolder = workspace.Tycoons
local TycoonTable = {}

--|| Functions ||--
local function waitForRequestBudget(requestType)
	local currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
	while currentBudget < requestAmount do
		currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
		wait(5)
	end
end

local function CharacterAdded(character, SpawnPos)
	character.PrimaryPart.CFrame = SpawnPos.CFrame * CFrame.new(0,5,0)
	print("Loaded character to their island!")
end

local function setUp(player)
	
	local userId = player.UserId
	local key = "Player_" .. userId

	local StatsFolder = Instance.new("Folder")
	StatsFolder.Name = "StatsFolder"

	local BuildingsFolder = Instance.new("Folder")
	BuildingsFolder.Name = "BuildingsFolder"
	
	local SpecialPurchaseFolder = Instance.new("Folder")
	SpecialPurchaseFolder.Name = "SpecialPurchaseFolder"

	--// Stats to be saved \\--
	local Cash = Instance.new("NumberValue")
	Cash.Name = "Cash"

	local OwnsTycoon = Instance.new("BoolValue")
	OwnsTycoon.Name = "OwnsTycoon"
	
	local TycoonSavedCash = Instance.new("NumberValue")
	TycoonSavedCash.Name = "TycoonSavedCash"
	
	local inGroup = Instance.new("BoolValue")
	inGroup.Name = "inGroup"
	
	local TimePlayed = Instance.new("NumberValue")
	TimePlayed.Name = "TimePlayed"
	
	local TotalMoneyEarned = Instance.new("NumberValue")
	TotalMoneyEarned.Name = "TotalMoneyEarned"
	
	local Kills = Instance.new("NumberValue")
	Kills.Name = "Kills"
	
	local Deaths = Instance.new("NumberValue")
	Deaths.Name = "Deaths"
	
	local OwnsAutoCollector = Instance.new("BoolValue")
	OwnsAutoCollector.Name = "OwnsAutoCollector"
	
	local OwnsColorChanger = Instance.new("BoolValue")
	OwnsColorChanger.Name = "OwnsColorChanger"
	
	local Owns2xCash = Instance.new("BoolValue")
	Owns2xCash.Name = "Owns2xCash"		
	
	local ColorTheme1 = Instance.new("Color3Value")
	ColorTheme1.Name = "ColorTheme1"
	
	local ColorTheme2 = Instance.new("Color3Value")
	ColorTheme2.Name = "ColorTheme2"
	
	local ColorTheme3 = Instance.new("Color3Value")
	ColorTheme3.Name = "ColorTheme3"
	
	--|| Stats to not be saved ||--
	local PlayerBools = Instance.new("Folder")
	PlayerBools.Name = "PlayerBools"
	PlayerBools.Parent = player
	
	local AlreadyOwnsTycoon = Instance.new("BoolValue")
	AlreadyOwnsTycoon.Name = "AlreadyOwnsTycoon"
	AlreadyOwnsTycoon.Parent = PlayerBools
	
	local TouchedAirDrop = Instance.new("BoolValue")
	TouchedAirDrop.Name = "TouchedAirDrop"
	TouchedAirDrop.Value = false
	TouchedAirDrop.Parent = PlayerBools
	
	local CurrentTycoon = Instance.new("StringValue")
	CurrentTycoon.Name = "CurrentTycoon"
	CurrentTycoon.Parent = PlayerBools
	

	local success, returnValue

	repeat 
		waitForRequestBudget(Enum.DataStoreRequestType.GetAsync)
		success, returnValue = pcall(dataStore.GetAsync, dataStore, key)
	until success or not Players:FindFirstChild(player.Name)

	if success then
			
		if returnValue == nil then
			print("Player has no data!")
			returnValue = {
				Cash = 0,
				OwnsTycoon = false,
				TycoonSavedCash = 0,
				inGroup = false,
				TimePlayed = 0,
				TotalMoneyEarned = 0,
				Kills = 0,
				Deaths = 0,
				OwnsAutoCollector = false,
				OwnsColorChanger = false,
				Owns2xCash = false,
				ColorTheme1 = Color3.fromRGB(91, 154, 76),
				ColorTheme2 = Color3.fromRGB(51, 88, 130),
				ColorTheme3 = Color3.fromRGB(17, 17, 17),
			}
		end	
		
		
		--// Loads all the player's saved data from returnValue in DataStore
		
		Cash.Value = returnValue["Cash"] or 0	
		OwnsTycoon.Value = returnValue["OwnsTycoon"] or false
		TycoonSavedCash.Value = returnValue["TycoonSavedCash"] or 0
		inGroup.Value = returnValue["inGroup"] or false
		TimePlayed.Value = returnValue["TimePlayed"] or 0
		TotalMoneyEarned.Value = returnValue["TotalMoneyEarned"] or 0
		Kills.Value = returnValue["Kills"] or 0
		Deaths.Value = returnValue["Deaths"] or 0
		OwnsAutoCollector.Value = returnValue["OwnsAutoCollector"] or false
		OwnsColorChanger.Value = returnValue["OwnsColorChanger"] or false
		Owns2xCash.Value = returnValue["Owns2xCash"] or false
		ColorTheme1.Value = Color3.new(returnValue["ColorTheme1"].R, returnValue["ColorTheme1"].G, returnValue["ColorTheme1"].B) or Color3.fromRGB(91, 154, 76)
		ColorTheme2.Value = Color3.new(returnValue["ColorTheme2"].R, returnValue["ColorTheme2"].G, returnValue["ColorTheme2"].B) or Color3.fromRGB(51, 88, 130)
		ColorTheme3.Value = Color3.new(returnValue["ColorTheme3"].R, returnValue["ColorTheme3"].G, returnValue["ColorTheme3"].B) or Color3.fromRGB(17, 17, 17)
		

		if returnValue["Building"] ~= nil then
			for _, building in pairs(returnValue["Building"]) do
				if building ~= nil and type(building) == "string" then 
					local createdBuilding = Instance.new("StringValue")
					createdBuilding.Name = building 
					createdBuilding.Value = building
					createdBuilding.Parent = BuildingsFolder
				end
			end
		end
		
		if returnValue["SpecialPurchase"] ~= nil then
			for _, purchase in pairs(returnValue["SpecialPurchase"]) do
				if purchase ~= nil and type(purchase) == "string" then
					local savedPurchase = Instance.new("StringValue")
					savedPurchase.Name = purchase
					savedPurchase.Value = purchase
					savedPurchase.Parent = SpecialPurchaseFolder
				end
			end
		end
		
		
		--// Parent the values into their respective folders
		
		Cash.Parent = StatsFolder
		OwnsTycoon.Parent = StatsFolder
		TycoonSavedCash.Parent = StatsFolder
		inGroup.Parent = StatsFolder
		TimePlayed.Parent = StatsFolder
		TotalMoneyEarned.Parent = StatsFolder
		Kills.Parent = StatsFolder
		Deaths.Parent = StatsFolder
		OwnsAutoCollector.Parent = StatsFolder
		OwnsColorChanger.Parent = StatsFolder
		Owns2xCash.Parent = StatsFolder
		ColorTheme1.Parent = StatsFolder
		ColorTheme2.Parent = StatsFolder
		ColorTheme3.Parent = StatsFolder
		
		StatsFolder.Parent = player	
		BuildingsFolder.Parent = player
		SpecialPurchaseFolder.Parent = player

		print("Data was loaded for player.")
		
		coroutine.wrap(function()
			while task.wait(1) do
				TimePlayed.Value += 1
			end
		end)()
		
		for _, Tycoon in pairs(TycoonsHolder:GetChildren()) do
			if Tycoon:FindFirstChild("Occupied").Value == false then
				if not table.find(TycoonTable, player.Name) then
					
					Tycoon:FindFirstChild("Occupied").Value = true
					table.insert(TycoonTable, player.Name)
					
					print(TycoonTable)
					
					local Components 		= Tycoon:FindFirstChild("Components")
					local Tycoon_Owner 		= Tycoon:FindFirstChild("TycoonOwner")
					local Cash_To_Collect 	= Tycoon:FindFirstChild("CashToCollect")
					local Owner				= Tycoon:FindFirstChild("Owner")
					local PlayerPicture		= Components:FindFirstChild("PlayerPicture"):FindFirstChild("ImageLabel")
					local Claimpad 			= Components:FindFirstChild("Claim Pad")
					local Claim 			= Claimpad:FindFirstChild("Pad")
					local Current_Owner 	= Claimpad:FindFirstChild("Gui"):FindFirstChild("CurrentOwner")
					local userId 			= player.UserId
					local thumbType 		= Enum.ThumbnailType.HeadShot
					local thumbSize 		= Enum.ThumbnailSize.Size420x420
					local content, isReady 	= Players:GetUserThumbnailAsync(userId, thumbType, thumbSize)
					
					Owner.Value = player.Name
					Tycoon_Owner.Value = player
					PlayerPicture.Image = content
					Current_Owner.Text = "Owner: "..tostring(Tycoon_Owner.Value)			
					
					player.RespawnLocation = Components:FindFirstChild("Spawn")
					CurrentTycoon.Value = Tycoon.Name
					
					local char = player.Character or player.CharacterAdded:Wait()
					if char then
						CharacterAdded(char, Components:FindFirstChild("Spawn"))
					end
					
					if OwnsTycoon.Value == false then							
						OwnsTycoon.Value = true
						print("New tycoon!")
					elseif OwnsTycoon.Value == true then
						Cash_To_Collect.Value = StatsFolder.TycoonSavedCash.Value
						TycoonSetup:Fire(Tycoon.Name, player)
						SetColorTheme:Fire(Tycoon, ColorTheme1.Value, ColorTheme2.Value, ColorTheme3.Value)
						print("Loading tycoon info.")
					end
				end
			end
		end	
	else
		warn("There was an error!" .. returnValue)
	end
end


local function save(player, dontWait)
	local userId = player.UserId
	local key = "Player_" .. userId
	local StatsFolder = player:FindFirstChild("StatsFolder")
	local BuildingsFolder = player:FindFirstChild("BuildingsFolder")
	local SpecialPurchaseFolder = player:FindFirstChild("SpecialPurchaseFolder")

	if StatsFolder then
		
		local Building;
		local SpecialPurchase;
		
		local CT1 = {}
		CT1.R = StatsFolder.ColorTheme1.Value.R
		CT1.G = StatsFolder.ColorTheme1.Value.G
		CT1.B = StatsFolder.ColorTheme1.Value.B
		
		local CT2 = {}
		CT2.R = StatsFolder.ColorTheme2.Value.R
		CT2.G = StatsFolder.ColorTheme2.Value.G
		CT2.B = StatsFolder.ColorTheme2.Value.B
		
		local CT3 = {}
		CT3.R = StatsFolder.ColorTheme3.Value.R
		CT3.G = StatsFolder.ColorTheme3.Value.G
		CT3.B = StatsFolder.ColorTheme3.Value.B

		local StatsTable = {
			Cash = StatsFolder.Cash.Value,
			OwnsTycoon = StatsFolder.OwnsTycoon.Value,
			TycoonSavedCash = StatsFolder.TycoonSavedCash.Value,
			inGroup = StatsFolder.inGroup.Value,
			TimePlayed = StatsFolder.TimePlayed.Value,
			TotalMoneyEarned = StatsFolder.TotalMoneyEarned.Value,
			Kills = StatsFolder.Kills.Value,
			Deaths = StatsFolder.Deaths.Value,
			OwnsAutoCollector = StatsFolder.OwnsAutoCollector.Value,
			OwnsColorChanger = StatsFolder.OwnsColorChanger.Value,
			Owns2xCash = StatsFolder.Owns2xCash.Value,
			ColorTheme1 = CT1,
			ColorTheme2 = CT2,
			ColorTheme3 = CT3,
			Building = {},
			SpecialPurchase = {},
		}

		if #BuildingsFolder:GetChildren() ~= 0 then
			for _, building in pairs(BuildingsFolder:GetChildren()) do
				table.insert(StatsTable.Building, building.Name)
			end
		end
		
		if #SpecialPurchaseFolder:GetChildren() ~= 0 then
			for _, purchase in pairs(SpecialPurchaseFolder:GetChildren()) do
				table.insert(StatsTable.SpecialPurchase, purchase.Name)
			end
		end

		local success, returnValue
		repeat
			if not dontWait then
				waitForRequestBudget(Enum.DataStoreRequestType.SetIncrementAsync)
			end	
			success, returnValue = pcall(dataStore.SetAsync, dataStore, key, StatsTable)
		until success

		if success then
			print("Data was saved successfully.")
		else
			print("There was an error! " .. returnValue)
		end
	end
end

local function onShutdown()
	if RunService:IsStudio() then
		wait(2)
	else
		local finished = Instance.new("BindableEvent")
		local allPlayers = Players:GetPlayers()
		local leftPlayers = #allPlayers

		for _, player in ipairs(allPlayers) do
			coroutine.wrap(function()
				save(player)
				leftPlayers -= 1
				if leftPlayers == 0 then
					finished:Fire()
				end
			end)()
		end

		finished.Event:Wait()
	end
end

local function DeleteTycoon(player)
	
	print(player.Name.." has left the game!")
	
	for _, Tycoon in pairs(TycoonsHolder:GetChildren()) do

		local TycoonOwner = Tycoon:FindFirstChild("TycoonOwner").Value
		local CashToSave = Tycoon:FindFirstChild("CashToCollect")
		if TycoonOwner then
			if TycoonOwner == player then

				local StatsFolder = player:FindFirstChild("StatsFolder")
				local TycoonSavedCash = StatsFolder:FindFirstChild("TycoonSavedCash")

				TycoonSavedCash.Value = CashToSave.Value

				local StatsFolder = player:FindFirstChild("StatsFolder")
				local TimePlayed = StatsFolder:FindFirstChild("TimePlayed")
				
				Tycoon:Destroy()
				
				TycoonCloneHolder:FindFirstChild(Tycoon.Name).Parent = TycoonsHolder


				print(player.Name.." has left the game. Successfully destroyed their tycoon!")
			end
		end
	end
end


for _, player in ipairs(Players:GetPlayers()) do
	coroutine.wrap(setUp)(player)
end


Players.PlayerAdded:Connect(function(player)
	
	print(player.Name.." has joined the experience!")
	
	
	delay(1, function()
		setUp(player)
	end)
end)

Players.PlayerRemoving:Connect(function(player)
	table.remove(TycoonTable, TycoonTable[player.Name])

	DeleteTycoon(player)
	
	save(player)
	
	print(TycoonTable)
	
	print(player.Name.. " has left the experience!")
	
end)

game:BindToClose(onShutdown)

UpdateDeaths.OnServerEvent:Connect(function(player)
	local StatsFolder = player:FindFirstChild("StatsFolder")
	local Deaths = StatsFolder:FindFirstChild("Deaths")
	Deaths.Value += 1
end)


while true do
	wait(300)
	for _, player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(save)(player)
	end
	print("Data has been auto-saved.")
end

Hello I’m currently working on a tycoon game and have followed a beginner Datastore Tutorial from the devforum. Currently, everything works as intended however I do receive datastore throttling queues when the player leaves after an autosave has already occurred. So I believe it saves twice. (There are 8 players per server). I have few questions:

  • Should I just have autosave be the main way of saving data and not have saving on leaving?
  • How can I prevent queue throttling and does it save data after it finishes queuing or does it completely void the saving?
  • Should I be worried about it throttling and adding the requests to the queue?
  • Lastly, how could I possibly improve my code?

Thank you!

It saves the data when the requests is sent. When the que is not full, no request gets dropped. Even if the player leaves it saves.

1 Like

Usually you will get a queued request if the players autosave occurs and then they leave the game at virtually the same time, i.e. not much time between save requests. Also if you are testing this in Studio or on a server with just one player the request will happen twice, one on PlayerRemoved and another on BindToClose because the server also closed because there was no on left in it.

Under normal circumstances this will not happen an awful lot and should not be a cause for worry. There is also the condition when the autosave is fired, then the player instantly leaves, and the server closes. This will send 3 requests but they will be queued, this means they will all happen but it will just take longer for the second and third requests. You should save on PlayerRemoved because they may do something important just before leaving, and if the autosave was fired earlier their data will be old data and will not include the important thing they just did.

1 Like

Thank you for the in-depth reply! I’m glad to hear that requests in queue aren’t necessarily anything to worry about. In an incompletely unrelated note, I did have an issue of Tycoon:Destroy() not being called after a player left the game. Could the queueing be causing the tycoon to not be destroyed? Prior to posting the code, I had the DeleteTycoon() after the save(). Could there be a possibility that the PlayerRemoving event doesn’t fire sometimes?

Personally I would handle the creation and deletion of the Tycoon in a separate script to the DataStore script. You can hook PlayerAdded/Removing there and it wont interfere or complicate the logic in your DataStore script. You can use Binds to probe the DataStore script and pass the required info to the TycoonScript. It may be easier to figure out why the deletion is not occurring.

And personally I would also drop the Instance Values and use binds/remotes to Get/Set info in a tabulated copy of the DataStore indexed using the players UserId.

1 Like

I’ll definitely try separating the creation/deletion of the tycoons from the datastore script to see why. Thank you for the help!

1 Like