Save your player data with ProfileService! (DataStore Module)

I don’t think this would be a good idea for a feature.

The point behind ProfileService is that it’s very versatile because you can write your own Interfaces. Creating any feature would just result in bugs because for every game the way that data is saved, got, manipulated, etc is very different.

Instead of creating a feature, you should instead write your own deserializer and serializer within your data interface.

1 Like

You might like this then!

First timers should have this set to 1. The only problem might be is that this number would increase past 1 in cases when the first player join was unsuccessful. HOWEVER, you may flag profiles with a single loaded session count so you could secure first timer handling for when the player does successfully join the game.

Do some experimenting.

2 Likes

I love this module, and have been using it after switching from DataStore 2. However, players are still reporting data loss every once in awhile. I am currently just using the standard loading/releasing in player joining/leaving, while updating the values I want to save in Profile.Data. I assume I am missing something important, as data loss is becoming increasingly frustrating. Any help would be greatly appreciated! I love using this module and would hate to have to go find another one.

1 Like

Loving this module so far :smiley:

Just wondering @loleris , am I using global updates correctly here? I am getting a “DataStore request was added to queue” when running this code:

Gifting code
local userId = <gifting user id>
local player = <player gifting to userId>
local unlockType = <custom enum>
local amount = <some integer>

myProfileStore:GlobalUpdateProfileAsync(
"Player_" .. userId,
function(globalUpdates)
    print("start GlobalUpdateProfileAsync")
    local giftData = {
        FromUserId = player.UserId,
        UnlockTypeValue = unlockType.Value,
        Params = {amount}
    }

    local updated = false

    -- add gift to existing active GiftType.UNLOCKTYPE global update entry if any
    for _, update in ipairs(globalUpdates:GetActiveUpdates()) do
        local updateId = update[1]
        local updateData = update[2]

        if updateData.TypeValue == GameEnum.GlobalUpdateType.GIFT.Value then
            updateData.Gifts[#updateData.Gifts + 1] = giftData

            updated = true
            globalUpdates:ChangeActiveUpdate(updateId, updateData)

            warn(
                "PlayerDataService.ProfileStore:GlobalUpdateProfileAsync()::ChangeActiveUpdate:: Stacked gift to " ..
                    "Player_" .. userId .. "'s profile: UnlockType:",
                unlockType,
                "Params:",
                unpack(giftData.Params)
            )

            break
        end
    end

    print("updated exisiting entry:",updated)
    if not updated then
        -- if no existing GiftType.UNLOCKTYPE entry found then add a new one
        local gifts = {}
        gifts[#gifts + 1] = giftData

        globalUpdates:AddActiveUpdate(
            {
                TypeValue = GameEnum.GlobalUpdateType.GIFT.Value,
                --
                Gifts = gifts
            }
        )

        warn(
            "PlayerDataService.ProfileStore:GlobalUpdateProfileAsync()::AddActiveUpdate:: Created gift for " ..
                "Player_" .. userId .. "'s profile: UnlockType:",
            unlockType,
            "Params:",
            unpack(giftData.Params)
        )
    end
end)

Here is the Request Queue warning:

Server output during the second gift

The first time a player sends a gift to userId the method does not throw any warnings. So I did not include that output here. Any subsequent time a player gifts to that same userId the function passed to GlobalUpdateProfileAsync runs twice in a row for some reason (even though I call the method once), which is what the included output shows (red highlights the first run, blue highlights the duplicate run). The first time it runs as expected but on its duplicate run it throws the “Request queue full” warning. The weird part is I am getting gifts only once although the output shows it’s running that function twice in one call.

See troubleshooting - your data will not save if you’re trying to save unserializable data. Review the official code example in the documentation and make sure you’re not dumping data to ProfileService when the player leaves instead of doing that constantly. you should be using LoadProfileAsync in ForceLoad mode.

You can find info on queue warnings in troubleshooting page of documentation.

GlobalUpdateProfileAsync callback fires multiple times because it’s called by the UpdateAsync callback which can be called “as many times as needed” based on Roblox documentation. The proccess is complicated, but you can find information here:

1 Like

Of course in the end it’s just a roblox quirk lmao, thanks for the quick reply!

Thanks for the reply. I’m not sure what’s occurring- I know nothing in the PlayerRemoving function is erroring, and all data is changed in-game before the player leaves.

The only possible things I can think it might be are:

  1. An in-game loop might be trying to change data after the player leaves. But it does so by firing a BindableEvent inside the Player hierarchy, so it should be unable to, correct?
  2. I keep seeing Profile:Save() in the docs, but I don’t call it in my game at all. Should I be calling that every minute or so, or does ProfileService do this for me automatically?
1 Like

If anyone could give me a precise rundown of how they are implementing this system in their games, I would be incredibly grateful. The amount of players losing data is embarrassing, and I know I cannot continue like how it is. I know I am doing something wrong somewhere. Thank you!

Does that mean you’re thinking of adding backups…?

In a way, yes but also no.

Roblox said:

Which is unlike DS2, in a way DS2 can be great, I do think it becomes a bit complicated sometimes but even then, this example says that backups for this datastore update will be “emergency” backups. And don’t seem as if they will be auto-gotten.

ProfileService NEEDS to save every certain amount of time, the way session locking works I believe, is that, it will get your data, check if it has been loaded recently and retrieve and cache it only if the time has passed.

Basically, let’s say every time you get your data, you’re also telling it to save a timestamp (os.time()), and if this time stamp + some number has already passed, it’s unlocked data. Else, it’s session locked. Its hard to explain, but it compares time to save data.

Once you leave the game it will make the timestamp 0 I believe, to unlock it.

The auto save will keep updating that time stamp to keep it locked, and with the necessary values to keep it locked. A system where you only store a “true” or “false” isn’t good. Primarily because something could happen and the player could be stuck with the profile locked forever. So timestamps fix that, once that auto save time has passed, the player can get his data. That’s why profile service works on cached data, so that it also replace the old data with the current data. And not just make a useless call just to change the timestamp.


So we’ll, hm I think I forgot to answer your question.

No, you don’t need to use :Save(), you should be fine.


I have a question for loleris, am I going crazy or did profile service separate data into chunks to allow big data to be saved…? I might have gotten confused at something…? Not sure.

I keep getting this issue whenever I try to require the DataManager:

ReplicatedStorage.ProfileService.ProfileService:1591: [ProfileService]: profile_store_name must be a string

I found the issue I used the : operator opposed to . when I tried to call GetProfileStore()

2 Likes

Im getting this error trying to save string values : invalid argument #3 (string expected, got nil) (I fixed it somehow it was remaining old tables)

1 Like

So for a game, not necesarrily mine, and to be clear, I am posting for myself, it crashes at random, I’m wondering if there is a way to utilize profile:IsActive() and Locked and Active updates (No clue what those are) to retrieve this lost data.

I BELIEVE ProfileService doesn’t keep any backups. I think it does have its own data safety mechanisms, but it doesn’t keep backups or anything about past data. (From what I know)

Not sure if anyone else has asked, is it probably a safe idea to keep Profile Service in one script to handle everything and use BindableEvents to invoke requests from other server sided scripts?

Or is there a method of tapping into the Profiles table on one from another server script.

I would suggest using a “Handler” module. This module loads data on player join and Releases the profile on player leave. Inside that module you can have functions that add data for a certain player. Something like this:

-- Get the data object of a player (will yield until data is available)
function dataManager.GetData(player)
	local profile = CachedProfiles[player]
	while not profile do
		profile = CachedProfiles[player]
		game:GetService("RunService").Heartbeat:Wait()
	end
	return profile.Data
end

-- Set the saved data to something
function dataManager.Set(player, dataKey, newValue)
	local profile = CachedProfiles[player]
	if profile and profile.Data and profile.Data[dataKey] then
		profile.Data[dataKey] = newValue
	end
end

-- Increment a stored number with another number
function dataManager.Increment(player, dataKey, increment)
	local profile = CachedProfiles[player]
	if profile and profile.Data and profile.Data[dataKey] then
		-- Check if both values are numbers
		if typeof(profile.Data[dataKey]) == "number" and typeof(increment) == "number" then
			profile.Data[dataKey] += increment
		end
	end
end

You require this “Handler” module on whatever (server)script you want to modify a player’s data.

Hi,
is there any event or a way that would invoke a function right away after data is saved or auto saved?