Save your player data with ProfileService! (DataStore Module)

Hello, I am using profile service and using the sample code provided, but apparently when calling profile:Release() the method is missing.

So u have to make sure that you call :Rlease() is on player in a table data like this

Local PlayersProfile = {} – this hold player datas profile

So shen player leavd
PlayersProfile[plr].Profile:Release()

Hey, I’m loving this module so far, except that I’m having some issues with template implementation.

I want players to have randomised values in some of their data keys when they first join (random races, character size, etc etc) and this does not seem to work properly with this module as the template is defined once with no option to redefine it. This means that the math.random and Random:NextNumber functions run once at runtime, then keep those values for the rest of the server uptime. Therefore, all new players in the same server will have the same data to start instead of different random values.

Does anyone know of a workaround/fix to this?

After the profile loads check for a flag (e.g. if Profile.Data.IsFirstLoad ~= true then).
You can react to the absence of the flag and make your one time alterations and
finally set the flag (e.g. Profile.Data.IsFirstLoad = true)

1 Like

This is implemented now, but I’m running into another issue: I get a warning on game start and nothing works. I’ve never tried ProfileService before, so this could just be a silly mistake that I made.

Here’s the warning:

Warning
[ProfileService]: DataStore API error [Store:"PlayerData";Key:"Player_664884959"] - "104: Cannot store Dictionary in data store. Data stores can only accept valid UTF-8 characters."

And here’s my code for handling ProfileService (I am using Knit, and storing the template in a separate module. Most of the code is pulled directly from the ProfileService documentation.)

Main Module
-- Get Knit, services and modules
local knit = require(game.ReplicatedStorage.Packages.Knit)
local ProfileService = require(script.Parent.Parent.Modules.ProfileService)
local DataTemplate = require(script.Parent.Parent.Modules.DataTemplate)
local Players = game:GetService("Players")

-- Internal variables
local ProfileStore = ProfileService.GetProfileStore(
	"PlayerData",
	DataTemplate:GetTemplate()
)

local Profiles = {}

-- Make the service
local DataService = knit.CreateService {
	Name = "DataService",
	Client = {}
}

-- Random value function
local function RandomValueSet(profile : any)
	-- On first join, make a new template for random values to set
	if profile.Data.FirstLoad == true then
		profile.Data.FirstLoad = false
		local template = DataTemplate:GetTemplate()
		profile.Data.SkinTone = template.SkinTone
		profile.Data.Name = template.Name
		profile.Data.Face = template.Face
	end
end

-- Data loading function
local function PlayerAdded(player : 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()
		end)
		if player:IsDescendantOf(Players) == true then
			Profiles[player] = profile
			-- A profile has been successfully loaded! Set random values if first load.
			RandomValueSet(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("Unable to load data. Try rejoining :)") 
	end
end

-- Data retrieval function
function DataService:GetValue(player : Player, key : string)
	if Profiles[player] then
		if Profiles[player].Data[key] then
			return Profiles[player].Data[key]
		else
			warn("Player " .. player.Name .. " does not have data for value " .. key .. "!")
		end
	else
		warn("Player " .. player.Name .. " has no profile!")
	end
end

-- Data overwriting function
function DataService:SetValue(player : Player, key : string, value : any)
	if Profiles[player] then
		Profiles[player].Data[key] = value
	else
		warn("Player " .. player.Name .. " has no profile!")
	end
end

-- On init, connect functions and retrieve data for existing players
function DataService:KnitInit()
	for i, v in pairs(Players:GetPlayers()) do
		task.spawn(PlayerAdded, v)
	end
	
	Players.PlayerAdded:Connect(PlayerAdded)
	
	Players.PlayerRemoving:Connect(function(player)
		local profile = Profiles[player]
		if profile ~= nil then
			profile:Release()
		end
	end)
end

return DataService
Template Module
local knit = require(game.ReplicatedStorage.Packages.Knit)
local DataTemplate = {}

-- Internal variables
local storage = require(knit.ServerModules.Storage)
local nametable = storage.NameDataBase
local skintonetable = storage.SkinToneDataBase
local facetable = storage.FaceDataBase

-- Retrieval function
function DataTemplate:GetTemplate()
	return {
		FirstLoad = true,
		Tissues = 0,
		SkinTone = skintonetable[math.random(1, 16)],
		Name = nametable[math.random(1, #nametable)],
		Face = facetable[math.random(1, #facetable)],
		Curse = "None",
		Clothing = "Junguard",
		SpawnLocation = "Cavern",
		DataValues = {}
	}
end

return DataTemplate
1 Like

Roblox logs do not provide good enough reasons why something can’t be saved in the DataStore, but you can probably easily figure this out with the help of troubleshooting page of ProfileService wiki - Saving data which Roblox cannot serialize.

1 Like

ListenToRelease should always be used and ListenToHopReady just as an addition, since its more for teleports

With ProfileService, how can I save my data through Roblox Opencloud API datastores?

2 Likes

Hey @loleris! I just started using your module, it works great in fact. I was making a module to manage my data with, though I have ran into a major problem. I don’t understand how to use the global updates feature, the description is very vague on it.

Basically, I have a DataPlugin:setGlobalKey(userId: number, updateType: string, sendData: dictionary) function that uses ProfileStore:GlobalUpdateProfileAsync(), and I have a DataPlugin:getGlobal Key(player: Player, keyName: string) function. I need help with the meanings of the functions, and how to set it up mostly.

I agree that the global update system for ProfileService is very confusing - Initially I thought I would give developers full control over writing and editing global update buffers, but that spawned lots of methods to manage all of it where as removing the option to edit global updates would minimize everything to one method to handle global updates. I believe there could be an easy modification to ProfileService to emulate that write-only global update simplified interface on top of the current methods.

This video features all the methods and where you need to call them to make global updates work Global Updates: ProfileService Tutorial Part 2 (Roblox Studio) - YouTube

I’ve looked at the api more, I think I understand it now. But it would be nice if you could give profileservice’s global updates and update.

Alright, I think I have the global updates working. But for some reason, I keep getting error 104 “cannot store dictionary in datastore”, while im sending the example code: {yes = "yes"} I don’t get it.

Check this post Save your player data with ProfileService! (DataStore Module) - #986 by loleris

Curious, if I want to do a player mailing service, where they can send people mail, would this be bad? As everytime a player sends mail, I’d have to get players data (more than likely an offline player)? My question is basically, what are the limits on stuff like GlobalUpdateProfileAsync? I already have a 15 second time delay, so players or spammers cant just join a server and instantly send a million messages, as well as prevent them from always trying to make a million data get requests, but I’m not sure how well of a solution this is?
(ignore my TODO’s, more just wanting to know if what I have in regards to the load and update is good and safe)

function MailService:Send(player, mailInfo)
	if table.find(self.SentMail, player) then
		return false, "Sending mail too fast!"
	end

	table.insert(self.SentMail, player)
	task.delay(MAIL_DELAY, function()
		local PlayerIndex = table.find(self.SentMail, player)
		if PlayerIndex then
			table.remove(self.SentMail, PlayerIndex)
		end
	end)

	local Data = DataService:Get(player)
	if not Data then
		return false, "Failed to find data!"
	end

	local RecipientData = DataService.ProfileStore:LoadProfileAsync("Player_" .. mailInfo.Receipient)
	if not RecipientData then -- bad??
		return false, "Failed to find data!"
	end

	-- TODO see if ReceipientData has too many messages

	-- TODO get mailInfo.Receipient UserId and apply below as key
	local ProfileKey = "Player_" .. player.UserId
	DataService.ProfileStore:GlobalUpdateProfileAsync(ProfileKey, function(globalUpdates)
		globalUpdates:AddActiveUpdate({
			Type = "Mail",
			Sender = player.Name,
			TimeSent = os.time(),
			Message = mailInfo,
		})
	end)
end
1 Like

I’m trying to load in the player’s data once the player joins but the script that loads the data runs before the module saves the data, is there a way to fix this?

How can I view a players GlobalUpdates? I have a way to send messages to offline players and it caps at 3 messages per player, however, their data doesnt update till they rejoin the server, so until then, you can just keep sending them messages, as I’m unsure how to check the global updates for if theyve already got 3 messages from you waiting?

DataService.ProfileStore:GlobalUpdateProfileAsync(ProfileKey, function(globalUpdates)
		globalUpdates:AddActiveUpdate({
			Type = "Mail",
			Sender = SenderId,
			TimeSent = tostring(os.time()),
			Message = FilteredMessage,
			-- TODO Send extra data??
		})
	end)

You should use the GetActiveUpdate method
image

If I try putting a table inside of the profile template table the table inside of the profile template table just returns as nil, even if I put an example string in there. I haven’t found any tutorials that show how to work with tables inside of the profile template and I’m stumped.

Is SkinTone a Color3 value? This could be resulting in this issue and you might need to do something like this.

Dw, I solved this a while back by just storing the skintones in a database that is indexed using strings e.g. “Blue”, “Green”. I should’ve made a reply when I solved it.