Replicate your states with ReplicaService! (Networking system)

@RobloxWobot @loleris

It seems like the backend of the ReplicaService is causing the Replica:Destroy() function to not properly removing the Replica from the CreatedClassTokens table. I found a fix which is going to the DestroyReplicaAndDescendantsRecursive function or line 373 in the ReplicaService module script and I added this line of code which solves the issue of erroring that this class token has been created upon calling ReplicaService.NewReplica() with the same name for the Replica ClassToken, and destroying the replica and creating the same replica with the same ClassToken name.

This is the forked version of the DestroyReplicaAndDescendantsRecursive function:

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

Only difference is this line:

CreatedClassTokens[replica.Class] = nil	

Hope this fixes an issue.

3 Likes

Wow, that appears to be working, thank you!
It appears you found a bug and patched it?

This fix could be worth patching the original Replicaservice with. I can’t be the only one running in to this problem? I simply don’t know if I coded something strange or the problem was only with Replicaservice. If the problem was Replicaservice then more people must have ran into this bug before.

I sure do!

Regarding if anyone has faced this before, I think there are many people found it when they playtest it with multiple players.

So what did we learn from it?

Always playtest with multiple people.

1 Like

Why even use Replica.Class as a data carrier for UserId when you also have Replica.Tag and Replica.Data. Your solution is the equivalent of literally giving every Player instace in a roblox game a different ClassName.

Class tokens should be declared outside of replica creation and reused every time a replica representing that class is defined.

In your scenario the class token has to be called just “Player” and be set to it’s own variable OUTSIDE of ReplicaService.NewReplica(). Afterwards you pass that same variable to the ClassToken parameter.

I’ll correct my minimal example to have the class token declared outside the .NewReplica() function to avoid confusion.

3 Likes

Check the parent of the character model when .CharacterAdded fires - If the model is not inside the DataModel, the instance reference will not replicate to clients.

Love the module.
I’m using it in combination with ProfileService and after getting into it it is really intuitive.
As someone mentioned before, a ReplicaRemoved listener would be useful.

Does ProfileService save all the metadata from nested replicas or does it skip the metadata?

ProfileService has no ReplicaService-related integrations and will attempt to save Profile.Data members as they are.

Profile.Data should only contain references to Replica.Data tables instead of Replica objects themselves. Consequently, it’s impractical to nest replicas for profiles as you’ll probably end up replicating some data twice through multiple replicas, but you can have a flat hierarchy of multiple replicas per profile.

2 Likes

How does :SetValues and :ListenToChange work?

I currently have this which from my understanding is the data you wanna send/replicate to the client

	local Replica = RS.NewReplica({
		ClassToken = RS.NewClassToken("Stats"),
		Data = {
			FP = Profile.Data.FP, --(fighter points) basically indicates your skill and where you are in your rank
			Rank = Profile.Data.Rank, --rank name
			Wins = Profile.Data.Wins,
			Loses = Profile.Data.Loses
		},
		Replication = Player,
	})

And then I tried testing how to change values with :SetValues I basically just copied the code from the docs, but kept getting an error saying “attempt to index nil with ‘FP’” I’m guessing it has something to do with {“Data”} so not sure should go here.

	Replica:SetValues({"Data"},{ 
		FP = 6000,
		Rank = "Master",
		Wins = 100,
		Loses = 200,
	})

And then I also tried testing :ListenToChanges, but I just have no idea what this means nvm I realized this is supposed to be on the client I was using it the wrong way

Replica:ListenToChange(path, listener) --> [ScriptConnection]
--   listener   [function] (new_value, old_value)

Sorry for the poorly written post my 2am brain is like half working

I have a class for game data and replicas for the contents of that game data. It’s like this:

Class: GameData

  • Replica: Items (table)
  • Replica: Worlds (table)

I was wondering if this is a good way to implement it into ReplicaService and what mutator should I use to completely overwrite a replica’s data with a new table?

It’s hard to mess things up. You should only be concerned about rapidly changing large amounts of data, but even that is unlikely when you have around a 30 - 60 KB/s of constant data budget. You can actually get away with replicating megabytes of data, but it should only be done when the game is loading new areas and the player is supposed to wait cause you’re going to notice the game freezing up for a bit.

You should apply “if it works - it works” mentality here and just take a look at your game network usage once in a while.

You should be able to set Replica.Data to a new table inside a write function mutator, though I wasn’t intending it to support that.

3 Likes

Here’s my implementation. I don’t think it’s very good, but I’m not sure how to improve it either.

local ReplicaController = require(MGMDL.ReplicatedModules.Madwork.ReplicaController)
local RequiredConnections = 2
local ConnectionCount = 0

return function(Count)
	ConnectionCount += Count
	if ConnectionCount >= RequiredConnections then
		ReplicaController:RequestData()
	end
end

Could I see yours?

ListenToNewKey is not printing, but ListenToChange prints. Does anyone know why?

1 Like

If you’re updating the entire Pets table through Replica:SetValue({“Pets”}, value) instead of Replica:SetValue({“Pets”, key}, value), you’re not going to trigger the new key listener. Likewise, doing the latter variant will disable the Pets table change listener since the table reference would remain the same.

1 Like

I have the same issue, it’s gone when I switch to a different place very weird.

I’m a bit confused, why is it necessary to not add on more listeners once you have requested data? It is making it a lot more hard to use for my states system, where when each player joins they get assigned a state. Sorry for the trouble, otherwise great module though!

1 Like

I second this. It makes it a lot harder to build ReplicaService as a dependency for one of my game system’s modules because I’m not guaranteed that any of the scripts will definitely call the ‘Request’ function on the controller module. This means that even if I wanted to, I can’t create portable modules to use across multiple games.

2 Likes

Makes sense. I suggest waiting for my future remake of ReplicaService which will be based around extendable classes with replicated state

2 Likes

I have a question about the efficiency of an application of this module.

I have a stat called “Time Played” in my game, which goes up one for every player in game every second. I also have a custom player list, which is sorted in descending order by Time Played.

I have other stats that don't go up every second, but go up fairly often for each player. Currently I'm replicating data to the client for use in the player list like this:

Would creating a Replica for every player to store their data, and having each replica replicate to every client be more efficient? Or would it be less efficient?

Most of these data points you speak of will have negligible impact to memory and network bandwidth use - you’ll have a hard time even seeing them show up in the performance stats.

3 Likes