Datastore Tutorial for Beginners

Basically, we are kicking the player if the data fails to load. There are two situations where the data returned will be nil: If it fails to load and when the data is just empty (the player is playing for the first time / there’s no previous data). We must deal with the difference between those two situations to avoid dataloss (this is not the only way to avoid it), here is an example:

You might have seen things like this on some bad datastore tutorials:

local DefaultCash = 10
local PlayerCash = CashDatastore:GetAsync(player_id) or DefaultCash

Well, it works. If the player has no cash stored, meaning they’re playing for the first time, :GetAsync method will return nil, and we use the default value in this case. But there’s a huge problem here, what if the data fails to load? Simple, :GetAsync will return nil, and we are going to reset the player’s cash. Imagine someone playing for a long time to get a good amount of cash and for an error like this, their cash is reset.

This is why I’m kicking the player and not saving the data (< this is important) if it fails to load. There are many others solutions for this case, like retry to load the data, rejoin the player with TeleportService, you can also think about a new solution, just use your imagination and don’t forget to test if it actually works.

I hope this makes sense :slight_smile:

1 Like

Well, I have been based on two DataStores that I have seen in the developer forum, and then I tried to combine a little of both. Use the GEILER123456 method to save the data and use BindClose, then use the part of your method to kick the player if there is possible data loss; After all this, I don’t know if my DataStore will work fine

local DataStoreService = game:GetService("DataStoreService")
local configurationsData = DataStoreService:GetDataStore("ConfigurationsData")

Players.PlayerAdded:Connect(function(player)
	local DataFolder = Instance.new("Configuration")
	DataFolder.Name = "SettingsData"
	DataFolder.Parent = player
	local GraphicsFolder = Instance.new("Folder")
	GraphicsFolder.Name = "Graphics"
	GraphicsFolder.Parent = DataFolder
	local GameFolder = Instance.new("Folder")
	GameFolder.Name = "GameSettings"
	GameFolder.Parent = DataFolder
	local KeysFolder = Instance.new("Folder")
	KeysFolder.Name = "Keys"
	KeysFolder.Parent = DataFolder

	local success, KeyInfo = pcall(function()
		return configurationsData:GetAsync(player.UserId)
	end)
	if not success then
		warn(KeyInfo)
		player:Kick("Has sido expulsado del juego para evitar la pérdida de datos, ingrese de nuevo / You have been kicked from the game to avoid data loss, please log in again.")
	elseif success then
		if KeyInfo ~= nil then
			GraphicsFolder:WaitForChild("Shadow").Value = KeyInfo.shadows
			shadowSettings[KeyInfo.shadows ](player)
			GraphicsFolder:WaitForChild("Water").Value = KeyInfo.water
			waterSettings[KeyInfo.water](player)
			GraphicsFolder:WaitForChild("Tree").Value = KeyInfo.tree_texture
			treeSettings[KeyInfo.tree_texture](player)

			GameFolder:WaitForChild("FPS").Value = KeyInfo.fps
			ViewFPS[KeyInfo.fps](player)
			GameFolder:WaitForChild("Ping").Value = KeyInfo.ping
			ViewPing[KeyInfo.ping](player)
			GameFolder:WaitForChild("BrilloVol").Value = KeyInfo.gamma
			VolumenDeBrillo["Gamma"](player, KeyInfo.gamma)
			GameFolder:WaitForChild("PlayerChose").Value = KeyInfo.friends
			LumberDrop[KeyInfo.friends](player)
			GameFolder:WaitForChild("Language").Value = KeyInfo.language
			LanguageConfg[KeyInfo.language](player, "PlayerAdded")
			GameFolder:WaitForChild("SlotsInventory").Value = KeyInfo.SizeSlots
			SlotsInventory["SlotsSize"](player, KeyInfo.SizeSlots)

			KeysFolder:WaitForChild("Crouch").Value = KeyInfo.crouch
			CrouchKey[KeyInfo.crouch](player)
		else
			GraphicsFolder:WaitForChild("Shadow").Value = "Medio"
			shadowSettings.Medio(player)
			GraphicsFolder:WaitForChild("Water").Value = "Medio"
			waterSettings.Medio(player)
			GraphicsFolder:WaitForChild("Tree").Value = "Enabled"
			treeSettings.Enabled(player)

			GameFolder:WaitForChild("FPS").Value = "UnEnabled"
			ViewFPS.UnEnabled(player)
			GameFolder:WaitForChild("Ping").Value = "UnEnabledPing"
			ViewPing.UnEnabledPing(player)
			GameFolder:WaitForChild("BrilloVol").Value = 0.5
			VolumenDeBrillo["Gamma"](player, 0.5)
			GameFolder:WaitForChild("PlayerChose").Value = "ForLocalPlayer"
			LumberDrop.ForLocalPlayer(player)
			GameFolder:WaitForChild("Language").Value = "English"
			LanguageConfg.English(player)
			GameFolder:WaitForChild("SlotsInventory").Value = 90
			SlotsInventory["SlotsSize"](player, 90)

			KeysFolder:WaitForChild("Crouch").Value = "OnePress"
			CrouchKey.OnePress(player)
		end
	end
end)

local function save(player, dontWait)
	local SettingsData = player:FindFirstChild("SettingsData")
	if SettingsData then
		local Graphics = SettingsData:FindFirstChild("Graphics")
		local GameSettings = SettingsData:FindFirstChild("GameSettings")
		local Keys = SettingsData:FindFirstChild("Keys")
		if Graphics and GameSettings and Keys then

			local Send_Data = {
				shadows = player.SettingsData.Graphics.Shadow.Value;
				water = player.SettingsData.Graphics.Water.Value;
				tree_texture = player.SettingsData.Graphics.Tree.Value;

				fps =player.SettingsData.GameSettings.FPS.Value;
				ping = player.SettingsData.GameSettings.Ping.Value;
				gamma = player.SettingsData.GameSettings.BrilloVol.Value;
				friends = player.SettingsData.GameSettings.PlayerChose.Value;
				language = player.SettingsData.GameSettings.Language.Value;
				SizeSlots = player.SettingsData.GameSettings.SlotsInventory.Value;

				crouch = player.SettingsData.Keys.Crouch.Value;
			}
			local Success,Error
			repeat
				Success,Error = pcall(configurationsData.UpdateAsync, configurationsData, player.UserId,function(Data)
					return Send_Data
				end)
			until Success 
			if Success then
				print("Se ha guardado correctamente / Has been saved successfully")
			else
				print("hubo un error al guardar / there was an error saving")
				warn(Error)
			end
		end	
	end
end

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

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

Players.PlayerRemoving:Connect(save)
1 Like

I changed it to fit my values and it gave me a error saying DataStore request was added to queue. If request queue fills, further requests will be dropped. Try sending fewer requests

The code provided no longer works.

Running it presents you with a data save fail and Unable to cast value to function.

The code works perfectly fine for me,

Can you send me the line where it errors?

1 Like

Hi, the code has one more end, check the output.


Sorry for replying so late but this tutorial worked perfectly for my games. Thanks @iFlameyz!

1 Like

Hello.
I did this, and it works but seems a bit hardcoded as I must save as like 14 different items.
I tried to use for-loops to do this but it isn’t working.
Code:


-- // Assigning variables //
local DataStoreService = game:GetService("DataStoreService")
local dataStore = DataStoreService:GetDataStore("PlaneSkinOwnedSave") -- This can be changed to whatever you want

local function saveData(player) -- The functions that saves data
	local skins = player.OwnedSkins
	tableToSave = {
	}
	for i,v in pairs(skins:GetDescendants()) do
		if v.Name == "Owned" then
			table.insert(tableToSave, v.Value)
			print(v.Value)
		end
	end
	local success, err = pcall(function()
		dataStore:SetAsync(player.UserId, tableToSave) -- Save the data with the player UserId, and the table we wanna save
	end)

	if success then -- If the data has been saved
		print("Data has been saved!")
	else -- Else if the save failed
		print("Data hasn't been saved!")
		warn(err)		
	end
end
game.Players.PlayerAdded:Connect(function(player) -- When a player joins the game
	wait(1)
	-- // Assigning player stats //
	local skins = player.OwnedSkins
	local toGive = {}
	for i,v in pairs(skins:GetDescendants()) do
		if v.Name == "Owned" then
			table.insert(toGive, v.Value)
		end
	end
	local data -- We will define the data here so we can use it later, this data is the table we saved
	local success, err = pcall(function()
		data = dataStore:GetAsync(player.UserId) -- Get the data from the datastore
	end)
	if success and data then -- If there were no errors and player loaded the data
		for count = 1,14 do
			print(data[count])
			toGive[count] = data[count]
		end
	else -- The player didn't load in the data, and probably is a new player
		print("The player has no data!") -- The default will be set to 0
	end

end)
game.Players.PlayerRemoving:Connect(function(player) -- When a player leaves
	local success, err  = pcall(function()
		saveData(player) -- Save the data
	end)
	if success then
		print("Data has been saved")
	else
		print("Data has not been saved!")
	end
end)
game:BindToClose(function() -- When the server shuts down
	for _, player in pairs(game.Players:GetPlayers()) do -- Loop through all the players
		local success, err  = pcall(function()
			saveData(player) -- Save the data
		end)
		if success then
			print("Data has been saved")
		else
			print("Data has not been saved!")
		end
	end
end)

No errors in dev log

1 Like

Hi i tried using your datastore tutorial but whenever i leave the game it doesnt save the data. i didnt changed anything in the code. just added the values and another folder.

may i ask if how can i store a instance without need the player id like

i put a part in basepart

then i abandoned the server

then i went back the part is still in the basepart

how can i use datastore in there without needing a players key

You can use anything as the key which will be like accessing a lock with all of your information

Maybe you’re changing the values on the client side and not server

Extremely helpful and informative, thanks mate!

I also really appreciate the helpful comments and explanation for what each code segment does and why! Very helpful for newer scripters who may have no idea what each segment does.

As an extra side note by the way, in Part 2 for the final code at the top, the second to last ‘end’ still has a parenthesis even though it doesn’t need it.

game:BindToClose(function() – When the server shuts down
for _, player in pairs(game.Players:GetPlayers()) do
saveData(player) – Save the data
end)
end)

1 Like

What if I wanted to make a shop system with weapons, And it saves what ones you already own? Would I have to make them separate like

local Itemone = Instance.new("IntValue")
	Itemone.Name = "RustySword"
	Itemone.Parent = leaderstats

Or would I be able to do something like making the data ["Rusty Sword","Crossbow",,"Dark heart"] all in one string?

well you can do both, you can save everything into 1 string and then get the names by using string.split, or if you want to save it into 1 array then you can loop through each value for the savedWeapons

What a misinformed article lol. While it is obviously bad practice to use while wait() do in cases when a conditional could easily be substituted into the while statement (like the example provided in the doc), there are many cases like the example provided by @Urxpaly45 where it really doesn’t matter and— in fact— increases readability.

Acting as if someone is “a bad programmer” (as the article states) simply for following a certain convention— irrespective of the circumstances surrounding the use case— proves the only bad programmer is the one peddling this misinformation. And exposes their lack of critical thinking skills.

while wait() do is a tool in your coding arsenal. Good programmers know when to use it and when not to. Bad programmers read opinion and accept it as doctrine.

2 Likes

There’s not even a need to do any while wait statements. Just use RunService

RunService.Heartbeat:Connect(function()
    -- fires once every frame
    task.wait(1) -- add this to make it wait one second every frame before firing code
end)

This wouldn’t function, it would simply fire every frame because the task.wait does not debounce any events, the wait is useless

1 Like

Event listeners run in separate threads lol this wouldn’t function as intended.

Would this be good to handle if data wasn’t loaded in? (Ex. There was an error loading data).

I changed the if & elseif statements to check for:

  • Success and data > load in the data.
  • Success but no data returned > set the players data to 0 (assuming they are a new player)
  • Otherwise if no success > Kick the player
	local data -- We will define the data here so we can use it later, this data is the table we saved
	local success, err = pcall(function()

		data = dataStore:GetAsync(player.UserId) -- Get the data from the datastore

	end)

	if success and data then -- If there were no errors and player loaded the data

		Rank.Value = data[1] -- Set the money to the first value of the table (data)
		Tokens.Value = data[2] -- Set the coins to the second value of the table (data)
	
	elseif success then -- The player didn't load in the data, and probably is a new player
		print("The player has no data!") -- The default will be set to 0
	else
		warn(err)
		player:Kick("Data loading error - check your connection & Roblox status")
	end
end)