Save your player data with ProfileService! (DataStore Module)

You are right about that.
I already managed to handle the seller taking it off sale before the purchase registers, but the other issues persist.
I guess it will have to be made in the way you are describing.
But wouldn’t holding seller’s profile for some time after player leaves help at least a bit? The fact that the seller won’t be able to join back until after a few seconds isn’t such a big deal.
Is there more potential for error in doing that than it would solve?

You can try doing that in hopes to prevent leave duping - in this case you’ll need to customize ProfileService not_released_handler to wait for as long as possible for the last session to be released without triggering a remote release (e.g. Return “Repeat” 5 times and then return “ForceLoad” for the last time)

2 Likes

Okay, great to know. Thanks a lot!

Hey, I read that you can use ProfileService for things that are detached from Players like groups, but I can’t find any examples.

How could you do something like a group chat/wall? Or some other data that multiple players in different servers can edit at the same time?

The only solutions I can think of are:

  1. To juggle the profile lock between servers so that as long as atleast 1 member is online, the profile will be loaded in and listening to/acting on global updates.
    Or move the lock to the server where a player wants to send a message from every time.
  2. To store things like a group chat/wall separately from data that can only be edited by the owner (so you never need to lock it from multiple servers), but then you still can’t use ProfileService for that data.
  3. To just use :OverwriteAsync() and hope for the best.

But none of these seem like good solutions to me.

ProfileService is for one server access implementations only

Are there any plans to make this compatible with LuaU’s type system/!strict mode?

2 Likes

Hey there! Me again.

I’ve been having problems with Profile:Reconcile(). Basically, whenever I add/remove values from the template, all other values end up being reset. Is this intentional or?

Edit:
I forgot I was testing on a local place and data didn’t save. :sweat:

You might be trying to reconcile arrays - the reconcile function is designed for dictionaries only. Reconciliation can be a very specialised thing and that’s why you have to personally understand how Profile:Reconcile() works from the reconciliation code piece in the PS wiki.

There’s no single function that can understand the user’s intention on reconciliation.

1 Like

Alright, thanks for clearing that up. But I have another question. How does the AddUserIds function work exactly? Does it allow other accounts to access the data?

If I wanted to manipulate the profiles in some way before they’re unloaded due to the game closing, how could I do that?

Hey! Im using profile service, and im not sure if im doing something wrong. In my game, data saves and loads fine until there are like 12 people in a server, then it spams this message in output logs:

As to how i handle my data, i use a table and alter the values from there. the only way i can see these request queuing, from what i can understand from the module, is if i was manually calling a save function every second or so, but that isnt the case.

Am i altering the tables values to fast?

My main data script: PlayerDataService - Pastebin.com

Example on how I alter data : PlayerData.Stats.Strength += 1 (PlayerData being PlayerDataService.new() )

I should specify that it spams this (for every player or so) every second

This also stops some things in my game from working (for example, someone purchases gems with Robux, takes the Robux because the purchase isnt the issue but doesnt update the data sometimes due to a filled queue I presume. This request gets spammed a ton per second)

Hey there!

I’ve been considering trying to migrate my game’s normal DataStoreService to use ProfileService as this seems like an amazing and much more viable solution considering we’ve had problems with data loss in the past

However, I need to ask if it’s possible to migrate without deleting player data on accident? I was never good with Data Saving, and I have the data of over 100 different players over the last two years saved using the normal DataStoreService. If I wanted to migrate to ProfileService, would I really have to wipe it out??

Why is ProfileService not loading in my game (a sfoth iv remake)???

I have all things DataStore related like API Services enabled in the game settings but it just doesn’t load.

The script is a ModuleScript called ProfileManager in serverscriptservice and this is the code

local ProfileService = require(script.ProfileService)
local Players = game:GetService("Players")
local BadgeService = game:GetService("BadgeService")
local MarketplaceService = game:GetService("MarketplaceService")

local Profiles = {}

local DataManager = {}

local saveStructure = {

	Main = {
		{
			KOs = 0;
			Wipeouts = 0;
		}
	},

	Badges = {
		Welcome = 2130186399;
		Kills5 = 2130186410;
		Kills20 = 2130186426;
		Kills97 = 2130186461;
		LovedOfMuses = 2130186563;
	},

	Gamepasses = {

		

	},

	Products = { -- developer_product_id = function(profile)

	},
	PurchaseIdLog = 50, 



}



----------------------------------- PROFILE STORES


local PlayerProfileStore = ProfileService.GetProfileStore("Player", saveStructure.Main)


----------------------------------- PRIVATE FUNCTIONS





local function createLeaderstats(player)
	local profile = Profiles[player]

	local folder = Instance.new("Folder")
	folder.Name = "leaderstats"
	folder.Parent = player

	local KOs = Instance.new("IntValue")
	KOs.Name = "KOs"
	KOs.Value = profile.Data.KOs
	KOs.Parent = folder

	local Wipeouts = Instance.new("IntValue")
	Wipeouts.Name = "Wipeouts"
	Wipeouts.Value = profile.Data.Wipeouts
	Wipeouts.Parent = folder


	spawn(function()
		while true do
			local profile = Profiles[player]

			if profile ~= nil then
				KOs.Value = profile.Data.KOs
				Wipeouts.Value = profile.Data.Wipeouts
			else
				break
			end

			wait(0.1)
		end
	end)
end


local function handleLockedUpdate(globalUpdates, update)
	local id = update[1]
	local data = update[2]
	
	if data.UpdateType == "This" then
		
	end
	
	
	globalUpdates:ClearLockedUpdate(id)
end


local function onPlayerAdded(player)
	local profile = PlayerProfileStore:LoadProfileAsync(
		"Player".. player.UserId,
		"ForceLoad"

	)

	if profile then
		profile:ListenToRelease(function()
			Profiles[player] = nil
			player:Kick()

		end)

		if player:IsDescendantOf(Players) then
			Profiles[player] = profile
			
			
			
			local globalUpdates = profile.GlobalUpdates
			
			for index, update in pairs(globalUpdates:GetActiveUpdates()) do
				globalUpdates:LockActiveUpdate(update[1])
			end
			
			for index, update in pairs(globalUpdates:GetLockedUpdates()) do
				handleLockedUpdate(globalUpdates, update)
			end
			
			globalUpdates:ListenToNewActiveUpdate(function(id, data)
				globalUpdates:LockActiveUpdate(id)
			end)
			
			globalUpdates:ListenToNewLockedUpdate(function(id, data)
				handleLockedUpdate(globalUpdates, {id, data})
			end)
			
			wait(0.5)
			
			createLeaderstats(player)
			
		else
			profile:Release()
		end
	else
		player:Kick("Issue with data loading. Rejoin.")
	end

	
	
	
	if not BadgeService:UserHasBadge(player.UserId, saveStructure.Badges.Welcome) then
		BadgeService:AwardBadge(player.UserId, saveStructure.Badges.Welcome)
		local badgeUI = require(game.ServerScriptService.BadgeNotification)
		badgeUI.OpenUI(player.UserId, saveStructure.Badges.Welcome, 1)
	end
	
	
	
	
	local function CharacterAdded(char)
		local humanoid = char:WaitForChild("Humanoid")

		humanoid.Died:Connect(function()
			local playerProfile = Profiles[player]
			if playerProfile ~= nil then
				playerProfile.Data.Wipeouts += 1
--				if not BadgeService:UserHasBadge(player.UserId, saveStructure.Badges.Death) then
--					BadgeService:AwardBadge(player.UserId, saveStructure.Badges.Death)
--					local badgeUI = require(game.ServerScriptService.BadgeNotification)
--					badgeUI.OpenUI(player.UserId, saveStructure.Badges.Death, 1)
--				end 
			end

			local killerValue = humanoid:FindFirstChild("creator")
			if killerValue ~= nil then
				local killer = killerValue.Value
				local killerProfile = Profiles[killer]

				if killerProfile ~= nil then

						killerProfile.Data.KOs += 1

					if killerProfile.Data.KOs == 5 then
						if not BadgeService:UserHasBadge(killer.UserId, saveStructure.Badges.Kills5) then
							BadgeService:AwardBadge(killer.UserId, saveStructure.Badges.Kills5)
							local badgeUI = require(game.ServerScriptService.BadgeNotification)
							badgeUI.OpenUI(killer.UserId, saveStructure.Badges.Kills5, 1)
						end
					elseif killerProfile.Data.KOs == 20 then
						if not BadgeService:UserHasBadge(killer.UserId, saveStructure.Badges.Kills20) then
							BadgeService:AwardBadge(killer.UserId, saveStructure.Badges.Kills20)
							local badgeUI = require(game.ServerScriptService.BadgeNotification)
							badgeUI.OpenUI(killer.UserId, saveStructure.Badges.Kills20, 1)
						end
					elseif killerProfile.Data.KOs == 97 then
						if not BadgeService:UserHasBadge(killer.UserId, saveStructure.Badges.Kills97) then
							BadgeService:AwardBadge(killer.UserId, saveStructure.Badges.Kills97)
							local badgeUI = require(game.ServerScriptService.BadgeNotification)
							badgeUI.OpenUI(killer.UserId, saveStructure.Badges.Kills97, 2)
						end
					end
				end
			end
		end)
	end

	if player.Character ~= nil then
		CharacterAdded(player.Character)
	end




















end

Players.PlayerAdded:Connect(onPlayerAdded)

local function onPlayerRemoving(player)
	local profile = Profiles[player]

	if profile then
		profile:Release()
	end
end

Players.PlayerRemoving:Connect(onPlayerRemoving)



workspace.JohnLovedOfMuses.Touched:Connect(function(hit)
	if hit.Parent:FindFirstChild("HumanoidRootPart") then
		local player = game.Players:GetPlayerFromCharacter(hit.Parent)
		--local profile = Profiles[player]
		
		if not BadgeService:UserHasBadge(player.UserId, saveStructure.Badges.LovedOfMuses) then
			BadgeService:AwardBadge(player.UserId, saveStructure.Badges.LovedOfMuses)
			local badgeUI = require(game.ServerScriptService.BadgeNotification)
			badgeUI.OpenUI(player.UserId, saveStructure.Badges.LovedOfMuses, 2)
		end
		
	end
end)





----------------------------------- PRODUCT FUNCTIONS






function PurchaseIdCheckAsync(profile, purchase_id, grant_product_callback) --> Enum.ProductPurchaseDecision
	-- Yields until the purchase_id is confirmed to be saved to the profile or the profile is released

	if profile:IsActive() ~= true then

		return Enum.ProductPurchaseDecision.NotProcessedYet

	else

		local meta_data = profile.MetaData

		local local_purchase_ids = meta_data.MetaTags.ProfilePurchaseIds
		if local_purchase_ids == nil then
			local_purchase_ids = {}
			meta_data.MetaTags.ProfilePurchaseIds = local_purchase_ids
		end

		-- Granting product if not received:

		if table.find(local_purchase_ids, purchase_id) == nil then
			while #local_purchase_ids >= saveStructure.PurchaseIdLog do
				table.remove(local_purchase_ids, 1)
			end
			table.insert(local_purchase_ids, purchase_id)
			task.spawn(grant_product_callback)
		end

		-- Waiting until the purchase is confirmed to be saved:

		local result = nil

		local function check_latest_meta_tags()
			local saved_purchase_ids = meta_data.MetaTagsLatest.ProfilePurchaseIds
			if saved_purchase_ids ~= nil and table.find(saved_purchase_ids, purchase_id) ~= nil then
				result = Enum.ProductPurchaseDecision.PurchaseGranted
			end
		end

		check_latest_meta_tags()

		local meta_tags_connection = profile.MetaTagsUpdated:Connect(function()
			check_latest_meta_tags()
			-- When MetaTagsUpdated fires after profile release:
			if profile:IsActive() == false and result == nil then
				result = Enum.ProductPurchaseDecision.NotProcessedYet
			end
		end)

		while result == nil do
			task.wait()
		end

		meta_tags_connection:Disconnect()

		return result

	end

end

local function GetPlayerProfileAsync(player) --> [Profile] / nil
	-- Yields until a Profile linked to a player is loaded or the player leaves
	local profile = Profiles[player]
	while profile == nil and player:IsDescendantOf(Players) == true do
		task.wait()
		profile = Profiles[player]
	end
	return profile
end

local function GrantProduct(player, product_id)
	-- We shouldn't yield during the product granting process!
	local profile = Profiles[player]
	local product_function = saveStructure.Products[product_id]
	if product_function ~= nil then
		product_function(profile)
	else
		warn("ProductId " .. tostring(product_id) .. " has not been defined in Products table")
	end
end

local function ProcessReceipt(receipt_info)

	local player = Players:GetPlayerByUserId(receipt_info.PlayerId)

	if player == nil then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	local profile = GetPlayerProfileAsync(player)

	if profile ~= nil then

		return PurchaseIdCheckAsync(
			profile,
			receipt_info.PurchaseId,
			function()
				GrantProduct(player, receipt_info.ProductId)
			end
		)

	else
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

end

----- Initialize -----

for _, player in ipairs(Players:GetPlayers()) do
	task.spawn(onPlayerAdded, player)
end

MarketplaceService.ProcessReceipt = ProcessReceipt




















function DataManager:Get(player)
	local profile = Profiles[player]

	if profile then
		return profile
	end
end

function DataManager:GetProfileStore()
	return PlayerProfileStore
end



return DataManager

There’s literally no error, and every time I try putting print() commands to see where the script stops (including one in the first line of code) (I removed them tho) they just don’t show up which proves the script is just not loading/running in the game…

I think I found the issue. I think ModuleScripts don’t run automatically anymore or something

MS never ran automatically, that’s why they’re a module.

1 Like

They used to and actually still do in some of my older games that used them for ProfileService

If you look in essentially every guide/tutorial video on ProfileService (such as the ones linked in the main post itself) you can see people use ProfileService with modules usually named stuff like “ProfileCacher” or “ProfileManager”, etc)) and they’ve always functioned like they would on a serverscript.

So even if they’re not supposed to run automatically, why remove the function for that if it doesn’t make anything worse, but in fact makes it easier to use this kind of stuff as you can just Module:Get() player data and use ProfileService in several scripts rather than managing everything in the same script

Hello, I’ve got a problem and probably the first one to encounter this problem…
I’ve got about 4-5 values inside ProfileTemplate and only the last one seems to not create it? I don’t exactly know how this works but however whenever I try adding a number to it it just says attempted to add number to nil or something like that? I’ve followed everything I found however I didn’t find much.

Could you show us your template please?