-
Notifications
You must be signed in to change notification settings - Fork 1
/
ReplicatedPseudoInstance.lua
299 lines (239 loc) · 9.09 KB
/
ReplicatedPseudoInstance.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
-- Auto-Replicating PseudoInstances
-- @documentation https://rostrap.github.io/Libraries/Classes/ReplicatedPseudoInstance/
-- @author Validark
--[[
ReplicatedPseudoInstances are PseudoInstances which, when inherited from, automatically replicate.
CONSTRAINTS:
You can't have read-only values in a class which auto-replicates.
If you want that, use internal values and make a Get() function
Events of ReplicatedPseudoInstances should always be fired with LocalPlayer as the first parameter
BEHAVIOR:
PseudoInstances, when instantiated, replicate to all subscribers.
A "subscriber" is a Player which objects should be replicated to.
A single Player is a subscriber if the Object is a Descendant of their Player object
Every Player is a Subscriber if the Object is or is a Descendant of Workspace or ReplicatedStorage
Replication has two phases:
Initial Replication: this is when a table value is sent over with all the data in an object
Partial Replication: this is when a single property is updated
IMPLEMENTATION:
PseudoInstances with lower ParentalDepth are replicated before Objects with higher ParentalDepths
A ParentalDepth is the number of Parents an Object has before reaching game
This must be the case, because we can't replicate Objects which are parented to other PseudoInstances until after those Parental PseudoInstances exist
--]]
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local HttpService = game:GetService("HttpService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local IsServer = RunService:IsServer()
local IsClient = RunService:IsClient()
local Resources = require(ReplicatedStorage:WaitForChild("Resources"))
local Debug = Resources:LoadLibrary("Debug")
local Enumeration = Resources:LoadLibrary("Enumeration")
local SortedArray = Resources:LoadLibrary("SortedArray")
local PseudoInstance = Resources:LoadLibrary("PseudoInstance")
local Templates = Resources:GetLocalTable("Templates")
local RemoteEvent = Resources:GetRemoteEvent("PseudoInstanceReplicator")
local RemoteFunction = Resources:GetRemoteFunction("PseudoInstanceStartupVerify")
local AutoReplicatedInstances = {}
local LoadedPlayers = setmetatable({}, {__mode = "k"})
local FireClient
local function YieldUntilReadyToFire(Player, ...)
repeat until LoadedPlayers[Player] or not wait()
FireClient(Player, ...)
end
function FireClient(Player, ...)
local Old = LoadedPlayers[Player]
if Old then
LoadedPlayers[Player] = Old + 1
RemoteEvent:FireClient(Player, Old + 1, ...)
else
coroutine.resume(coroutine.create(YieldUntilReadyToFire), Player, ...)
end
end
local function FireAllClientsExcept(Player1, ...)
local Playerlist = Players:GetPlayers()
for i = 1, #Playerlist do
local Player2 = Playerlist[i]
if Player1 ~= Player2 then
FireClient(Player2, ...)
end
end
end
local SubscribingIndividuals = {} -- For when only ONE player receives updates
local ParentalDepths = {}
-- A SortedArray of Ids to objects sorted according to Parental depth
-- This will ensure that you don't replicate child instances and try to set their parents before the parents exist
local ReplicationOrder = SortedArray.new(nil, function(a, b)
local d_a = ParentalDepths[a]
local d_b = ParentalDepths[b]
if d_a == d_b then
return a < b
else
return d_a < d_b
end
end)
local function ReplicateUpdateToInterestedParties(self, Id, i, v)
if AutoReplicatedInstances[Id] then
FireAllClientsExcept(nil, self.__class.ClassName, Id, i, v)
else
local PlayerToReplicateTo = SubscribingIndividuals[Id]
if PlayerToReplicateTo then
FireClient(PlayerToReplicateTo, self.__class.ClassName, Id, i, v)
end
end
end
local function OnPropertyChanged(self, i)
local v = self[i]
local Id = self.__id
if i == "Parent" then
local PlayerToReplicateTo
if v then
local ReplicateToAllPlayers = v == Players or v == Workspace or v == ReplicatedStorage or v:IsDescendantOf(Workspace) or v:IsDescendantOf(ReplicatedStorage)
if not ReplicateToAllPlayers and v:IsDescendantOf(Players) then
PlayerToReplicateTo = v
while PlayerToReplicateTo.ClassName ~= "Player" do
PlayerToReplicateTo = PlayerToReplicateTo.Parent
end
end
-- If replicating to the server, we want to cache these and replicate them upon player joining (conditional upon parent)
if ReplicateToAllPlayers then
-- Get parental depth and cache it
local ParentalDepth = 0
local Current = self
repeat
Current = Current.Parent
ParentalDepth = ParentalDepth + 1
until Current == nil
local Position = ReplicationOrder:Find(Id)
ParentalDepths[Id] = ParentalDepth
AutoReplicatedInstances[Id] = self
if Position then
ReplicationOrder:SortIndex(Position)
else
ReplicationOrder:Insert(Id)
end
FireAllClientsExcept(SubscribingIndividuals[Id], self.__class.ClassName, Id, self.__rawdata)
SubscribingIndividuals[Id] = nil
return
elseif PlayerToReplicateTo then
SubscribingIndividuals[Id] = PlayerToReplicateTo
FireClient(PlayerToReplicateTo, self.__class.ClassName, Id, self.__rawdata)
end
end
if not PlayerToReplicateTo then
local PreviousSubscriber = SubscribingIndividuals[Id]
if PreviousSubscriber then
FireClient(PreviousSubscriber, self.__class.ClassName, Id)
SubscribingIndividuals[Id] = nil
end
end
-- If Parent was set to something that doesn't ReplicateToAllPlayers, take it out of auto-replicate
if AutoReplicatedInstances[Id] then -- Destroy the element if it was previously replicating
-- Destroy for everyone but who we are now replicating to (may be nil)
FireAllClientsExcept(PlayerToReplicateTo, self.__class.ClassName, Id)
AutoReplicatedInstances[Id] = nil
ReplicationOrder:RemoveElement(Id)
end
else
ReplicateUpdateToInterestedParties(self, Id, i, v)
end
end
if IsServer then
Players.PlayerAdded:Connect(function(Player)
if RemoteFunction:InvokeClient(Player) then -- Yield until player loads
local NumReplicationOrder = #ReplicationOrder
for i = 1, NumReplicationOrder do
local Id = ReplicationOrder[i]
local self = AutoReplicatedInstances[Id]
RemoteEvent:FireClient(Player, i, self.__class.ClassName, Id, self.__rawdata)
end
LoadedPlayers[Player] = NumReplicationOrder
end
end)
RemoteEvent.OnServerEvent:Connect(function(Player, ClassName, Id, Event, ...) -- Fire events on the Server after they are fired on the client
Event = (Templates[ClassName].Storage[Id] or Debug.Error("Object not found"))[Event]
-- On the server, the first parameter will always be Player. This removes a duplicate.
-- This also adds some security because a client cannot simply spoof it
Event:Fire(Player, select(2, ...))
end)
elseif IsClient then
local OnClientEventNumber = 1 -- Guarenteed that this will resolve in the order in which replication is intended to occur
RemoteEvent.OnClientEvent:Connect(function(EventNumber, ClassName, Id, RawData, Assigned) -- Handle objects being replicated to clients
repeat until OnClientEventNumber == EventNumber or not wait()
local Template = Templates[ClassName]
if not Template then
Resources:LoadLibrary(ClassName)
Template = Templates[ClassName] or Debug.Error("Invalid ClassName")
end
local Object = Template.Storage[Id]
if not Object then
Object = PseudoInstance.new(ClassName, Id)
Template.Storage[Id] = Object
end
local RawDataType = type(RawData)
if RawDataType == "table" then
for Property, Value in next, RawData do
if Object[Property] ~= Value then
Object[Property] = Value
end
end
elseif RawDataType == "nil" then
Object:Destroy()
elseif RawDataType == "string" then
Object[RawData] = Assigned
else
Debug.Error("Invalid RawData type, expected table, nil, or string, got %s", RawDataType)
end
OnClientEventNumber = OnClientEventNumber + 1
end)
function RemoteFunction.OnClientInvoke()
return true
end
end
local Ids = 0 -- Globally shared Id for instances, would break beyond 2^53 instances ever
return PseudoInstance:Register("ReplicatedPseudoInstance", {
Storage = false; -- Mark this Class as abstract
Internals = {"__id"};
Properties = {};
Events = {};
Methods = {
Destroy = function(self)
local Id = self.__id
if Id then
self.__class.Storage[Id] = nil
ReplicationOrder:RemoveElement(Id)
if IsServer then -- Replicate Destroy
ReplicateUpdateToInterestedParties(self, Id)
end
SubscribingIndividuals[Id] = nil
AutoReplicatedInstances[Id] = nil
end
self:super("Destroy")
end;
};
Init = function(self, Id)
self:superinit()
if IsServer then
if not Id then
Id = Ids + 1
Ids = Id
end
self.Changed:Connect(OnPropertyChanged, self)
elseif IsClient then
if Id then
for Event in next, self.__class.Events do
if Event ~= "Changed" then
self[Event]:Connect(function(...)
RemoteEvent:FireServer(self.__class.ClassName, Id, Event, ...)
end)
end
end
end
end
if Id then
(self.__class.Storage or Debug.Error(self.__class.ClassName .. " is an abstract class and cannot be instantiated"))[Id] = self
self.__id = Id
end
end;
})