Save your player data with ProfileService! (DataStore Module)

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.

2 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

Whenever I require the module script that requires the ProfileService Module, the whole script where I required the module script no longer functions in-studio. Is it built to only be ran in-game?

It only stores data in-game. You’ll have to check in a live game to make sure it works.

After a recent update you can access the live DataStore keys if you enable Roblox API access - see ProfileService wiki troubleshooting section. This functionality might change a bit in the near future.

2 Likes

You’ve made a mistake somewhere. Familiarize with ModuleScript documentation and fix your code.

1 Like

I might sound a bit oblivious in this post, but I just want to double check to make sure I have all the details down before I make a choice to switch.

Does ProfileService do fully automatic saving assuming I’m using the module as intended with the methods in the examples?

What’s the ideal data replication setup for ProfileService? I’ve been trying to brainstorm the lightest, most network efficient solution, but I’m having trouble coming up with something conclusive.

  • Ideally, there would be some way of the client to request/read their data without having to call any RemoteFunctions? (the server would update something they can read when their data changes?)

Thanks.

Not any that I know of, ProfileService is a module that handles Data Saving not Data Replication, you can make a system for that however you’d like to. It’s really not that hard nor that many options to choose from, you either use ValueBase or Remotes for it.

This is probably due to preference and different games have different ways of dealing with Replication, the Author doesn’t want to interfere with that freedom.

You can read more about which way to do it on the Forum, there are many Posts available

maybe in the future we’ll have an add on for the Module :wink:

1 Like

I’m aware the ProfileService is for data saving, I was just wondering if there was any suggested best way to go about it (similar to the data handler example posted). I’ll get to looking around even more.

How would i get data from the same datastore from another script?
for example a main script loads the data first to give daily rewards and to process gifts, then another script that is suppose to read the same Profile for if they own a weapon to display, doing this is throwing an error saying that [[ProfileService]: Profile of ProfileStore “PlayerData” with key “Player_248033263” is already loaded in this session], am i doing smthing wrong? or do i have to get the data in another way

2 Likes

You can use ModuleScripts as they return the same Data (cache) to all Scripts within the same context.

In general you do not what to load a profile twice in the same game server (JobId)

You can learn more here:

2 Likes