Save your player data with ProfileService! (DataStore Module)

Thank you for the clarification & help :+1:

Hey, how would you use CorruptionSignal etc? do they function the same as events?
Looked at the wrong places in the docs, found it. My bad
I always find something by myself right after posting it :roll_eyes:

Hey, I’m experiencing some issues with loading in data when a player joins.
It saves the data just fine (printing when they leave), but when they rejoin it gets reset again.
I’m unsure whether this is something on my part or on yours.

Could you contact me on discord? Pyseph#0001
Thanks!

It doesn’t work inside studio, by the way.
Try disabling all scripts that use datastore service, as sometimes they can interfere with ProfileService.

Yeah, I figured it was something to do with studio :confused:
I tested in-game, but I still got the same result. Unsure what it is, as I have a data network module which handles all data related stuff: I removed everything that had to do with datastore service.

Issue has been resolved, thank you loleris for helping me out.
The issue was that after rewriting the data network to use this module, I forgot to double check for serialization :roll_eyes:

Can we have a toggle to set whether or not we want ProfileService to save to live keys while still maintaining Studio access to API services? I don’t like the all-or-nothing nature of the setting. DataStore2 has a feature like this where the inclusion of a BoolValue named SaveInStudio dictates this and by default, will not save to live keys if the BoolValue does not exist at all.

1 Like

Can you give me an example of when you need API access enabled, but live datastores disabled?

1 Like

Hey, Im quite new to using data stores, Ive been using datastore2 for my previous project. How many extra data stores such as ordered data stores for leaderboards and data stores for processing receipts it can handle together with this? Also, does it require player being online in order for me to edit their data? (I assume so…)

Good job my southern brotha (read username…) :wink: :smiley:

ProfileService sits at around 10% to 18% of a game’s DataStore call budget when every player owns their own Profile in a live game. Using lots of DataStore calls is a more advanced topic and you’ll need to read official documentation to find your answers:

ProfileService API allows you to edit player data regardless of whether they’re online, either by simply loading their profile or with the use of GlobalUpdates.

3 Likes

Well this module sounds like a keeper! Im aware of the game limits of datastore calls, so the 10-18% was helpful... ny understanding is, theres a total data limit, but also request limits for speciffic key? Thing i forgot to ask is if this module works with any "profiles", not only player data, for example, those very processed receipt ids? Sorry, i`m sure you mentioned it, i just missed it.

ProfileService is more specialised for player data - use custom DataStore code for other features.

3 Likes

Parts of my game use normal DataStores right now to access global data that doesn’t change often. Additionally, I like to test for different situations using different save data “templates” or my own user data but I don’t want the testing I do to reflect onto the live DataStore. Right now, I develop within the same game as the live server, just different places, so the DataStores are technically shared (although I could set up using different scopes, but then I would lose being able to use my “live” data for testing)

Does ProfileService use journelling (recording what has changed, rather than overriding) on table values?

ProfileService was created with performance as it’s #1 priority, so you can’t rollback data as of now… However, based on Roblox’s roadmap, we might have first-party rollback support in the very near future and ProfileService should be compatible with it as soon as it comes out.

1 Like

Just a quick performance nitpick. I dont see the need to run this every frame?

RunService.Heartbeat:Connect(function(dt)
		-- 1) Auto saving: --
		local auto_save_list_length = #AutoSaveList
		if auto_save_list_length > 0 then
			local auto_save_index_speed = SETTINGS.AutoSaveProfiles / auto_save_list_length
			local current_tick = tick()
			while current_tick - LastAutoSave > auto_save_index_speed do
				LastAutoSave = LastAutoSave + auto_save_index_speed
				local profile = AutoSaveList[AutoSaveIndex]
				if current_tick - profile._load_timestamp < SETTINGS.AutoSaveProfiles then
					-- This profile is freshly loaded - auto-saving immediately after loading will cause a warning in the log:
					profile = nil
					for i = 1, auto_save_list_length - 1 do
						-- Move auto save index to the right:
						AutoSaveIndex = AutoSaveIndex + 1
						if AutoSaveIndex > auto_save_list_length then
							AutoSaveIndex = 1
						end
						profile = AutoSaveList[AutoSaveIndex]
						if current_tick - profile._load_timestamp >= SETTINGS.AutoSaveProfiles then
							break
						else
							profile = nil
						end
					end
				end
				-- Move auto save index to the right:
				AutoSaveIndex = AutoSaveIndex + 1
				if AutoSaveIndex > auto_save_list_length then
					AutoSaveIndex = 1
				end
				-- Perform save call:
				-- print("[ProfileService]: Auto updating profile - profile_store_name = \"" .. profile._profile_store._profile_store_name .. "\"; profile_key = \"" .. profile._profile_key .. "\"")
				if profile ~= nil then
					coroutine.wrap(SaveProfileAsync)(profile) -- Auto save profile in new thread
				end
			end
		end
		-- 2) Issue queue: --
		-- Critical state handling:
		if ProfileService.CriticalState == false then
			if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
				ProfileService.CriticalState = true
				ProfileService.CriticalStateSignal:Fire(true)
				CriticalStateStart = tick()
				warn("[ProfileService]: Entered critical state")
			end
		else
			if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
				CriticalStateStart = tick()
			elseif tick() - CriticalStateStart > SETTINGS.CriticalStateLast then
				ProfileService.CriticalState = false
				ProfileService.CriticalStateSignal:Fire(false)
				warn("[ProfileService]: Critical state ended")
			end
		end
		-- Issue queue:
		while true do
			local issue_tick = IssueQueue[1]
			if issue_tick == nil then
				break
			elseif tick() - issue_tick > SETTINGS.IssueLast then
				table.remove(IssueQueue, 1)
			else
				break
			end
		end
end)

I replaced it with this so it runs less often and it runs just fine.

local timer = 0
-- Auto saving and issue queue managing:
RunService.Heartbeat:Connect(function(dt)
	timer = timer + dt
	if timer >= 0.35 then
		timer = 0
		-- 1) Auto saving: --
		local auto_save_list_length = #AutoSaveList
		if auto_save_list_length > 0 then
			local auto_save_index_speed = SETTINGS.AutoSaveProfiles / auto_save_list_length
			local current_tick = tick()
			while current_tick - LastAutoSave > auto_save_index_speed do
				LastAutoSave = LastAutoSave + auto_save_index_speed
				local profile = AutoSaveList[AutoSaveIndex]
				if current_tick - profile._load_timestamp < SETTINGS.AutoSaveProfiles then
					-- This profile is freshly loaded - auto-saving immediately after loading will cause a warning in the log:
					profile = nil
					for i = 1, auto_save_list_length - 1 do
						-- Move auto save index to the right:
						AutoSaveIndex = AutoSaveIndex + 1
						if AutoSaveIndex > auto_save_list_length then
							AutoSaveIndex = 1
						end
						profile = AutoSaveList[AutoSaveIndex]
						if current_tick - profile._load_timestamp >= SETTINGS.AutoSaveProfiles then
							break
						else
							profile = nil
						end
					end
				end
				-- Move auto save index to the right:
				AutoSaveIndex = AutoSaveIndex + 1
				if AutoSaveIndex > auto_save_list_length then
					AutoSaveIndex = 1
				end
				-- Perform save call:
				-- print("[ProfileService]: Auto updating profile - profile_store_name = \"" .. profile._profile_store._profile_store_name .. "\"; profile_key = \"" .. profile._profile_key .. "\"")
				if profile ~= nil then
					coroutine.wrap(SaveProfileAsync)(profile) -- Auto save profile in new thread
				end
			end
		end
		-- 2) Issue queue: --
		-- Critical state handling:
		if ProfileService.CriticalState == false then
			if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
				ProfileService.CriticalState = true
				ProfileService.CriticalStateSignal:Fire(true)
				CriticalStateStart = tick()
				warn("[ProfileService]: Entered critical state")
			end
		else
			if #IssueQueue >= SETTINGS.IssueCountForCriticalState then
				CriticalStateStart = tick()
			elseif tick() - CriticalStateStart > SETTINGS.CriticalStateLast then
				ProfileService.CriticalState = false
				ProfileService.CriticalStateSignal:Fire(false)
				warn("[ProfileService]: Critical state ended")
			end
		end
		-- Issue queue:
		while true do
			local issue_tick = IssueQueue[1]
			if issue_tick == nil then
				break
			elseif tick() - issue_tick > SETTINGS.IssueLast then
				table.remove(IssueQueue, 1)
			else
				break
			end
		end
	end
end)
4 Likes

That’s what i’m trying to accomplish for my data, is there are source for this right now? Because I have a hard time knowing on how to merge saves without interfering with the data that had already been saved.

1 Like

Edited…
I have 2 question about the merging process.

  1. Would I be able to merge things inside tables? Here’s my code:
{
	Currency = { 
     -- Merge  (add a table) here
     Tokens = 0, Gems = 0, Event_Currency = 0, CurrencyBag = 0
    },
		
	Inventory = {
        -- Merge  (add a table) here
		Knives = {{"DefaultKnife"}, KnifeEquip = {"DefaultKnife"}},
		Pets = {{"Rabbit"}, PetEquip = {"Rabbit"}},
		Radios = {{}, RadioEquip = {}},
		Effects = {{}, EffectEquip = {}},
		Titles = {{}, TitleEquip = {}}
}
  1. With the merging process, you could add tables, but could you remove tables as well?
1 Like

Anything is possible. At this point I recommend you choose the solution that is the most comfortable for your personal coding knowledge level - whether it’s automatic merging or a custom code that checks individual parts in your data table.

1 Like

Is updating the data being saved automatic? Or is that something we would have to add ourselves? In EncodedLua’s video it seemed to suggest that a coroutine or spawn() function would be required to continually update the data.

1 Like

It’s assumed that anything you store in Profile.Data is going to persist automatically assuming you properly call Profile:Release() after finishing work and not write to Profile.Data after Profile:ListenToRelease() is triggered.

3 Likes