Save your player data with ProfileService! (DataStore Module)

Thanks @okeanskiy for creating a video guide for ProfileService!
:arrow_right: YouTube playlist

31 Likes

And thank you as well for the support :+1:

If anyone has any questions about my videos, my DMs are open as well.

22 Likes

How is this better then the already existing DataStore2 which handles dataloss just fine?

9 Likes

Short answer:
YouTube: Roblox - Dungeon Quest How To Duplicate Items (2020) (PATCHED)
The duplication exploit was patched by the developer himself, since DataStore2 does not offer session locking - he wouldn’t have to bother with such bugs with a module like ProfileService and now Dungeon Quest’s economy is under a permanent impact.

Data loss is not the enemy here, item duplication is:
Ninja Legends item duplication
Bubblegum simulator item duplication
Murder Mystery 2 item duplication
…And basically every game that had trading ever.
Except my game The Mad Murderer 2 which never had reports of item duplication because I am using session locking.

Long answer:
ProfileService is both simple to use and a professional-grade DataStore implementation - it means this module is just as useful as DataStore2 for the little developer, but when your game grows large and you go for huge features like trading, gifting, hopping between universe places, you’ll be pulling your hair out trying to manage the stability of such complex systems and preventing players from destroying your economy.

A few ProfileService vs DataStore2 points (Most relevant, descending):

  • DataStore2 does not session-lock internally. Your first trading system made with DataStore2 is 99% guaranteed to have duplication loopholes unless you understand session locking (and are willing to script this nightmare yourself).
  • DataStore2 is (massively) overrated and kind of built around false claims (claims that are not relevant in 2020) of DataStoreService being completely unreliable. First off, for what my name is worth, just know that I personally do not support such claims at all and thus see manual versioning as a waste of your game’s processing resources. ProfileService will, theoretically, only lose data when literally everyone else in the Roblox community will be losing data alongside with you. When DataStore2 developers talk about data loss, they refer to this problem specifically when DataStoreService returned nil instead of your saved data - Roblox would not afford making mistakes like that in the future and I don’t believe this will be a reoccurring problem which DataStore2 swears to defend you from. DataStore2 will not efficiently protect you from any other type of data loss such as caused by developer error (and neither will ProfileService).
  • To an experienced developer, ProfileService is SO MUCH MORE easier to use, because it does not overwhelm you with tons of getter and setter methods.
  • ProfileService has analytics endpoints and DataStore2 does not - this is crucial for big commercial projects!!!
  • If you’re familiar with DataStore2 API, DataStore2.Combine() was not something that should’ve existed at all - it just adds a technical obscurity layer to the primary function of DataStore2.
  • DataStore2 API nomenclature is a bit of a mess - almost everyone keeps getting confused between DataStore2 API objects and Roblox API objects since the creator picked the same names.
  • DataStore2 is bound to the Player instance. There’s no way to load offline player data or create savable data profiles that belong to a non-player entity while using the DataStore2 API.
  • DataStore2 source code might be harder to read and understand than ProfileService for a beginner.

If you’re part of the herd then at least try to learn a little about what exactly we’re all praising :joy:

51 Likes

I am interested in this, what exactly is “session locking” and how hard would it be to switch over from a game currently using Datastore2?

4 Likes

If your game does not have trading and won’t have it in the future, then there’s no emergency to switch data store modules. I’d say moving between two datastore modules for a game is a difficult challenge and you should avoid doing things like that if possible.

Session-locking basically prevents Server #2 from loading player data before Server #1 finishes saving changes to the same player data - this is how item duplicates can be made in many games that have trading. As far as you should be concerned - using session-locking is mandatory in preventing item duplication loopholes.

19 Likes

I just uploaded a video analyzing the issues (with powerpoint-tier animations) that ProfileService addresses, as well as an in-depth getting started guide to use with a small sword fighting minigame. The place file can be found in the comment section.

18 Likes

Would Profile:IsActive() be able to be checked from ProfileStore:ViewProfileAsync()? I know you can’t write/save any data, but I was wondering if you would be able to check to see if a Profile was locked through IsActive() and ForceLoad it or send a GlobalUpdate depending on is it returns true or false.

-- Example --
local profile_key = KeyHere
local viewProfile = ProfileStore:ViewProfileAsync(profile_key)

if viewProfile:IsActive() == true then
    -- Send Global Update Stuff Here
else
    local profile = ProfileStore:LoadProfileAsync(
        profile_key,
        "ForceLoad"
    )
end
4 Likes

That’s what the not_released_handler is for:

local get_place_id, get_game_job_id
local profile = GameProfileStore:LoadProfileAsync(
    "Player_" .. player.UserId,
    function(place_id, game_job_id)
        get_place_id, get_game_job_id = place_id, game_job_id
        return "Cancel"
    end
)
if profile ~= nil then
    -- Profile was not session locked, and is now session locked to this server
elseif get_place_id ~= nil then
    -- Profile is (most likely) session locked by a remote server
else
    -- Ultra rare case of nothing working... Just kick the player
end

This way ProfileService will make only one UpdateAsync() call in the scenario the profile is not released instead of doing a UpdateAsync() once to check it and then UpdateAsync() again to load it (Which would also result in a queue warning in the dev console).

HOWEVER, be cautious that there are no guarantees that the server of "game_job_id" is alive - this is the case where the profile is indefinetly locked and you will only be able to load the profile using “ForceLoad”.

(Just make sure the profile is not already loaded in the same server, or this code will error)

9 Likes

I saw that you are using :UpdateAsync() to get the get the player’s data. Is there a reason that you use this over the traditional :GetAsync()?

3 Likes

:UpdateAsync() is essentially a :GetAsync() and :SetAsync() combined (it can literally replace either of those two methods), with some additional functionality which helps data be stable when accessed from several servers at the same time. Session-locking needs to set some data inside a profile when the profile is loaded - that’s the main reason why I need :UpdateAsync().

12 Likes

I would advise against this, as it can interfere with DataStore saving and loading sometimes. When using DataStores, I always try to save only when joining and leaving (as well as game binded close).

2 Likes

What kind of interference exactly are you experiencing when doing periodic DataStore saving calls?

4 Likes

In the past, I’ve actually had the Data Store interfere with itself when attempting to auto-save. Auto-saving increases the risk of exhausting the data store, which can especially cause issues for users trying to load into the game and retrieve data.

The result, from my experience, was unintentional data loss for some users at some point or another, or outdated data. When periodic saving was removed, this issue was fixed entirely.

With that being said, some developers may already be using data stores for global leader-boards, game-wide data, etc.

3 Likes

Well I think the goal in effective data management is not to use as few DataStore calls as possible, but stay well within the limits - ProfileService sits at around 10% to 18% of a game’s DataStore call budget when every player owns their own Profile. Auto saving is essential to keep data as secure as possible and not risk having players revert to their past save snapshot after a server crashes with hours of progress lost instead of just seconds.

As for “DataStore interfering with itself” - you could’ve been just running into DataStore throttling due to improper call timing since setting a single key too frequently will throttle your calls much sooner than you run out of DataStore call budget.

14 Likes

So if I understand correctly, I could do something like this. I want to test to see if the Profile is currently loaded in that server. If it is not (if not testProfile then), I want to run your code; otherwise if it is in that server, just send a Global Update.
Now the question I have is, if the “ultra rare case” happens, would I be able to Kick the player then ForceLoad right after to make the current server grab the profile, and does that “ultra rare case” include the game_job_id not being alive?

function PlayerHandler.Test(id)
	local testProfile = Profiles[id]
	
	if not testProfile then
		
		local place_id, game_job_id
		local profile = PlayerDataStore:LoadProfileAsync(
			playerKey.. id,
			function(placeID, gameID)
				place_id, game_job_id = placeID, gameID
				return "Cancel"
			end
		)
		
		if profile ~= nil then
			-- Set Data because Server Grabbed Profile
		elseif place_id ~= nil then
			-- Send Global Update because Profile Locked
		else
			-- Kick User with "id"
		end
		
	elseif testProfile then
		-- Send Global Update
	end
end
5 Likes

GlobalUpdates are meant specifically for saving some sort of data to a Profile regardless of whether the Profile is in a server or not and it certainly streamlines this kind of a task. You should also consider NOT using GlobalUpdates for profiles loaded on the same server since they’re going to take up to a whole minute to proccess instead of just applying something to your data instantly.

If giving a player some sort of an item is not your goal or you want data to move as fast as possible then you shouldn’t be using GlobalUpdates - you weren’t really clear on what you’re trying to achieve.

My guesses are, you should be using MessagingService - Set up a message topic composed of a server’s JobId (like "ProfileCheck_" .. game.JobId) and just try to communicate with that server and wait for the message back from it with a timeout - this would be a much faster solution than GlobalUpdates because GlobalUpdates were not designed for this. If you don’t get a message back from the server that is supposedly locking the profile (after waiting for like 20 seconds), then assume it is indefinetly locked and “ForceLoad” the profile and let the player know that it can take a bit longer to load their data.

7 Likes

Thanks, I ended up using the MessagingService, I was just going to try and do it with GlobalUpdates because it wasn’t all important. But how long does it usually take for a profile to load? The profile itself takes around 30 to 60 seconds.

2 Likes

I had similar issues when making my video, and I figured out that the issue is probably near the end of the :BindToClose near the bottom of the module. If you replace all of the ands with ors in the while loop that waits for the profiles to save/load, then it should fix the issue most of the time.

Mine looks like this now:

while on_close_save_job_count > 0 or ActiveProfileLoadJobs > 0 or ActiveProfileSaveJobs > 0 do
	RunService.Heartbeat:Wait()
end
4 Likes

Ah epic, thanks. I was confused because it didn’t look like I had done anything different besides naming when it came to loading.

3 Likes