[v.28] ReplicatedRegistry | Simple server-client & client-server synchronized table registry

[{…} → {…}] ReplicatedRegistry

ReplicatedRegistry.rbxm (8.7 KB)

ReplicatedRegistry is a module I made that you can very easily use to sync table changes between server and client (or vice versa).

It is a single module with 0 dependencies, unlike charm-sync or Replica, and it is very easy to use.

Documentation
  1. ReplicatedRegistry.SetTable(registerKey: any, tbl: T?, filter: ((sender: Player?, key: any, value: any, path: {any}) -> boolean)?) -> (T?, Registree<T>)

    • Registers the table into the module so that it is subject to replication signals from the other side (or deregisters a table so that it is no longer subject to replication signals from the other side, if tbl is passed as nil).

      If a filter is specified, if a specific change does not pass the filter it is rejected (aka when the filter returns false). (This is on a per key-value pair basis.)
      If the change should be accepted, the filter should return true.
      Note that if the change was sent from the server, sender is always nil.

  2. ReplicatedRegistry.GetTable(registerKey: any): Table?

    • Retrieves a registered table using its key, or nil if not found.

  3. ReplicatedRegistry.WaitForTable(registerKey: any, timeOut: number?): Table?

    • Waits timeOut or math.huge seconds until the table with the specified key is found, then that table is returned.

  4. ReplicatedRegistry.SendChanges(replicationMode: "ToClient"|"ToAllClients"|"ToServer", registerKey: any, player: Player?, unreliable: boolean?)

    • Sends all changes (since SendChanges/SetTable was last called) to the side specified by replicationMode.
      If replicationMode is “ToClient”, then the player argument is required.
      If unreliable is passed as true, then the function will replicate changes via the unreliable changed remote event instead of the normal one.

  5. ReplicatedRegistry.RequestTable(registerKey: any): Table?

    • A client-only function that requests the table associated with registerKey from the server. Returns nil if rejected, the requested table if else.

  6. ReplicatedRegistry.OnRecieve(registerKey: any, fn: (sender: Player?, tbl: Table) -> ()): () -> ()

    • Sets a callback function to be called when a replication signal for the registerKey is sent to this side.
      Returns a function to disconnect the callback.

  7. ReplicatedRegistry.SetOnRequest(fn: (player: Player, registerKey: any) -> boolean)

    • A server-side function to set the filter for ReplicatedRegistry.RequestTable calls.
      By default, this filter only accepts RequestTable requests if the key matches/is the player’s UserId.
      However, the purpose of this function is so you can change that.

  8. ReplicatedRegistry.GetFilter(nameToArgs: FilterList): Filter

    • Returns a composite filter containing all filters specified within nameToArgs.
      The returned filter can be used in ReplicatedRegistry.SetTable()
Examples

The following example demonstrates simple player data replication between clients and the server.

Server:

local ReplicatedRegistry = require(path.to.module)
local setTbl = ReplicatedRegistry.SetTable
local replicate = ReplicatedRegistry.SendChanges
local filters = ReplicatedRegistry.Filters

-- A filter that rejects all replication requests from the client
local norecv = filters.NoReceive(true)

game.Players.PlayerAdded:Connect(function(plr)
	local tbl = {
		JoinTime = os.time(),
		Kills = 0
	}
	setTbl(plr.UserId, tbl, norecv)
	
	tbl.Kills = 10
	replicate("ToClient", plr.UserId, plr)
end)

Client:

local player = game.Players.LocalPlayer

local ReplicatedRegistry = require(path.to.module)
local setTbl = ReplicatedRegistry.SetTable
local request = ReplicatedRegistry.RequestTable
local filter = ReplicatedRegistry.GetFilter {
   DontAcceptIfNil = true,
   RateLimit = 10
}

local tbl = setTbl(player.UserId, request(player.UserId), filter)
while task.wait(1) do
	print(tbl.Kills) -- 10
	print(tbl.JoinTime) -- Some big number
end

Look at how easy it is to set up server → client data replication!


And look how easy it is to set up and use it for player stats!

It’s so simple to use, it’s just a table!
I wouldn’t even know where to begin to do this with Replica

ReplicatedRegistry is also FAR more optimized than the likes of Replica, ReplicaService and charm-sync as it does not attempt to do any state management, only replication of table changes.

It also is far, far simpler, so you can modify it to your hearts content!

Hope you enjoyed this resource, and happy coding!

23 Likes

Package Version 10:

  • Added ReplicatedRegistry.OnKeyChanged()

Package Version 15:

  • The module now uses --!nonstrict
  • Added more clear names
  • Removed the seperate dict tables for callbacks and values, combining it into 1 dict mapping register key to Registrees which contain all data (such as filters, the actual table reference, callbacks, etc)
  • Added InvokeRequestCallback (a callback to be called on the server when a client calls .RequestTable) and Registrees (Where all registered tables are stored) as members of the ReplicatedRegistry return table, the exposing of Registrees allows you to replicate multiple tables at once

Note that theres a good chance i broke something in this update so please, PLEASE let me know

Package Version 18:

  • Fixed traversePathAndSet which was broken for nested tables for a while (Why didnt anyone tell me about this :sob: )
  • .RequestTable() now returns an empty table if one was not returned by the server

Known Issues:

  • traverseAndCompare doesn’t really work well rn and I’m trying to make it work, for now feel free to make your own function for it
1 Like

Package Version 21:

  • Fixed traverseAndCompare to… work :V

fire module :fire:, i look forward to future resources from you as i may or may not use a quite amount of modules you’ve created like actorgroup2, bufferconverter, and including this one

1 Like

Bufferconverter isnt really good (nor is the second one :V), nowadays I use the rewrite of bufferconverter2 only for communicating between actors

ActorGroup2 is still pretty good tho yeah, but preferably you should make whatever you’re trying to do not need to send a response back so everything can be done async (which is much faster) :V

Feel free to use my “Simple{name}” stuff tho, I’ve heard many of them r pretty good :0

ill be sure to. i also notice that in the documentation you seem to have misspelled “Receive” as it says “Recieve” instead:

ReplicatedRegistry.OnRecieve(registerKey: any, fn: (sender: Player?, tbl: Table) -> ()): () -> ()

Oh yea I fixed that in the actual module, someone in discord pointed it out :skull: I dont think imma fix it tho it’s pretty minor

Package Version 23:

  • Removed a print I left after testing

Package Version 26:

  • Fixed the default onrequest callback returning false falsely
  • Fixed OnKeyChanged not creating the callback table correctly

I have a question, its really stupid.

So im trying to create a NPC but having an accurate position on ALL clients is VERY important for this specific NPC.

I want to know if it would be viable to use this module to keep his position the same on the server and all clients by updating his position using the server and retrieving that value from the client and adjusting his position.

The REASON its so important is because he goes VERY fast and has modified physics.

I feel like im about to sound so stupid.

Idk youre probably better off using your own system lol, idk if the table change checking is fast enough for what you need

hello! I’m trying to use your module in order to sync up changes in a player’s table with their UI but I’m getting an odd “tables cannot be cyclic” error that I’m not sure how to fix.


This is where I set the table inside ReplicatedRegistry (note: this is a constructor function for a custom object)


It begins when I request a player’s table client-side.

The error begins on this line specifically. I’ve also looked at the OnServerInvoke function inside the module but have no idea how to fix it.

Thanks in advance (I literally just started using this resource, it looks very cool and helpful)

1 Like

Hi, this is just an error with remote events lol. Cyclic means your table contains another table which references itself, and you can’t send that.

ah, gotcha! I think I get the problem now so I’ll roll around back to this when I’ve fixed it. Going to try just replicating part of the table (the important stuff like current stats and stuff, no need to send everything over) and hopefully that’ll fix things

1 Like

Package Version 28:

  • Fixed nested tables not being copied in the stored copy of the table, causing it to not send changes because they exist in the copy

  • Fixed some minor issues i found while using this for my own game, it actually works pretty well now and is viable for 2-way sync

Coming in 29:

  • Ability to serde changes before sending them/when recieving them

  • Security checks and validation during recieval on the server (before changes get processed) to prevent spamming

How could i detect changes from the server? , Iv’e tried doing OnKeyChanged from the server but, it just won’t detect

local function OnProfileLoaded(Profile)
	local Player = Profile.Player
	local character = Player.Character

	if not Profile or not Player then end

	Profile.Profile.OnSave:Connect(function()
		print("saved")
	end)

	local char = Player.Character
	local leaderstats = nil
	if Player:FindFirstChild("leaderstats") then
		leaderstats = Player:WaitForChild("leaderstats")
	end 
	
	
	for _, values in pairs(leaderstats:GetChildren()) do
		for statName, statValue in pairs(Profile.Profile.Data.Stats_) do
			if values.Name == statName then
				values.Value = statValue
				
				local new_Changed = values:GetPropertyChangedSignal("Value"):Connect(function()
					Profile.Profile.Data.Stats_[statName] = values.Value
					replicate("ToClient", Player.UserId, Player)
					warn(statValue.." :", Profile.Profile.Data.Stats_[statName])
				end)
				
			end
		end
	end
	
	local newplayerdata = setTable(Player.UserId, Profile.Profile.Data)
	local OnRequest = ReplicatedRegistry.SetOnRequest(function(player, registerKey)
		warn(player, registerKey)
	end)
	
	replicate("ToClient", Player.UserId, Player)
	
	warn(newplayerdata)

end

I am using this with ProfileStore and my table looks like this

I am doing this because i want my leaderstats number values to also change when i change the table from Profile.Profile.Data aka doing this :

local profile_plr = ProfileManager:GetProfile(plr)
	
	print(profile_plr)
	
	profile_plr.Profile.Data.Stats_.Kills += 1

Hi, you need to call the SendChanges() (or in your case replicate()) function repeatedly in a time interval if you want it to constantly replicate, here you are only calling it once

SendChanges sends all changes since the last time SendChanges was called, so if you want you can send changes every like 1 second

What i usually do is loop through all currently active sessions (or in your case profiles) and SendChanges them all at once in a loop

2 Likes

Hello I am having problems with this module. I am trying to create a table on the server and when a player joins it logs him into the table.

When the player joins in his localscript it requests the table and gives positive, but when printing it prints an empty table

its not until moments later that the server changes one of its values that the player receives a table but only with those changed values, as if the others did not exist.

My question is, how can I make a player receive a complete table when joining even if there are no changed values? I have tried many things but it never ends up working right, I would like that when a player joins I can get the full table correctly and not just defined as you change the values.

An example of how is the game working rn:

Server:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ReplicatedRegistry = require(ReplicatedStorage:WaitForChild("ReplicatedRegistry"))

local sendChanges = ReplicatedRegistry.SendChanges
local filters = ReplicatedRegistry.Filters.NoReceive
ReplicatedRegistry.SetTable("playersinfo",{}, ReplicatedRegistry.Filters.NoReceive(true))
local playersinfo = ReplicatedRegistry.GetTable("playersinfo")

game.Players.PlayerAdded:Connect(function(player)
	playersinfo[player.UserId] = nil
	ReplicatedRegistry.SendChanges("ToAllClients", "playersinfo")
	playersinfo[player.UserId] = {
		currentHealth = 300,
		status = "offline"
	}
	print(playersinfo)
    -- Will print the table correctly ( {UserId} -> [currentHealth = 300, status = “offline”])
	ReplicatedRegistry.SendChanges("ToAllClients","playersinfo")
    
    task.wait(5)
	playersinfo[player.UserId] = {
		currentHealth = 300,
		status = "playing"
	}
	ReplicatedRegistry.SendChanges("ToAllClients","playersinfo")
	print(playersinfo)
	--Will print the table correctly ( {UserId} -> [currentHealth = 300, status = “playing”])
	task.wait(5)
	playersinfo[player.UserId] = {
		currentHealth = 200,
		status = "playing"
	}
	ReplicatedRegistry.SendChanges("ToAllClients","playersinfo")
	print(playersinfo)
	--Will print the table correctly ( {UserId} -> [currentHealth = 200, status = “playing”])


end)

Local:

local ReplicatedRegistry = require(game.ReplicatedStorage:WaitForChild("ReplicatedRegistry"))

playersinfo = ReplicatedRegistry.SetTable("playersinfo", ReplicatedRegistry.RequestTable("playersinfo"))
print("Recibed")
print(playersinfo)
-- Just print {}
task.wait(6) -- Example of waiting after the first change
print(playersinfo)
-- Prints ( {UserId} -> [status = «playing»]) but currentHealth does not exist
task.wait(6) -- Example of waiting after the second change
print(playersinfo)
-- Will print the table correctly ( {UserId} -> [currentHealth = 200, status = “playing”]), but just because all the values were changed