Saving a folder full of stats through Datastore

I’m trying to make a saving / load system for whenever a player joins & leaves his data would be either loaded or saved.

BaseStats folder includes the values that needs to be saved or loaded.
image

This is the script:

[ocal DataStore = game:GetService("DataStoreService"):GetDataStore("Guns")
local BaseStats = game.ServerStorage.BaseStats
local DataVersion = 1

game.Players.PlayerAdded:connect(function(play)
    local stats = game.ServerStorage.BaseStats:Clone()
    stats.Name = play.Name
    stats.Parent = game.ServerStorage.Stats
local Key = play.UserId.."Guns"..DataVersion
local gunTable
local success = pcall(function()
    gunTable = DataStore:GetAsync(Key)
end)
	if not success then
	print("Failed to retrieve data")
else
			if gunTable~=nil then
		for i,v in pairs(gunTable) do
	if stats:findFirstChild(v) then
stats[v].Value = true
			end
		end
	end
end
    play.CharacterAdded:connect(function(char)
        game.ReplicatedStorage.OnRespawn:FireClient(play)
    end)

end)
game.Players.PlayerRemoving:connect(function(play)
    if game.ServerStorage.Stats:FindFirstChild(play.Name) then

      local Key = play.UserId.."Guns"..DataVersion
    local gunTable = {}
        for i,v in pairs(game.ServerStorage.Stats:FindFirstChild(play.Name):GetChildren()) do
    if v.Value == true then
    table.insert(gunTable,v)
   		 end
    end
local success = pcall(function()
    DataStore:SetAsync(Key,gunTable)
end)
if not success then
	print("Data failed to save")
end
        game.ServerStorage.Stats:FindFirstChild(play.Name):Destroy()
    end
end)

The Output is always "Data Failed to save’, whenever the pcalls are removed, output is: pNKF1AS

4 Likes

You can’t store a tables arrays only in data stores, numbers, strings and tables I believe. What you could do is have a seperate data store for each value then convert it into an array.

1 Like

Another way that I have been trying to store data in 1s and 0s each digit represents a data value

So you have a data store that saves
00000000 if you have no trails

Let’s say purple is the first digit
10000000 means player owns the purple trail and you have to take the first number

1 Like

What you’re doing wrong is you can’t store an array in a Datastore. You need to encode it to JSON, like this:

[ocal DataStore = game:GetService("DataStoreService"):GetDataStore("Guns")
local httpService = game:GetService("HttpService")
local BaseStats = game.ServerStorage.BaseStats
local DataVersion = 1

game.Players.PlayerAdded:connect(function(play)
    local stats = game.ServerStorage.BaseStats:Clone()
    stats.Name = play.Name
    stats.Parent = game.ServerStorage.Stats
    local Key = play.UserId.."Guns"..DataVersion
    local gunTable
    local success = pcall(function()
        gunTable = httpService:JSONDecode(DataStore:GetAsync(Key))
    end)
    if not success then
        print("Failed to retrieve data")
    else
	if gunTable~=nil then
	    for i,v in pairs(gunTable) do
	        if stats:findFirstChild(v) then
                    stats[v].Value = true
	        end
	    end
        end
    end
    play.CharacterAdded:connect(function(char)
        game.ReplicatedStorage.OnRespawn:FireClient(play)
    end)

end)
game.Players.PlayerRemoving:connect(function(play)
    if game.ServerStorage.Stats:FindFirstChild(play.Name) then
        local Key = play.UserId.."Guns"..DataVersion
        local gunTable = {}
        for i,v in pairs(game.ServerStorage.Stats:FindFirstChild(play.Name):GetChildren()) do
            if v.Value == true then
              table.insert(gunTable,v)
            end
        end
        local success = pcall(function()
            DataStore:SetAsync(Key, httpService:JSONEncode(gunTable)) -- HERE
        end)
        if not success then
	    print("Data failed to save")
        end
        game.ServerStorage.Stats:FindFirstChild(play.Name):Destroy()
    end
end)

It turns your array into a string.

3 Likes

Strings as well. You might be able to store booleans as well.

1 Like

Lemme edit that real quick :wink: thanks for clearing that up

1 Like

Here is something that I did similarly a while ago. With some modifications you could probably get it to work with your system.
Example.rbxl (16.7 KB)

The code looks like this:

local dss = game:GetService("DataStoreService");
local players = game:GetService("Players");
local rs = game:GetService("ReplicatedStorage");

local userData = dss:GetDataStore("UserData");
local dataTemplate = script:WaitForChild("DataTemplate");
local forceSaveEvent = script:WaitForChild("ForceSave");

local dtTotal = #dataTemplate:GetDescendants();

function parseData(parent, data)
	if (data["obj"] == nil) then
		return;
	end
	local contents = data["content"];
	local object = Instance.new(data["obj"], parent);
	object.Name = data["name"];
	
	if (data["value"]) then
		object.Value = data["value"];
	end
	if (contents ~= {}) then
		--print(dump(contents));
		for i,v in pairs(contents) do
			local newTable = {obj=v.obj,content=v["content"],name=v.name};
			if (v.value) then
				newTable["value"] = v.value;
			end
			parseData(object, newTable);
		end
	end
	return object;
end

function getTableForm(parent)
	local newData = {["obj"]=parent.ClassName,["name"]=parent.Name};
	local newContent = {};
	
	if (#parent:GetChildren() > 0) then
		for i,v in pairs(parent:GetChildren()) do
			local tForm = getTableForm(v);
			table.insert(newContent, tForm);
		end
		if (parent.ClassName ~= "Folder") then
			newData["value"] = parent.Value;
		end
		newData["content"] = newContent;
		return newData;
	else
		if (parent.ClassName ~= "Folder") then
			newData["value"] = parent.Value;
		end
		newData["content"] = newContent;
		return newData;
	end
end

function dump(o)
   if type(o) == 'table' then
      local s = '{ '
      for k,v in pairs(o) do
         if type(k) ~= 'number' then k = '"'..k..'"' end
         s = s .. '['..k..'] = ' .. dump(v) .. ','
      end
      return s .. '} '
   else
      return tostring(o)
   end
end

forceSaveEvent.Event:Connect(function(player)
	local lsData = getTableForm(player:WaitForChild("leaderstats"))["content"];
	local pdData = getTableForm(player:WaitForChild("PlayerData"))["content"];
	userData:SetAsync(player.UserId, {["leaderstats"]=lsData,["player"]=pdData});
end)

players.PlayerAdded:Connect(function(player)
	local data = userData:GetAsync(player.UserId);
	
	if (data) then
		print("Load from data");
		local ls = parseData(player, {["obj"]="Folder",["content"]=data["leaderstats"],["name"]="leaderstats"});
		local pd = parseData(player, {["obj"]="Folder",["content"]=data["player"],["name"]="PlayerData"});
		if (ls and pd) then
			print("Loading successful");
		end
	else
		print("Load defaults");
		for i,v in pairs(dataTemplate:GetChildren()) do
			local d = getTableForm(v);
			while (d == nil) do
				wait(.05);
			end
			parseData(player, d);
		end
	end
end)

players.PlayerRemoving:Connect(function(player)
	local lsData = getTableForm(player:WaitForChild("leaderstats"))["content"];
	local pdData = getTableForm(player:WaitForChild("PlayerData"))["content"];
	userData:SetAsync(player.UserId, {["leaderstats"]=lsData,["player"]=pdData});
end)
2 Likes

To counteract this statement, you can store tables in DataStores, just not arrays, as the error states.

Also small little side note I’m going to add here: @Madchap32 provided a working example with the usage of HttpService:JSONEncode() and HttpService:JSONDecode(), but if you don’t want to use JSON encoding/decoding, you could transfer your indexes in the table to string indexes to make it a dictionary. Something like:

{
    ['moneystats'] = 123,
    ['premiumcurrency'] = 321,
    ['nickname'] = 'somenickname',
    ['you can add another dictionary!'] {
        ['swordsorsomething'] = true,
    }
}
2 Likes

No problem! I can’t like your post I’ve reached the limit of likes and I can’t do it for another six minutes. Yay.

1 Like

You can use the Value names as indexes and the Values as values.

If you loop through the children of your stats folder using GetChildren and an ipairs loop and create a table with this information you can then save it and load it from the datastore. To add indexes and values to your table you can do this tbl[index] = value

Because datastores can store dictionaries (tables with strings as their indexes) you can then store the table you’ve created.

When you load it you’ll want to apply the values in the saved table to the Values using FindFirstChild and the table index. You know that the index is the name of your Value instances, so you can use a pairs loop to loop through the saved table and get the indexes and values. Make sure you check if the Value exists though otherwise you’ll be trying to set the .Value property of nil if you decide to remove any values in the future.

3 Likes

thanks for the script, however the shop system breaks if I try to use your example

1 Like

There’s no need to JSON encode your data. You’d be increasing the size of the data since Datastores already JSON encode their data.

2 Likes

What is the shop system script?

2 Likes

Serverside: (ServerScriptService)

function onPurchase(player,item)
	local price = 0
	if item == "RainbowTrail" then
		price = 30
	elseif item == "BlueTrail" then
		price = 30
	elseif item == "PinkTrail" then
		price = 30
	elseif item == "GreenBlueTrail" then
		price = 30
	elseif item == "LimeTrail" then
		price = 30
	elseif item == "PinkBlueTrail" then
		price = 30
	elseif item == "PewdiepieTrail" then
		price = 399
	elseif item == "LennyFace" then
		price = 100
	end
	if player.XP.Value >= price then
		player.XP.Value = player.XP.Value - price
		local pstats = game.ServerStorage.Stats:FindFirstChild(player.Name)
		local val = pstats:FindFirstChild(item)
		val.Value = true
		return true
	else
		return false
	end
end

local function checktrail(player)
	for _, child in pairs(player.Character.Head:GetChildren()) do
		if child ~= nil then
			if child:IsA("Trail") then
				child:Destroy()
				player.Character.Head.Attachment0:Destroy()
				player.Character.HumanoidRootPart.Attachment1:Destroy()
				end
		
		end
	end
		for _, child in pairs(player.Character.HumanoidRootPart:GetChildren()) do
		if child ~= nil then
			if child:IsA("ParticleEmitter") then
				child:Destroy()
				end
		
		end
	end
end


function equipItem(player,item)
	local pstats = game.ServerStorage.Stats:FindFirstChild(player.Name)
	if pstats then
		local val = pstats:FindFirstChild(item)
		if val.Value == true then
			local head = player.Character.Head:GetChildren()
			local trail = game.ServerStorage.Items:FindFirstChild(item)
			checktrail(player)
			local newtrail = trail:Clone()
		
		if newtrail:IsA("Trail") then
			local A0 = Instance.new("Attachment",player.Character.Head)
			local A1 = Instance.new("Attachment",player.Character.HumanoidRootPart)
			A0.Name = "Attachment0"
			A1.Name = "Attachment1"
			A1.Position = Vector3.new(0, -0.8, 0)
			newtrail.Attachment0 = A0
			newtrail.Attachment1 = A1
			newtrail.Parent = player.Character.Head		
			else if newtrail:IsA("ParticleEmitter") then
			newtrail.Parent = player.Character.HumanoidRootPart
			
			end
	
			return true

		end
	end
end
end


function returnValue(player,item)
	local stats = game.ServerStorage.Stats
	if stats:FindFirstChild(player.Name) then
		local pstats = stats:FindFirstChild(player.Name)
		if pstats:FindFirstChild(item) then
			return pstats:FindFirstChild(item).Value
		end
	end
end





game.ReplicatedStorage.OnPurchase.OnServerInvoke = onPurchase
game.ReplicatedStorage.ReturnValue.OnServerInvoke = returnValue
game.ReplicatedStorage.OnEquip.OnServerInvoke = equipItem
game.ReplicatedStorage:FindFirstChild("Remove").OnServerEvent:Connect(checktrail)

PurchaseHandler (Localscript inside the shop UI)

local trails = script.Parent.MainFrame:GetChildren()
local indexbutton = nil
local erro = game.Workspace.Music.errorsound

en = true

for i = 1, #trails do 
	if trails[i]:IsA("Frame")  and trails[i]:WaitForChild("TextButton") then
		local button = trails[i]:FindFirstChild("TextButton")
		button.MouseButton1Down:connect(function()
			if en == true then
				en = false
				local val = game.ReplicatedStorage.ReturnValue:InvokeServer(trails[i].Name)
				if val == false then
					local purchase = game.ReplicatedStorage.OnPurchase:InvokeServer(trails[i].Name)
					if purchase == true then
						script.Sound:Play()
						-- If purchase is true then the item has been bought successfuly, equip is now enabled
						button.Text = "EQUIP"
						button.TextColor3 = Color3.fromRGB(172, 172, 172)
						--
						button.BackgroundColor3 = Color3.fromRGB(165, 165, 165)
					else
						button.Text = "NO $$$$"
						button.BackgroundColor3 = Color3.fromRGB(255, 43, 46)
						button.Parent.BackgroundColor3 = Color3.fromRGB(255, 43, 46)
						erro:Play()
						wait(0.5)
						button.Text = "PURCHASE"
						button.BackgroundColor3 = Color3.fromRGB(255,255,255)
						button.Parent.BackgroundColor3 = Color3.fromRGB(255,255,255)
	
					end
				elseif val == true then
					local equip = game.ReplicatedStorage.OnEquip:InvokeServer(trails[i].Name)
					if equip == true then
						button.Text = "EQUIPPED"
						button.TextColor3 = Color3.fromRGB(112, 112, 112)
						button.BackgroundColor3 = Color3.fromRGB(106, 106, 106)

						if indexbutton ~= nil then
							indexbutton.Text = "EQUIP"
							indexbutton.TextColor3 = Color3.fromRGB(172, 172, 172)
						--
						button.BackgroundColor3 = Color3.fromRGB(165, 165, 165)
						end
						indexbutton = button
					end
				end
				wait(0.5)
				en = true
			end
		end)
	end
end

game.ReplicatedStorage.OnRespawn.OnClientEvent:connect(function()
	for i = 1, #trails do
		if trails[i]:WaitForChild("TextButton") then
			local button = trails[i]:FindFirstChild("TextButton")
			local val = game.ReplicatedStorage.ReturnValue:InvokeServer(trails[i].Name)
			if val == true then
				button.Text = "EQUIP"
				button.TextColor3 = Color3.fromRGB(172, 172, 172)
				--
				button.BackgroundColor3 = Color3.fromRGB(165, 165, 165)	
			end
		end
	end
end) 

image

Please make these edits to the code that @Madchap32 gave you,

local DataStore = game:GetService("DataStoreService"):GetDataStore("Guns")
local httpService = game:GetService("HttpService")
local BaseStats = game.ServerStorage.BaseStats
local DataVersion = 1

game.Players.PlayerAdded:connect(function(play)
    local stats = game.ServerStorage.BaseStats:Clone()
    stats.Name = play.Name
    stats.Parent = game.ServerStorage.Stats
    local Key = play.UserId.."Guns"..DataVersion
    local gunTable
    local success = pcall(function()
        gunTable = httpService:JSONDecode(DataStore:GetAsync(Key))
    end)
    if not success then
        print("Failed to retrieve data")
    else
	if gunTable~=nil then
	    for i,v in pairs(gunTable) do
	        if stats:FindFirstChild(v) then
                    stats[v].Value = true
	        end
	    end
        end
    end
    play.CharacterAdded:connect(function(char)
        game.ReplicatedStorage.OnRespawn:FireClient(play)
    end)

end)

I fixed a couple typos and added in a JSONDecode() to the stats.

1 Like

Right, you need to decode it, let me fix that in an edit.

1 Like

Alright, I fixed the example. There you go!

1 Like

Script seems to be working, thanks you for that.
but not sure whether it’s shophandler problem, but player still needs to buy all over again to update the val

1 Like