Save your player data with ProfileService! (DataStore Module)

ProfileService creates a little over 2 UpdateAsync calls per minute per active profile (Don’t forget to release your profiles).

I always advise using one profile per player whenever possible due to data being rapidly accessible in a single profile. You may, however, use more than one profile for individual players - pay attention to your DataStore call budget.

Thank you, that’s exactly what I wanted to know.

So my idea should work then because I have 8 players in a server. So it will be making roughly 32 calls per minute and the budget will be 140 calls per minute.

Hello, I have a question. Is it possible to check after loading a profile whether it is the first time that the profile has been loaded or not? I want to do something to players who are strictly only playing my game for the first time.

You could try to use ProfileStore:ViewProfileAsync(profile_key). Basically, if you know what the key would be, for example, profile_1111, for that user specifically then when you do local viewProfile = ProfileStore:ViewProfileAsync(profile_1111), viewProfile.Data or viewProfile.MetaData equals nil because the profile hasn’t been created.

If you’re looking at the Basic Usage example, under the PlayerAdded funcion you see the code below. If you change :LoadProfileAsync to :ViewProfileAsync then change profile to profile.Data you would be able to determine if it’s a new user or not. (profile.Data ~= nil is old user // profile.Data == nil is new user) After that, you would be able to run your custom code as long as you eventually load the actual profile.

Note, I’m not sure if you have to release the ViewProfileAsync loaded profile, but I don’t think so. You also want to remove “ForceLoad”. The API might help you a bit.

local profile = GameProfileStore:LoadProfileAsync(
    "Player_" .. player.UserId,
    "ForceLoad"
)
if profile ~= nil then

Just flag your profiles with a value when you first load them. If the profile already has that flag then it means it was already loaded before. I recommend using Profile:SetMetaTag() for this.

1 Like

This is the solution I will use. I feel as though it is a bit excessive though because you have to make a datastore call when I feel like you should just be able to get whether it is new data from LoadProfileAsync

The problem with this solution is that I already have existing player data and I did not set a tag for whether their profile has been loaded for the first time or not.

The rationale that loleris has for not natively supporting serialization is that data should always be in a savable state.

2 Likes

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:

2 Likes

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.