Help with datastores; Am I doing this right?

Hello. This is the first time I am using datastores, so I am not sure if I am doing it right. I know this isn’t a thing to mess with.

Please tell me if this is the right way or the bad way (or the really really really bad way), or if there’s a more efficient way to do so.

Also I am not really sure if I should be exposing this (incase of in-game exploiters or something).
*Names were changed.

local DataStoreService = game:GetService("DataStoreService")

local a= DataStoreService:GetDataStore("adatastore")
local b= DataStoreService:GetDataStore("bdatastore")
local c= DataStoreService:GetDataStore("cdatastore")
local d= DataStoreService:GetDataStore("ddatastore")
local e= DataStoreService:GetDataStore("edatastore")

game.Players.PlayerAdded:Connect(function(player)

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

local a= Instance.new("IntValue", leaderstats)
a.Name = "a"
a.Value = 0

local x= Instance.new("IntValue", leaderstats)
x.Name = "Life"
x.Value = 0

local b= Instance.new("IntValue", player)
b.Name = "b"
b.Value = 50000

local c= Instance.new("IntValue", player)
c.Name = "c"
c.Value = 100

local d = Instance.new("IntValue", leaderstats)
d.Name = "Gems"
d.Value = 0

local PowerData
local LifeData
local PowerNeededData
local PowerMultiplierData
local GemsData
local success, errormessage = pcall(function()
	PowerData = PowerDataStore:GetAsync(player.UserId.."-eeee")
	LifeData = LifeDataStore:GetAsync(player.UserId.."-fffff")
	PowerNeededData = PowerNeededDataStore:GetAsync(player.UserId.."-dddd")
	PowerMultiplierData = PowerMultiplierDataStore:GetAsync(player.UserId.."-bbbb")
	GemsData = GemsDataStore:GetAsync(player.UserId.."-xxxxxx")
end)

if success then
	power.Value = PowerData
	life.Value = LifeData
	PowerNeeded.Value = PowerData
	PowerMultiplier.Value = PowerMultiplierData
	Gems.Value = GemsData
	print("Data loaded.")
else
	print("There was an error getting your data. Please rejoin.")
	warn(errormessage)
end
end)

game.Players.PlayerRemoving:Connect(function(player)
local success, errormessage = pcall(function()
	PowerDataStore:SetAsync(player.UserId.."-xxxx", player.leaderstats.Power.Value)
	LifeDataStore:SetAsync(player.UserId.."-xxxx", player.leaderstats.Life.Value)
	PowerNeededDataStore:SetAsync(player.UserId.."-xxxxxxxxx", player.leaderstats.PowerNeeded.Value)
	PowerMultiplierDataStore:SetAsync(player.UserId.."-xxxxx", player.leaderstats.PowerMultiplier.Value)
	GemsDataStore:SetAsync(player.UserId.."-xxxx", player.leaderstats.Gems.Value)
end)

if success then
	print("Successfully saved player's data.")
else
	print("There was an error saving player's data.")
	warn(errormessage)
end

end)

Thanks ahead.

Here is something that may be a bit more efficient and straightforward to modify the template in the future that I made a while back. Instead of using multiple datastores it serializes two different folders (with all descendants) when saving and deserializes the data back into folders and values when it loads. To edit it go into the DataTemplate (in the script) and add any folders/values that you want inside the two existing folders. When the player data loads for the first time, whatever is in there will copy over into the player.

If you don’t want to use different code then I’d say just don’t use so many datastores. There isn’t a huge need for it. Put all the data in to one table then save the table to the userId.

Example.rbxl (16.7 KB)

The Code:

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)
3 Likes

Thanks for the reply. After working around it and understanding (ehhh… partly understanding) how it works - it saves the data correctly, but is there a way I can make new players’ data start from x?

Just edit the data template. It copies the default values from it.

1 Like

There are some improvements to be made to this code.

  • I advise you write a catcher function over directly connecting to PlayerAdded. You have several yielding functions at the top of your script so there is a possibility that the function will not fire for some players. You must account for new and existing players.[1]

  • Use ipairs to iterate over arrays, please. It

    • is faster than pairs,

    • shows that you are working with arrays and

    • has more canonical basis over pairs which is designed to work with dictionaries (arbitrary keys, not necessarily non-numerical however)

  • Not sure what the point of dump is. Consider JSONEncode and printing the result? It’s faster than a pure-Lua solution and that kind of thing is already done for you by the engine, so might as well make use of it over reinventing the wheel.

  • Wrap your DataStore calls in pcalls. This helps catch errors and determine how to move forward with code. You can also prevent saving in case of DataStore errors so that player data isn’t overwritten with an unintended set, thus causing irrevocable data loss.

[1]:

local function onPlayerAdded(player)
end

Players.PlayerAdded:Connect(onPlayerAdded)

for _, player in ipairs(Players:GetPlayers()) do
    onPlayerAdded(player)
end

's about it from me.

3 Likes

Thanks for the advice. I’ll make sure to keep this in mind for whenever I need this again.

What basis are you working off of? Have you actually tested this and in which VM are you referring to? ipairs is not slower than pairs. Remember that there is a difference in the case of using pairs and ipairs not only functionally but idiomatically as well. Use case is also worth consideration.

This was also discussed at RDC, under the breakout session Lua As Fast As Possible. You can also visit the thread Faster Lua VM: Studio beta, there are several discussions around generic and numeric for loops with special consideration to the generators pairs and ipairs.

  • ipairs is designed to work with arrays. pairs is designed to work with dictionaries. ipairs relies on chronological numeric indexes. pairs is for scenarios where keys aren’t known from the start, which in the case of dictionaries this is such a possibility.

  • Internally, an ipairs iteration is intended to work at an i+1 basis. ipairs is like a numeric for loop except you can work directly with the index of the element in the array as well as its value which are returned from the generator, which is ipairs itself.

  • Internally, a pairs generator is intended to work with the next function for arbitrary or unknown keys, as next returns a new element in a table given a key (see the Lua Globals page for the next function). As well, in pairs, you have no guarantee of order.

The test you wrote isn’t particularly accurate because it’s not being used in a practical scenario either. If you’re able to provide an actual sample in a real production environment, I’d love to see.