Save your player data with ProfileService! (DataStore Module)

I’m pretty sure there’s an example in the ReplicaService page.

I know that but how would I go about making a profile and replicate that to the client. The example just shows how to set up a class token. Could you help by showing me an example?

Can anyone show me an example of how to use GlobalUpdates? The API docs confuse me :sweat_smile:

Shout out to okeanskiy and his amazing video that is explaining how it works.
Watch it here: Global Updates: ProfileService Tutorial Part 2 (Roblox Studio) - YouTube

1 Like

umm Im kinda of a noob so i do not know much about how to deal with my problem, so i have the data saving fine for the most part but for some reason one of the data key thingies gets deleted or something and turns into nil for some reason. but its just one of the data thingies(im basically saying that if there was levels coins and exp to save, exp for some reason just gets nil SOMETIMES for some reason.) that turns into nil. The other ones like level and coins never did turn into nil. i do not know why it happenned, i set the script up with the video of okeanskiy. I hope you can help me.

local ServerScriptService = game:GetService("ServerScriptService")
local RS = game:GetService("ReplicatedStorage")

local ProfileService = require(RS.ProfileService)

local UiColorTable = {{1,1,134},{10,124,134},{12,255,247},{26,255,129},{23,135,6},{167,255,2}
,{255,255,3},{255,158,1},{255,74,14},{255,0,0},{255,6,72},{255,6,160},{96,0,103}}

local profileStore = ProfileService.GetProfileStore(
	"Player",{
		UiColorTheme = UiColorTable[math.random(1,13)];
		Sectoriat = 0;
		Level = 1;
		Exp = 0;
	}
)

local Profiles = {}

local function onPlayerAdded(player)
	local profile = profileStore:LoadProfileAsync(
		"Player_" .. player.UserId,
		"ForceLoad"
	)
	if profile then
		profile:ListenToRelease(function()
			Profiles[player] = nil
			player:Kick()
		end)
		
		if player:IsDescendantOf(PlayerService)then
			Profiles[player] = profile
		else
			profile:Release()
		end
	else
		player:Kick()
	end
end

local function onPlayerRemoved(player)
	local profile = Profiles[player]
	
	if profile then
		profile:Release()
	end
end

PlayerService.PlayerAdded:Connect(onPlayerAdded)
PlayerService.PlayerRemoving:Connect(onPlayerRemoved)

local DataManager = {}

function DataManager:Get(player)
	local profile = Profiles[player]
	
	if profile then
		return profile.Data
	end
end

function DataManager:GetProfileStore()
	return profileStore
end

return DataManager```

Note: Sectoriat is basically coins.

What about it isn’t working? Also use ``` on the top and below code.

Well as i stated, for some reason exp just keeps on turning into nil while the others have no problem, btw i have never changed any of the values to nil in my scripts so im kinda scared like if this happened to me a few times already, what would happen when i release my game. People would lose their exp and worst part is since it turns into nil i can’t do anything with exp since it causes a lot of errors like some parts of the scripts would just stop. Thats why i thought i would ask it here.

Did you add the Exp key into the profile template after originally testing it? If so, you’ll need to call :Reconcile() upon finding a profile to ensure that they have the most updated template.

Well then, i will try to write reconcile function , if i can’t get it to work hopefully you guys can help me.

I just want to point out that this method of saving player data has probably saved me from potential major headache, since working with datastores is a very delicate and tedious process that every new developer on the Roblox platform will eventually have to tackle with.

If done incorrectly, you could accidentally shoot yourself on the foot in the future. Having to deal with hundreds if not thousands of player lodging complaints at you on why their data is suddenly gone while having to rework on your hard-worked but to failed datastore system is a situation I and anyone else wouldn’t want to be in.

In this case, thank you very much for creating this ProfileService. As an inexperienced developer I had underestimated just how much this Service could save me not just time but also health.

7 Likes

I’m putting my player inventory data in an inventory folder. But this loop isn’t working and I don’t know what to do… please help

	for i, v in ipairs(profile.Data.Inventory) do
		local InvItem = Instance.new("IntValue")
		InvItem.Value = v
		InvItem.Name = i
		InvItem.Parent = Inventory
	end

What is the ‘Inventory’ and try to use pairs not ipairs for the loop.

Ok inventory is a folder in player

Thanks, It works. TYSM :bangbang:

1 Like

Why do I always get this when playing in real game, but not in studio. I am using ProfileService btw.

Are you using :Reconcile()?

This seems like a you problem, the leaderstats for YOU aren’t being created.

Additionally, check if the PlayerAdded event doesn’t get connected after something yields, like a require(assetId)

Try making the PlayerAdded connection into a function, and then have it handle it like this:

for _, plr in ipairs(game.Players:GetPlayers()) do
    coroutine.wrap(onPlayerAdded)(plr)
end

game.Players.PlayerAdded:Connect(onPlayerAdded)
1 Like

Loleris, I have a question, does the :ListenToHopReady() signal, additionally check with another UpdateAsync request, to see if the profile is released?

Thank you this worked! I didn’t do courtine for all players and did it outside of iteration of players.

1 Like

Profile:ListenToHopReady() triggers after the last DataStore UpdateAsync call has finished (It’s a yielding method) for said Profile - This is the last non-arbitrary event that we can get in terms of having a profile we just saved be instantly loaded elsewhere swiftly (Instead of receiving a session lock in the process of being removed which can prolong loading time). It has no additional DataStore API cost.

2 Likes

My profile doesn’t save, and I don’t even know why. Code:

ServerScript:

_G.forces = {

	[1] = {
		Name = "Default Force";
		Cost = 0;
		Force = 50;
	};
	[2] = {
		Name = "Fast Force";
		Cost = 15;
		Force = 120;
	};
	
};

local profileServiceTemplate = {
	Wins = 0;
	Has = _G.forces[1].Name;
	Force = _G.forces[1].Force;
	Purchased = { _G.forces[1] };
}

local profileService = require(script:WaitForChild("ProfileService"))

local store_Key = ("forcesAndWins")

local profileStore = profileService.GetProfileStore(store_Key, profileServiceTemplate)

local profiles = {}

local dataHandler = require(script:WaitForChild("DataHandler"))

local function playerAdded(plr)
    if plr then
        script:WaitForChild("leaderstats"):Clone().Parent = plr;

        local force = Instance.new("IntValue");

        force.Name = "Force";

		force.Parent = plr;
		
		local profile = profileStore:LoadProfileAsync(store_Key .. plr.UserId, "ForceLoad")
		
		if (profile ~= nil) then
			profile:Reconcile()
			profile:ListenToRelease(function()
				profile:Release()
				plr:Kick("Your data was released");
			end)
			if (plr:IsDescendantOf(Players)) then
				profiles[plr] = profile
				dataHandler:setUpDataWithProfile(plr, profile);
			else
				profile:Release()
			end
		else
			plr:Kick()
		end;
	end
end

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


Players.PlayerRemoving:Connect(function(plr)
	if (profiles[plr]) then
		profiles[plr]:Release()
	end
end)

Players.PlayerAdded:Connect(playerAdded);

My purchase and data saver handler:

local data = {}

local replicatedStorage = game:GetService("ReplicatedStorage")
local dataTransferEvent = Instance.new("RemoteEvent")
dataTransferEvent.Name = "DataTransfer"
dataTransferEvent.Parent = replicatedStorage.Remotes

local serverEquip = Instance.new("RemoteFunction")
serverEquip.Name = "serverEquip"
serverEquip.Parent = replicatedStorage.Remotes

data.profiles = {}

function data:getProfile(plr)
	return self.profiles[plr]
end

function data:setUpDataWithProfile(plr, profile)
	self.profiles[plr] = profile
	local forces = _G.forces
	local playerWins = plr:WaitForChild("leaderstats"):WaitForChild("Wins")
	if (not profile.Wins) then
		profile.Wins = 700 -- For testing purchases
	end
	if (not playerWins) then return end
	playerWins.Value = profile.Wins
	playerWins:GetPropertyChangedSignal("Value"):Connect(function()
		-- Wins have been changed, update it in the profile, too!
		profile.Wins = playerWins.Value
	end)
	playerWins.Value = profile.Wins
	if (not profile.Has) then
		profile.Has = forces[1].Name
	end
	if (not profile.Purchased) then
		profile.Purchased = { forces[1] }
	end
	local playerForce = plr:WaitForChild("Force")
	if (not profile.Force) then
		profile.Force = forces[1].Force
	end
	if (not playerForce) then return end
	playerForce.Value = profile.Force
	playerForce:GetPropertyChangedSignal("Value"):Connect(function()
		profile.Force = playerForce.Value
	end)
	print(profile.Has, profile.Purchased)
	dataTransferEvent:FireClient(plr, profile.Has, profile.Purchased, forces)
end

data.findForceByName = function(t,n)
	for _, fTable in pairs(t) do
		if fTable.Name == n then
			return fTable
		end
	end
end

data.checkRequirements = function(t,n,playerAmount)
	local force = data.findForceByName(t,n)
	if (force.Cost <= playerAmount) then
		return {true,force.Cost}
	end
	return nil
end

data.add = function(t,p,n)
	local force = data.findForceByName(t,n)
	if (force) then
		-- Insert it into a table
		table.insert(p,force)
		return true
	end
end

data.serverInvoke = function(player,name)
	if (not name or not (typeof(name) == "string")) then return end
	local playerProfile = data:getProfile(player)
	if (not playerProfile) then return end;
	local purchased = playerProfile.Purchased
	local forces = _G.forces
	local force = data.findForceByName(forces,name)
	if (not force) then player:Kick() return end
	local isFound = data.findForceByName(purchased,name)
	if (not isFound) then 
		-- Player does not own the force
		local leaderstats = player:FindFirstChild("leaderstats")
		if (not leaderstats) then return end
		local wins = leaderstats:FindFirstChild("Wins")
		if (not wins) then return end
		local canPurchase = data.checkRequirements(forces,name,wins.Value)
		if (canPurchase and canPurchase[1]) then
			-- Player can buy this
			local cost = canPurchase[2]
			local wasSuccessful = data.add(forces,purchased,name)
			if (wasSuccessful) then
				-- Purchase was successful
				-- warn("The purchase was successful")
				local forceInt = player:FindFirstChild("Force")
				if (not forceInt) then
					return;
				end
				wins.Value -= cost
				dataTransferEvent:FireClient(player,playerProfile.Has,playerProfile.Purchased,forces)
			end
		end
	else
		-- Player owns the force
		local _has = playerProfile.Has
		if (_has and _has ~= name) then
			-- Player wants to equip a different force
			playerProfile.Has = force.Name
			local playerForce = player:FindFirstChild("Force")
			if (not playerForce) then return end
			playerForce.Value = force.Force
			dataTransferEvent:FireClient(player,playerProfile.Has,playerProfile.Purchased,forces)
		end
	end
end;

serverEquip.OnServerInvoke = data.serverInvoke

return data

Would appreciate help :grin:

EDIT: Lol. I forgot to add in profile.Data.