Replica - Server to client state replication (Module)

Okay, so I currently have inventory system based on remote events (Server gives item id and which item it is and its state to player)
Will this be useful?

Yes, it will be easier to work with your system and cleanup your code as well as remove remoteevent clutter. :slight_smile:

1 Like

I will give my personal solution to this problem, it aint the best though:

function PlayerProfile:OnSetServer(replicaName: string, targetPath: {string}, callback: (any, any) -> ())
	local replica
	if replicaName == "Data" then
		replica = self.ReplicaData
	elseif replicaName == "Session" then
		replica = self.ReplicaSession
	elseif replicaName == "Shared" then
		replica = self.ReplicaSharedState
	elseif replicaName == "Christmas" then
		replica = self.ReplicaSharedChristmas
	else
		warn("OnSetServer: Unknown replica name:", replicaName)
		return
	end

	if not replica.OnSetServerCallbacks then
		replica.OnSetServerCallbacks = {}
	end

	local pathKey = table.concat(targetPath, ".")

	replica.OnSetServerCallbacks[pathKey] = replica.OnSetServerCallbacks[pathKey] or {}
	table.insert(replica.OnSetServerCallbacks[pathKey], callback)
end

You then would have to modify the ReplicaServer module to include a callback (Can be done without but ran into many issues).

function Replica:Set(path: {string}, value: any)
	local pointer = self.Data
	for i = 1, #path - 1 do
		pointer = pointer[path[i]]
	end
	
	
	local oldValue = pointer[path[#path]]
	pointer[path[#path]] = value

	
	
	local pathKey = table.concat(path, ".")
	if self.OnSetServerCallbacks and self.OnSetServerCallbacks[pathKey] then
		for _, cb in ipairs(self.OnSetServerCallbacks[pathKey]) do
			cb(value, oldValue)
		end
	end

	
	if WriteFlag == false then
		local self_id = self.Id
		if self.replication ~= nil then
			if self.replication[REPLICATION_ALL] == true then
				for player in pairs(ReadyPlayers) do
					RemoteSet:FireClient(player, self_id, path, value)
				end
			else
				for player in pairs(self.replication) do
					RemoteSet:FireClient(player, self_id, path, value)
				end
			end
		end
	end
end

Then create a listener on a script with your path targeting.
(granted it’s not modified to work with “OnChange”):

function abcdef:waitforPlayerData(Player)
	local startTime = os.time()
	repeat
		task.wait(0.5)
		if PlayerDataHandler[Player] then
			return PlayerDataHandler[Player]
		end
	until os.time() - startTime >= 60
	return false
end


function abcdef:ListenersSetup(Player)
	local playerData = self:waitforPlayerData(Player)
	if not playerData then return end

	playerData:OnSetServer("Data", {"Titles","Equipped"}, function(newValue, oldValue)
		print("Server detected a change in equipped title:", newValue, oldValue)
		self:SetupPlayerTags(Player)
	end)
end

Keep in mind I am not sure if this is the most efficient way to go about it, but it works for my use case as a quick botched job.

You can use this as an idea how you could get your implementation working.

1 Like

Im making a entity system and inside I store cooldowns and many others things.I want cooldown to replicate to only the player that owns it but not others players, how I could do that ?

Here is an example from my code I feel would be good way to show how OnSet could be used for both Server and Client usage without having to create an external method of listening.
Hope that there will be added support for server Listening since it would ease making connections between both sides.

-- Server

-- Replicated Player Data Table
local Rank = {
	Level = {
		['Count'] = 1,
	},
	
	XP = {
		['Count'] = 0
	}
}
...
-- Profile/Replica data
Profile.Data = Rank
replica = [some replica]
...
local function AddXP(amount)
    local currentXP = Profile.Data.Rank.XP.Count
   
    Profile.Data.Rank.XP.Count += amount -- Change to data made
    replica:Set({'Rank', 'XP', 'Count'}, currentXP + amount) -- Change to replica made
end

-- Listen to replica change since changing the profile data table itself 
-- does not have a 'Changed' connection to listen to
replica:OnSet({'Rank', 'XP', 'Count'}, function(oldXP, newXP)
    local CalculatedLevel = newXP // 500 -- Example Level Calculation
    
    Profile.Data.Rank.Level.Count = CalculatedLevel -- Change to data made
    replica:Set({'Rank', 'Level', 'Count'}, CalculatedLevel) -- Change to replica made
end)

AddXP(1000)
-- Client

...
replica:OnSet({'Rank', 'Level', 'Count'}, function(oldLvl, newLvl)
      LevelLabel.Text = tostring(newLvl) .. ' Level' -- Display data
end)

replica:OnSet({'Rank', 'XP', 'Count'}, function(oldXP, newXP)
      XPLabel.Text = tostring(newXP) .. ' XP' -- Display data
end)

If there is a better/current possible way to structure this without having to require server listening pls lmk (or if this is even a correct way to manage this)

Great resource as always!

You could always just add it to the function or even make a new function, a listener wouldn’t really be necessary in this case.

-- Profile/Replica data
Profile.Data = Rank
replica = [some replica]

local function AddXP(amount)
    local currentXP = Profile.Data.Rank.XP.Count
    local newXP = currentXP + amount
    local CalculatedLevel = newXP // 500 -- Example Level Calculation

    --> updating replica should update your data too
    replica:Set({'Rank', 'XP', 'Count'},  newXP) -- Change to replica made
    replica:Set({'Rank', 'Level', 'Count'},  CalculatedLevel ) -- Change to replica made
end

or

-- Profile/Replica data
Profile.Data = Rank
replica = [some replica]

local function UpdateLevel()
   local currentXP = Profile.Data.Rank.XP.Count
   local CalculatedLevel = newXP // 500 -- Example Level Calculation

   replica:Set({'Rank', 'Level', 'Count'},  CalculatedLevel ) -- Change to replica made
end

local function AddXP(amount)
    local currentXP = Profile.Data.Rank.XP.Count
    local newXP = currentXP + amount

    --> updating replica should update your data too
    replica:Set({'Rank', 'XP', 'Count'},  newXP) -- Change to replica made

    UpdateLevel()
end
1 Like

Yeah that could work. Although I have many other “datapoints” within my players data that would require to make custom functions for when they are set. I just feel that being able to listen to changes for each of these datapoints with :OnSet() on the server would likely simplify the structure.
Thanks for your help!

1 Like

Looks interesting, so I am guessing this acts kind of like a way to securely communicate data to and from the client? Is it possible to set multiple players via a list/tuple or is one replica per player?

If so I feel this could be useful for communicating objective data between multiple clients like a display for example.

So like:

Tags = {
  Player = {player1, player2, player3}
}

I don’t exactly understand how it works too well. If there was some “live” examples in forms of videos or rbxl/rbxm’s then that would help me understand a bit better as I am more of a visual learner than a reader.

This is actually a nice little example of how to use profile store, thanks for this.

2 Likes

Is there a way to check if a value was changed on the server?

just check that with the server like you would any other way? Even if you need to hack and slash a remote to make that.

1 Like

Is it necessary to replicate every player’s data to all players by using Replica:Replicate() which as far as I can see is commonly used, even in the documentation? Does it come with any extra memory cost or increased network traffic?

I would appreciate an explanation, earlier I was trying to use replica for a project and I wasn’t sure about this.

Read original post for details:

2 Likes

when is documentation gonna be released?

4 Likes

I’m sure this question has been asked so many times. But what does this actually do?
The documentation of both this and ReplicaService are way too abstract for me to understand. Does it just replicate variables/values automatically?

Is there a destroyed event on the client? If not, could you consider adding a one so it’s easier to know when a player is unsubscribed or when a replica is destroyed?

I didn’t like how there is no :OnTableInsert or :OnTableRemove listeners, so I made a fork of ReplicaClient that adds exactly that.

Code
--[[
MAD STUDIO

-[Replica]---------------------------------------
	
	State replication with life cycle handling and individual client subscription control.
	
	WARNING: Avoid numeric tables with gaps & non string/numeric keys - They can't be replicated!
			
	Members:
	
		Replica.IsReady        [bool]
		Replica.OnLocalReady   [Signal] ()
	
	Functions:
	
		Replica.RequestData() -- Requests the server to start sending replica data
		
		Replica.OnNew(token, listener) --> [Connection]
			token      [string]
			listener   [function] (replica)
		
		Replica.FromId(id) --> [Replica] or nil
		
	Members [Replica]:
	
		Replica.Tags             [table] Secondary Replica identifiers
		Replica.Data             [table] (Read only) Table which is replicated
		
		Replica.Id               [number] Unique identifier
		Replica.Token            [string] Primary Replica identifier
		
		Replica.Parent           [Replica] or nil
		Replica.Children         [table]: {[replica] = true, ...}
		
		Replica.BoundInstance    [Instance] or nil -- WARNING: Will be set to nil after destruction
		
		Replica.OnClientEvent    [Signal] (...)
		
		Replica.Maid             [Maid]
	
	Methods [Replica]:
	
		-- [path]: {"Players", 2312310, "Health"} -- A path defines a key branch within Replica.Data
		-- Listeners are called after Replica.Data mutation.
	
		Replica:OnSet(path, listener) --> [Connection] -- (Only for :Set(); For :SetValues() you can use :OnChange())
			listener   [function] (new_value, old_value)
			
		Replica:OnTableInsert(path, listener) --> [Connection] -- (Only for :TableInsert())
			listener   [function] (inserted_value, inserted_index)
			
		Replica:OnTableRemove(path, listener) --> [Connection] -- (Only for :TableRemove())
			listener   [function] (removed_value, removed_index)
			
		Replica:OnWrite(function_name, listener) --> [Connection]
			listener   [function] (...)
			
		Replica:OnChange(listener) --> [Connection]
			listener   [function] (action, path, param1, param2?)
				-- ("Set",         path, value, old_value)
				-- ("SetValues",   path, values)
				-- ("TableInsert", path, value, index)
				-- ("TableRemove", path, value, index)
				
		Replica:GetChild(token [string]) --> [Replica] or nil -- Searches for a child replica with given token name
		
		Replica:FireServer(...) -- Fire a signal to server-side listeners for this specific Replica; Must be subscribed
		Replica:UFireServer(...) -- Same as "Replica:FireServer()", but using UnreliableRemoteEvent
		
		Replica:Identify() --> [string] -- Debug

		Replica:IsActive() --> [bool]
	
--]]

----- Dependencies -----

local ReplicaShared = game.ReplicatedStorage.ReplicaShared
local Remote = require(ReplicaShared.Remote)
local Signal = require(ReplicaShared.Signal)
local Maid = require(ReplicaShared.Maid)

----- Private -----

local BIND_TAG = "Bind"
local CS_TAG = "REPLICA" -- CollectionService tag
local MAID_LOCK = {}
local REQUEST_DATA_REPEAT = 2

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

local DataRequestStarted = false

local TokenReplicas = {} -- [token] = {[replica] = true, ...}
local Replicas = {} -- [id] = Replica, ...
local BindReplicas = {} -- [id] = Replica, ... -- Unannounced Replicas waiting for their binds to stream in
local BindInstances = {} -- [id] = Instance, ...

local NewReplicaListeners = {} -- [token] = {[connection] = true, ...}

local RemoteRequestData = Remote.New("ReplicaRequestData") -- Fired client-side when the client loads for the first time

local RemoteSet = Remote.New("ReplicaSet")                 -- (replica_id, path, value)
local RemoteSetValues = Remote.New("ReplicaSetValues")     -- (replica_id, path, values)
local RemoteTableInsert = Remote.New("ReplicaTableInsert") -- (replica_id, path, value, index)
local RemoteTableRemove = Remote.New("ReplicaTableRemove") -- (replica_id, path, index)
local RemoteWrite = Remote.New("ReplicaWrite")             -- (replica_id, fn_id, ...)
local RemoteSignal = Remote.New("ReplicaSignal")           -- (replica_id, ...)
local RemoteParent = Remote.New("ReplicaParent")           -- (replica_id, parent_id)
local RemoteCreate = Remote.New("ReplicaCreate")           -- (creation, root_id?) or ({creation, ...})
local RemoteBind = Remote.New("ReplicaBind")               -- (replica_id)
local RemoteDestroy = Remote.New("ReplicaDestroy")         -- (replica_id)
local RemoteSignalUnreliable = Remote.New("ReplicaSignalUnreliable", true)   -- (replica_id, ...)

local WriteLibCache: {[ModuleScript]: {[string | number]: {Name: string, Id: number, fn: (...any) -> (...any)}}} = {}
local ReplicationFlag = false

local function LoadWriteLib(module: ModuleScript)

	local write_lib = WriteLibCache[module]

	if write_lib ~= nil then
		return write_lib -- WriteLib module was previously loaded
	end

	local loaded_module = require(module)

	local function_list = {} -- fn_id = {fn_name, fn}

	for key, value in pairs(loaded_module) do
		table.insert(function_list, {key, value})
	end

	table.sort(function_list, function(item1, item2)
		return item1[1] < item2[1] -- Sort functions by their names - this creates a consistent indexing on server and client-side
	end)

	write_lib = {} -- {["fn_name" | fn_id] = {Id = fn_id, fn = fn}, ...}

	for fn_id, fn_entry in ipairs(function_list) do
		local entry_table = {Name = fn_entry[1], Id = fn_id, fn = fn_entry[2]}
		write_lib[fn_entry[1]] = entry_table
		write_lib[fn_id] = entry_table
	end

	WriteLibCache[module] = write_lib

	return write_lib

end

----- Public -----

export type Connection = {
	Disconnect: (self: Connection) -> (),
}

export type Replica = {
	Tags: {[any]: any},
	Data: {[any]: any},
	Id: number,
	Token: string,
	Parent: Replica?,
	Children: {[Replica]: boolean?},
	BoundInstance: Instance?,
	OnClientEvent: {Connect: (self: any, listener: (...any) -> ()) -> ({Disconnect: (self: any) -> ()})},
	Maid: typeof(Maid),

	OnSet: (self: any, path: {}, listener: () -> ()) -> (Connection),
	OnTableInsert: (self: any, path: {}, listener: () -> ()) -> (Connection),
	OnTableRemove: (self: any, path: {}, listener: () -> ()) -> (Connection),
	OnWrite: (self: any, function_name: string, listener: (...any) -> ()) -> (Connection),
	OnChange: (self: any, listener: (action: "Set" | "SetValues" | "TableInsert" | "TableRemove", path: {any}, param1: any, param2: any?) -> ()) -> (Connection),
	GetChild: (self: any, token: string) -> (Replica?),
	FireServer: (self: any, ...any) -> (),
	UFireServer: (self: any, ...any) -> (),
	Identify: (self: any) -> (string),
	IsActive: (self: any) -> (boolean),
}

local Connection = {}
Connection.__index = Connection

local FreeRunnerThread

--[[
	Yield-safe coroutine reusing by stravant;
	Sources:
	https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063
	https://gist.github.com/stravant/b75a322e0919d60dde8a0316d1f09d2f
--]]

local function AcquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquired_runner_thread = FreeRunnerThread
	FreeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	FreeRunnerThread = acquired_runner_thread
end

local function RunEventHandlerInFreeThread(...)
	AcquireRunnerThreadAndCallEventHandler(...)
	while true do
		AcquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

function ConnectionNew(t, fn)

	local self = setmetatable({
		t = t,
		fn = fn,
	}, Connection)

	t[self] = true

	return self

end

function ConnectionFire(self, ...)
	if not FreeRunnerThread then
		FreeRunnerThread = coroutine.create(RunEventHandlerInFreeThread)
	end
	task.spawn(FreeRunnerThread, self.fn, ...)
end

function Connection:Disconnect()
	self.t[self] = nil
end

local Replica = {
	IsReady = false,
	OnLocalReady = Signal.New(),
}
Replica.__index = Replica

local function ReplicaNew(id: number, self_creation: {}) -- self_creation = {token, tags, data, parent_id, write_module}

	local write_lib = nil
	if self_creation[5] ~= nil then
		write_lib = LoadWriteLib(self_creation[5])
	end

	local token = self_creation[1]
	local parent = BindReplicas[self_creation[4]] or Replicas[self_creation[4]]

	local self = setmetatable({
		Tags = self_creation[2],
		Data = self_creation[3],
		Id = id,
		Token = token,
		Parent = parent,
		Children = {},
		BoundInstance = nil,
		OnClientEvent = Signal.New(),
		Maid = Maid.New(MAID_LOCK),

		self_creation = self_creation,

		write_lib = write_lib,

		set_listeners = {}, -- [key] = {[connection] = true, ...}, ...
		table_insert_listeners = {}, -- [key] = {[connection] = true, ...}, ...
		table_remove_listeners = {}, -- [key] = {[connection] = true, ...}, ...
		write_listeners = {}, -- [key] = {[connection] = true, ...}, ...
		changed_listeners = {}, -- [connection] = true, ...

	}, Replica)

	if parent ~= nil then
		parent.Children[self] = true
	end

	return self

end

function Replica.RequestData()

	if DataRequestStarted == true then
		return
	end

	DataRequestStarted = true

	task.spawn(function()

		RemoteRequestData:FireServer()

		while task.wait(REQUEST_DATA_REPEAT) do
			if Replica.IsReady == true then
				break
			end
			RemoteRequestData:FireServer()
		end

	end)

end

function Replica.OnNew(token: string, listener: (replica: Replica) -> ()): Connection

	if type(token) ~= "string" then
		error(`[{script.Name}]: "token" must be a string`)
	end

	local listeners = NewReplicaListeners[token]

	if listeners == nil then
		listeners = {}
		NewReplicaListeners[token] = listeners
	end

	local existing_replicas = TokenReplicas[token]

	local connection = ConnectionNew(listeners, listener)

	if existing_replicas ~= nil then
		for replica in pairs(existing_replicas) do
			ConnectionFire(connection, replica)
		end
	end

	return connection

end

function Replica.FromId(id: number): typeof(Replica)?
	return Replicas[id]
end

function Replica.Test()
	return {
		TokenReplicas = TokenReplicas, -- [token] = {[replica] = true, ...}
		Replicas = Replicas, -- [id] = Replica, ...
		BindReplicas = BindReplicas, -- [id] = Replica, ... -- Unannounced Replicas waiting for their binds to stream in
		BindInstances = BindInstances, -- [id] = Instance, ...
	}
end

function Replica:OnSet(path: {}, listener: () -> ()): Connection
	local path_key = table.concat(path, ".")
	local listeners = self.set_listeners[path_key]
	if listeners == nil then
		listeners = {}
		self.set_listeners[path_key] = listeners
	end

	return ConnectionNew(listeners, listener)
end

function Replica:OnTableInsert(path: {}, listener: () -> ()): Connection
	local path_key = table.concat(path, ".")
	local listeners = self.table_insert_listeners[path_key]
	if listeners == nil then
		listeners = {}
		self.table_insert_listeners[path_key] = listeners
	end

	return ConnectionNew(listeners, listener)
end

function Replica:OnTableRemove(path: {}, listener: () -> ()): Connection
	local path_key = table.concat(path, ".")
	local listeners = self.table_remove_listeners[path_key]
	if listeners == nil then
		listeners = {}
		self.table_remove_listeners[path_key] = listeners
	end

	return ConnectionNew(listeners, listener)
end

function Replica:OnWrite(function_name: string, listener: (...any) -> ()): Connection
	local listeners = self.write_listeners[function_name]
	if listeners == nil then
		listeners = {}
		self.write_listeners[function_name] = listeners
	end

	return ConnectionNew(listeners, listener)
end

function Replica:OnChange(listener: (action: "Set" | "SetValues" | "TableInsert" | "TableRemove", path: {any}, param1: any, param2: any?) -> ()): Connection
	return ConnectionNew(self.changed_listeners, listener)
end

function Replica:GetChild(token: string): Replica?
	if type(token) ~= "string" then
		error(`[{script.Name}]: "token" must be a string`)
	end
	for replica in pairs(self.Children) do
		if replica.Token == token then
			return replica
		end
	end
	return nil
end

function Replica:FireServer(...)
	RemoteSignal:FireServer(self.Id, ...)
end

function Replica:UFireServer(...)
	RemoteSignalUnreliable:FireServer(self.Id, ...)
end

function Replica:Identify(): string
	local tag_string = ""
	local first_tag = true
	for key, value in pairs(self.Tags) do
		tag_string ..= `{if first_tag == true then "" else ";"}{tostring(key)}={tostring(value)}`
		first_tag = false
	end
	return `[Id:{self.Id};Token:{self.Token};Tags:\{{tag_string}\}]`
end

function Replica:IsActive(): boolean
	return self.Maid:IsActive()
end

function Replica:Set(path: {string}, value: any)

	if ReplicationFlag ~= true then
		error(`[{script.Name}]: "Set()" can't be called outside of WriteLibs client-side`)
	end

	-- Apply local change:

	local pointer = self.Data
	for i = 1, #path - 1 do
		pointer = pointer[path[i]]
	end
	local last_key = path[#path]
	local old_value = pointer[last_key]
	pointer[last_key] = value

	-- Firing signals:

	if next(self.set_listeners) ~= nil then
		local listeners = self.set_listeners[table.concat(path, ".")]
		if listeners ~= nil then
			for connection in pairs(listeners) do
				ConnectionFire(connection, value, old_value)
			end
		end
	end
	for connection in pairs(self.changed_listeners) do
		ConnectionFire(connection, "Set", path, value, old_value)
	end

end

function Replica:SetValues(path: {string}, values: {[string]: any})

	if ReplicationFlag ~= true then
		error(`[{script.Name}]: "SetValues()" can't be called outside of WriteLibs client-side`)
	end

	-- Apply local change:

	local pointer = self.Data
	for _, key in ipairs(path) do
		pointer = pointer[key]
	end
	for key, value in pairs(values) do
		pointer[key] = value
	end

	-- Firing signals:

	for connection in pairs(self.changed_listeners) do
		ConnectionFire(connection, "SetValues", path, values)
	end

end

function Replica:TableInsert(path: {string}, value: any, index: number?): number

	if ReplicationFlag ~= true then
		error(`[{script.Name}]: "TableInsert()" can't be called outside of WriteLibs client-side`)
	end

	-- Apply local change:

	local pointer = self.Data
	for _, key in ipairs(path) do
		pointer = pointer[key]
	end
	if index ~= nil then
		table.insert(pointer, index, value)
	else
		table.insert(pointer, value)
		index = #pointer
	end

	-- Firing signals:

	if next(self.table_insert_listeners) ~= nil then
		local listeners = self.table_insert_listeners[table.concat(path, ".")]
		if listeners ~= nil then
			for connection in pairs(listeners) do
				ConnectionFire(connection, value, index)
			end
		end
	end
	for connection in pairs(self.changed_listeners) do
		ConnectionFire(connection, "TableInsert", path, value, index)
	end

	return index

end

function Replica:TableRemove(path: {string}, index: number): any

	if ReplicationFlag ~= true then
		error(`[{script.Name}]: "TableRemove()" can't be called outside of WriteLibs client-side`)
	end

	-- Apply local change:

	local pointer = self.Data
	for _, key in ipairs(path) do
		pointer = pointer[key]
	end
	local removed_value = table.remove(pointer, index)

	-- Firing signals:

	if next(self.table_remove_listeners) ~= nil then
		local listeners = self.table_remove_listeners[table.concat(path, ".")]
		if listeners ~= nil then
			for connection in pairs(listeners) do
				ConnectionFire(connection, removed_value, index)
			end
		end
	end
	for connection in pairs(self.changed_listeners) do
		ConnectionFire(connection, "TableRemove", path, removed_value, index)
	end

	return removed_value

end

function Replica:Write(function_name: string, ...): ...any

	if ReplicationFlag ~= true then
		error(`[{script.Name}]: "Write()" can't be called outside of WriteLibs client-side`)
	end

	-- Apply local change:

	local write_lib_entry = self.write_lib[function_name]
	local return_params = table.pack(write_lib_entry.fn(self, ...))

	-- Firing signals:

	local listeners = self.write_listeners[function_name]
	if listeners ~= nil then
		for connection in pairs(listeners) do
			ConnectionFire(connection, ...)
		end
	end

	return table.unpack(return_params)

end

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

	if is_depth_call ~= true then
		if replica.Parent ~= nil then
			replica.Parent.Children[replica] = nil
		end
	end

	local id = replica.Id

	-- Clear replica references:
	local token_replicas = TokenReplicas[replica.Token]
	if token_replicas ~= nil then
		token_replicas[replica] = nil
	end
	if Replicas[id] == replica then
		Replicas[id] = nil
	end
	if BindReplicas[id] == replica then
		BindReplicas[id] = nil
	end
	-- Cleanup:
	replica.Maid:Unlock(MAID_LOCK)
	replica.Maid:Cleanup()
	-- Bind cleanup:
	replica.BoundInstance = nil
end

local function ReplicaToBindBuffer(replica, is_depth_call)

	-- Copy replica group:

	local copy_replica = ReplicaNew(replica.Id, replica.self_creation)
	BindReplicas[replica.Id] = copy_replica

	for group_replica in pairs(replica.Children) do
		ReplicaToBindBuffer(group_replica, true)
	end

	-- Destroy original:

	if is_depth_call ~= true then
		DestroyReplica(replica)
	end

	return copy_replica

end

local function ReplicaFromBindBuffer(replica, announce_buffer)

	local top_call = false

	if announce_buffer == nil then
		top_call = true
		announce_buffer = {}
	end

	BindReplicas[replica.Id] = nil

	local token = replica.Token
	local token_replicas = TokenReplicas[token]

	if token_replicas == nil then
		token_replicas = {}
		TokenReplicas[token] = token_replicas
	end

	token_replicas[replica] = true
	Replicas[replica.Id] = replica

	table.insert(announce_buffer, replica)

	for group_replica in pairs(replica.Children) do
		ReplicaFromBindBuffer(group_replica, announce_buffer)
	end

	if top_call == true then
		for _, replica in ipairs(announce_buffer) do
			local listeners = NewReplicaListeners[replica.Token]
			if listeners ~= nil then
				for connection in pairs(listeners) do
					ConnectionFire(connection, replica)
				end
			end
		end
	end

end

local function CreationScan(nested_creation, iterator, parent_id)
	local entries = nested_creation[parent_id]
	if entries ~= nil then

		table.sort(entries, function(a, b)
			return a.Id < b.Id
		end)

		for _, entry in ipairs(entries) do
			iterator(entry.Id, entry.SelfCreation)
			CreationScan(nested_creation, iterator, entry.Id)
		end

	end
end

local function BreadthCreationSort(creation: {}, root_id: number?, iterator: (Id: number, SelfCreation: table) -> ())

	-- self_creation = {token, tags, data, parent_id, write_module}
	local top_creation = {} -- {Id = id, SelfCreation = self_creation}, ...
	local nested_creation = {} -- [parent_id] = {{Id = id, SelfCreation = self_creation}, ...}, ...
	local error_creation = {} -- {Id = id, SelfCreation = self_creation}, ... -- Missing parents

	if type(creation[1]) == "table" then -- creation pack {creation, ...}

		for _, packed_creation in ipairs(creation) do

			for string_id, self_creation in pairs(packed_creation) do
				local entry = {Id = tonumber(string_id), SelfCreation = self_creation}
				local parent_id = self_creation[4]
				if parent_id == 0 or entry.Id == root_id then
					table.insert(top_creation, entry)
				elseif packed_creation[tostring(parent_id)] ~= nil then
					local entries = nested_creation[parent_id]
					if entries == nil then
						entries = {}
						nested_creation[parent_id] = entries
					end
					table.insert(entries, entry)
				else
					table.insert(error_creation, entry)
				end
			end

		end

	else

		for string_id, self_creation in pairs(creation) do
			local entry = {Id = tonumber(string_id), SelfCreation = self_creation}
			local parent_id = self_creation[4]
			if parent_id == 0 or entry.Id == root_id then
				table.insert(top_creation, entry)
			elseif creation[tostring(parent_id)] ~= nil then
				local entries = nested_creation[parent_id]
				if entries == nil then
					entries = {}
					nested_creation[parent_id] = entries
				end
				table.insert(entries, entry)
			else
				table.insert(error_creation, entry)
			end
		end

	end

	table.sort(top_creation, function(a, b)
		return a.Id < b.Id
	end)

	local result = {}

	for _, entry in ipairs(top_creation) do
		iterator(entry.Id, entry.SelfCreation)
		CreationScan(nested_creation, iterator, entry.Id)
	end

	if #error_creation ~= 0 then -- An error occured while replicating a replica group.

		local msg = `[{script.Name}]: GROUP REPLICATION ERROR - Missing parents for:\n`

		for i = 1, math.min(#error_creation, 50) do
			local entry = error_creation[i]
			local self_creation = entry.SelfCreation
			local tag_string = ""
			local first_tag = true
			for key, value in pairs(self_creation[2]) do
				tag_string ..= `{if first_tag == true then "" else ";"}{tostring(key)}={tostring(value)}`
				first_tag = false
			end
			msg ..= `[Id:{entry.Id};ParentId:{self_creation[4]};Token:{self_creation[1]};Tags:\{{tag_string}\}]\n`
		end

		if #error_creation > 50 then
			msg ..= `(hiding {50 - #error_creation} more)\n`
		end

		msg ..= "Traceback:\n" .. debug.traceback()

		warn(msg)

	end

	return result

end

local function GetInternalReplica(id)
	local replica = Replicas[id] or BindReplicas[id]
	if replica == nil then
		error(`[{script.Name}]: Received update for missing replica [Id:{id}]`)
	end
	return replica
end

----- Init -----

RemoteRequestData.OnClientEvent:Connect(function()

	if Replica.IsReady == true then
		return
	end

	Replica.IsReady = true
	print(`[{script.Name}]: Initial data received`)
	Replica.OnLocalReady:Fire()

end)

RemoteSet.OnClientEvent:Connect(function(id: number, path: {}, value: any)
	local replica = GetInternalReplica(id)
	ReplicationFlag = true
	local success, msg = pcall(replica.Set, replica, path, value)
	ReplicationFlag = false
	if success ~= true then
		error(`[{script.Name}]: Error while updating replica:\n{replica:Identify()}\n` .. msg)
	end
end)

RemoteSetValues.OnClientEvent:Connect(function(id: number, path: {}, values: {})
	local replica = GetInternalReplica(id)
	ReplicationFlag = true
	local success, msg = pcall(replica.SetValues, replica, path, values)
	ReplicationFlag = false
	if success ~= true then
		error(`[{script.Name}]: Error while updating replica:\n{replica:Identify()}\n` .. msg)
	end
end)

RemoteTableInsert.OnClientEvent:Connect(function(id: number, path: {}, value: any, index: number?)
	local replica = GetInternalReplica(id)
	ReplicationFlag = true
	local success, msg = pcall(replica.TableInsert, replica, path, value, index)
	ReplicationFlag = false
	if success ~= true then
		error(`[{script.Name}]: Error while updating replica:\n{replica:Identify()}\n` .. msg)
	end
end)

RemoteTableRemove.OnClientEvent:Connect(function(id: number, path: {}, index: number)
	local replica = GetInternalReplica(id)
	ReplicationFlag = true
	local success, msg = pcall(replica.TableRemove, replica, path, index)
	ReplicationFlag = false
	if success ~= true then
		error(`[{script.Name}]: Error while updating replica:\n{replica:Identify()}\n` .. msg)
	end
end)

RemoteWrite.OnClientEvent:Connect(function(id: number, fn_id: number, ...)
	local replica = GetInternalReplica(id)
	local fn_name = replica.write_lib[fn_id].Name
	ReplicationFlag = true
	local success, msg = pcall(replica.Write, replica, fn_name, ...)
	ReplicationFlag = false
	if success ~= true then
		error(`[{script.Name}]: Error while updating replica:\n{replica:Identify()}\n` .. msg)
	end
end)

local function RemoteSignalHandle(id: number, ...)
	local replica = GetInternalReplica(id)
	replica.OnClientEvent:Fire(...)
end

RemoteSignal.OnClientEvent:Connect(RemoteSignalHandle)
RemoteSignalUnreliable.OnClientEvent:Connect(RemoteSignalHandle)

RemoteParent.OnClientEvent:Connect(function(id: number, parent_id: number)

	local replica = GetInternalReplica(id)
	local old_parent = replica.Parent
	local new_parent = GetInternalReplica(parent_id)

	old_parent.Children[replica] = nil
	new_parent.Children[replica] = true
	replica.Parent = new_parent
	replica.self_creation[4] = parent_id

	if BindReplicas[old_parent.Id] ~= nil and Replicas[parent_id] ~= nil then
		-- Replica streaming in:
		ReplicaFromBindBuffer(replica)
	elseif Replicas[old_parent.Id] ~= nil and BindReplicas[parent_id] ~= nil then
		-- Replica streaming out:
		ReplicaToBindBuffer(replica)
	end

end)

RemoteCreate.OnClientEvent:Connect(function(creation: {}, root_id: number?) -- (creation) or ({creation, ...})

	local announce_buffer = {} -- {replica, ...} -- Announce these

	BreadthCreationSort(creation, root_id, function(id: number, self_creation: {}) -- self_creation = {token, tags, data, parent_id, write_module}

		local parent_id = self_creation[4]
		local replica = ReplicaNew(id, self_creation)
		local is_bind_buffered = false

		if parent_id == 0 then -- Top replica

			if replica.Tags[BIND_TAG] == true then
				local bound_instance = BindInstances[id]
				replica.BoundInstance = bound_instance

				if bound_instance == nil then
					is_bind_buffered = true
				end
			end

		elseif BindReplicas[parent_id] ~= nil then

			is_bind_buffered = true

		end

		if is_bind_buffered == true then

			BindReplicas[id] = replica

		else

			local token = replica.Token
			local token_replicas = TokenReplicas[token]

			if token_replicas == nil then
				token_replicas = {}
				TokenReplicas[token] = token_replicas
			end

			token_replicas[replica] = true
			Replicas[id] = replica

			table.insert(announce_buffer, replica)

		end

	end)

	for _, replica in ipairs(announce_buffer) do
		local listeners = NewReplicaListeners[replica.Token]
		if listeners ~= nil then
			for connection in pairs(listeners) do
				ConnectionFire(connection, replica)
			end
		end
	end

end)

RemoteBind.OnClientEvent:Connect(function(id: number)

	local replica = GetInternalReplica(id)
	replica.Tags[BIND_TAG] = true

	local bound_instance = BindInstances[id]
	replica.BoundInstance = bound_instance

	if bound_instance == nil then
		ReplicaToBindBuffer(replica)
	end

end)

RemoteDestroy.OnClientEvent:Connect(function(id: number)
	local replica = GetInternalReplica(id)
	DestroyReplica(replica)
end)

-- Replica bind system using CollectionService:

local function OnBindInstanceAdded(instance: NumberValue)

	local id = instance.Value
	local bound_instance = instance.Parent
	BindInstances[id] = bound_instance

	local replica = BindReplicas[id]

	if replica ~= nil then
		replica.BoundInstance = bound_instance
		ReplicaFromBindBuffer(replica)
	end

end

local function OnBindInstanceRemoved(instance: NumberValue)

	local id = instance.Value
	BindInstances[id] = nil

	local replica = Replicas[id]

	if replica ~= nil then
		ReplicaToBindBuffer(replica)
	end

end

CollectionService:GetInstanceAddedSignal(CS_TAG):Connect(function(instance: NumberValue)
	if instance:IsA("NumberValue") == true then
		OnBindInstanceAdded(instance)
	end
end)

CollectionService:GetInstanceRemovedSignal(CS_TAG):Connect(function(instance: NumberValue)
	if instance:IsA("NumberValue") == true then
		OnBindInstanceRemoved(instance)
	end
end)

for _, instance: NumberValue in pairs(CollectionService:GetTagged(CS_TAG)) do
	if instance:IsA("NumberValue") == true then
		OnBindInstanceAdded(instance)
	end
end

return Replica

I also couldn’t help but notice that some of the types seem to be incorrect?
For instance, OnSet has this type:

OnSet: (self: any, path: {}, listener: () -> ()) -> (Connection),

While it should be (from what I understand)

OnSet: (self: any, path: {string}, listener: (value: any, old_value: any) -> ()) -> (Connection),

The Remote module also seems to make a type error in ReplicaClient.

2 Likes


everything works fine but i keep getting this error, how do i fix it?

you should listen to Replica.NewReadyPlayer signal so you are sure that your player is ready to get the data

This removed the error but now the replica doesnt work anymore
image