Save your player data with ProfileService! (DataStore Module)

I don’t think you’ll be able to - I’ve seen these errors too. If you think that every Roblox log error is safe from humanly mistakes made by engine developers… Then yeah just return Enum.ProductPurchaseDecision.PurchaseGranted to ProcessReciept callback immediately and you won’t have any errors.

I personally interpreted the ProcessReciept system differently.

2 Likes

Im experiencing an issue where a replica containing the player profile does not get received by the client.
Both the client and server uses modules.

Server code:

local function playerAdded(player)
	local profile = profileStore:LoadProfileAsync("Player_"..player.UserId) -- get profile

	if profile ~= nil then
		profile:AddUserId(player.UserId) -- GDPR

		profile:Reconcile() -- add missing stuff

		profile:ListenToRelease(function() -- loaded on another server
			profiles[player].Replica:Destroy()
			profiles[player] = nil

			player:Kick("Your data might have been loaded on another server, please rejoin")
		end)

		if player:IsDescendantOf(players) == true then -- loaded and player didnt leave
			HandleData(player, profile) -- this just changes profile.Data before its sent to the client

			-- create profile object with replica
			local profileObject = {
				Profile = profile,
				Replica = replicaService.NewReplica({
					ClassToken = profileToken,
					Tags = {Player = player},
					Data = profile.Data,
					Replication = player,
				}),
				Player = player,
			}

			profiles[player] = profileObject
		else -- left before loaded
			profile:Release()
		end
	else -- could not load
		player:Kick("Could not load data, please rejoin")
	end
end

Client code:

local dataUtil = {}

local plr = game.Players.LocalPlayer

print(0)
local controller = require(game.ReplicatedStorage.UtilityModules.ReplicaService.ReplicaController)
print(.5)

controller.ReplicaOfClassCreated("PlayerProfile", function(replica)
	print(3)
	if replica.Tags.Player == plr then
		print("Data has been received!")
	end
end)

print(1)
controller.RequestData()
print(2)

return dataUtil

You can see I already added debug prints to the client side, and 0, 0.5, 1 and 2 get printed, but 3 does not. ([ReplicaController]: Initial data received does not get printed either)

What am I doing wrong here?

I tried making a repro for the issue but was unsuccessful.

EDIT: Looks like this was caused by mistakenly adding a ReplicaRemoteEvents folder into replicatedStorage beforehand, causing the module to make a duplicate. Whoops

Thank you for the solution.

Is it not possible to store instances inside tables, or what am I doing wrong?
I’m using a remoteFunction to get the data.ItemsSaved table into a local script, where I display the saved tools in a list and where I have the code to save the tools.

When saving a tool, the local script sends the Instance to the server script which is supposed to table.insert into the ItemsSaved the tool, but when doing that it seems it takes some time and then it errors, with the “Cannot store Dictionary in data store. Data stores can only accept valid UTF-8 characters.” error, and the game takes a long time to close too. I know the rest of the scripts are working because im also saving bank money and its working, even while studio testing the data stays (Studio api access is on) so what could be the issue here?

could it be that it only saves instances without their children?

You can’t save instances in datastores.

You should try Dark Reader browser extension :wink:

Converter Module: Instance ↔ Table | Save Instances to DataStores and More! try this

@SrJarr

This module saves data automatically so no need

Yeah I realized some time after posting the comment, it makes sense for datastores to not store instances tbh, thankfully I can just save the data of the tools in some other format and then recreate the tools when reading the data from the profile.

Is there a event that you could possibly add that once the ProfileStore loads you can connect a event and execute some code when the ProfileStore is loaded?

Unless this is already achievable with some function or something. Please be sure to tell me.

I modified the basic module they provide to handle the profiles and let me know when a new profile has been loaded or unloaded in my game.

ProfileHandler:

local ProfileHandler = {}

-- https://madstudioroblox.github.io/ProfileService/
-- 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:GetService("ServerScriptService").ProfileService.ProfileService)

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

local Players = game:GetService("Players")

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

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

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

local function DoSomethingWithALoadedProfile(player, profile)
  if profile.Data.Money == nil then profile.Data.Money = 0 end
  for _, func in pairs(whenProfileLoadedConnections) do
    func(player, profile)
  end
end

local function PlayerAdded(player)
  local profile = ProfileStore:LoadProfileAsync("Player_" .. player.UserId)
  if profile ~= nil then
    profile:AddUserId(player.UserId) -- GDPR compliance
    profile:Reconcile() -- Fill in missing variables from ProfileTemplate (optional)
    profile:ListenToRelease(function()
      Profiles[player] = nil
      -- The profile could've been loaded on another Roblox server:
      player:Kick("Failed to load your data from datastore. To avoid data corruption we kicked you. Try again later.")
    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("Failed to load your data from datastore. To avoid data corruption we kicked you. Try again later.") 
  end
end

----- Initialize -----

-- In case Players have joined the server earlier than this script ran:
for _, player in ipairs(Players:GetPlayers()) do
  task.spawn(PlayerAdded, player)
end

----- Connections -----

Players.PlayerAdded:Connect(PlayerAdded)

Players.PlayerRemoving:Connect(function(player)
  local profile = Profiles[player]
  if profile ~= nil then
    for _, func in pairs(whenProfileReleasingConnections) do
      func(player, profile)
    end
    profile:Release()
  end
end)

function ProfileHandler:ConnectWhenProfileLoaded(name: string, func)
  -- Use this in any other serverscript to listen when a profile has been loaded
  -- The function sends the Player and the Profile as a parameter
  -- ProfileHandler:ConnectWhenProfileLoaded("Something", function (player, profile) end)
  if whenProfileLoadedConnections[name] ~= nil then error("Connection already defined. Disconnect it first.") return end
  --print("New connection to when loaded", name)
  whenProfileLoadedConnections[name] = func
end

function ProfileHandler:DisconnectWhenProfileLoaded(name: string, func)
  -- Disconnects the funcion above
  --print("New disconnection call to when loaded", name)
  whenProfileLoadedConnections[name] = nil
end

function ProfileHandler:ConnectWhenProfileReleasing(name: string, func)
  -- Use this in any other serverscript to listen when the player left the game
  -- and is about to be released
  -- The function sends the Player and the Profile as a parameter
  -- ProfileHandler:ConnectWhenReleasing("Something", function (player, profile) end)
  if whenProfileReleasingConnections[name] ~= nil then error("Connection already defined. Disconnect it first.") return end
  --print("New connection to when releasing", name)
  whenProfileReleasingConnections[name] = func
end

function ProfileHandler:DisconnectWhenProfileReleasing(name: string, func)
  -- Disconnects the funcion above
  --print("New disconnection call to when profile releasing", name)
  whenProfileReleasingConnections[name] = nil
end

return ProfileHandler

Then from any other script I can do:

For example a cash Leaderstats

local ProfileHandler = require(game.ServerScriptService.ProfileHandler)
local UNIQUE_IDENTITY = "LeaderstatsCash"

-- will fire each time ProfileService loads a profile
ProfileHandler:ConnectWhenProfileLoaded(UNIQUE_IDENTITY, function (player, profile)
  -- loads from the profile to the leaderstats
  player.leaderstats.Cash.Value = profile.Data.Cash
end)

-- will fire just before ProfileService unloads a profile
ProfileHandler:DisconnectWhenProfileReleasing(UNIQUE_IDENTITY, function (player, profile)
  -- saves from the leaderstats to the profile
  profile.Data.Cash = player.leaderstats.Cash.Value
end)

2 Likes

@loleris
Is there a way to control new datastore saves?
In my old script

local Datastore = game:GetService("DataStoreService"):GetDataStore("0.00.15.6")

I was able to do something like this where by increasing the “version” ("0.00.15.6") I was able to reset all data. However in this system you did something…different. I’m not too advanced yet so I kinda need help with it

My game’s players report that there seems to have some data returned to the old version randomly. I’ve followed all your steps and checked if there is any problem with data loss.

(Average data loading time is 0.5 and all under 1 sec)

Codes followed

-- Initialize

local ProfileTemplate = {
	Coin = 0,
	Stage = 0,
	CurrentDied = "湲곕낯",
	Died = {
		"湲곕낯"
	}
}

local ProfileService = require(script.ProfileService)
local TableToString = require(script.TableToString)

local Players = game:GetService("Players")

local ProfileStore = ProfileService.GetProfileStore(
	"WayToSchoolInOhioDataStore",
	ProfileTemplate
)

_G.Profiles = {}

-- resolve method

local function OnCharacterAdded(char)
	game:GetService("RunService").Stepped:Wait()
	local plr = game.Players:GetPlayerFromCharacter(char)
	char:WaitForChild("HumanoidRootPart").CFrame = workspace.Checkpoints[tostring(plr.TeleportedStage.Value)].CFrame + Vector3.new(0,3.25,0)
end

function OnPlayerAdded(plr)
	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	stats.Parent = plr

	local stage = Instance.new("IntValue")
	stage.Name = "Stage"
	stage.Value = _G.Profiles[plr].Data.Stage
	stage.Parent = stats

	local coin = Instance.new("IntValue")
	coin.Name = "Coin"
	coin.Value = _G.Profiles[plr].Data.Coin
	coin.Parent = stats

	local TeleStage = Instance.new("IntValue")
	TeleStage.Name = "TeleportedStage"
	TeleStage.Value = stage.Value
	TeleStage.Parent = plr
	
	local DiedFolder = Instance.new("Folder", plr)
	DiedFolder.Name = "DiedFolder"

	for i,v in pairs(_G.Profiles[plr].Data.Died) do
		local diedvalue = Instance.new("BoolValue", DiedFolder)
		diedvalue.Name = v
	end

	local DiedEffect = Instance.new("StringValue", plr)
	DiedEffect.Name = "DiedEffect"
	
	DiedEffect.Value = _G.Profiles[plr].Data.CurrentDied

	stats.Stage:GetPropertyChangedSignal("Value"):Connect(function()
		_G.Profiles[plr].Data.Stage = stats.Stage.Value
	end)
	stats.Coin:GetPropertyChangedSignal("Value"):Connect(function()
		_G.Profiles[plr].Data.Coin = stats.Coin.Value
	end)

	if plr.Character then OnCharacterAdded(plr.Character) end
	plr.CharacterAdded:Connect(OnCharacterAdded)

	while plr.Parent and wait(5) do
		coin.Value += 1
	end
end

-- load

local function PlayerAdded(player)
	local elapsed = os.clock()
	local profile = ProfileStore:LoadProfileAsync("Player_" .. player.UserId)
	if profile ~= nil then
		print("[DataSaver] "..player.Name.."'s data successfully loaded, is taken " .. math.floor((os.clock() - elapsed) * 10) / 10 .." seconds, and is \n", TableToString(profile.Data))
		
		local update = true
		
		profile:AddUserId(player.UserId)
		profile:Reconcile()
		profile:ListenToRelease(function()
			print("[DataSaver] ".."Release "..player.Name.."'s data as \n", TableToString(profile.Data))
			
			_G.Profiles[player] = nil
			player:Kick("Released your data")
		end)
		
		if player:IsDescendantOf(Players) == true then
			_G.Profiles[player] = profile
			task.spawn(OnPlayerAdded, player)
		else
			profile:Release()
		end
	else
		player:Kick("[DataSaver] DataStore loading failure, It may because of roblox server down")
	end
end

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

Players.PlayerAdded:Connect(PlayerAdded)

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

@loleris where is the set async function on the profile?

ProfileService save almost every 30 seconds the data and when the profile is released there is a Save function but recommended only for GlobalUpdate

then what if you’re leaving the game?

release profile on PlayerRemoved event.You should take a look on Youtube tutorial

Absolutely lovely resource! Have been using it for years now. One question however, is it fine to have 2 separate profiles per player to separate game save data and monetary save data? I do frequent wipes on players’ save data as the game moves to different stages (pre-alpha, alpha, beta, etc.), however, I do not want to wipe monetary data to maintain purchases, for instance tracking dev product purchases. Best example I can give is my game’s bank system: users can purchase up to 20 additional bank pages to expand their bank space beyond what is naturally obtainable, however, to track this, I have to store it in the player’s data. Is my approach viable or should I look at implementing a refactoring system that will allow me to wipe certain parts of the player’s data and maintain the monetary data? I just wanna know if 2 profiles per player is fine or if it’s better to go with the refactoring system that I mentioned above.

even if only 1 profile is recommended 2 profile will just consume x2 request. I have also 2 profiles on my game and i never had problem even on test with 20 people

1 Like