How could this datastore code be improved?

Hello, I have this data store code and I was wondering how it could be improved

--|| SERVICES ||--
local RunService = game:GetService("RunService")
local DataStoreService = game:GetService("DataStoreService")

--|| MODULES ||--
local GlobalFunctions = require(script.GlobalFunctions)
local StarterData = require(script.StarterData)
local Properties = require(script.Properties)

local module = {}
local cachedTable = {}

local Datastore = DataStoreService:GetDataStore(Properties.datastore)

function module:output(msg)
	if Properties.prints then
		print("[DATABASE]: "..msg)
	end
end

function module:startData(Player)
	--> create logs for player, purpose is to cache
	cachedTable[Player] = {
		data = {},
		lastSave = os.clock(),
		dataLoaded = false,
	}
	
	module:loadData(Player)
end

function module:endData(Player)
	module:saveData(Player)
	if cachedTable[Player] then
		cachedTable[Player] = nil
	end
end

function module:loadNewData(tbl, obj)
	if typeof(obj) == "Instance" and obj:IsA("Folder") then
		for index, value in pairs(tbl) do
			if not obj:FindFirstChild(index) then
				if typeof(value) == "table" then
					local newFolder = Instance.new("Folder")
					newFolder.Name = index
					newFolder.Parent = obj
					module:createInstances(value, newFolder)
	  			else
					local newValue = Instance.new(GlobalFunctions.getObjFromValue(value))
					newValue.Value = value
					newValue.Name = index
					newValue.Parent = obj
				end
			elseif obj:FindFirstChild(index) then
				if typeof(value) == "table" then
					--> We want to repeat what we just did
					module:loadNewData(value, obj[index])
				else 
					-- Don't do anything in this case
				end
			end
		end
	elseif typeof(obj) == "table" then
		for index, value in pairs(tbl) do
			if not obj[index] then
				if typeof(value) == "table" then
					obj[index] = GlobalFunctions.deepTableCopy(value)
				else
					obj[index] = value	
				end	
			else
				if typeof(value) == "table" then
					module:loadNewData(value, obj[index])	
				end
			end
		end
	end
end

function module:saveData(Player)
	--> Conditions

	if not cachedTable[Player] or not cachedTable[Player].dataLoaded or not cachedTable[Player].data then
		module:output("Could not SAVE "..Player.Name.."'s data due to their data not being loaded yet.")
	end
	
	--> Load in all the instance data before saving it:
	local playerc = Player:GetChildren()
	for i = 1, #playerc do
		--> If the object is a folder and we have data on it then
		if StarterData[playerc[i].Name] and playerc[i]:IsA("Folder") and StarterData[playerc[i].Name].createInstance then
			--> Get the data from the folders
			cachedTable[Player].data[playerc[i].Name] = module:getInstanceTree(playerc[i])
		end
	end
	
	--> saving
	local userId, playerData = Player.UserId, GlobalFunctions.deepTableCopy(cachedTable[Player].data)
	local Success, Error, Attempts = nil, nil, 0
	coroutine.resume(coroutine.create(function()
		repeat
			Attempts += 1
			Success, Error = pcall(function()
				Datastore:SetAsync(userId, playerData)
			end)
		task.wait(1)
		until Success or Attempts >= Properties.retries do end
		
		if not Success then
			module:output("An Erroror has occured while trying to save "..userId.."'s data; "..Error)
		elseif Success then
			if cachedTable[Player] then
				cachedTable[Player].lastSave = os.clock()
			end	
			module:output("Saved "..userId.."'s data with no Errorors!")
		end
	end))
end

function module:loadData(Player)
	if not Properties.load
	or (not Properties.studioLoad and RunService:IsStudio()) then
		module:output("Could not LOAD "..Player.Name.."'s data due to properties being set to false \n[load]: "..tostring(Properties.load).."\n[studioLoad]: "..tostring(Properties.studioLoad))
	end
	
	local userId = Player.UserId
	local Success, Error, Attempts, data = nil, nil, 0, nil
	coroutine.resume(coroutine.create(function()
		repeat 
			Attempts += 1		
			if Player then
				Success, Error = pcall(function()
					data = Datastore:GetAsync(userId)
				end)
			end
		until Success or Attempts >= Properties.retries do end
		
		if not Success then
			module:output("An Erroror has occured while trying to load " .. userId .. "'s data; " .. Error)
			Player:Kick("[DATABASE]: We were unable to retrive you're data; please try again later.")
		elseif Success then
			--> If player has data
			if data then
				module:output("Data was detected for " .. userId .. "; loading it")
				
				--> Load in data from them instances
				for index, plrData in next, data do
					if StarterData[index] and StarterData[index].createInstance then
						local folder = Instance.new("Folder")
						folder.Name = index
						folder.Parent = Player
						module:createInstances(plrData, folder)
					end
				end
				--> Load in any new things from starterData
				for index, info in next, StarterData do
					--> If you wish to create instances
					if info.createInstance then
						if Player:FindFirstChild(index) then
							module:loadNewData(info.data, Player[index])
						else
							local newFolder = Instance.new("Folder")
							newFolder.Name = index
							newFolder.Parent = Player
							module:createInstances(info.data, newFolder)	
						end
					else
						if not data[index] then
							data[index] = {}
						end
						module:loadNewData(info.data, data[index])
					end	
				end
				cachedTable[Player].data = data -- cache the data
			else
				module:output("There was no data for "..userId.." available; loading in new data")
				--> Let's load in the default data
				for dataCategory, data in next, StarterData do
					cachedTable[Player][dataCategory] = GlobalFunctions.deepTableCopy(data.data)
					if data.createInstance then
						local newFolder = Instance.new("Folder")
						newFolder.Name = dataCategory
						newFolder.Parent = Player
						module:createInstances(data.data, newFolder)
					end
				end	
			end
			
			cachedTable[Player].dataLoaded = true -- signal we have loaded out data
		end
	end))
end

function module:getData(Player)
	--> Yields to wait for loaded data
	local yieldTime = os.clock()
	while not cachedTable[Player] or not cachedTable[Player].dataLoaded do
		RunService.Stepped:Wait()
		if os.clock() - yieldTime >= Properties.yieldTimeout then
			module:output(Player.Name.."'s getData callback has timed out: "..(os.clock() - yieldTime))
			return nil
		end
	end
	return cachedTable[Player].data
end

function module:createInstances(tbl, Parent)
	--> Loop through table
	for index, value in next, tbl do
		if typeof(value) == "table" then
			local newFolder = Instance.new("Folder")
			newFolder.Name = index
			newFolder.Parent = Parent
			module:createInstances(value, newFolder)
		else
			local newInstance = Instance.new(GlobalFunctions.getObjFromValue(value))
			newInstance.Name = index
			newInstance.Value = value
			newInstance.Parent = Parent
		end
	end
end

function module:getInstanceTree(int)
	local newTable = {}
	local intChildren = int:GetChildren()
	for i = 1, #intChildren do
		if intChildren[i]:IsA("Folder") then
			newTable[intChildren[i].Name] = module:getInstanceTree(intChildren[i])
		else
			newTable[intChildren[i].Name] = intChildren[i].Value
		end
	end
	return newTable
end

if Properties.autoSave then
	coroutine.resume(coroutine.create(function()
		while true do
			for Player, Data in next, cachedTable do
				if Data.dataLoaded and os.clock() - Data.lastSave >= Properties.autoSaveInterval then
					Data.lastSave = os.clock() -- Reset the timer
					--> Save data in new thread incase of Errors; this doesn't break datastore
					coroutine.resume(coroutine.create(function()
						module:saveData(Player) -- save the data
					end))
				end
			end
			task.wait(1)
		end
	end))
end

if Properties.safeSave then
	game:BindToClose(function()
		for Player, Data in next, cachedTable do
			Data.lastSave = os.clock()
			--> new thread		
			coroutine.resume(coroutine.create(function()
				module:saveData(Player)			
			end))
			Player:Kick("[Database]: Game is shutting down for an update; please hold still as we secure your data")
		end
	end)
end

return module
2 Likes

You should use Profile Service. It’s used by lots of big games and protects against all forms of data loss. If you don’t write your data store correctly, sometimes saving will fail. This occurrence usually happens when your game gets lots of traffic. Now, to avoid the risk, I suggest you use it. I even use it for my game and there has been no data loss reports since:

Lots of front page games like Bed Wars and Mad City uses this module too. It’s also pretty easy to set up. These YouTubers will tell you to cache their data in a module script AND set the value of a leaderstat too, which I find quite awkward, since the leaderstat basically caches the value too, so why cache it twice? So, what I do is I save the player’s data when they leave.

1 Like

Would something like this work so that the players will keep there data?

local DataSystem = {}

local Players = game:GetService("Players")

local ProfileService = require(script.ProfileService)
local Properties = require(script.Properties)
local UserData = require(script.UserData)

local Datastore = game:GetService("DataStoreService"):GetDataStore(Properties.datastore)

local DataVersion = 01
local DatastoreKey = "PlayerData_" .. tostring(DataVersion)
local GameProfileStore = ProfileService.GetProfileStore(DatastoreKey, UserData)
local Profiles = {}

function DataSystem:GetData(Player)
	local Profile = Profiles[Player]
	if Profile == nil then warn(Player.Name .. "s, Profile Is Nil") return end
	
	return Profiles[Player]
end

function DataSystem:LoadProfile(Player)

	local UserId = tostring(Player.UserId)
	local Key = "Player-" .. UserId
	local Profile = GameProfileStore:LoadProfileAsync(Key, "ForceLoad")
	print(Profile)
	if Profile == nil then Player:Kick("Data Wasn't Found, Please Rejoin!") return end

	Profile:Reconcile()
	Profile:ListenToRelease(function()
		Profiles[Player] = nil
		Player:Kick(ProfileService.ReleaseKickMessage)
	end)

	if Player:IsDescendantOf(Players) then
		Profiles[Player] = Profile
		LoadData(Player)
	else
		Profile:Release()
	end

	if Profile.Data.PlayerData.DataUpdated ~= true and not Player:GetAttribute("NewPlayer") then

		local userId = Player.UserId
		local Success, Error, Attempts, data = nil, nil, 0, nil
		repeat 
			Attempts += 1		
			if Player then
				Success, Error = pcall(function()
					data = Datastore:GetAsync(userId)
				end)
			end
		until Success or Attempts >= Properties.retries do end

		if data then
			Datastore:RemoveAsync(Player.UserId)
			Profile.Data = data
			Profile.Data.PlayerData.DataUpdated = true
		end

	end
end

function DataSystem:RemoveProfile(Player)
	local Profile = Profiles[Player]
	if Profile == nil then warn(Player.Name .. "s, Profile Is Nil") return end
	
	Profile:Release()
end

function LoadData(Player)
	local Profile = Profiles[Player]
	if Profile == nil then warn(Player.Name .. "s, Profile Is Nil") return end
	
-- for testing
	local Leaderstats = Instance.new("Folder")
	Leaderstats.Name = "leaderstats"
	Leaderstats.Parent = Player
	
	local Data = Profile.Data
	
	local Crash = Instance.new("IntValue")
	Crash.Name = "Cash"
	Crash.Value = Data.PlayerData.PlayTime
	Crash.Parent = Leaderstats
end


return DataSystem
1 Like

I suggest you copy and paste the code that the post gives you and then implement your stats. I suggest you keep the comments there for a little bit though.

1 Like

So, use this code and make it work with my stats?

-- ProfileTemplate table is what empty profiles will default to.
-- Updating the template will not include missing template values
--   in existing player profiles!
local ProfileTemplate = {
    Cash = 0,
    Items = {},
    LogInTimes = 0,
}

----- Loaded Modules -----

local ProfileService = require(game.ServerScriptService.ProfileService)

----- Private Variables -----

local Players = game:GetService("Players")

local ProfileStore = ProfileService.GetProfileStore(
    "PlayerData",
    ProfileTemplate
)

local Profiles = {} -- [player] = profile

----- Private Functions -----

local function GiveCash(profile, amount)
    -- If "Cash" was not defined in the ProfileTemplate at game launch,
    --   you will have to perform the following:
    if profile.Data.Cash == nil then
        profile.Data.Cash = 0
    end
    -- Increment the "Cash" value:
    profile.Data.Cash = profile.Data.Cash + amount
end

local function DoSomethingWithALoadedProfile(player, profile)
    profile.Data.LogInTimes = profile.Data.LogInTimes + 1
    print(player.Name .. " has logged in " .. tostring(profile.Data.LogInTimes)
        .. " time" .. ((profile.Data.LogInTimes > 1) and "s" or ""))
    GiveCash(profile, 100)
    print(player.Name .. " owns " .. tostring(profile.Data.Cash) .. " now!")
end

local function PlayerAdded(player)
    local profile = ProfileStore:LoadProfileAsync("Player_" .. player.UserId)
    if profile ~= nil then
        profile:AddUserId(player.UserId) -- GDPR compliance
        profile:Reconcile() -- Fill in missing variables from ProfileTemplate (optional)
        profile:ListenToRelease(function()
            Profiles[player] = nil
            -- The profile could've been loaded on another Roblox server:
            player:Kick()
        end)
        if player:IsDescendantOf(Players) == true then
            Profiles[player] = profile
            -- A profile has been successfully loaded:
            DoSomethingWithALoadedProfile(player, profile)
        else
            -- Player left before the profile loaded:
            profile:Release()
        end
    else
        -- The profile couldn't be loaded possibly due to other
        --   Roblox servers trying to load this profile at the same time:
        player:Kick() 
    end
end

----- Initialize -----

-- In case Players have joined the server earlier than this script ran:
for _, player in ipairs(Players:GetPlayers()) do
    task.spawn(PlayerAdded, player)
end

----- Connections -----

Players.PlayerAdded:Connect(PlayerAdded)

Players.PlayerRemoving:Connect(function(player)
    local profile = Profiles[player]
    if profile ~= nil then
        profile:Release()
    end
end)
1 Like

Yeah, except you can do this:

Players.PlayerRemoving:Connect(function(player)
    local profile = Profiles[player]
    if profile ~= nil then
        local data = profile.Data

        data.Cash = player.leaderstats.Cash.Value

        profile:Release()
    end
end)
1 Like

So, would something like this work?

Players.PlayerRemoving:Connect(function(player)
	local profile = Profiles[player]
	if profile ~= nil then
		local data = profile.Data

		for i, Value in ipairs(player:WaitForChild("PlayerData"):GetDescendants()) do
			if Value:IsA("NumberValue") or Value:IsA("BoolValue") or Value:IsA("IntValue") and data[Value] then
				data[Value] = Value
			end
		end

		profile:Release()
	end
end)

1 Like

Yeah, except if you want to save weapons for example, you go through the weapons folder inside the player and save all the names.

1 Like

Like this?

data[Value.Name] = Value

Would that work if more than one value has the same name?

1 Like
local ProfileTemplate = {
  Weapons = {}
}

for i, v in pairs(player.Weapons:GetChildren()) do
  table.insert(data.Weapons,v.Name)
end
2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.