-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathReplicaService.lua
1081 lines (940 loc) · 36.3 KB
/
ReplicaService.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- local Madwork = _G.Madwork
--[[
{Madwork}
-[ReplicaService]---------------------------------------
(STANDALONE VERSION)
Lua table replication achieved through write function wrapping
Understanding ReplicaService requires in-depth knowledge of RemoteEvent API:
https://developer.roblox.com/en-us/articles/Remote-Functions-and-Events
WARNINGS FOR "Replica.Data" VALUES:
! Do not create numeric tables with gaps - attempting to replicate such tables will result in an error;
For UserId references, convert UserIds to strings for use as keys!
! Do not create mixed tables (some values indexed by number and others by string key), as only
the data indexed by number will be replicated.
! Do not index tables by anything other than numbers and strings.
! Do not reference functions
! Do not reference instances that are not replicated to clients
+ All types of userdata (Vector3, Color3, CFrame...) and Instances that are currently replicated
to the client will replicate through ReplicaService.
Members:
ReplicaService.ActivePlayers [table] {Player = true, ...} Players that have finished a handshake with ReplicaService
ReplicaService.NewActivePlayerSignal [ScriptSignal] (player)
ReplicaService.RemovedActivePlayerSignal [ScriptSignal] (player)
ReplicaService.Temporary [Replica] -- Non replicated replica for nested replica creation
ReplicaService.PlayerRequestedData [ScriptSignal] (player) -- Fired at the moment of player requesting data, before
-- replicating any replicas
Functions:
ReplicaService.NewClassToken(class_name) --> [ReplicaClassToken]
-- Class tokens prevent the developer from creating replica class name collisions
ReplicaService.NewReplica(replica_params) --> [Replica]
replica_params [table]:
{
ClassToken = replica_class_token, -- Primary replica identifier
-- Optional params:
Tags = {}, -- Secondary replica identifiers: {["tag_name"] = tag_value, ...}
Data = {}, -- Table to be replicated (Data is not deep copied - retains reference)
Replication = "All" or {[Player] = true, ...} or [Player], -- "Replication" and "Parent" are mutually exclusive
Parent = replica, -- If Parent is not provided, created Replica will be a top-level Replica
WriteLib = write_lib_module,
}
-- "Tags" and "Data" will default to empty tables;
-- "Replication" defaults to not replicated;
write_lib_module [ModuleScript] -- A shared write function library (ModuleScript Instance must be
replicated to clients); Create replicas with an assigned write_lib when network resources are limited.
Functions within write_lib receive numeric indexes and the functions themselves change the replica
data table through given parameters - this removes the need to send clients the "path" for data
updates thus greatly compressing packet size.
ReplicaService.CheckWriteLib(module_script) -- Run-time error check
module_script [ModuleScript] or nil -- nil will not error
Members [ReplicaClassToken]:
ReplicaClassToken.Class [string]
Members [Replica]:
Replica.Data [table] (Read only) Table which is replicated
Replica.Id [number] Unique identifier
Replica.Class [string] Primary Replica identifier
Replica.Tags [table] Secondary Replica identifiers
Replica.Parent [Replica] or nil
Replica.Children [table]: {replica, ...}
Methods [Replica]:
-- Dictionaries:
Replica:SetValue(path, value) -- !!! Avoid numeric tables with gaps
Replica:SetValues(path, values) -- values = {key = value, ...} !!! Avoid numeric tables with gaps
-- (Numeric) Arrays:
Replica:ArrayInsert(path, value) --> new_index -- Performs table.insert(path, value)
Replica:ArraySet(path, index, value) -- Can only set to an already existing index within the array
Replica:ArrayRemove(path, index) --> removed_value -- Performs table.remove(path, index)
path:
[string] = "TableMember.TableMember" -- Roblox-style path
[table] = {"Players", 2312310, "Health"} -- Key array path (Just use this always lol - string parsing is slow)
-- Write library:
Replica:Write(function_name, params...) --> return_params... -- Run write function with given parameters
on server and client-side; (For replicas constructed with write_lib)
-- Signals:
Replica:ConnectOnServerEvent(listener) --> [ScriptConnection] (player, params...) -- listener functions can't yield
Replica:FireClient(player, params...) -- Fire a signal to client-side listeners for this specific Replica
Replica:FireAllClients(params...)
-- Inheritance: (Only for descendant replicas; Can't create circular inheritance)
Replica:SetParent(replica)
-- Replication: (Only for top level replicas - child replicas inherit replication settings)
Replica:ReplicateFor("All")
Replica:ReplicateFor(player)
Replica:DestroyFor("All")
Replica:DestroyFor(player) -- WARNING: Don't selectively destroy for clients when replica is replicated to all;
You may only selectively destroy for clients if the replica was selectively replicated to clients
-- Debug:
Replica:Identify() --> [string]
-- Cleanup:
Replica:IsActive() --> is_active [bool] -- Returns false if the replica was destroyed
Replica:AddCleanupTask(task) -- Add cleanup task to be performed
Replica:RemoveCleanupTask(task) -- Remove cleanup task
Replica:Destroy() -- Destroys replica and all of its descendants (Depth-first)
task:
[function] -- Function to be invoked when the Replica is destroyed (function can't yield)
[RBXScriptConnection] -- Roblox script connection to be :Disconnect()'ed when the Replica is destroyed
[Object] -- Object with a :Destroy() method to be destroyed when the Replica is destroyed (destruction method can't yield)
--]]
local SETTINGS = {
}
local Madwork -- Standalone Madwork reference for portable version of ReplicaService/ReplicaController
do
local RunService = game:GetService("RunService")
local function WaitForDescendant(ancestor, instance_name, warn_name)
local instance = ancestor:FindFirstChild(instance_name, true) -- Recursive
if instance == nil then
local start_time = os.clock()
local connection
connection = ancestor.DescendantAdded:Connect(function(descendant)
if descendant.Name == instance_name then
instance = descendant
end
end)
while instance == nil do
if start_time ~= nil and os.clock() - start_time > 1
and (RunService:IsServer() == true or game:IsLoaded() == true) then
start_time = nil
warn("[" .. script.Name .. "]: Missing " .. warn_name .. " \"" .. instance_name
.. "\" in " .. ancestor:GetFullName() .. "; Please check setup documentation")
end
task.wait()
end
connection:Disconnect()
return instance
else
return instance
end
end
local RemoteEventContainer
if RunService:IsServer() == true then
RemoteEventContainer = Instance.new("Folder")
RemoteEventContainer.Name = "ReplicaRemoteEvents"
RemoteEventContainer.Parent = game:GetService("ReplicatedStorage")
else
RemoteEventContainer = WaitForDescendant(game:GetService("ReplicatedStorage"), "ReplicaRemoteEvents", "folder")
end
Madwork = {
GetShared = function(package_name, item_name)
-- Ignoring package_name as we're working without Madwork framework
return WaitForDescendant(game:GetService("ReplicatedStorage"), item_name, "module")
end,
GetModule = function(package_name, module_name)
return WaitForDescendant(game:GetService("ServerScriptService"), module_name, "module")
end,
SetupRemoteEvent = function(remote_name)
if RunService:IsServer() == true then
local remote_event = Instance.new("RemoteEvent")
remote_event.Name = remote_name
remote_event.Parent = RemoteEventContainer
return remote_event
else
return WaitForDescendant(RemoteEventContainer, remote_name, "remote event")
end
end,
Shared = {}, -- A Madwork package reference - ReplicaService will try to check this table
}
local MadworkScriptSignal = require(Madwork.GetShared("Madwork", "MadworkScriptSignal"))
Madwork.NewScriptSignal = MadworkScriptSignal.NewScriptSignal
Madwork.NewArrayScriptConnection = MadworkScriptSignal.NewArrayScriptConnection
end
----- Service Table -----
local ReplicaService = {
ActivePlayers = {}, -- {Player = true, ...}
NewActivePlayerSignal = Madwork.NewScriptSignal(), -- (player)
RemovedActivePlayerSignal = Madwork.NewScriptSignal(), -- (player)
PlayerRequestedData = Madwork.NewScriptSignal(), -- (player)
_replicas = {
--[[
[replica_id] = {
Data = {}, -- [table] Replicated Replica data table
Id = 1 -- [integer] (Read-only) Replica id
Class = "", -- [string] Primary Replica identifier
Tags = {PlayerId = 2312310}, -- [table] Secondary Replica identifiers
Parent = Replica, -- [Replica / nil] -- Child replicas inherit replication settings
Children = {}, -- [table] {replica, ...}
_creation_data = {["replica_id"] = {replica_class, replica_tags, data_table, parent_id / 0, write_lib_module / nil}, ...},
-- [table] A prepared table of all data a client will receive to construct this Replica and all it's descendants client-side
-- (Reference to top ancestor _creation_data table if child replica)
_replication = {}, -- [table] Selective replication settings (Reference to top ancestor _replication table if child replica)
-- Possible settings:
-- _replication = {["All"] = true} -- Replica will be replicated to all players
-- _replication = {Player = true, ...} -- Replica will be replicated to selected players
-- _replication = {} -- Replica is currently not replicated to anyone
_pending_replication = {}, -- [table] Selective replication to players who are not fully loaded in
_write_lib = {["function_name"] = {func_id, function}, ...} / nil, -- [table] List of wrapped write functions
_signal_listeners = {},
_maid = maid,
},
...
--]]
},
_top_level_replicas = { -- References to top level replicas for decreased load when handling new and leaving players
--[[
[replica_id] = Replica, -- [Replica]
...
--]]
},
_replica_class = nil, -- Injection hook
}
----- Loaded Services & Modules -----
local RateLimiter = require(Madwork.GetShared("Madwork", "RateLimiter"))
local MadworkMaid = require(Madwork.GetShared("Madwork", "MadworkMaid"))
----- Private Variables -----
local DefaultRateLimiter = RateLimiter.Default
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ActivePlayers = ReplicaService.ActivePlayers
local Replicas = ReplicaService._replicas
local TopLevelReplicas = ReplicaService._top_level_replicas
local rev_ReplicaRequestData = Madwork.SetupRemoteEvent("Replica_ReplicaRequestData") -- Fired client-side when the client loads for the first time
local rev_ReplicaSetValue = Madwork.SetupRemoteEvent("Replica_ReplicaSetValue") -- (replica_id, {path}, value)
local rev_ReplicaSetValues = Madwork.SetupRemoteEvent("Replica_ReplicaSetValues") -- (replica_id, {path}, {values})
local rev_ReplicaArrayInsert = Madwork.SetupRemoteEvent("Replica_ReplicaArrayInsert") -- (replica_id, {path}, value)
local rev_ReplicaArraySet = Madwork.SetupRemoteEvent("Replica_ReplicaArraySet") -- (replica_id, {path}, index, value)
local rev_ReplicaArrayRemove = Madwork.SetupRemoteEvent("Replica_ReplicaArrayRemove") -- (replica_id, {path}, index)
local rev_ReplicaWrite = Madwork.SetupRemoteEvent("Replica_ReplicaWrite") -- (replica_id, func_id, params...)
local rev_ReplicaSignal = Madwork.SetupRemoteEvent("Replica_ReplicaSignal") -- (replica_id, params...)
local rev_ReplicaSetParent = Madwork.SetupRemoteEvent("Replica_ReplicaSetParent") -- (replica_id, parent_replica_id)
local rev_ReplicaCreate = Madwork.SetupRemoteEvent("Replica_ReplicaCreate") -- (replica_id, {replica_data}) OR (top_replica_id, {creation_data}) or ({replica_package})
local rev_ReplicaDestroy = Madwork.SetupRemoteEvent("Replica_ReplicaDestroy") -- (replica_id)
local ReplicaIndex = 0
local LoadedWriteLibs = {} -- {[ModuleScript] = {["function_name"] = {func_id, function}, ...}, ...}
local WriteFunctionFlag = false
local CreatedClassTokens = {} -- [class_name] = true
local LockReplicaMethods = {} -- A metatable to be set for destroyed replicas
LockReplicaMethods.__index = LockReplicaMethods
----- Private functions -----
local function ParseReplicaBranch(replica, func) -- func(replica)
func(replica)
for _, child in ipairs(replica.Children) do
ParseReplicaBranch(child, func)
func(child)
end
end
local function GetWriteLibFunctionsRecursive(list_table, pointer, name_stack)
for key, value in pairs(pointer) do
if type(value) == "table" then
GetWriteLibFunctionsRecursive(list_table, value, name_stack .. key .. ".")
elseif type(value) == "function" then
table.insert(list_table, {name_stack .. key, value})
else
error("[ReplicaService]: Invalid write function value \"" .. tostring(value) .. "\" (" .. typeof(value) .. "); name_stack = \"" .. name_stack .. "\"")
end
end
end
local function LoadWriteLib(write_lib_module)
local get_write_lib = LoadedWriteLibs[write_lib_module]
if get_write_lib ~= nil then
return get_write_lib -- Write lib module was previously loaded
end
if write_lib_module.ClassName ~= "ModuleScript" then
error("[ReplicaService]: Invalid write_lib_module argument")
end
if write_lib_module:IsDescendantOf(ReplicatedStorage) == false then
local found_in_shared = false
for _, dir in pairs(Madwork.Shared) do
if write_lib_module:IsDescendantOf(dir) == true then
found_in_shared = true
break
end
end
if found_in_shared == false then
error("[ReplicaService]: Write library module must be a descendant of ReplicatedStorage or \"Shared\" directory")
end
end
local write_lib_raw = require(write_lib_module)
if type(write_lib_raw) ~= "table" then
error("[ReplicaService]: A write library ModuleScript must return a table")
end
local function_list = {} -- func_id = {func_name, func}
GetWriteLibFunctionsRecursive(function_list, write_lib_raw, "")
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)
local write_lib = {} -- {["function_name"] = {func_id, function}, ...}
for func_id, func_params in ipairs(function_list) do
write_lib[func_params[1]] = {func_id, func_params[2]}
end
LoadedWriteLibs[write_lib_module] = write_lib
return write_lib
end
local function StringPathToArray(path)
local path_array = {}
if path ~= "" then
for s in string.gmatch(path, "[^%.]+") do
table.insert(path_array, s)
end
end
return path_array
end
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
-- Swap metatables:
setmetatable(replica, LockReplicaMethods)
end
----- Public functions -----
-- Replica object:
local Replica = {}
Replica.__index = Replica
ReplicaService._replica_class = Replica
-- Dictionaries:
function Replica:SetValue(path, value)
local path_array = (type(path) == "string") and StringPathToArray(path) or path
-- Apply change server-side:
local pointer = self.Data
for i = 1, #path_array - 1 do
pointer = pointer[path_array[i]]
end
pointer[path_array[#path_array]] = value
-- Replicate change:
if WriteFunctionFlag == false then
local id = self.Id
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaSetValue:FireClient(player, id, path_array, value)
end
else
for player in pairs(self._replication) do
rev_ReplicaSetValue:FireClient(player, id, path_array, value)
end
end
end
end
function Replica:SetValues(path, values)
local path_array = (type(path) == "string") and StringPathToArray(path) or path
-- Apply change server-side:
local pointer = self.Data
for i = 1, #path_array do
pointer = pointer[path_array[i]]
end
for key, value in pairs(values) do
pointer[key] = value
end
-- Replicate change:
if WriteFunctionFlag == false then
local id = self.Id
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaSetValues:FireClient(player, id, path_array, values)
end
else
for player in pairs(self._replication) do
rev_ReplicaSetValues:FireClient(player, id, path_array, values)
end
end
end
end
-- (Numeric) Arrays:
function Replica:ArrayInsert(path, value) --> new_index
local path_array = (type(path) == "string") and StringPathToArray(path) or path
-- Apply change server-side:
local pointer = self.Data
for i = 1, #path_array do
pointer = pointer[path_array[i]]
end
table.insert(pointer, value)
-- Replicate change:
if WriteFunctionFlag == false then
local id = self.Id
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaArrayInsert:FireClient(player, id, path_array, value)
end
else
for player in pairs(self._replication) do
rev_ReplicaArrayInsert:FireClient(player, id, path_array, value)
end
end
end
return #pointer
end
function Replica:ArraySet(path, index, value)
local path_array = (type(path) == "string") and StringPathToArray(path) or path
-- Apply change server-side:
local pointer = self.Data
for i = 1, #path_array do
pointer = pointer[path_array[i]]
end
if pointer[index] ~= nil then
pointer[index] = value
else
error("[ReplicaService]: Replica:ArraySet() can only be used for existing indexes")
end
-- Replicate change:
if WriteFunctionFlag == false then
local id = self.Id
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaArraySet:FireClient(player, id, path_array, index, value)
end
else
for player in pairs(self._replication) do
rev_ReplicaArraySet:FireClient(player, id, path_array, index, value)
end
end
end
end
function Replica:ArrayRemove(path, index) --> removed_value
local path_array = (type(path) == "string") and StringPathToArray(path) or path
-- Apply change server-side:
local pointer = self.Data
for i = 1, #path_array do
pointer = pointer[path_array[i]]
end
local removed_value = table.remove(pointer, index)
-- Replicate change:
if WriteFunctionFlag == false then
local id = self.Id
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaArrayRemove:FireClient(player, id, path_array, index)
end
else
for player in pairs(self._replication) do
rev_ReplicaArrayRemove:FireClient(player, id, path_array, index)
end
end
end
return removed_value
end
-- Write library:
function Replica:Write(function_name, ...) --> return_params...
if WriteFunctionFlag == true then -- Chained :Write()
return self._write_lib[function_name][2](self, ...)
end
-- Apply change server-side:
WriteFunctionFlag = true
local return_params = table.pack(self._write_lib[function_name][2](self, ...))
WriteFunctionFlag = false
-- Replicate change:
local id = self.Id
local func_id = self._write_lib[function_name][1]
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaWrite:FireClient(player, id, func_id, ...)
end
else
for player in pairs(self._replication) do
rev_ReplicaWrite:FireClient(player, id, func_id, ...)
end
end
return table.unpack(return_params)
end
-- Signals:
function Replica:ConnectOnServerEvent(listener) --> [ScriptConnection]
if type(listener) ~= "function" then
error("[ReplicaService]: Only functions can be passed to Replica:ConnectOnServerEvent()")
end
table.insert(self._signal_listeners, listener)
return Madwork.NewArrayScriptConnection(self._signal_listeners, listener)
end
function Replica:FireClient(player, ...)
if (self._replication["All"] == true and ActivePlayers[player] == true)
or self._replication[player] ~= nil then
rev_ReplicaSignal:FireClient(player, self.Id, ...)
end
end
function Replica:FireAllClients(...)
local id = self.Id
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaSignal:FireClient(player, id, ...)
end
else
for player in pairs(self._replication) do
rev_ReplicaSignal:FireClient(player, id, ...)
end
end
end
-- Inheritance:
function Replica:SetParent(new_parent)
assert(type(new_parent) == "table", "[ReplicaService]: Invalid parent_replica")
assert(new_parent._replication ~= nil, "[ReplicaService]: Invalid parent_replica")
local circular_check = new_parent
while circular_check ~= nil do
circular_check = circular_check.Parent
if circular_check == self then
error("[ReplicaService]: Can't parent replica to it's descendant")
end
end
local old_parent = self.Parent
if old_parent == nil then
error("[ReplicaService]: Can't change parent for top level replicas")
end
if new_parent == old_parent then
return
end
local replica_id = self.Id
self.Parent = new_parent
table.remove(old_parent.Children, table.find(old_parent.Children, self))
table.insert(new_parent.Children, self)
local old_replication = old_parent._replication
local new_replication = new_parent._replication
if old_replication ~= new_replication then -- Top level ancestor changed:
local old_creation_data = old_parent._creation_data
local new_creation_data = new_parent._creation_data
self._creation_data = new_creation_data
-- Create temporary creation data:
local temporary_creation_data = {} -- [string_id] = creation_data_of_one
ParseReplicaBranch(self, function(transfered_replica)
local replica_id_string = tostring(transfered_replica.Id)
temporary_creation_data[replica_id_string] = old_creation_data[replica_id_string]
transfered_replica._replication = new_replication -- Swapping _replication reference for reparented replicas
end)
temporary_creation_data[tostring(replica_id)][4] = new_parent.Id
-- Modify creation data for top replicas:
for string_id, creation_data_of_one in pairs(temporary_creation_data) do
old_creation_data[string_id] = nil
new_creation_data[string_id] = creation_data_of_one
end
-- Inform clients about the change:
-- 1) Clients who have this replica AND the new parent replica only need to know the new parent id
local no_replication_check = ActivePlayers
if new_replication.All ~= true then
no_replication_check = new_replication
elseif old_replication.All ~= true then
no_replication_check = old_replication
end
for player in pairs(no_replication_check) do
if (old_replication[player] == true or old_replication.All == true) and (new_replication[player] == true or new_replication.All == true) then
rev_ReplicaSetParent:FireClient(player, replica_id, new_parent.Id)
end
end
-- 2) Create for clients that have the new parent replica, but not the old parent replica
local replicate_for_players = {}
if old_replication.All ~= true then
if new_replication.All ~= true then
for player in pairs(new_replication) do
if old_replication[player] ~= true then
replicate_for_players[player] = true
end
end
else
for player in pairs(ActivePlayers) do
if old_replication[player] ~= true then
replicate_for_players[player] = true
end
end
end
end
for player in pairs(replicate_for_players) do
rev_ReplicaCreate:FireClient(player, replica_id, temporary_creation_data)
end
-- 3) Destroy for clients that do not have the new parent replica
if new_replication.All ~= true then
if old_replication.All ~= true then
for player in pairs(old_replication) do
if new_replication[player] ~= true then
rev_ReplicaDestroy:FireClient(player, replica_id)
end
end
else
for player in pairs(ActivePlayers) do
if new_replication[player] ~= true then
rev_ReplicaDestroy:FireClient(player, replica_id)
end
end
end
end
else -- Top level ancestor did not change:
self._creation_data[tostring(replica_id)][4] = new_parent.Id
if old_replication.All == true then
for player in pairs(ActivePlayers) do
rev_ReplicaSetParent:FireClient(player, replica_id, new_parent.Id)
end
else
for player in pairs(old_replication) do
rev_ReplicaSetParent:FireClient(player, replica_id, new_parent.Id)
end
end
end
end
-- Replication:
function Replica:ReplicateFor(param)
if self.Parent ~= nil then
error("[ReplicaService]: Replica:ReplicateFor() can only be used for top level replicas")
end
if Replicas[self.Id] == nil then
error("[ReplicaService]: Can't change replication settings for a destroyed replica")
end
local replication = self._replication
local pending_replication = self._pending_replication
if replication["All"] ~= true then
if param == "All" then
-- Create replica for clients who weren't replicated to yet:
local id = self.Id
for player in pairs(ActivePlayers) do
if replication[player] == nil then
rev_ReplicaCreate:FireClient(player, id, self._creation_data)
end
end
-- Clear selective replication settings:
for player in pairs(replication) do
replication[player] = nil
end
for player in pairs(pending_replication) do
pending_replication[player] = nil
end
-- Set replication to all:
replication["All"] = true
elseif ActivePlayers[param] == true then
if replication[param] == nil then
-- Create replica for client:
replication[param] = true
rev_ReplicaCreate:FireClient(param, self.Id, self._creation_data)
end
elseif typeof(param) == "Instance" then
if param.ClassName ~= "Player" then
error("[ReplicaService]: Invalid param argument")
end
pending_replication[param] = true
end
else
if param ~= "All" then
error("[ReplicaService]: Don't selectively replicate for clients when replica is replicated to All - :DestroyFor(\"All\") first")
end
end
end
function Replica:DestroyFor(param)
if self.Parent ~= nil then
error("[ReplicaService]: Replica:DestroyFor() can only be used for top level replicas")
end
if Replicas[self.Id] == nil then
error("[ReplicaService]: Can't change replication settings for a destroyed replica")
end
local replication = self._replication
if replication[param] ~= nil and ActivePlayers[param] == true then
-- Destroy replica for client:
replication[param] = nil
rev_ReplicaDestroy:FireClient(param, self.Id)
elseif param == "All" then
local id = self.Id
if replication["All"] == true then
-- Destroy replica for all active clients:
replication["All"] = nil
for player in pairs(ActivePlayers) do
rev_ReplicaDestroy:FireClient(player, id)
end
else
-- Destroy replica for all clients who were replicated to:
for player in pairs(replication) do
replication[player] = nil
rev_ReplicaDestroy:FireClient(player, id)
end
end
elseif replication["All"] == true then -- Don't do this
error("[ReplicaService]: Don't selectively destroy for clients when replica is replicated to All")
elseif typeof(param) == "Instance" then
if param.ClassName ~= "Player" then
error("[ReplicaService]: Invalid param argument")
end
self._pending_replication[param] = nil
end
end
-- Debug:
function Replica:Identify() --> [string]
local tag_string = ""
local first_tag = true
for tag_key, tag_val in pairs(self.Tags) do
tag_string = tag_string .. (first_tag and "" or ";") .. tostring(tag_key) .. "=" .. tostring(tag_val)
end
return "[Id:" .. tostring(self.Id) .. ";Class:" .. self.Class .. ";Tags:{" .. tag_string .. "}]"
end
-- Cleanup:
function Replica:IsActive() --> is_active [bool]
return Replicas[self.Id] ~= nil
end
function Replica:AddCleanupTask(task)
return self._maid:AddCleanupTask(task)
end
function Replica:RemoveCleanupTask(task)
self._maid:RemoveCleanupTask(task)
end
function Replica:Destroy()
-- Destroy replica for all clients who were replicated to:
local id = self.Id
if Replicas[id] == nil then
return
end
if self._replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaDestroy:FireClient(player, id)
end
else
for player in pairs(self._replication) do
rev_ReplicaDestroy:FireClient(player, id)
end
end
local class_name = self.Class
CreatedClassTokens[class_name] = false
-- Recursive destruction
DestroyReplicaAndDescendantsRecursive(self)
end
-- Module functions:
function ReplicaService.NewClassToken(class_name) --> [ReplicaClassToken]
if type(class_name) ~= "string" then
error("[ReplicaService]: class_name must be a string")
end
if CreatedClassTokens[class_name] == true then
error("[ReplicaService]: Token for replica class \"" .. class_name .. "\" was already created")
end
CreatedClassTokens[class_name] = true
return {
Class = class_name
}
end
function ReplicaService.ReturnClassToken(class_name) --> [ReplicaClassToken]
if type(class_name) ~= "string" then
error("[ReplicaService]: class_name must be a string")
end
if CreatedClassTokens[class_name] == true then
return {
Class = class_name
}
end
end
function ReplicaService.NewReplica(replica_params) --> [Replica]
local class_token = replica_params.ClassToken
local replica_tags = replica_params.Tags or {}
local data_table = replica_params.Data or {}
local replication_settings = replica_params.Replication
if type(class_token) ~= "table" or type(class_token.Class) ~= "string" then
error("[ReplicaService]: missing or invalid replica_params.ClassToken argument")
end
if type(replica_tags) ~= "table" then
error("[ReplicaService]: replica_params.Tags must be a table")
end
if type(data_table) ~= "table" then
error("[ReplicaService]: replica_params.Data must be a table")
end
local replica_class = class_token.Class
ReplicaIndex = ReplicaIndex + 1
local parent = replica_params.Parent
local replication
local pending_replication
local creation_data
if parent ~= nil then
if Replicas[parent.Id] == nil then
error("[ReplicaService]: Passed replica_params.Parent replica is destroyed")
end
end
if parent ~= nil and replication_settings ~= nil then
error("[ReplicaService]: Can't set replica_params.Replication for a replica that has a parent")
elseif replication_settings == nil then
replication = {}
pending_replication = {}
else
-- Parsing replica_params.Replication:
if type(replication_settings) == "table" then -- Must be a player list {player = true, ...} OR an empty table {}
if replication_settings["All"] ~= nil then
error("[ReplicaService]: To replicate replica to all, do replica_params.Replication = \"All\"")
end
replication = {}
pending_replication = {}
for player in pairs(replication_settings) do
if typeof(player) ~= "Instance" or player.ClassName ~= "Player" then
error("[ReplicaService]: Invalid replica_params.Replication")
end
if ActivePlayers[player] == true then
replication[player] = true
else
pending_replication[player] = true
end
end
elseif replication_settings == "All" then
replication = {["All"] = true}
pending_replication = {}
elseif typeof(replication_settings) == "Instance" then -- Must be a player
if replication_settings.ClassName == "Player" then
if ActivePlayers[replication_settings] == true then
replication = {[replication_settings] = true}
pending_replication = {}
else
replication = {}
pending_replication = {[replication_settings] = true}
end
else
error("[ReplicaService]: Invalid value for param1")
end
else
error("[ReplicaService]: Invalid value for replica_params.Replication (" .. tostring(replication_settings) .. ")")
end
end
-- Load write_lib_module if present:
local write_lib = nil
if replica_params.WriteLib ~= nil then
write_lib = LoadWriteLib(replica_params.WriteLib)
end
-- Getting references to parent replication and creation data:
if parent ~= nil then
replication = parent._replication
pending_replication = parent._pending_replication
creation_data = parent._creation_data
else
creation_data = {}
end
local creation_data_of_one = {replica_class, replica_tags, data_table, (parent ~= nil) and parent.Id or 0, replica_params.WriteLib}
creation_data[tostring(ReplicaIndex)] = creation_data_of_one
-- New Replica object table:
local replica = {
Data = data_table,
Id = ReplicaIndex,
Class = replica_class,
Tags = replica_tags,
Parent = parent,
Children = {},
_creation_data = creation_data,
_replication = replication,
_pending_replication = pending_replication,
_write_lib = write_lib,
_signal_listeners = {},
_maid = MadworkMaid.NewMaid(),
}
setmetatable(replica, Replica)
if parent ~= nil then
table.insert(parent.Children, replica)
end
-- Replicating new replica:
if replication["All"] == true then
for player in pairs(ActivePlayers) do
rev_ReplicaCreate:FireClient(player, ReplicaIndex, creation_data_of_one)
end
else
for player in pairs(replication) do
rev_ReplicaCreate:FireClient(player, ReplicaIndex, creation_data_of_one)
end
end
-- Adding replica to replica list:
Replicas[ReplicaIndex] = replica
if parent == nil then
TopLevelReplicas[ReplicaIndex] = replica
end
return replica
end
function ReplicaService.CheckWriteLib(module_script)
if module_script ~= nil then
LoadWriteLib(module_script)
end
end
----- Initialize -----
-- Creating LockReplicaMethods members:
do
local keep_methods = {
Identify = true,
AddCleanupTask = true,
RemoveCleanupTask = true,
Destroy = true,
IsActive = true,
}
for method_name, func in pairs(Replica) do
if method_name ~= "__index" then
if keep_methods[method_name] == true then