CustomReplicator
CustomReplicator.lua
1local Players = game:GetService("Players")
2local RunService = game:GetService("RunService")
3
4local CustomReplicator = {}
5CustomReplicator.__index = CustomReplicator
6CustomReplicator.Tag = "CustomReplicator"
7
8local CONTAINER_NAME = "CustomReplicatorContainer"
9local CLONE_HOST_NAME = "CustomReplicatorClones"
10local REMOTE_FOLDER_NAME = "CustomReplicator"
11local CLIENT_TAG = "CustomReplicatorClient"
12local DEFAULT_INTERVAL = 0.1
13local DEFAULT_PROXIMITY_DISTANCE = 250
14local PROXIMITY_DISTANCE_ATTR = "ReplicatorProximityDistance"
15local INT16_MIN = -32768
16local INT16_MAX = 32767
17local YAW_SCALE = 10000
18
19local liveComponents = {}
20local container = nil
21local remotes = nil
22
23local function ensureContainer()
24 if container and container.Parent then
25 return container
26 end
27 container = workspace:FindFirstChild(CONTAINER_NAME)
28 if not container then
29 container = Instance.new("Camera")
30 container.Name = CONTAINER_NAME
31 container.Parent = workspace
32 end
33 return container
34end
35
36local function cloneHostFor(player)
37 local playerGui = player:FindFirstChildOfClass("PlayerGui")
38 if not playerGui then return nil end
39 local host = playerGui:FindFirstChild(CLONE_HOST_NAME)
40 if not host then
41 host = Instance.new("Folder")
42 host.Name = CLONE_HOST_NAME
43 host.Parent = playerGui
44 end
45 return host
46end
47
48function CustomReplicator.Start()
49 ensureContainer()
50
51 local sharedRemote = CustomReplicator.BaseClass.sharedRemote
52 remotes = {
53 UpdateTransform = sharedRemote(REMOTE_FOLDER_NAME, "UpdateTransform"),
54 UpdateCFrame = sharedRemote(REMOTE_FOLDER_NAME, "UpdateCFrame"),
55 AnimationStart = sharedRemote(REMOTE_FOLDER_NAME, "AnimationStart"),
56 AnimationStop = sharedRemote(REMOTE_FOLDER_NAME, "AnimationStop"),
57 }
58
59 RunService.Heartbeat:Connect(function()
60 for component in liveComponents do
61 component:_update()
62 end
63 end)
64
65 CustomReplicator.Observers.observePlayer(function(player)
66 player:WaitForChild("PlayerGui", 10)
67 cloneHostFor(player)
68 for component in liveComponents do
69 component:playerAdded(player)
70 end
71 return function()
72 for component in liveComponents do
73 component:playerRemoved(player)
74 end
75 end
76 end)
77end
78
79function CustomReplicator.new(instance, options)
80 local self = setmetatable({}, CustomReplicator)
81 options = options or {}
82
83 self.trove = CustomReplicator.Trove.new()
84 self.instance = instance
85 self.formerParent = instance.Parent
86 self.clientInstances = {}
87 self.playingAnimations = {}
88 self.lastStamp = 0
89 self.interval = instance:GetAttribute("ReplicationInterval") or DEFAULT_INTERVAL
90 self.priority = instance:GetAttribute("Priority") == true
91 self.proximityDistance = instance:GetAttribute("ProximityDistance") or DEFAULT_PROXIMITY_DISTANCE
92
93 if options.replicateTargets then
94 self:_setTargetsFromList(options.replicateTargets)
95 else
96 local ownerUserId = instance:GetAttribute("OwnerUserId")
97 if typeof(ownerUserId) == "number" then
98 self.targetUserIds = { [ownerUserId] = true }
99 end
100 end
101
102 ensureContainer()
103 instance.Parent = container
104
105 self:_hookAnimations()
106 liveComponents[self] = true
107
108 for _, player in Players:GetPlayers() do
109 self:playerAdded(player)
110 end
111
112 return self
113end
114
115function CustomReplicator:_setTargetsFromList(players)
116 self.targetUserIds = {}
117 for _, player in players do
118 self.targetUserIds[player.UserId] = true
119 end
120end
121
122function CustomReplicator:_isTargeted()
123 return self.targetUserIds ~= nil
124end
125
126function CustomReplicator:_eligible(player)
127 if self.targetUserIds then
128 return self.targetUserIds[player.UserId] == true
129 end
130 return true
131end
132
133function CustomReplicator:setReplicateTargets(players)
134 if players == nil then
135 self.targetUserIds = nil
136 else
137 self:_setTargetsFromList(players)
138 end
139
140 for _, player in Players:GetPlayers() do
141 local eligible = self:_eligible(player)
142 local hasClone = self.clientInstances[player] ~= nil
143 if eligible and not hasClone then
144 self:playerAdded(player)
145 elseif not eligible and hasClone then
146 self:_removeClone(player)
147 end
148 end
149end
150
151function CustomReplicator:playerAdded(player)
152 if self.clientInstances[player] then return end
153 if not self:_eligible(player) then return end
154
155 local host = cloneHostFor(player)
156 if not host then return end
157
158 local clone = self.instance:Clone()
159 clone:RemoveTag(CustomReplicator.Tag)
160 clone:PivotTo(self.instance:GetPivot())
161 if not self:_isTargeted() then
162 clone:SetAttribute(PROXIMITY_DISTANCE_ATTR, self.proximityDistance)
163 end
164 clone:AddTag(CLIENT_TAG)
165 clone.Parent = host
166
167 self.clientInstances[player] = clone
168
169 for _, animData in self.playingAnimations do
170 remotes.AnimationStart:FireClient(player, clone, animData)
171 end
172end
173
174function CustomReplicator:_removeClone(player)
175 local clone = self.clientInstances[player]
176 if not clone then return end
177 self.clientInstances[player] = nil
178 if clone.Parent then
179 clone:Destroy()
180 end
181end
182
183function CustomReplicator:playerRemoved(player)
184 self:_removeClone(player)
185end
186
187function CustomReplicator:getClientInstance(player)
188 return self.clientInstances[player]
189end
190
191function CustomReplicator:_update()
192 local instance = self.instance
193 if not instance or not instance.Parent then return end
194
195 local now = os.clock()
196 if not self.priority and (now - self.lastStamp) < self.interval then
197 return
198 end
199 self.lastStamp = now
200
201 local pivot = instance:GetPivot()
202
203 if self.priority then
204 for player, clone in self.clientInstances do
205 remotes.UpdateCFrame:FireClient(player, clone, pivot)
206 end
207 return
208 end
209
210 local position = pivot.Position
211 local _, yaw = pivot:ToEulerAnglesYXZ()
212
213 local transform = buffer.create(8)
214 buffer.writei16(transform, 0, math.clamp(math.floor(position.X), INT16_MIN, INT16_MAX))
215 buffer.writei16(transform, 2, math.clamp(math.floor(position.Y), INT16_MIN, INT16_MAX))
216 buffer.writei16(transform, 4, math.clamp(math.floor(position.Z), INT16_MIN, INT16_MAX))
217 buffer.writei16(transform, 6, math.clamp(math.floor(yaw * YAW_SCALE), INT16_MIN, INT16_MAX))
218
219 for player, clone in self.clientInstances do
220 remotes.UpdateTransform:FireClient(player, clone, transform)
221 end
222end
223
224function CustomReplicator:_hookAnimations()
225 local animator = self.instance:FindFirstChildWhichIsA("Animator", true)
226 if not animator then return end
227
228 local ok, connection = pcall(function()
229 return animator.AnimationPlayed:Connect(function(track)
230 self:_onAnimationPlayed(track)
231 end)
232 end)
233
234 if ok and connection then
235 self.trove:Add(connection)
236 end
237end
238
239function CustomReplicator:_onAnimationPlayed(track)
240 local animation = track.Animation
241 if not animation or animation.AnimationId == "" then return end
242
243 local animData = {
244 id = animation.AnimationId,
245 name = animation.Name,
246 looped = track.Looped,
247 priority = track.Priority,
248 }
249
250 self.playingAnimations[animData.name] = animData
251
252 for player, clone in self.clientInstances do
253 remotes.AnimationStart:FireClient(player, clone, animData)
254 end
255
256 track.Stopped:Once(function()
257 self.playingAnimations[animData.name] = nil
258 for player, clone in self.clientInstances do
259 remotes.AnimationStop:FireClient(player, clone, animData.name)
260 end
261 end)
262end
263
264function CustomReplicator:destroy()
265 liveComponents[self] = nil
266
267 for _, clone in self.clientInstances do
268 if clone.Parent then
269 clone:Destroy()
270 end
271 end
272 self.clientInstances = {}
273
274 local instance = self.instance
275 if instance then
276 if self.formerParent then
277 instance.Parent = self.formerParent
278 else
279 instance:Destroy()
280 end
281 end
282end
283
284return CustomReplicator
285