Save your player data with ProfileService! (DataStore Module)

ProfileService might receive rollback management in the near future

5 Likes

Nevermind! I figured out the issue.

The fact that DataStore2 is bound to the Player instance was a deal-breaker for me. Will be sure to use this once I start implementing saves.

3 Likes

Really great resource, really great. The fact that DS2 is overrated makes this profile service is better

1 Like

Hey! If we are making an RPG for example will we have to script the update of data manually?

For Example: A player first plays a game and gets the default table. I now update the game and add a few more items. Is there a function to update the player’s data (so id have to add these items to the data) or will I have to script that myself?

This is a truly great module, thank you very much for this @loleris! I was using Datastore2 primarily for its backup capability and its caching, but ProfileService is a must-have. I watched the posted tutorials, read all the comments here and in the videos, looked over the API, and skimmed over the code… and I am switching over today. These were the reasons I didn’t 100% like Datastore2 which are nonexistent with ProfileService:

  • Stored data on the player instance (this causes some future-related problems)
  • Massive amounts of redundancy when saving player data which will never be used
  • This Roblox mistake left Datastore2 developers in a bad position.
    • If I recall correctly, if a player didn’t log back in within a certain time, then their savefile was deleted. Since saves are tied to player instances then accessing the data was…problematic
  • No protection against duplicated items
    • I plan to implement trading and knew duplication would be a problem. My initial plan didn’t involve session-locking and probably would’ve failed as a result.
  • No built-in autosave
    • You need an autosave in case a player has played in the same server for an hour, then that server crashes. Then what? That player will probably quit your game, even if they loved it, because of the wasted time. I created one myself and had to work out some quirks to get it working the way I wanted…this is another big point to switch over since it’s built-in.

I’m impressed by what ProfileService has to offer:

  • Item duplication protection
  • Data caching
  • Comprehensive API documentation with great examples
  • Built-in autosave that evenly distributes among players within the server + uses coroutines
  • Transparency with any caveats (very few)
  • Useful comments in the code that show expected table structures
  • Lots of analytics for those who like to read logs (I)
  • ProfileStore.Mock - Customize your datastore Testing
  • :Reconcile - Updating Datastores
  • DeepCopyTable - Handles nested keys
  • :GlobalUpdateProfileAsync - Allows the functionality of gifts or messages between players (even offline)
  • :WipeProfileAsync - GDPR compliance
  • :SetMetaTag - Updating profile information
  • AssumeDeadSessionLock - Automatically releases profiles if the session doesn’t update within 30 minutes
  • Enough information that answered every single question that I had
  • +Solves a ton of other issues

A truly high-quality datastore module, thanks again!

14 Likes

Is there a recommended way to tell the client when their data changes?

Currently, I do something like this with AeroGameFramework

-- SERVER
local profile = self:Get(player)

if (profile) then
    profile.Data.Cash += 50
end

-- Tell client their data changed
self:FireClient("DataUpdate", player, profile.Data)



-- CLIENT
local data = {}

DataService.DataUpdate:Connect(function(newData)
    data = newData or {}
end)
2 Likes

Can you not require the module on client? cuz it gave me an error saying BindToClose can only be called on the server. Stack Begin Script 'ReplicatedStorage.DataManager.ProfileService', Line 242

DataStoreService is a server-side only feature on Roblox, so ProfileService is server exclusive.

1 Like

This is probably one of the most unfair arguments I’ve seen going on in this thread. Bounding the data to the Player Instance doesn’t seem much a breaker, it’s what the entire module is dedicated for.

I’ve been using DataStore2 for a while and I’m debating whether it’s worth the transfer or not. People keep pointing out the 80 seconds hypothetical which I don’t think it terrible (at least because there’s nothing we can do about it).

I might test it out in a solo place for testing later on, but I remain unsure.

How would you make persistent servers or global leaderboards with DataStore2? I don’t want to use multiple systems to achieve variations of the same function.

2 Likes

There some problems I am facing where the ProfileService or my wrapper is yielding infinitely

wrapper-

local Rs = game:GetService("ReplicatedStorage");
local Players = game:GetService("Players");
local ProfileService = require(script:WaitForChild("ProfileService"));

local ProfileStore = ProfileService.GetProfileStore(
	"Player",
	{
		Cash = 0
	}
);

local Profiles = {};

local function MakeLeaderstats(player)
	local profile = Profiles[player];
	
	local leaderstats = Instance.new("Folder", player);
	leaderstats.Name = "leaderstats";
	
	local Cash = Instance.new("IntValue", leaderstats);
	Cash.Name = "Cash"
	Cash.Value = profile.Data.Cash
	
	local thread = coroutine.wrap(function()
		while true do
			Cash.Value = profile.Data.Cash
			game:GetService("RunService").Heartbeat:Wait();
		end
	end)
	thread();
end

function OnPlayerAdded(player)
	local profile = ProfileStore:LoadProfileAsync(
		"Player_" .. player.UserId,
		"ForceLoad"
	);
	
	profile:Reconcile();
	if profile ~= nil then
		profile:ListenToRelease(function()
			Profiles[player] = nil;
			player:Kick();
		end)
		
		if player:IsDescendantOf(Players) then
			Profiles[player] = profile;
			MakeLeaderstats(player);
		else
			profile:Release();
		end
	else
		player:Kick();
	end;
end;

for _,player in ipairs(Players:GetPlayers()) do
	coroutine.wrap(OnPlayerAdded)(player)
end
Players.PlayerAdded:Connect(OnPlayerAdded);

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

local DataManager = {};

function DataManager:Get(player)
	local profile = Profiles[player];
	
	if profile then
		return profile.Data;
	end;
end;

return DataManager;

script which isn’t working

local ProfileService = require(game.ReplicatedStorage:WaitForChild("DataManager"));
game.Players.PlayerAdded:Connect(function(player)
	print(player.Name) -- Doesn't fire
	print(ProfileService:Get(player).Cash) --Doesn't fire
end)

I made a post about this issue here

so a person suggested this solution

local ProfileService 

coroutine.wrap(function()
ProfileService = require(game.ReplicatedStorage:WaitForChild("DataManager"));
end)()

game.Players.PlayerAdded:Connect(function(player)
if not ProfileService then 
   repeat wait()  
       print("something") -- this prints infinitely  
   until ProfileService 
end
   print(player.Name) -- Doesn't fire
   print(ProfileService:Get(player).Cash) --Doesn't fire
end)

Refer to the official example (top post) - towards the bottom there’s a for loop that iterates over existing players before .PlayerAdded connection is made.

On a different note, your code that “waits for ProfileService” is loading your wrapper module (line 4, WaitForChild name parameter) - these are errors on your own end.

Isn’t this what you refering to?

Yes I know I want to use wrapper reffered as ProfileService how @okeanskiy’s tutorial showed.

Make sure your DataManager is in ReplicatedStorage.

image

Just a few questions as I’m rather new with using this module. With clans/factions having no session locking (not releasing the profile, not listening to release, and having different servers load the profile) could it be setup like this?

local module = {}
local ProfileService = require(script.ProfileService)
local ClanProfileStore = ProfileService.GetProfileStore("ClanSaveData",{})
local ClanProfile = ClanProfileStore:LoadProfileAsync("Clan","ForceLoad")	
function module.ReturnClanProfile()
	return ClanProfile
end
return module
--planning to add more functions, which is why I'm not just returning clanprofile

I’m planning to add real time cross server updates to these clans. Would using messaging service and informing all servers of new changes and then having those servers save that data be the most effective/efficient way to provide real time updates?

For example, in server A someone does something that makes their clan score go up by 3, another member of that same clan is in another server, server B, now server A would send a message to all running servers to update their clan score. This way any changes server B makes reflects the true amount of clan score they have (and of course server B would be doing the same for any changes it makes).

Could I use this with storing data that isn’t a players data?

ProfileService is designed for profiles that are acessed by one server at a time - this will not work.

Yeah, you can use ProfileService for anything other than players as long as the individual profiles are used on one server at a time - otherwise you’ll need to make custom code with update listeners and messagingservice.

2 Likes

How would I get the profile? Could i just use a string, like an ID? because what I understand is that it uses the player object? “Profiles[player]”

Edit:Nevermind, I didn’t realize how it was set up fully.