Opinions on how I handle my datastores

Since I haven’t been programming for that long, I decided to try create a datastore module for my own little project to improve on my skills. I don’t know if what I’ve done is of a decent standard so any feedback on how to improve it is appreciated.

local DS = {}
local DataStoreService = game:GetService("DataStoreService"):GetDataStore("A2")

function CREATE_PROFILE(player,DataTable)
	local MainFolder = Instance.new("Folder",player)
	MainFolder.Name = "Profile"
	local Coins = Instance.new("IntValue",MainFolder)
	Coins.Name = "Coins"
	local GamepassesFolder = Instance.new("Folder",MainFolder)
	GamepassesFolder.Name = "GamepassesFolder"
	local sounds = Instance.new("Folder",MainFolder)
	sounds.Name = "SoundsFolder"
	local CurrentSound = Instance.new("StringValue",MainFolder)
	CurrentSound.Name = "CurrentSound"
	local RegionsFolder = Instance.new("Folder",MainFolder)
	RegionsFolder.Name = "RegionsFolder"
	local UIScaleValue = Instance.new("StringValue",MainFolder)
	UIScaleValue.Name = "UIScaleValue"
	local IsBannedValue = Instance.new("BoolValue",MainFolder)
	IsBannedValue.Name = "IsBanned"
	
	if DataTable == nil then
		print("New Player")
		Coins.Value = 0
		CurrentSound.Value = "Basic Breathe"
		IsBannedValue.Value = false
		UIScaleValue.Value = "1"
		local region = Instance.new("Folder",RegionsFolder)
		region.Name = "Forest"
		local breathe = Instance.new("Folder",sounds)
		breathe.Name = "Basic Breathe"
	else

		Coins.Value = DataTable[1]
		CurrentSound.Value = DataTable[2]
		IsBannedValue.Value = DataTable[3]
		UIScaleValue.Value = DataTable[4]

		for i = 1,#DataTable[5],1 do
			local Item = Instance.new("Folder",RegionsFolder)
			Item.Name = DataTable[5][i]
		end

		for i = 1,#DataTable[6],1 do
			local Item = Instance.new("Folder",sounds)
			Item.Name = DataTable[6][i]
		end
		
	end
end

local function GET_VALUES(player)
	local profile = player:FindFirstChild("Profile")
	local values = {}
	if profile then

		table.insert(values,profile:WaitForChild("Coins").Value)
		table.insert(values,profile:WaitForChild("CurrentSound").Value)
		table.insert(values,profile:WaitForChild("IsBanned").Value)
		table.insert(values,profile:WaitForChild("UIScaleValue").Value)

		local regions = {}
		for i,v in pairs(profile:WaitForChild("RegionsFolder"):GetChildren()) do
			table.insert(regions,v.Name)
		end
		table.insert(values,regions)

		local sounds = {}
		for i,v in pairs(profile:WaitForChild("SoundsFolder"):GetChildren()) do
			table.insert(sounds,v.Name)
		end
		table.insert(values,sounds)
		
		return values
	else
		return nil
	end
end
	
function DS:GET(player)
	local DataTable = nil
	while true do
		print("Fetching "..player.Name.."'s data.")
		wait(5)
		local status = pcall(function()
			DataTable = DataStoreService:GetAsync(player.UserId)
		end)
		if status == true then
			CREATE_PROFILE(player,DataTable)
			break
		else
			print("Error fetching "..player.Name.."'s data. Retrying in 5s")
		end
	end
end


function DS:SET(player)
	local values = GET_VALUES(player)
	if values then
		while true do
			print("Saving "..player.Name.."'s data.")
			local status = pcall(function()
				DataStoreService:SetAsync(player.UserId,values)
			end)
			if status == true then
				print("Successfully saved "..player.Name.."'s data.")
				break
			else
				print("Error saving "..player.Name.."'s data. Retrying in 5s")
				wait(5)
			end
			
		end
	else
		print("Player data not loaded, abandon save.")
	end
end
	
return DS
1 Like

Saving Roblox objects such as Folders and Values is much less efficient than saving the string of a JSON encoded table saved with the key of the UserId:

local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetDataStore("A2")
local HttpService = game:GetService("HttpService")

function createProfile(player)
    local profile = {}
    profile.playerId = player.UserId --UserId does not change, unlike username
    profile.coins = 0
    profile.currentSound = "Basic Breathe"
    profile.UIScale = 1
    profile.region = "Forest"
    profile.breath = "Basic Breathe"
    return profile
end

function loadProfile(player)
    local profile
    local success, err = pcall(function()
        profile = DataStore:GetAsync(player.UserId)
    end)
    if not success then print(err) return {} end --Throws an error and ends the function if the :GetAsync fails
    profile = HttpService:JSONDecode(profile)
    return profile
end

function saveProfile(profile)
    local playerId = profile.playerId
    profile =  HttpService:JSONEncode(profile)
    local success, err = pcall(function()
        DataStore:SetAsync(playerId, profile)  -- Replace this with UpdateAsync when you understand datastores better, as it's a better method to use.
    end)
    if not success then print(err) return false
    else return true end
end
3 Likes

Thanks for the suggestion. However, I have one question, the reason why I went with instances was due to the reason that clients could easily read data that they need, with tables, it seems much harder/slower to obtain information. Any suggestions?

I would suggest a Script or ModuleScript on the server which, when a player joins, adds their profile to a table and returns data on a RemoteEvent being invoked. So on the server your final script would look something like:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerService = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local DSS = game:GetService("DataStoreService")

local DataStore = DSS:GetDataStore("A2")

local playerProfiles = {}

function createProfile(player)
    local profile = {}
    profile.playerId = player.UserId --UserId does not change, unlike username
    profile.coins = 0
    profile.currentSound = "Basic Breathe"
    profile.UIScale = 1
    profile.region = "Forest"
    profile.breath = "Basic Breathe"
    return profile
end

function loadProfile(player)
    local profile
    local success, err = pcall(function()
        profile = DataStore:GetAsync(player.UserId)
    end)
    if not success then print(err) return {} end --Throws an error and ends the function if the :GetAsync fails
    profile = HttpService:JSONDecode(profile)
    return profile
end

function saveProfile(profile)
    local playerId = profile.playerId
    profile =  HttpService:JSONEncode(profile)
    local success, err = pcall(function()
        DataStore:SetAsync(playerId, profile)  -- Replace this with UpdateAsync when you understand datastores better, as it's a better method to use.
    end)
    if not success then print(err) return false
    else return true end
end

ReplicatedStorage.GetPlayerProfile.OnServerInvoke = function(player)
    return playerProfiles[player.UserId]
end

-- When a player joins the game their data is loaded or generated, and they're added to the playerProfiles register
Players.PlayerAdded:Connect(function(player)
    local profile = loadProfile(player)
    if not profile then profile = createProfile(player)
    playerProfiles[player.UserId] = profile
end)

-- When a player leaves the game their data is saved and removed from the active register
Players.PlayerRemoving:Connect(function(player)
    local playerId = player.UserId
    saveProfile(playerProfiles[playerId])
    playerProfiles[playerId] = nil
end)

Then to get the player’s details, you can simply use a LocalScript to fire the RemoteFunction:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

function getProfile()
    local profile = ReplicatedStorage.GetPlayerProfile:InvokeServer()  -- No need to pass the player variable, as this is done automatically
    return profile
end

-- Example for getting coins
function getCoins()
    local profile = getData()
    local coins = profile.coins
    print("Player has "..coins.." coins.")
    return coins
end
1 Like

For a neater, organised touch, wrap actual methods with pcall rather than an anonymous function. Also, try making your function variables local rather than global.

local function loadProfile(player)
    local success, result = pcall(DataStore.GetAsync, DataStore, player.UserId)
    if not success then
        warn(err)
        return {}
    end
    return HttpService:JSONDecode(result)
end

local function saveProfile(profile)
    local playerId = profile.playerId
    local success, result = pcall(DataStore.SetAsync, DataStore, playerId, profile)
    if not success then
        warn(result)
    end
    return success -- Subsequent if statement is redundant
end

Whilst wrapping the actual methods could be a useful idea, I wrote them in anonymous functions to make it easier for 0lmx and others (including myself) to follow; I had the else statement there for the same reason. After all, “code is read much more than it is written”.

Sorry for replying so late, however, when I tried this method, it worked all fine, but then there was one specific issue… I have Guis in my project that display the inventory and coins of a player, which have to be constantly updated as they are ever changing, and I don’t think constantly firing remote events to update the data would be good…

For a GUI that is changing too often to be controlled by RemoteEvents, you would use two separate variables - one on the server, one on the client. Whenever a value is changed, it should be updated on the client side immediately, and updated on the server side as normal (that is, with checks to ensure no foul play is afoot).
Then, when displaying the value on a Gui you simply reference the client-side variable. There’s no risk here as if an exploiter changes this value it only changes the displayed value, rather than the server-side value used when purchasing items etc. in the example of coins.

If you understand the Laggy Cannons tutorial then this may be an easier concept to grasp.

For a GUI that’s opened on the click of a button, simply fire the RemoteEvent when the button is .Activated.

1 Like

Ah. I see. Thank you once again!

1 Like