Save your player data with ProfileService! (DataStore Module)

It won’t get removed. You would need to have your own function for that.

local function ReconcileWithExtraKeyRemoval(Profile)
    Profile:Reconcile()
    
    local ProfileData = Profile.Data
    for key in pairs(ProfileData) do
        if DataTemplate[key] == nil then
            ProfileData[key] = nil
        end
    end
end

You can’t remove indexes from nested tables (sub tables if you don’t know what that means) because that means that you would not be able to have things like weapons, for example:

Profile.Data.Weapons = {
    "Sword",
    "Axe",
    "Hammer"
}

These values don’t exist in the data template,
– so looking for extra “sub tables” would remove things like these.

1 Like

Hey! With the recent datastore v2 announcements, do you plan on updating this module utilizing the new features roblox provides?

11 Likes

I’m also wondering the same, this seems like quite a big update and hopefully it can open up doors to more features or at least optimise the current features.

Seeing UserIds get tagged for GDPR requests would be really useful. Especially if roblox decide to automate GDPR requests provided the right data is tagged.

Is there anyway to use :GetOrderedDataStore in ProfileService? I’ve made an entire game framework with ProfileService, but ran into a problem when making global leaderboards. Has anyone found a way to accomplish this? I’m genuinely not sure, been here for about 2 hours trying to script it. But can’t find anyway, all help appreciated. Thanks!

[08/14/2021]

I grinded this 10 hours non-stop. Even I think that I deserve some praise. Anyways here’s the moment we’ve been waiting for…

Github commit 565ddd8

ProfileService - DataStore v2 edition

(But backwards-compatible, lol)


Full DataStore v2 support. You’re not going to miss a thing.

I’ve written all the explanations in the docs, so there’s not much I can really say here… Epic DSv2 speedrun?

I’d say the safety of this module is around 99% - I did extensive artificial testing plus all the original tests that helped ProfileService stay 99.99% germ-proof through every other update. I will react to error reports pretty fast.


Summary:

New ProfileStore methods:

New Profile members:

New Profile methods:


As always, you may update the module through github or the Roblox library.

168 Likes

bro a datastore update isnt worth ur eyesight

Anyways, amazing module, nice work!

8 Likes

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()?