Save your player data with ProfileService! (DataStore Module)

Madwork - ProfileService

ProfileService on GitHub / Join my Dicord group to chat with me

ProfileService is a stand-alone ModuleScript that specialises in loading and auto-saving
DataStore profiles.

It’s documented:
ProfileService wiki

It’s open source:
Roblox library

Watch while you eat pizza on the couch - YouTube tutorials:
ProfileService tutorial playlist by @okeanskiy
Session-locking explained and savable leaderstats by @EncodedLua
(Will add new tutorials as they come)

A DataStore Profile (Later referred to as just Profile) is a set of data which is meant to be loaded up
only once inside a Roblox server and then written to and read from locally on that server
(With no delays associated with talking with the DataStore every time data changes) whilst being
periodically auto-saved and saved immediately once after the server finishes working with the Profile.

The benefits of using ProfileService for your game’s profiles are:

  • Easy to learn, and eventually forget - ProfileService does not give you any data getter or setter functions. It gives you the freedom to write your own data interface.

  • Built for massive scalability - low resource footprint, no excessive type checking. Great for 100+ player servers. ProfileService automatically spreads the DataStore API calls evenly within the auto-save loop timeframe.

  • Already does the things you wouldn’t dare script yourself (but should) - session-locking is essential to keeping your data protected from multiple server editing - this is a potential cause of item loss or item duplication loopholes. ProfileService offers a very comprehensive and short API for handling session-locking yourself or just letting ProfileService do it automatically for you.

  • Future-proof - with features like MetaTags and GlobalUpdates, you will always be able to add new functionality to your profiles without headaches.

  • Made for ambitious projects - ProfileService is a profile object abstraction detached from the Player instance - this allows the developer to create profiles for entities other than players, such as: group-owned houses, savable multiplayer game instances, etc.


ProfileService is part of the Madwork framework
Developed by loleris

Example code:

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

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

local ProfileService = require(game.ServerScriptService.ProfileService)

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

local Players = game:GetService("Players")

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

local Profiles = {} -- [player] = profile

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

local function GiveCash(profile, amount)
    -- If "Cash" was not defined in the ProfileTemplate at game launch,
    --   you will have to perform the following:
    if profile.Data.Cash == nil then
        profile.Data.Cash = 0
    end
    -- Increment the "Cash" value:
    profile.Data.Cash = profile.Data.Cash + amount
end

local function DoSomethingWithALoadedProfile(player, profile)
    profile.Data.LogInTimes = profile.Data.LogInTimes + 1
    print(player.Name .. " has logged in " .. tostring(profile.Data.LogInTimes)
        .. " time" .. ((profile.Data.LogInTimes > 1) and "s" or ""))
    GiveCash(profile, 100)
    print(player.Name .. " owns " .. tostring(profile.Data.Cash) .. " now!")
end

local function PlayerAdded(player)
    local profile = GameProfileStore:LoadProfileAsync(
        "Player_" .. player.UserId,
        "ForceLoad"
    )
    if profile ~= nil then
        profile:ListenToRelease(function()
            Profiles[player] = nil
            -- The profile could've been loaded on another Roblox server:
            player:Kick()
        end)
        if player:IsDescendantOf(Players) == true then
            Profiles[player] = profile
            -- A profile has been successfully loaded:
            DoSomethingWithALoadedProfile(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() 
    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

----- Connections -----

Players.PlayerAdded:Connect(PlayerAdded)

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

This sounds extremely useful, as I find it difficult sometimes to create reliable datastores/properly handle saving and loading player data.

I assume from reading the post that this module handles errors when loading/saving data?

8 Likes

It silently handles all errors while maintaining expected behavior of all its methods and it sends the errors to the endpoints (like ProfileService.IssueSignal) if you wish to set up analytics. Errors will also appear as warnings in the developer log by default.

10 Likes

Really like how efficient this module is. The Global Update element is extremely useful. Especially given it has no additional expense as far a date store calls go. The ease of use and error handling further boost the Mad greatness (pun intended) of the Profile Service. Will definitely be making use of it in a future project.

3 Likes

Came in clutch with this module loleris, I was just looking for a datastore module to use for my game. Thanks.

3 Likes

This is so nicely written and absolutely useful! Legend!

2 Likes

This is a really efficient module, I don’t think people realise that abusing datastores doesn’t make a better datastore. They kind of go with the “if it works it works” attitudes of things.

Thanks for making every compulsive perfectionist’s dream come true.

3 Likes

This is definitely great for newer developers!

However, would there be an advantage to using this over a standard data-storage handler?

In addition, does this provide competition to DataStore2, which is already used by many?

3 Likes

On the contrary, it’s a module well suited for beginners while pretty powerful for advanced scripters - ProfileService is disconnected from Player join / leave events and offers no :Get() or :Set() methods for the profile data - all of this is left to be managed by the developer according to their personal preferences. ProfileService API is short and to the point - it does nothing but handle profiles and it does it perfectly.

Also this module is based on the DataStore implementation I’ve been using in The Mad Murderer 2 where data loss AND item duping were never reported except the time when Roblox did a big oopsie.

4 Likes

Awesome! I’ll have to try it soon for newer projects, I’ll have to come back with results soon :slight_smile:

2 Likes

Amazing job on this! The use of a single DataStore API endpoint had me baffled for a minute, but after looking through it I found your solution to session locking ingenious. This seems like a completely superior data management solution to any others that I’ve seen and I have no doubt in its ability to gain traction.

3 Likes

Will ProfileService automatically release if the server crashes without calling BindToClose? You could possibly abuse MessagingService to check if a server is still alive.

1 Like

If the server crashes the Profile will indeed become session locked by a dead session… Actually I decided to opt-out not_released_handler = “StealSession” functionality which would let the developer instantly steal the session lock for a profile, because it would not be a 100% safe way to handle data. Basically, the dead session locked profile problem solves itself when the developer is using ProfileService as they regularly would:

After passing not_released_handler = “ForceLoad” (or return "ForceLoad" via function) the player would be forced to wait for the game to load for little over a minute. ProfileStore would fail to make the dead session release the profile and would “steal” the session lock after a built-in timeout (which is, again, about 80’ish seconds). Try to make the player enjoy the passage of time via an animation that would simulate progress, some sort of entertaining animation or even a small minigame.

If, however, your game has profiles that may persist after a player leaves, then you would first try to teleport the player to the JobId returned by the not_released_handler (“Cancel” the profile load request to finish the ProfileStore:LoadProfileAsync() call and use TeleportToPlaceInstance. If the teleport fails, call ProfileStore:LoadProfileAsync() again with “ForceLoad” on this session. Obviously in such system you would need to check if a profile is loaded inside the server for that player, because calling ProfileStore:LoadProfileAsync() again before releasing the same profile will cause an error.

If you wish to create your own custom logic of asking a LIVE remote session to release the profile, then the return "Repeat" callback for not_released_handler can be handy - Passing “Repeat” waits for 15 seconds and retries session locking the profile. If the profile has not been released, not_released_handler is called again.

4 Likes

This is awesome @loleris, thanks for sharing us your resources

- can you add a source file for using with rojo on Github? nvm

  • does it use MessageService internally as well?
1 Like

ProfileService can be applied within your Rojo workflow as any regular ModuleScript you would create for your game yourself.

ProfileService only relies on :UpdateAsync() to power all of its features - it will not know whether the Roblox server that has the profile currently locked is actually alive or dead. However, in practice we don’t need to know that as most Roblox servers will not be crashing everyday and the only downside to Roblox servers crashing one day is that your profile requests will be handled in a minute instead of 1-2 seconds.

1 Like

.OnUpdate doesn’t work how does this module address that?

  • so I was wondering how the code detects changes across servers?

I know that the code is compatible, I meant set up like this for Rojo https://github.com/Sleitnick/AeroGameFramework/tree/master see src Folder nvm I thought it wasn’t a standalone module like most, sorry my bad

I haven’t got to read the source yet

1 Like

Instead of subscribing to changes it scans for changes periodically every auto-save which happens every 30 seconds for every active Profile.

This post was flagged by the community and is temporarily hidden.

4 Likes

A wiki page with a completely barebones “getting started” example could be very useful for beginner scripters - one of the reasons some prefer DS2. For example, omitting the player:IsDescendantOf if statement for the sake of simplicity. Also I might have misread the example, but there doesn’t seem to be a use for Workspace.

A quick example using how global updates might interest people too.

4 Likes

I might use this as a secondary backup