Data storing/presenting using ProfileService and ReplicaService!

Hey y’all, today I am going to teach you how to do some data stores in Roblox!

BOOOOOOOOOOOOOOOOOO, another Data Store tutorial, smh.
Why can’t this guy teach us something better?
I am tired of learning all of these stupid data storing tutorials, please make something more unique.

Okay, we are not going to use Roblox’s integrated DataStoreService for this tutorial. Instead, we are going to use 2 useful modules which are made by the legendary man himself, @loleris!
ProfileService
ProfileService serves as a module for loading and saving data, without worrying anything that could cause item loss or item duplication, because of its feature known as Session Locking. This is actually slightly better than DataStore 2.0 which was made by another developer.

ReplicaService
ReplicaService basically allows you to replicate almost anything or states which are server-side. The reason why we need this module is because ProfileService is designed for server side, meaning that our data is server-side, which means clients are unable to receive their own data to present. ReplicaService was designed to overcome this, as stated in the ProfileService module introduction post!

Why are you making a tutorial of this?


Well, although there are videos about teaching how to use ProfileService, there isn’t any tutorials that teach you how to use ReplicaService, excluding loleris himself giving us a basic usage of it in the API page. So, after I done some testing with it, I am confident myself have the ability to teach you guys how to use it. To any experience users of both modules, do correct me if I made any mistakes!

Before continuing, you must have a basic understanding of DataStoreService, even though ProfileService backends would handle this. This is because we are going to talk about some of DataStoreService’s function which leads to why sometimes item loss or item duplication can happen.

You also need to get the modules as well.

Let's start learning!

Setting up player added and removing events

First things first, we must parent the modules in their own respective parents.

For ReplicaService, follow the following hierachy stated by loleris.
https://madstudioroblox.github.io/ReplicaService/images/HowItsMade.jpg

You can just remove the examples folder if you don’t wanna test it out first.
Next, parent your ProfileService module script somewhere you like, I like to put it in ServerStorage.
image

Don’t worry about the ReplicaService folder.
Create a module script named DataManager, place it somewhere else you like. Again, I like ServerStorage.

Okay, now we can finally start scripting this big thing. To make things easier for you to learn, we will start by learning the basic usages of ProfileService first.

  1. First, define the following services and modules which we need to setup in the DataManager module script that you just created.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)
  1. Next, we need to get a data store, or known as ProfileStore, from ProfileService. To do that, we will use a function called .GetProfileStore().
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore")

CashProfileStore is the name of our ProfileStore, there is a second argument we need to provide inside this function, which is known as the template for that ProfileStore. So, if we access to this ProfileStore, we would get a data template containing all of the things that are available inside the template. For our template, we will just create a dictionary and add a key value pair where the key is named as Cash and the value, acting as the starting cash, is 100.

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
    Cash = 100
})
  1. Next, we need something to refer the player’s profile so we could modify their data. A profile in ProfileService just basically means a unique thing to refer ones’ data. You get the idea, it’s pretty straightforward. So, we have to create an empty table containing all of the player profiles, and we will index it using the player instance itself.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
    Cash = 100
})

local Profiles = {}
  1. Now, we will need to return something from this module script right? So just make it that this module will return our functions.
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
    Cash = 100
})

local Profiles = {}

local DataManager = {}

return DataManager
  1. Next, we wanna make it so that whenever a player joins, we load out their data, and when a player leaves we save it. So, create 2 functions and connect them to their own respective events as stated below:
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
    Cash = 100
})

local Profiles = {}

local DataManager = {}

local function OnPlayerAdded(player)

end

local function OnPlayerRemoved(player)

end

Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)

return DataManager
  1. Let’s start scripting the PlayerAdded event’s connected function first. In here, we first need to get their profile from the ProfileStore, using the :LoadProfileAsync() function.
local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")
end

Just like the DataStoreService, you need an unqiue identifier key in order to retrieve one’s data. So in this case, the key will be "CashProfileStpre"..player.UserId. Then, we put in a second argument which tells ProfileService what to do if the profile is not released. You can pass in a function that will run what happens if the profile is unable to be retrieved. The reason why sometimes the profile is unable to be retrieved is because that profile is being session locked.

Allow me to explain what is session lock. Let’s say, you have Player A and Player B. Player A trades with Player B, Player A puts a super cool item, and if Player A somehow can keep that cool item even after it is being traded, then they would have infinite copies of that cool item, and this is a problem known as item duplication.

Consider that Player B is Player A’s alt account. The way how this works is that right after the trade is completed, Player A will immediately leave and rejoin the game instantly. When Player A left the game, the DataStoreService:SetAsync() function will take some time in order to save the data, but once they join the game, the DataStoreService:GetAsync() function will run right before the DataStoreService:SetAsync() finished saving the player’s data. And if everything goes well and everything completes in under a second, item duplication will occur. This is called a race condition.

So, how do we prevent this happening? DataStore 2.0 is unable to solve this because they cannot handle item loss/duplication, neither can LuckyDataStore although they do have the session locking feature, I believe some people said that the code inside it isn’t that great and optimized which I could be wrong, but this is where ProfileService is useful! It has the session locking feature, which basically if a data is being modified, for exampled being saved, loaded or etc, ProfileService will make sure to ‘lock’ that profile to prevent any other functions to overwrite it. For example, if a profile is being loaded or already loaded in Server A, and Server B is trying to load the data, Server B will unable to do it because Server A locked the profile in order to prevent disruptions while Server A is modifying the data. With this, item loss/duplication is solved! But that’s not it.

We passed "ForceLoad" in the second argument of the function, which basically means that if Server B uses this option, it will indefinitely load the data, meaning it will try to load that profile regardless of whether or not the profile is being session locked. This sometimes can help if the profile is being dead session locked, which basically means the profile is locked forever, this is called stealing.

There are more options if you would like to try, but we are going for "ForceLoad".
image

  1. Now, sometimes the function will return nil, mostly because of a Roblox server trying to load the same profile at the same time when another server is doing the same thing. This case should be extremely rare to occur, as stated in the API page. If it returns nil, we need to :Kick() the player and let them rejoin in order to load their own profile again.
local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

    if PlayerProfile ~= nil then

    else
        player:Kick("Unable to load your data. Please rejoin.")
    end
end
  1. Next, if we successfully get their profile, we are going to do a sanity check, we need to check if the player is still in the game after the profile is being loaded, we can’t just leave it hanging around there. So, we will save their data by using a function called :Release(). :Release() in simple terms, just saves the data of the profile that called this function.
local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

    if PlayerProfile ~= nil then
        if player:IsDescendantOf(Players) then
            
        else
            PlayerProfile:Release()
        end
    else
        player:Kick("Unable to load your data. Please rejoin.")
    end
end

Now, what if I tell you, you can make a function to run whenever we call Profile:Release()? This is because we need a function to remove the key value pair inside the Profiles dictionary that we created at the beginning of our script. As I’ve said, once we successfully load the player’s profile, we can put it in a dictionary so we could refer it more easily, that’s why we need a function to run so. To do this, we can connect this function inside Profile:ListenToRelease().

local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

    if PlayerProfile ~= nil then
        PlayerProfile:ListenToRelease(function()
            Profiles[player] = nil
        end)
        if player:IsDescendantOf(Players) then
            
        else
            PlayerProfile:Release()
        end
    else
        player:Kick("Unable to load your data. Please rejoin.")
    end
end

Profile:ListenToRelease() will run whenever Profile:Release() is called for a profile, allowing us to run a function that is attached inside it and in this case, it will remove our Profile instance from the table.

Now if everything goes smooth (AKA if the player is still in the game after joining), then we need to put their Profile instance to the Profiles table, adding an unique key along with the player’s Profile to.

local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

    if PlayerProfile ~= nil then
        PlayerProfile:ListenToRelease(function()
            Profiles[player] = nil
        end)
        if player:IsDescendantOf(Players) then
            Profiles[player] = PlayerProfile
        else
            PlayerProfile:Release()
        end
    else
        player:Kick("Unable to load your data. Please rejoin.")
    end
end

And that should be it for our OnPlayerAdded function!

  1. Alright, now we can start scripting our OnPlayerRemoved function, which is pretty short considering we are only going to save our player’s data.
local function OnPlayerRemoved(player)
    local PlayerProfile = Profiles[player]

    if PlayerProfile then
        PlayerProfile:Release()
    end
end

In here, we basically just check if the player’s profile is in the table, if it is save their data. Sometimes it could be nil if our ProfileStore failed to load their data.

  1. Now, sometimes Players.PlayerRemoving() event may not get fired due to the server shutting down itself too quick before the event can get fired, in this case you should implement the same function in game:BindToClose().
game:BindToClose(function()
	table.foreach(Players:GetPlayers(),function(_,player) -- table.foreach() basically iterates the dictionary.
		OnPlayerRemoved(player)
	end)
end)

CHECKPOINT
Congratulations on reaching here! You should have this code now:

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
    Cash = 100
})

local Profiles = {}

local DataManager = {}

local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

    if PlayerProfile ~= nil then
        PlayerProfile:ListenToRelease(function()
            Profiles[player] = nil
        end)
        if player:IsDescendantOf(Players) then
            Profiles[player] = PlayerProfile
        else
            PlayerProfile:Release()
        end
    else
        player:Kick("Unable to load your data. Please rejoin.")
    end
end

local function OnPlayerRemoved(player)
    local PlayerProfile = Profiles[player]

    if PlayerProfile then
        PlayerProfile:Release()
    end
end

Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
game:BindToClose(function()
	table.foreach(Players:GetPlayers(),function(_,player)
		OnPlayerRemoved(player)
	end)
end)

return DataManager

You can rest now if you want, give yourself a big pat on your shoulder.

Adding a function to modify our data

Alright, the next thing we are going to add in our DataManager module is a function which can modify our data, in this tutorial our 'Cash" data.

  1. Sometimes it maybe useful for us to get a player’s Profile for something else. So, we are going to create a function named GetData() which will return a player’s profile. To do so, we simply index their Profile by using their Player instance, after that if it exists then we will return the Profile, otherwise we will use give out a warning.
function DataManager:GetData(player)
	local PlayerProfile = Profiles[player]
	
	if PlayerProfile then
		return PlayerProfile
	else
		warn("[DataManager]: CANNOT GET DATA FOM"..player.Name..".")
	end
end
  1. Next, we are going to make a function named as SetData(), which basically helps us to set a specified data with the data we want to set to. First off, we are going to create 3 parameters, namely the player’s profile we are going to modify, the name of the data we are going to modify and lastly the new data value for that data value
function DataManager:SetData(player,dataToSet : string,newDataValue)
	local PlayerProfile = self:GetData(player)

	if PlayerProfile then
        
	end
end

Now, this step is optional for you, but I like to make sure the type of the data we are setting is the same as the type of the data value that should be stored inside the data. For example, since Cash can only be measured or described in numbers/integers, if we are setting a new data value for it, we wanna make sure the new data value is a number/integer. So we can do an if statement along with the typeof() function to do so.

function DataManager:SetData(player,dataToSet : string,newDataValue)
	local PlayerProfile = self:GetData(player)
	if PlayerProfile then
		local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
		
		if OldDataValueType == typeof(newDataValue) then
			
		end
	end
end

After this, if everything goes right, we can safely set the data to something else.

function DataManager:SetData(player,dataToSet : string,newDataValue)
	local PlayerProfile = self:GetData(player)
	if PlayerProfile then
		local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
		
		if OldDataValueType == typeof(newDataValue) then
			PlayerProfile.Data[dataToSet] = newDataValue
		end
	end
end

Perfect! This should conclude our DataManager module! Now, we are going to use it from a server script and start modifying a player’s data.
For the sake of this tutorial, we are going to make a part so that whenever you click on it, it will increase your cash value by 1. So first off, spawn a part, get a click detector inside it and get a script inside the click detector and let’s begin declaring a few variables first.

  1. First off, require the module and connect the ClickDetector.MouseClick event.
    image
local DataManager = require(game.ServerStorage.DataManager)

script.Parent.MouseClick:Connect(function(plr)
	
end)
  1. Now, when someone clicked the ClickDetector instance, we first get their profile to retrieve their current cash value, and then we are calling DataManager:SetValue to set the player’s cash value, in this case increment the current cash value by 1.
local DataManager = require(game.ServerStorage.DataManager)

script.Parent.MouseClick:Connect(function(plr)
	local Profile = DataManager:GetData(plr)

    print(Profile.Data["Cash"])
	DataManager:SetData(plr,"Cash",Profile.Data["Cash"] + 1) -- if Profile.Data["Cash"] = 10, then 10 + 1 = 11.
    print(Profile.Data["Cash"])
end)

I also added two print statements before and after the change of the data to compare.
And this should be it! You may publish this game and test it in a real game and see the magic works! Here’s an example of it working:


Now when I rejoin, look at that!

CHECKPOINT
You are amazing! You finally have a working data storing code using ProfileService! But that’s not it, in the next part, we are going to use ReplicaService to replicate and present the data to their respective cleints.
Here is the full code:

DataManager module script:

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ProfileService = require(ServerStorage.ProfileService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
	Cash = 100
})

local Profiles = {}

local DataManager = {}

local function OnPlayerAdded(player)
	local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

	if PlayerProfile ~= nil then
		PlayerProfile:ListenToRelease(function()
			Profiles[player] = nil
		end)
		if player:IsDescendantOf(Players) then
			Profiles[player] = PlayerProfile
		else
			PlayerProfile:Release()
		end
	else
		player:Kick("Unable to load your data. Please rejoin.")
	end
end

local function OnPlayerRemoved(player)
	local PlayerProfile = Profiles[player]

	if PlayerProfile then
		PlayerProfile:Release()
	end
end

function DataManager:GetData(player)
	local PlayerProfile = Profiles[player]

	if PlayerProfile then
		return PlayerProfile
	else
		warn("[DataManager]: CANNOT GET DATA FOM"..player.Name..".")
	end
end

function DataManager:SetData(player,dataToSet : string,newDataValue)
	local PlayerProfile = self:GetData(player)
	if PlayerProfile then
		local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])

		if OldDataValueType == typeof(newDataValue) then
			PlayerProfile.Data[dataToSet] = newDataValue
		end
	end
end

Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
game:BindToClose(function()
	table.foreach(Players:GetPlayers(),function(_,player)
		OnPlayerRemoved(player)
	end)
end)

return DataManager

On click detector clicked event:

local DataManager = require(game.ServerStorage.DataManager)

script.Parent.MouseClick:Connect(function(plr)
	local Profile = DataManager:GetData(plr)
	
	print(Profile.Data["Cash"])
	DataManager:SetData(plr,"Cash",Profile.Data["Cash"] + 1) -- if Profile.Data["Cash"] = 10, then 10 + 1 = 11.
	print(Profile.Data["Cash"])
end)
Pairing up with ReplicaService

Just to get everyone on the right track, ReplicaService is basically a module that allows you replicate anything especially only server-sided data to the client. The reason why we need it is because if you try to send the player’s Profile.Data, throuhg a remote event or any cross-server communication instances from Roblox, the client would see it as nil.
image

BUT, if we use ReplicaService, it will make a copy of the data and make it so that it is available to our client!
image

Before, we get started, I would like to teach you guys how to use ReplicaService first.
In ReplicaService, you have to create something called a token, in which this handles as a replication unit to clients and allowing them too retrieve data inside each token. This makes it easier for you to create certain replication for two different things, such as one token to replicate profile data, and the other one maybe replicate other things. We first have to create a token through the function ReplicaService.NewReplica(), in which we have to provide a dictionary as its parameters. The pattern of the dictionary must be like this:

{
    ClassToken = ReplicaService.NewClassToken("TokenExample"), -- TokenExample will be the name of this token.
    Data = {0}, -- the data that will be replicated to the clients listening for this token
    Replication = "All" -- this token will be replicated to everyone in the game and will also replicate to players that joined the game after the creation of this token. We will be able to choose which player can only listen for this token.
} 

You can read more about each of these properties here from the API page. You don’t have to worry about the Tags, Parent, and WriteLib variable.

  1. We first wanna go to our DataManager module script’s OnPlayerAdded() function, which we wanna make it so that whenever a player joins the game, we create a token with a unique name for them to get Profile.Data to their screen and parent their replica token to a table containing all of the player’s replica token in a game so we can later refer it. Along with that, we wanna make sure to destroy their replica token when they leave the game, because we can’t just let their replica token hanging around there in the game until the server shuts down. So first off, add a new variable at the starting of the module script named DataReplicas. Then, we will need to call Replica:Destroy() upon a player leaving the experience.
    Don’t forget to require the ReplicaService module itself too!
local ProfileService = require(ServerStorage.ProfileService)
local ReplicaService = require(ServerScriptService.ReplicaService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
	Cash = 100
})

local Profiles = {}
local DataReplicas = {}

local DataManager = {}

local function OnPlayerAdded(player)
    local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

    if PlayerProfile ~= nil then
        PlayerProfile:ListenToRelease(function()
            Profiles[player] = nil
            DataReplicas[player] = nil
            DataReplicas[player]:Destroy
        end)
        if player:IsDescendantOf(Players) then
            Profiles[player] = PlayerProfile
            local DataReplica = ReplicaService.NewReplica({
                ClassToken = ReplicaService.NewClassToken("DataToken_"..player.UserId), -- since a player's user id is unique, we will name it according to their user id to create a unique name for the class token.
                Data = PlayerProfile.Data,
                Replication = player -- this player that joined the game can only access to this replica!
            })
            DataReplicas[player] = DataReplica
        else
            PlayerProfile:Release()
        end
    else
        player:Kick("Unable to load your data. Please rejoin.")
    end
end

NOTE: BEFORE YOU CONTINUE, GO TO REPLICASERVICE MODULE SCRIPT AND FIND LINE 373 AND REPLACE THE WHOLE FUNCTION WITH THIS LINE:
(loleris did told me this isn’t a bug, and I should just use other properties of the replica to correctly create the replica, but I’m too lazy and don’t wanna learn how so)

local function DestroyReplicaAndDescendantsRecursive(replica, not_first_in_stack)
	-- Scan children replicas:
	for _, child in ipairs(replica.Children) do
		DestroyReplicaAndDescendantsRecursive(child, true)
	end

	local id = replica.Id
	-- Clear replica entry:
	Replicas[id] = nil
	-- Cleanup:
	replica._maid:Cleanup()
	-- Remove _creation_data entry:
	replica._creation_data[tostring(id)] = nil
	-- Clear from children table of top parent replica:
	if not_first_in_stack ~= true then -- ehhhh... Yeah.
		if replica.Parent ~= nil then
			local children = replica.Parent.Children
			table.remove(children, table.find(children, replica))
		else
			TopLevelReplicas[id] = nil
		end
	end
	CreatedClassTokens[replica.Class] = nil	
	-- Swap metatables:
	setmetatable(replica, LockReplicaMethods)
end
  1. Next, we are going to also modify our SetData function so that whenever a player’s data value is changed, their replica will also set the data to the data value that is going to be set to the player’s Profile.Data. The Replica itself has a function called Replica:SetValue(). It takes 2 argument, the first argument is the name of the data we are going to set specified in a table form, and then the next argument is going to be the new value to be updated.
function DataManager:SetData(player,dataToSet : string,newDataValue)
	local PlayerProfile = self:GetData(player)
    local DataReplica = DataReplicas[player]
	if PlayerProfile and DataReplica then
		local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])
		
		if OldDataValueType == typeof(newDataValue) then
			PlayerProfile.Data[dataToSet] = newDataValue
            DataReplica:SetValue({"Cash"}, newDataValue)
		end
	end
end

Perfect! Now, we are going to do some scripting on the client side, where we will make our client to listen for any changes from this replica token. So go ahead, insert a LocalScript in StarterPlayerScripts.

  1. Firstly, define Players service for referring the local player’s UserId to get the name of their respective replica token, then define ReplicatedStorage in order to get ReplicaController module script.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer

local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))
  1. Next, we will refer a function of ReplicaController, which is the ReplicaController.ReplicaOfClassCreated(), where the first argument is the name of the replica token we want our client to listen for and attach a function at the second argument to be ran after the creation of the requested replica token is completed. In this case, we must make it so that they only listen for their own replica token, how? Remember where I said we will create a unique name for each player’s replica token using their UserId? This is where it is handy!
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer

local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))

ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica)  -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.

end)

After that, we will need to make our replica to listen for its changes of Data, in this case we put Profile.Data as the data we want to replicate in this token, and Profile.Data = {Cash = 100}, so it would make sense for us to listen for the changes of this Cash’s value. To listen the change, we can do Replica:ListenToChange() where the first argument is the name of the key/data we want to listen for and a function which it will also pass in an argument which contains the newly/updated value of the key/data we are listening for.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer

local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))

ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica)  -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
    replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
		print("Data changed!")
		-- your logic to show the updated value
	end)
end)

Now, this is the part where you get creative and start scripting your logic on how you will present the updated data to the player’s screen. You can present it on a ScreenGui using a TextLabel, which is what I am gonna do. So go ahead and create a simple ScreenGui to show out the updated data along with its value.
image


image
And here is the logic of presenting the cash using the TextLabel.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local CashTextLabel = PlayerGui:WaitForChild("ScreenGui").TextLabel

local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))

ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica)  -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
    replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
		print("Data changed!")
		-- your logic to show the updated value
        CashTextLabel.Text = "Cash: "..newVal
	end)
     CashTextLabel.Text = "Cash: "..replica.Data["Cash"] -- it is a good idea to also set the value outside the scope of Replica:ListenToChange() so that when the replica token's creation is finished, we will instantly present the data from Replica.Data
end)
  1. ONE LAST STEP! Make sure you call ReplicaController.RequestData() at the last line of this local script, always. This is stated in the API page:
    image
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local CashTextLabel = PlayerGui:WaitForChild("ScreenGui").TextLabel

local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))

ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica)  -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
    replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
		print("Data changed!")
		-- your logic to show the updated value
        CashTextLabel.Text = "Cash: "..newVal
	end)
     CashTextLabel.Text = "Cash: "..replica.Data["Cash"] -- it is a good idea to also set the value outside the scope of Replica:ListenToChange() so that when the replica token's creation is finished, we will instantly present the data from Replica.Data
end)

ReplicaController.RequestData()

We are finally done with everything! Give it a test in the real game and see the magic happens!

When I rejoin, the cash is saved and loaded to us!
image
Congratulations! You successfully learnt how to use ProfileService and replicate server-sided things to the client using ReplicaService! You are amazing, keep up the good work and enjoy this beautiful system working for you. Here are the final scripts:
DataManager module script:

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileService = require(ServerStorage.ProfileService)
local ReplicaService = require(ServerScriptService.ReplicaService)

local CashProfileStore = ProfileService.GetProfileStore("CashProfileStore",{
	Cash = 100
})

local Profiles = {}
local DataReplicas = {}

local DataManager = {}

local function OnPlayerAdded(player)
	local PlayerProfile = CashProfileStore:LoadProfileAsync("CashProfileStore"..player.UserId,"ForceLoad")

	if PlayerProfile ~= nil then
		PlayerProfile:ListenToRelease(function()
			Profiles[player] = nil
			DataReplicas[player] = nil
                        DataReplicas[player]:Destroy
		end)
		if player:IsDescendantOf(Players) then
			Profiles[player] = PlayerProfile
			local DataReplica = ReplicaService.NewReplica({
				ClassToken = ReplicaService.NewClassToken("DataToken_"..player.UserId), -- since a player's user id is unique, we will name it according to their user id to create a unique name for the class token.
				Data = PlayerProfile.Data,
				Replication = player -- this player that joined the game can only access to this replica!
			})
			DataReplicas[player] = DataReplica
		else
			PlayerProfile:Release()
		end
	else
		player:Kick("Unable to load your data. Please rejoin.")
	end
end

local function OnPlayerRemoved(player)
	local PlayerProfile = Profiles[player]

	if PlayerProfile then
		PlayerProfile:Release()
	end
end

function DataManager:GetData(player)
	local PlayerProfile = Profiles[player]

	if PlayerProfile then
		return PlayerProfile
	else
		warn("[DataManager]: CANNOT GET DATA FOM"..player.Name..".")
	end
end

function DataManager:SetData(player,dataToSet : string,newDataValue)
	local PlayerProfile = self:GetData(player)
	local DataReplica = DataReplicas[player]
	if PlayerProfile and DataReplica then
		local OldDataValueType = typeof(PlayerProfile.Data[dataToSet])

		if OldDataValueType == typeof(newDataValue) then
			PlayerProfile.Data[dataToSet] = newDataValue
			DataReplica:SetValue({"Cash"}, newDataValue)
		end
	end
end

Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoved)
game:BindToClose(function()
	table.foreach(Players:GetPlayers(),function(_,player)
		OnPlayerRemoved(player)
	end)
end)

return DataManager

The script to set our data:

local DataManager = require(game.ServerStorage.DataManager)

script.Parent.MouseClick:Connect(function(plr)
	local Profile = DataManager:GetData(plr)
	
	print(Profile.Data["Cash"])
	DataManager:SetData(plr,"Cash",Profile.Data["Cash"] + 1) -- if Profile.Data["Cash"] = 10, then 10 + 1 = 11.
	print(Profile.Data["Cash"])
end)

Local script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local CashTextLabel = PlayerGui:WaitForChild("ScreenGui",10).TextLabel

local ReplicaController = require(ReplicatedStorage:WaitForChild("ReplicaController"))

ReplicaController.ReplicaOfClassCreated("DataToken_"..Player.UserId,function(replica)  -- the ReplicaOfClassCreated function will pass in the requested replica token in the attached function's argument.
	replica:ListenToChange({"Cash"},function(newVal) -- listening for our cash value to change.
		print("Data changed!")
		-- your logic to show the updated value
		CashTextLabel.Text = "Cash: "..newVal
	end)
	CashTextLabel.Text = "Cash: "..replica.Data["Cash"] -- it is a good idea to also set the value outside the scope of Replica:ListenToChange() so that when the replica token's creation is finished, we will instantly present the data from Replica.Data
end)

ReplicaController.RequestData()
Frequently Asked Questions

Q: Why do you solely use ProfileService for the data storing and not something else like DataStore2 or LuckyDataStore?
A: As I’ve mentioned, DataStore2 does not have the session locking feature(if I am correct), while LuckyDataStore does has this feature, the code and safety behind it isn’t that promising.
LuckyDataStore - An Easy to Use Saving Module with Session Locking and Auto-Saving - #14 by angrybino
ProfileService will handle everything at its backend so you don’t have to worry about any errors, plus it is easy as DataStore2.

Q: I wanna use this tutorial to replace my current data storing script. How can I do so? Do I need to move all my current data store to ProfileService’s ProfileStore?
A: I did had this issue when I was transferring my game’s data store script to ProfileStore, but one thing I noticed is that whatever function you called to get a data store from any custom data storing module script, the backends will always use DataStoreService:GetDataStore() to get the data store, so you don’t have to worry about moving everything to a new data store, just call the function with the same exact name of your current data store name and it should load out any existing saved data out!

will add more Q&As soon

Thank you guys so much for lending me your eyes to read this tutorial, I hope you are benefitted from it. Please leave a feedback if you wanna ask anything or having any doubts.

Edits

EDIT 1: 2/10/2021
I forgot to mention that when removing the replica, you have to call :Destroy() on the player’s replica when they leave, thanks for @RobloxWobot first bringing up this issue!

Once again, thank you, @loleris for making these 2 legendary modules!

Tutorial writted by,
notzeussz.

80 Likes

there is a grammar error at the beginning, wanted to let you know

1 Like

“I don’t meant”
is the error

it doesn’t sound right

2 Likes

what would happen if an exploiter changes the UserId in (“DataToken_” … Player.UserId)?

They can’t. That gets executed on the server side, exploiter has no control there whatsoever.
Even if they could change it though, that’d only make things worse for them, as they wouldn’t be able access the token, making their client side messed up.

1 Like

@TopBagon Your reply is correct, but if they change the UserId to an existing player’s userid in the server, it won’t replicate the data to them since each and every replica token is only replicable to respective players.

1 Like

Even the one on the local script?

Yes, exploiters cannot change or view any server scripts, only local scripts. But even though they change the user id to someone’s userid in the server to listen for their replica, the replica won’t replicate anything as the replica will only replicate to certain clients.

1 Like

Tutorial1.rbxl (99.1 KB)

Hi,

I have followed your tutorial but I am getting the error in the screenshot and can’t see what I am doing wrong.

Thanks
J

First mistake is about the script is trying to require the ReplicaService module but it couldn’t. This is because you tried to find it in ServerStorage, where it should be in ServerScriptService.

Hi,

Thanks for the help, ReplicaService is in the ServerScriptService and still I get the error.

Do you have a working project/example I can have?

I’m sorry but if you’re just gonna copy the code, I’m afraid you will not gain any knowledges from this.

I see that your ServerScriptService variable is not presented, which is why it gives that error. I encourage you to create a variable which gets this service and use it to refer ReplicaService.

Is it possible with this module etc to save up to 20/30+ values? And if yes, do i need to make some more other changes to the code?

This depends on DataStoreService itself, as every custom data storing modules, every module has the same limit of data you can set and call. Please read this APJ page for more info:

You don’t have to change much of your code, just change your template to your desired template.

Thank you for this guide.
Regarding ReplicaService you write: ‘Along with that, we wanna make sure to destroy their replica token when they leave the game…
Perhaps I’m missing something but I can’t find where the replica token get destroyed when they leave?

This function will be called when a player leaves the game, inside it we destroy their replica by setting it to nil in the dictionary. That’s essentially how you remove something from a dictionary, which is assigning the key’s value to nil.

1 Like

Thank you for your reply!

I get replication to work but when I try this on a live server with multiple people and leave and enter again I get the following message ‘Token for replica class “replica name here” was already created.’

The relevant code I’m using is this:

local UserID = player.UserId

PLAYER_REPLICA[player] = ReplicaService.NewReplica({
	ClassToken = ReplicaService.NewClassToken("PlayerReplica_"..UserID),
	Data = profile.Data,
	Replication = player,
})

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

profile:ListenToRelease(function()

	Profiles[player] = nil
	PLAYER_REPLICA[player] = nil
end)

The ListenToRelease is running. I’ve tested with a print statement.
Any help with this would be greatly appreciated.

Is this under the scope of the Players.PlayerAdded event?

Yes. And everything work just fine but only the first time.

The problem is when I leave and enter the server again (another player keep the server alive). It’s as if the Replica Token is not removed when the player disconnects so when ReplicaService tries to create the Token it is already there from previous connection. But the ListenToRelease triggers correctly when I disconnect so I’m quite puzzled why it’s not working.

Oh, I know why now and it is my fault.

I forgot to mention that you have to destroy the Replica by using a function, and not just setting it to nil through the table since we are only just clearing the replica from the list, but it is still there, so do me a favor.

Inside the Profile:ListenToRelease() function, after setting both player profile and Replica to nil, add a next line where you reference the player’s Replica and call :Destroy() on it, like so;

PLAYER_REPLICA[player]:Destroy()