Save your player data with ProfileService! (DataStore Module)

Message to early birds who grabbed ProfileService yesterday:

Made much better code examples of querying past versions in ProfileService: https://madstudioroblox.github.io/ProfileService/api/#profilestoreprofileversionquery

Also fixed passing nil as second argument sort_direction for ProfileStore:ProfileVersionQuery() resulting in an error. github and Roblox library versions have already been updated so just switch your module with a new one.

9 Likes

Any usecases of Profile:AddUserId? For GDPR compliance, Roblox already gives you the userid of that player.

1 Like

Profile:AddUserId() marks the profile’s DataStore key with the UserId which would supposedly help Roblox delete the key on their own instead of sending you direct messages to remove it manually (In the near future, possibly).

I think what’s going to happen is that Roblox will always remember every player that played your game and will send you a GDPR notice unless they find a DataStore key tagged with the UserId.

10 Likes

I have an issue.

Within my game, I’ve separated the two scripts

One, there is the DataApi (which is the main area for the data)
And the other which loads the player in.
Inside the loader script, I ask for the player’s profile from the DataApi script using a function that returns the profile. However, it errors saying “attempted to index ‘Data’ with nil”

The profilescript

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

local ProfileTemplate = require(script.Data)

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

local ProfileService = require(game.ServerScriptService.ProfileService)

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

local Players = game:GetService("Players")

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


local Profiles = {}

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

function PlayerAdded(player)
	local profile = GameProfileStore:LoadProfileAsync(
		"Player_" .. player.UserId,
		"ForceLoad"
	)
	
	print(profile.Data.Coins)
	
	if profile ~= nil then
		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("Released")
		end)
		if player:IsDescendantOf(Players) == true then
			Profiles[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("Failed to load DataProfile (This is probably a result of Roblox data outage.)") 
	end
end

----- Initialize -----

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

----- Give Data to other scripts -----

function module:Get(Player)
	local profile = Profiles[Player]
	if profile then
		print("yeah")
		return profile
	end
end

----- Connections -----

Players.PlayerAdded:Connect(PlayerAdded)

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



return module

I use the Get() function to return the profile to other scripts.

I’ve tried making waits inside the loader script but this doesn’t solve the issue.

Any help?

1 Like

ProfileTemplate could be wrong, are you sure the Data module returns a table?

That is what I am confused about.

It for some reason works just fine in other scripts but it does not return anything.
Before I had the forceload portion in the loader script instead of the data script, and it worked with everything else, but that would cause data errors. How can I get it to return the profile?

I am a little confused by what you mean by this

In the explorer
image

In the Data script, it just returns a table which has a list of values.

Are you sure you are calling the function with Player’s Instance and not Player’s name?

Also you should set the Profile in the dictionary to nil when player is leaving.

1 Like

Yes, I am calling the function with the Player’s instance.

You might wanna consider have a :WaitForProfile function.
Get a signal module, then, you can implement it something like this: (pseudo code)

local ON_PROFILE_CACHED = Signal.new()

-- On loading the profile
PROFILES[player] = profile
ON_PROFILE_CACHED:Fire(player, profile)

--

function ProfileHandler:WaitForProfile(player)
    --\\ This function CAN return nil, and it will do so if the player has left.

    do
        local profile = PROFILES[player]
        if profile then
            return profile
        end
    end

    if player.Parent ~= Players then
        return
    end

    while true do
        local playerFromProfile, profile = ON_PROFILE_CACHED:Wait()
        if playerFromProfile == player then
            return profile
        end
        if player.Parent ~= Players then
            return
        end
    end
end
1 Like

Reading the documentation got me confused…

:OverwriteAsync():
Using this method for editing latest player data when the player is in-game can lead to several minutes of lost progress - it should be replaced by [:LoadProfileAsync()]

How would I use :LoadProfileAsync() instead of :OverwriteAsync() when player is active?

  1. How do I know if the player is active?
  2. Wouldn`t that kick the active player by default if profile is stolen?
  3. Wouldn`t loading and manipulating data be slower than Overwriting?

Profile:ClearGlobalUpdates()
Are these updates that would come from the old save or are these currently live ones that will be eliminated? Why would you want it?

What would be the use case for .RobloxMetaData if it`s so limited and ProfileService already has this great feature?

CtrlF “runing” for typo.

You just use it like you would load regular player data - Load, edit, release.

:LoadProfileAsync() will wait for a remote server to do it’s final auto-save and create a profile release signal which would notify developer code not to attempt editing the profile anymore. If there’s no remote server currently using the profile, :LoadProfileAsync() will return a Profile much more instantly.

GlobalUpdates are stored in the profile itself under the same DataStore key, but outside Profile.Data, like metada. Profile:ClearGlobalUpdates() can be used for profile payloads retrieved through :ViewProfileAsync() and :ProfileVersionQuery() to clear GlobalUpdates data in the profile payload. Profile payloads don’t get auto-saved or released - any changes made to them would only be saved to the DataStore if you called :OverwriteAsync().

If you’re not using GlobalUpdates in your game at all, then this method will have no effect to data and can be omitted.

And if you’re actually using GlobalUpdates… Using Profile:ClearGlobalUpdates() when rolling back is not mandatory, in which case a player would recover GlobalUpdates in the exact state they existed in the profile snapshot that was rolled back to.

Yeah. .RobloxMetaData is currently useless and Profile.MetaData.MetaTags, Profile:SetMetaTag(), Profile:GetMetaTag() should be used instead (the former having a 300 character (300 byte) limit, the latter (combined with Profile.Data) having a 4 megabyte limit).

Roblox is promising “querying and indexing based on such metadata will be supported in future releases”, so ProfileService is just being prepared for it.

5 Likes

Wanted to use this module, I have some questions though:

  • Is there a readable way of using serialization? For instance, in DataStore2 the saved data can get serialized, but when reading the data its deserialized.

  • Is there anything I would need to do with BindToClose? Or is it done automatically?

  • For replication, would I need to do:

    profile.Data.Coins = 100
    leaderstats.Coins.Value = 100
    

    Or do I need to use some OnUpdate function?

  • Is it advised to ALWAYS Release profiles and ListenToHopReady() before teleporting?

  • Is it safe to use more than 1 profile at once? (for place specific data)

  • Do you still need to perform this check:

    if profile.Data.Cash == nil then
        profile.Data.Cash = 0
    end
    

    …if you use profile:Reconcile()?

ProfileService discourages serialization, especially when DataStore keys have a limit of 4 megabytes.

Check the module source to answer questions like that. Yes - ProfileService does this automatically.

The official ProfileService replication solution combo is ReplicaService. If it’s too advanced for you, then you’ll have to make up your own code that both updates Profile.Data and replicates to clients.

Not using :ListenToHopReady() could only result in slightly longer load times after teleportation. You’ll have to set up teleportation in your game and test it yourself.

Yes. Also ProfileService is NOT going to work for multi-server access at the same time - one profile will only be loaded on one server at a time.

You don’t have to check this if you’re using :Reconcile().

3 Likes

Thank you for the answers!

Also to clear up some confusion, when I said

I meant, is it safe to load more than 1 profile for a single player. The reason for this is that my game uses multiple places, and each one has a small set of data that is only needed in that place.

I dont know if its worth setting up a different profile per place, or just adding a table to the main profile for each place. (given that Im not hitting the datastore limits anytime soon, my current data table includes everything and uses only 2013 characters)

So is doing something like

profile.Data.Coins = 100
player.leaderstats.Coins.Value = profile.Data.Coins

Considered incorrect usage of ProfileService? Or is this completely fine?

1 Like

You can do both of those things you’re talking about. I would personally not exceed 2 profiles per player, though.

3 Likes

question, I might be missing something but does this support deep copying? like if I remove a variable/key in the defaultstats/template it’ll also get removed in the data?

was trying to remove the ‘What’ variable on the default profile template but it seems to not work so i went ahead and used an module that i found that deep copies everything for me
image

See this, @LucasTutoriaisSaimo have already explained it few posts above, hope this helps!

1 Like

Oh, thanks! I thought it had some hidden stuff that does it for me.

How would I go about listing all the profiles (like all saved)? Since I would like to see everyone’s data and ensure that everything is correct.

Something like this:

local profiles = someMagicalFunction()
for _, profile in pairs(profiles) do
    print(profile.Data)
end