DataStores - Beginners to Advanced

I don’t think there is too much documentation about this, you can read about it here, though, it’s in the first blue box.

can i get help with my datastore?
when i join the game every value just become 100 and cant save
here is my script:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local dataStore = DataStoreService:GetDataStore("DataStoreName") --put your DataStore name here

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

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

	local click = Instance.new("IntValue")
	click.Name = "Clicks"
	local diamond = Instance.new("IntValue")
	diamond.Name = "Diamonds"
	local rebirth = Instance.new("IntValue")
	rebirth.Name = "Rebirths"

	local data = dataStore:GetAsync(key)

	click.Value = data or 0
	diamond.Value = data or 100
	rebirth.Value = data or 0

	click.Parent = leaderstats
	diamond.Parent = leaderstats
	rebirth.Parent = leaderstats
	leaderstats.Parent = player
end

local function save(player)
	local userId = player.UserId
	local key = "Player_" .. userId
	local leaderstats = player:FindFirstChild("leaderstats")

	if leaderstats then
		local clickValue = leaderstats.Clicks.Value
		dataStore:SetAsync(key, clickValue)
		local diamondValue = leaderstats.Diamonds.Value
		dataStore:SetAsync(key, diamondValue)
		local rebirthValue = leaderstats.Rebirths.Value
		dataStore:SetAsync(key, rebirthValue)
	end
end

local function onShutDown()
	wait(1)
end

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

Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutDown)

while true do
	wait(60)
	for _,player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(save)(player)
	end
end

It’s the way you are setting data, you are overriding the recent value set each time. Instead, you should save your data in an array or dictionary:
(pseudo-code)

local data = GetAsync(userId) --(simplified, I think you should make this more secure)
--data is your array/dictionary now
click.Value = data[1] or default
diamond.Value = data[2] or default
rebirth.Value = data[3] or default

...
local tableToSave = {clickValue, diamondValue, rebirthValue}
SetAsync(userId, tableToSave) --(simplified, I think you should make this more secure)

sorry i am confused at where should i put that in?

It was pseudo-code, to implement it, you would probably change your setUp function to

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

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

	local click = Instance.new("IntValue")
	click.Name = "Clicks"
	local diamond = Instance.new("IntValue")
	diamond.Name = "Diamonds"
	local rebirth = Instance.new("IntValue")
	rebirth.Name = "Rebirths"

	local data = dataStore:GetAsync(key)

	click.Value = (data and data[1]) or 0
	diamond.Value = (data and data[2]) or 100
	rebirth.Value = (data and data[3]) or 0

	click.Parent = leaderstats
	diamond.Parent = leaderstats
	rebirth.Parent = leaderstats
	leaderstats.Parent = player
end

and your save function to

local function save(player)
	local userId = player.UserId
	local key = "Player_" .. userId
	local leaderstats = player:FindFirstChild("leaderstats")

	if leaderstats then
		local clickValue = leaderstats.Clicks.Value
        local diamondValue = leaderstats.Diamonds.Value
        local rebirthValue = leaderstats.Rebirths.Value
        local toSave = {clickValue, diamondValue, rebirthValue}

        dataStore:SetAsync(key, toSave)
	end
end
1 Like

Hey, thank you very much for your tutorial! It was extremely helpful and your code is actually really good.

Recently learning Lua (I come from Java) a huge pet peeve of mine is tutorials not using proper coding practices. It makes it more difficult to understand and generally adds a lot of extra and unnecessary code!

The only suggestion I have is that I would’ve liked you to show how to save multiple stats and items as well.

Although, I’m pretty sure I figured this out on my own by creating a table.

I’ll paste the code here in case anyone is interested to see how you can do it, or provide feedback into my method of doing it!

local Players = game:GetService('Players')
local ServerStorage = game:GetService('ServerStorage')
local DataStoreService = game:GetService('DataStoreService')
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local dataStore = DataStoreService:GetDataStore("2222")
local items = ReplicatedStorage:WaitForChild("Items")

local function waitForRequestBudget(requestType)
	local currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
	while currentBudget < 1 do
		currentBudget = DataStoreService:GetRequestBudgetForRequestType(requestType)
		wait(5)
	end
end

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

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

	local coins = Instance.new("IntValue")
	coins.Name = "Coins"

	local strength = Instance.new("IntValue")
	strength.Name = "Strength"

	local inventory = Instance.new("Folder")
	inventory.Name = "inventory"
	inventory.Parent = player

	local tool = Instance.new("StringValue")
	tool.Name = "Tool"
	tool.Value = "nil"
	tool.Parent = inventory

	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
		print(returnValue)
		if returnValue == nil then
			returnValue = {
				Coins = 0,
				Strength = 0,
				Tool = "Phone1"
			}
		end

		coins.Value = returnValue["Coins"] or 0
		strength.Value = returnValue["Strength"] or 0
		player.inventory.Tool.Value = returnValue["Tool"] or "Phone1"

		player.CharacterAdded:Connect(function()
			if returnValue["Tool"] ~= nil or returnValue["Tool"] ~= "nil"  then
				for _, tool in ipairs(items:GetChildren()) do
					if tool:IsA("Tool") and tool.Name == returnValue["Tool"] then
						local clonedTool = tool:Clone()
						clonedTool.Parent = player.Backpack
						break
					end
				end
			else
				local clonedTool = items.Phone1:Clone()
				clonedTool.Parent = player.Backpack
			end
		end)
		coins.Parent = leaderstats
		strength.Parent = leaderstats
		leaderstats.Parent = player
	else
		print("Data Loading ERROR!!!")
	end
end

local function save(player, donttWait)
	local userID = player.UserId
	local key = "Player_" .. userID
	local leaderstats = player:FindFirstChild("leaderstats")

	if leaderstats then
		local coinValue = leaderstats.Coins.Value
		local strengthValue = leaderstats.Strength.Value
		local tool = player.inventory.Tool.Value
		local dataTable = {
			Coins = coinValue,
			Strength = strengthValue,
			Tool = tool
		}
		local success, returnValue 
		repeat
			if not donttWait  then
				waitForRequestBudget(Enum.DataStoreRequestType.SetIncrementAsync)				
			end
			success, returnValue = pcall(dataStore.UpdateAsync, dataStore, key, function()
				return dataTable
			end)
		until success

		if success then
			print("Data Saved!")
		else
			print("Data Saving ERROR!!!")
		end
	end
end


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

-- Delays the game from shutting down by X seconds so the PlayerRemoving Event will 100% fire
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

Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutdown)

-- Auto-Save all Players Every 600 seconds
while true do 
	wait(600)
	for _, player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(save)(player)
	end
end
1 Like

thank you for your guide! now i already understand!

1 Like

This doesn’t work please help me : (

1 Like

Elaborate please (send your code, your output and describe the problem).

Does this still work/are there any problems I’ll run into when using this? I wanna create my own datastore instead of just using ProfileService/whatever and this tutorial seems really helpful.

1 Like

There could be, this is barely tested, I made this for the purpose of showing some techniques that can be used when making DataStores.

As I already said in the tutorial, I recommend using ProfileService/other known DataStore modules instead of this if you just want a safe DataStore in your game since they will probably be a lot safer, they are heavily tested because of the amount of people that use them which makes bugs more unlikely.

@GEILER123456 Hey, I’m very new to Datastore I only had only been using it for a week. So, I found this topic and use some solution and method but my problem still occur… why doesn’t my datastore save?

local Datastore = game:GetService("DataStoreService"):GetDataStore("Data")
local Save = {"Data1", "Data2"}

game.Players.PlayerAdded:Connect(function(player)
	local folder = Instance.new("Folder")
	local Brick = Instance.new("IntValue")
	local Money = Instance.new("IntValue")
	
	folder.Name = "leaderstats"
	folder.Parent = player
	Money.Value = 0
	Money.Name = "Money"
	Money.Parent = folder
	Brick.Name = "Brick"
	Brick.Value = 0
	Brick.Parent = folder
	
	local success, err = pcall(function()
		Save[1] = Datastore:GetAsync(player.UserId)
		Money.Value = Save[1]
		Brick.Value = Save[2]
	end)
	if success then
		print(player.Name .. " Data has been successfully loaded!")
	else
		print(string.upper(player.Name .. " Data was not save " .. err))
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	local success, err = pcall(function()
		Datastore:SetAsync(player.UserId, Save)
		player.leaderstats.Money.Value = Save[1]
		player.leaderstats.Brick.Value = Save[2]
	end)
	if success then
		print(player.Name .. " Data has been successfully saved!")
	else
		print(string.upper(player.Name .. " Data was unsuccessful while saving " .. err))
	end
end)
1 Like

Keep in mind that this is a serverscript, you are dealing with multiple players.

		Save[1] = Datastore:GetAsync(player.UserId)
		Money.Value = Save[1]
		Brick.Value = Save[2]

doesn’t make much sense, same as

		Datastore:SetAsync(player.UserId, Save)
		player.leaderstats.Money.Value = Save[1]
		player.leaderstats.Brick.Value = Save[2]

When using :GetAsync(), you want to receive a table, since you want to save two values.
When saving, you want to save a table with those two values.

Here, a DataStore very similar to the one in the first part of my tutorial, just modified a bit so it includes your Money and Brick values:

local Players = game:GetService("Players")
local DataStore = game:GetService("DataStoreService"):GetDataStore("Data")

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

	local leaderstats = Instance.new("Folder")
	local money = Instance.new("IntValue")
	local brick = Instance.new("IntValue")

	leaderstats.Name = "leaderstats"
	money.Name = "Money"
	brick.Name = "Brick"

	local success, data = pcall(DataStore.GetAsync, DataStore, key)
	if success then
		print(data)
		data = data or {0, 0} --if no data was saved, then 0 will be the default for money and brick
		money.Value = data[1]
		brick.Value = data[2]

		money.Parent = leaderstats
		brick.Parent = leaderstats
		leaderstats.Parent = player
	else
		print("There was an error while getting the data: " .. data)
	end
end

local function save(player)
	local userId = player.UserId
	local key = "Player_" .. userId
	local leaderstats = player:FindFirstChild("leaderstats")

	if leaderstats then
		local toSave = {leaderstats.Money.Value, leaderstats.Brick.Value}
		local success, ret = pcall(DataStore.SetAsync, DataStore, key, toSave)

		if success then
			print("Successfully saved.")
			print(toSave)
		else
			print("There was an error while saving: " .. ret)
		end
	end
end

local function onShutDown()
	task.wait(2)
end

Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutDown)

This is of course not safe yet,

so I recommend to use a popular DataStore module like ProfileService or make it safer (by adding retries etc., most of which I know I mentioned in the tutorial).

This by far is one of the best posts I’ve seen thus far on devfourm. :+1: :+1:

1 Like
local success, ret, data
    repeat
        waitForRequestBudget()
        success, ret = pcall(dataStore.UpdateAsync, dataStore, key, function(oldData)
            oldData = oldData or default

            if oldData.SessionLock then
                --He is still sessionlocked, so just wait
                if os.time() - oldData.SessionLock < 1800 then
                    --session is alive
                    ret = "Wait"
                else
                    --session is dead, take over
                    oldData.SessionLock = os.time()
                    data = oldData
                    return data
                end
            else
                oldData.SessionLock = os.time()
                data = oldData
                return data
            end
        end)

        if ret == "Wait" then
            wait(5)
        end
    until success or not Players:FindFirstChild(name)

Do you think there’s a better alternative way to do this?
When I used it in my script it the data came back nil

Are there any errors?
Are you using

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

local dataStore = DataStoreService:GetDataStore("Players")
local default = {
	SessionLock = false,
	Cash = 0
}
local updateAsync = Enum.DataStoreRequestType.UpdateAsync

local function waitForRequestBudget()
	local currentBudget = DataStoreService:GetRequestBudgetForRequestType(updateAsync)

	while currentBudget < 1 do
		currentBudget = DataStoreService:GetRequestBudgetForRequestType(updateAsync)
		wait(5)
	end
end

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

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

	local cash = Instance.new("IntValue")
	cash.Name = "Cash"

	local success, ret, data
	repeat
		waitForRequestBudget()
		success, ret = pcall(dataStore.UpdateAsync, dataStore, key, function(oldData)
			oldData = oldData or default

			if oldData.SessionLock then
				--He is still sessionlocked, so just wait
				if os.time() - oldData.SessionLock < 1800 then
					--session is alive
					ret = "Wait"
				else
					--session is dead, take over
					oldData.SessionLock = os.time()
					data = oldData
					return data
				end
			else
				oldData.SessionLock = os.time()
				data = oldData
				return data
			end
		end)

		if ret == "Wait" then
			wait(5)
		end
	until success or not Players:FindFirstChild(name)

	if success then
		cash.Value = data.Cash

		cash.Parent = leaderstats
		leaderstats.Parent = player
	end
end

local function save(player, dontLeave, dontWait)
	local userId = player.UserId
	local key = "Player_" .. userId

	local leaderstats = player:FindFirstChild("leaderstats")

	if leaderstats then
		local cashValue = leaderstats.Cash.Value
		local success

		repeat
			if not dontWait then
				waitForRequestBudget()
			end
			success = pcall(dataStore.UpdateAsync, dataStore, key, function()
				return {
					SessionLock = dontLeave and os.time() or nil,
					Cash = cashValue
				}
			end)
		until success
	end
end

local function onShutdown()
	if RunService:IsStudio() then
		task.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, nil, true)
				leftPlayers -= 1
				if leftPlayers == 0 then
					finished:Fire()
				end
			end)()
		end

		finished.Event:Wait()
	end
end

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

Players.PlayerAdded:Connect(setUp)
Players.PlayerRemoving:Connect(save)
game:BindToClose(onShutdown)

while true do
	wait(60)
	for _,player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(save)(player, true)
	end
end

?
It seems to work for me.

When going into the Explorer and changing the data/typing in a command into the command bar, make sure you are in Server mode, not Client mode.

Yeah I’m using that script
Is it because I’m saving a dictionary to the datastore?

local function playerSave(player, dontLeave, dontWait)
	local playerKey = "Player_" .. player.UserId
	local leaderstats = player:FindFirstChild("leaderstats")
	
	if leaderstats then
		local tixAmount = leaderstats.Tix.Value
		local winAmount = leaderstats.Wins.Value
		local areasDiscovered = areasDiscoveredTable(player)
		local ventsUnlocked = unlockedVentsTable(player)
		local itemsDiscovered = unlockedItemsTable(player)
		
		local success
		repeat
			if not dontWait then
				waitForRequestBudget()
			end
			success = pcall(playerData.UpdateAsync, playerData, playerKey, function()
				return {
					SessionLock = dontLeave and os.time() or nil,
					Tix = tixAmount,
					Wins = winAmount,
					Items = itemsDiscovered,
					Areas = areasDiscovered,
					Vents = ventsUnlocked,
				}
			end)
		until success
	end
end

I replaced the playerSave with this

I just tried repeating the loop until data was not nil, and it kept its oldData.SessionLock at 1648759648
Also it seems the if ret == “Wait” then line never runs
I’m pretty sure what’s happening is success is always returning true regardless if it returns the data or not, making data always nil for some reason

Edit: Found the problem, for some reason my SessionLock in the datastore was set to a number instead of false, so when I joined it would break the data loading process

Does the data store name have to be “Name”?

No, it can be any string.

Keep in mind that if you’ve already set it to a string and now change it to another string, it will be like a new DataStore so it won’t have the data from your old one.

1 Like