Replica - Server to client state replication (Module)

They do return a connection - check the source code.

1 Like

There is no recommendation for .OnNew() usage - use it as many times as you need without worrying about performance.

1 Like

Should this be paired with another networking module like BridgeNet2 or warp? I’m asking this because I need to change an attributes value and making a replica just to change an attributes value seems overkill.

Hello.

Is this setup with any package managers such as wally?

Is there any example code you can share using this? I’m not quite sure if I’m supposed to yield until its fired or do a connection and then disconnect after.

Here’s an example:

--- Server-Side
local replicaServer = require(path.to.ReplicaServer)

--- It is recommended to create a token once only if theres a similar replica that uses the same token.
local myToken = replicaServer.Token("MyTestToken")

local myReplica = replicaServer.New({
	Token = myToken,
	Data = {
		Fruits = {"Banana", "Apple", "Orange"}
	}
})

--- Optional:
MySecondReplica = replicaServer.New({
	Token = myToken,
	Data = {
		Meats = {"Chicken", "Porkchop"}
	}
})

--- Call this if you want to replicate to all players in the game and future players that join later.
--- Should only be called once for this replica.
--- It's either you only call this:
myReplica:Replicate()

--- Or you can use this:
--- This is fired for every player once the client replica on their side is loaded and ready to recieve data.
--- This insures that the player will recieve the data of the replica.
replicaServer.NewReadyPlayer:Connect(function(player)
	myReplica:Subscribe(player)
	MySecondReplica:Subscribe(player)
end)

--- Note: I am referencing this from the old replica service
--- but you do not not need to call Subscribe for each player if you use the Replicate() Method.
--- If you don't use the Replicate() Method and wanted to control how your replicas replicated for each player.
--- Then use the Subscribe() method instead. Either one of the methods should only be used.

For the client side we usually set up it like this:

--- Client-Side
local replicaClient = require(path.to.ReplicaClient)

--- Initilize on the client
--- Should only be called once in the entire code base (No other client scripts should call this again)
replicaClient.RequestData() 


--- To get the replica you created, in this case I created "MyTestToken" replica
--- We use the method OnNew from replica client.

--- since I created 2 replicas this event will fire twice.
--- Which ever which the order of replicas created on the server this will fire in-order as-well
replicaClient.OnNew("MyTestToken", function(myReplica)
	if myReplica.Data.Fruits then
		print(myReplica.Data.Fruits) ---> prints: {"Banana", "Apple", "Orange"} (For the first replica)
	elseif myReplica.Data.Meats then
		print(myReplica.Data.Meats) ---> prints: {"Chicken", "Porkchop"} (For the second replica)
	end
	--- Note: 
	--- this is not a recommended way to do this since they don't have the same Data structure
	--- It will be hard to manage (This just shows what happens if you have 2 replica with the same token)
end)

The documentation on the new Replica is not finished yet so I might miss something important details.

While I was porting the new Replica, I am not sure if this is a bug.

I am creating replicas with the same token like this:

local count = 0
function data:CreateEquippedReplicaUnit(unitId, unitInfo)
	local equippedUnit = replicaServer.New({
		Token = classTokens.EquippedUnit,
		Tags = {Id = unitId, PlayerId = self.player.UserId, Type = unitInfo.Details.Type},
		Data = unitInfo,
	})
	equippedUnit:SetParent(self.replica)
	count += 1
	print(count)
	return equippedUnit
end

I am counting the amount of replica for debugging.

On the client side:

local activeSlots = {}
	
replicaClient.OnNew("EquippedUnit", function(replica)
	if replica.Tags.PlayerId ~= player.UserId then return end
	if activeSlots[replica.Tags.Id] ~= nil then return end
	activeSlots[replica.Tags.Id] = LoadSlot(replica.Tags.Id, replica.Data)
	
	replica.Maid:Add(function()
		if activeSlots[replica.Tags.Id] then
			activeSlots[replica.Tags.Id]() --- call to cleanup
			activeSlots[replica.Tags.Id] = nil
		end
	end)
end)

For some weird reason this listener fires multiple times with the same replica hence duplicating the slots that’s already loaded. I fixed it by just checking if the Id in the tags are already in the ActiveSlots table.

In this current scenario there are 7 replicas being created.
What I expect should happen:

  • 7 Replicas Created
  • 7 Events fired

What actually happened:

  • 7 Replicas Created
  • 7 - 10 Events Fired (It changes whenever I Start and Stop Play Solo)

Console prints:
image

image

Perhaps you should check in-game and not in-studio

It should not matter weather its in-game or in-studio.

Thanks for the report!

It’s hard for me to make out what could be the problem with your currently provided data and there were automated tests (however not public at the moment) that tested parenting functionality for Replica.

It would be SUPER useful for me if you could make a minimal amount of code that could reproduce this error without any additional unnecessary code and make it into a Roblox place file or just short client-side and server-side scripts - this could help me fix the problem very fast!

I can’t seem to replicate on a new place i will try looking further.

I doubled check my code on my current project:

  1. Making sure its only being created on one place or one section of the entire codebase.

  2. Making sure the module script that handles creating the replica is only called once.

  3. Making sure that the “Data” being passed inside the arguments when creating the replica is a Deep copy table.

But still to no avail. I’ll give an update when I found it or If I can reproduce it consistently.

You can differentiate two replicas by their Replica.Id - Unique incremental id’s are assigned to replicas by the server. If Replica.OnNew() really does fire more than once for the same Replica.Id, then that’s a serious bug I’d address given enough material to trace it.

I tried printing the replica.Id it printed id 7 and id 9 twice.

image

I also have a unique string identifier inside the tags of each replica.

Replica Id: 6, 7 and 9 with a tag Id prints twice also.
image

Try stripping away as much game code as you can with the problem still persisting and send the files to me.

Also note that Replica:BindToInstance() will make a Replica get destroyed and created again for a client if the bound instance streams out and then in again - you didn’t mention using that, though.

I’ve reviewed the Replica code again and noticed that there might’ve been no prevention for parenting Replicas to themselves and there’s a possibility you might have received that kind of behaviour because of it. Are you parenting replicas to themselves by any chance?

Try changing this block of code under the server module of Replica:

	while recursion_check ~= nil do
		recursion_check = recursion_check.Parent
		if recursion_check == self then
			error(`[{script.Name}]: Can't set descendant Replica as parent`)
		end
	end

to this:

	while recursion_check ~= nil do
		if recursion_check == self then
			error(`[{script.Name}]: Can't set descendant Replica as parent`)
		end
		recursion_check = recursion_check.Parent
	end

Oh my… I found the issue…

replicaClient.OnNew("EquippedUnit", function(replica)
		print(replica.Id)
		local currentUnitId = replica.Tags.Id
		
		if replica.Tags.PlayerId == player.UserId and activeSlots[currentUnitId] == nil then
			activeSlots[currentUnitId] = LoadSlot(currentUnitId, replica.Data)

			replica.Maid:Add(function()
				if activeSlots[currentUnitId] then
					activeSlots[currentUnitId]() --- call to cleanup
					activeSlots[currentUnitId] = nil
				end
			end)
		end
	end)

When I remove this line:
activeSlots[currentUnitId] = LoadSlot(currentUnitId, replica.Data)

It fixed the multiple calls for some weird reason!?
maybe it is because of the function scope or possible cyclic? or the coroutine thread that calls the function listener that I don’t understand. I don’t know…

But hey it is a progress!

Although its still very weird, the LoadSlot() function is just a simple function that creates a class object and returns it.

Sorry for the inconvenience.

Adding task.defer() inside the function listener callback also fixes the problem.

replicaClient.OnNew("EquippedUnit", function(replica)
	task.defer(function()
		print(replica.Id)
		local currentUnitId = replica.Tags.Id

		if replica.Tags.PlayerId == player.UserId and activeSlots[currentUnitId] == nil then
			activeSlots[currentUnitId] = LoadSlot(currentUnitId, replica.Data)

			replica.Maid:Add(function()
				if activeSlots[currentUnitId] then
					activeSlots[currentUnitId]:Destroy() --- call to cleanup
					activeSlots[currentUnitId] = nil
				end
			end)
		end
	end)
end)

I looked through the ReplicaClient code I noticed there was an implementation of coroutine reusing this might be the problem? I am not familiar with the benefit of this though but that might give it a hint. Since I should not need to defer this listener.

Whatever you’re using to “patch” your problem is not going to be as valuable as whatever causes the problem in the first place to me. If you desire any help I’ll need simple steps to reproduce the problem or there’s nothing I can do about it!

When and how many times .OnNew() is called should not be affected by anything you’d put inside it’s listeners because it wraps everything in new coroutines.

I couldn’t help but notice that with Replica when you Set the value with the same value, it would count that and replicate it to the client, I noticed this wasn’t present in ReplicaService, is this intentional or a bug?

Can be replicated by just doing this on the server (with both Replica and ReplicaService):

local ReplicaService = require(game.ServerScriptService.ReplicaService)

local test_replica = ReplicaService.NewReplica({
	ClassToken = ReplicaService.NewClassToken("TestReplica"),
	Data = {Value = 0},
	Replication = "All",
})

while task.wait(1) do
	test_replica:SetValue({"Value"}, 1)
end

How can i use replicalistener on server? is it possible?