azul-sync 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/push.d.ts +2 -0
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +46 -1
- package/dist/push.js.map +1 -1
- package/package.json +1 -1
- package/plugin/README.md +0 -54
- package/plugin/sourcemap.json +0 -272
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Actor/AzulSync.server.luau +0 -906
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/AzulService.luau +0 -573
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Config.luau +0 -31
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Enums.luau +0 -11
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Serializer.luau +0 -929
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CollapsibleTitledSection.luau +0 -214
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ColorPicker.luau +0 -360
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CustomTextButton.luau +0 -170
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/DropdownMenu.luau +0 -363
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/HorizontalLine.luau +0 -43
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ImageButtonWithText.luau +0 -181
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledCheckbox.luau +0 -295
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledColorInputPicker.luau +0 -294
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledMultiChoice.luau +0 -163
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledNumberInput.luau +0 -312
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledRadioButton.luau +0 -55
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledSlider.luau +0 -151
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledTextInput.luau +0 -222
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledToggleButton.luau +0 -73
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/StatefulImageButton.luau +0 -125
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalScrollingFrame.luau +0 -100
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalSpacer.luau +0 -35
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticallyScalingListFrame.luau +0 -107
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/GuiUtilities.luau +0 -429
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/RbxGui.luau +0 -4363
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/UI.luau +0 -477
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/WebSocketClient.luau +0 -161
|
@@ -1,573 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
local AzulService = {}
|
|
3
|
-
|
|
4
|
-
-- Services
|
|
5
|
-
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
6
|
-
local HttpService = game:GetService("HttpService")
|
|
7
|
-
|
|
8
|
-
-- Modules
|
|
9
|
-
local parent = script.Parent
|
|
10
|
-
if not parent then error("AzulService: missing parent container") end
|
|
11
|
-
|
|
12
|
-
local Serializer = require("./Serializer")
|
|
13
|
-
local CONFIG = require("./Config")
|
|
14
|
-
local Enums = require("./Enums")
|
|
15
|
-
|
|
16
|
-
-- Logging helpers
|
|
17
|
-
local function debugPrint(...)
|
|
18
|
-
if CONFIG.SILENT_MODE or not CONFIG.DEBUG_MODE then return end
|
|
19
|
-
print(`[🐛 AzulService]:`, ...)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
local function infoPrint(...)
|
|
23
|
-
if CONFIG.SILENT_MODE then return end
|
|
24
|
-
print(`[AzulService]:`, ...)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
local function isScript(instance)
|
|
28
|
-
return instance:IsA("Script") or instance:IsA("LocalScript") or instance:IsA("ModuleScript")
|
|
29
|
-
end
|
|
30
|
-
--[=[
|
|
31
|
-
Utility: Rebuild service set from config
|
|
32
|
-
@param serviceSet { [string]: boolean } - The service set to rebuild
|
|
33
|
-
]=]
|
|
34
|
-
function AzulService.rebuildServiceSet(serviceSet)
|
|
35
|
-
table.clear(serviceSet)
|
|
36
|
-
for _, name in ipairs(CONFIG.SERVICE_LIST) do
|
|
37
|
-
serviceSet[name] = true
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
--- Utility: Clear all GUID attributes
|
|
42
|
-
--- This is for legacy support; we now use :GetDebugId() for stable GUIDs
|
|
43
|
-
function AzulService.clearAllGUIDAttributes()
|
|
44
|
-
local guidAttributeName = plugin:GetSetting("GUID_ATTRIBUTE") or "AzulSyncGUID"
|
|
45
|
-
|
|
46
|
-
for _, instance in game:GetDescendants() do
|
|
47
|
-
if instance:GetAttribute(guidAttributeName) then instance:SetAttribute(guidAttributeName, nil) end
|
|
48
|
-
end
|
|
49
|
-
infoPrint("Cleared all legacy GUID attributes from instances.")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
--- Utility: Check if instance should be excluded from sync
|
|
53
|
-
function AzulService.isExcluded(instance: Instance, serviceSet: { [string]: boolean })
|
|
54
|
-
if not instance then return true end
|
|
55
|
-
|
|
56
|
-
local fullName = instance:GetFullName()
|
|
57
|
-
for _, ancestorName in CONFIG.EXCLUDED_PARENTS do
|
|
58
|
-
if fullName:find(ancestorName) then return true end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
-- Walk up to the service
|
|
62
|
-
local current: Instance? = instance
|
|
63
|
-
while current do
|
|
64
|
-
if current.Parent == game then
|
|
65
|
-
local inList = serviceSet[current.Name] ~= nil
|
|
66
|
-
|
|
67
|
-
if CONFIG.LIST_TYPE == Enums.listType.WHITELIST then
|
|
68
|
-
-- Whitelist: only allow services in the list
|
|
69
|
-
-- debugPrint(`Included {instance} in whitelist`)
|
|
70
|
-
return not inList
|
|
71
|
-
else
|
|
72
|
-
-- Blacklist: exclude services in the list
|
|
73
|
-
-- debugPrint(`Excluded {instance} in blacklist`)
|
|
74
|
-
return inList
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
current = current.Parent
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
-- Not under DataModel
|
|
81
|
-
return true
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
--- Utility: Get or create instance by path segments
|
|
85
|
-
function AzulService.getOrCreatePath(pathSegments: { string }): Instance
|
|
86
|
-
local current: Instance = game
|
|
87
|
-
for index, segment in ipairs(pathSegments) do
|
|
88
|
-
local nextNode: Instance? = current:FindFirstChild(segment)
|
|
89
|
-
|
|
90
|
-
if not nextNode then
|
|
91
|
-
if index == 1 then
|
|
92
|
-
local ok, service = pcall(function()
|
|
93
|
-
return game:GetService(segment)
|
|
94
|
-
end)
|
|
95
|
-
if ok and service then nextNode = service end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
if not nextNode then
|
|
99
|
-
local newFolder = Instance.new("Folder")
|
|
100
|
-
newFolder.Name = segment
|
|
101
|
-
newFolder.Parent = current
|
|
102
|
-
nextNode = newFolder
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
current = nextNode :: Instance
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
return current
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
function AzulService.isProtectedRobloxContainer(instance: Instance?): boolean
|
|
113
|
-
if not instance then return false end
|
|
114
|
-
|
|
115
|
-
-- Top-level services cannot be destroyed or reparented
|
|
116
|
-
local okService = pcall(function()
|
|
117
|
-
return game:GetService(instance.Name)
|
|
118
|
-
end)
|
|
119
|
-
if instance.Parent == game and okService then return true end
|
|
120
|
-
|
|
121
|
-
-- Certain StarterPlayer children are locked (StarterPlayerScripts, StarterCharacterScripts, StarterGear)
|
|
122
|
-
local parent = instance.Parent
|
|
123
|
-
if parent and parent.ClassName == "StarterPlayer" then
|
|
124
|
-
local name = instance.Name
|
|
125
|
-
if name == "StarterPlayerScripts" or name == "StarterCharacterScripts" or name == "StarterGear" then
|
|
126
|
-
return true
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
return false
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
--- Utility: Generate or retrieve GUID for instance (use Roblox debug IDs to avoid attribute churn)
|
|
134
|
-
function AzulService.getOrCreateGUID(
|
|
135
|
-
instance,
|
|
136
|
-
trackedInstances: { [Instance]: string },
|
|
137
|
-
guidMap: { [string]: Instance },
|
|
138
|
-
usedGuids: { [string]: boolean }
|
|
139
|
-
): string
|
|
140
|
-
local cached = trackedInstances[instance]
|
|
141
|
-
if cached then return cached end
|
|
142
|
-
|
|
143
|
-
local guid = instance:GetDebugId(0)
|
|
144
|
-
trackedInstances[instance] = guid
|
|
145
|
-
guidMap[guid] = instance
|
|
146
|
-
usedGuids[guid] = true
|
|
147
|
-
return guid
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
--- Utility: Get instance path
|
|
151
|
-
function AzulService.getInstancePath(instance: Instance): { string }?
|
|
152
|
-
local path = {}
|
|
153
|
-
local current: Instance? = instance
|
|
154
|
-
|
|
155
|
-
while current and current ~= game do
|
|
156
|
-
table.insert(path, 1, current.Name)
|
|
157
|
-
current = current.Parent
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
-- If current became nil, the instance is no longer under DataModel
|
|
161
|
-
if current ~= game then return nil end
|
|
162
|
-
|
|
163
|
-
return path
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
--- Convert instance to data format
|
|
167
|
-
function AzulService.instanceToData(
|
|
168
|
-
instance,
|
|
169
|
-
trackedInstances: { [Instance]: string },
|
|
170
|
-
guidMap: { [string]: Instance },
|
|
171
|
-
usedGuids: { [string]: boolean },
|
|
172
|
-
options: { includeProperties: boolean? }?
|
|
173
|
-
): { [string]: any }?
|
|
174
|
-
local guid = AzulService.getOrCreateGUID(instance, trackedInstances, guidMap, usedGuids)
|
|
175
|
-
local path = AzulService.getInstancePath(instance)
|
|
176
|
-
if not path then return nil end
|
|
177
|
-
|
|
178
|
-
local parentGuid: string? = nil
|
|
179
|
-
local parent = instance.Parent
|
|
180
|
-
if parent then
|
|
181
|
-
if parent == game then
|
|
182
|
-
parentGuid = "root"
|
|
183
|
-
else
|
|
184
|
-
parentGuid = AzulService.getOrCreateGUID(parent, trackedInstances, guidMap, usedGuids)
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
local data: { [string]: any } = {
|
|
189
|
-
guid = guid,
|
|
190
|
-
className = instance.ClassName,
|
|
191
|
-
name = instance.Name,
|
|
192
|
-
path = path,
|
|
193
|
-
parentGuid = parentGuid,
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if isScript(instance) then data.source = (instance :: Script).Source end
|
|
197
|
-
|
|
198
|
-
if options and options.includeProperties then
|
|
199
|
-
local properties = Serializer.collectSerializedProperties(instance, {
|
|
200
|
-
getInstanceRefData = function(referenceInstance: Instance)
|
|
201
|
-
local referencePath = AzulService.getInstancePath(referenceInstance)
|
|
202
|
-
if not referencePath then return nil end
|
|
203
|
-
|
|
204
|
-
local referenceGuid =
|
|
205
|
-
AzulService.getOrCreateGUID(referenceInstance, trackedInstances, guidMap, usedGuids)
|
|
206
|
-
return {
|
|
207
|
-
guid = referenceGuid,
|
|
208
|
-
path = referencePath,
|
|
209
|
-
}
|
|
210
|
-
end,
|
|
211
|
-
})
|
|
212
|
-
if next(properties) ~= nil then data.properties = properties end
|
|
213
|
-
|
|
214
|
-
local attributes = instance:GetAttributes()
|
|
215
|
-
if next(attributes) ~= nil then data.attributes = attributes end
|
|
216
|
-
|
|
217
|
-
local tags = Serializer.collectSerializedTags(instance)
|
|
218
|
-
if #tags > 0 then data.tags = tags end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
return data
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
-- Utility: set script source safely (handles large sources via ScriptEditorService)
|
|
225
|
-
function AzulService.setScriptSource(scriptInstance: Script | LocalScript | ModuleScript, source: string)
|
|
226
|
-
local ok, err = pcall(function()
|
|
227
|
-
if ScriptEditorService and ScriptEditorService.UpdateSourceAsync then
|
|
228
|
-
ScriptEditorService:UpdateSourceAsync(scriptInstance, function(...)
|
|
229
|
-
return source
|
|
230
|
-
end)
|
|
231
|
-
else
|
|
232
|
-
(scriptInstance :: Script).Source = source
|
|
233
|
-
end
|
|
234
|
-
end)
|
|
235
|
-
|
|
236
|
-
if not ok then warn(`[AzulService]: Failed to set script source for {scriptInstance}`, err) end
|
|
237
|
-
|
|
238
|
-
return ok
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
--- Create/update instance data with dedup tracking (returns data if changed, nil if duplicate)
|
|
242
|
-
function AzulService.instanceToDataDedup(
|
|
243
|
-
instance: Instance,
|
|
244
|
-
trackedInstances: { [Instance]: string },
|
|
245
|
-
guidMap: { [string]: Instance },
|
|
246
|
-
usedGuids: { [string]: boolean },
|
|
247
|
-
lastInstanceUpdate: { [string]: { key: string, t: number } }
|
|
248
|
-
): { [string]: any }?
|
|
249
|
-
local data = AzulService.instanceToData(instance, trackedInstances, guidMap, usedGuids)
|
|
250
|
-
if not data then return nil end
|
|
251
|
-
|
|
252
|
-
local guid = data.guid :: string
|
|
253
|
-
local parentKey = (data.parentGuid :: string?) or ""
|
|
254
|
-
local key = table.concat(data.path :: { string }, "/")
|
|
255
|
-
.. "|"
|
|
256
|
-
.. (data.className :: string)
|
|
257
|
-
.. "|"
|
|
258
|
-
.. (data.name :: string)
|
|
259
|
-
.. "|"
|
|
260
|
-
.. parentKey
|
|
261
|
-
local now = tick()
|
|
262
|
-
local last = lastInstanceUpdate[guid]
|
|
263
|
-
if last and last.key == key and now - last.t < 0.05 then
|
|
264
|
-
-- Ignore duplicate bursts from multiple property signals firing at once
|
|
265
|
-
return nil
|
|
266
|
-
end
|
|
267
|
-
lastInstanceUpdate[guid] = { key = key, t = now }
|
|
268
|
-
|
|
269
|
-
return data
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
type InstanceData = {
|
|
273
|
-
guid: string,
|
|
274
|
-
className: string,
|
|
275
|
-
name: string,
|
|
276
|
-
path: { string }, -- ["ReplicatedStorage", "Modules", "Foo"]
|
|
277
|
-
parentGuid: string?, -- parent instance GUID
|
|
278
|
-
source: string?, -- Only present for Script/LocalScript/ModuleScript
|
|
279
|
-
properties: { [string]: any }?, -- Record<string, unknown>;
|
|
280
|
-
attributes: { [string]: any }?, -- Record<string, unknown>;
|
|
281
|
-
tags: { string }?,
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
--- Apply snapshot instances to the workspace
|
|
285
|
-
function AzulService.applySnapshotInstances(instances: { InstanceData }): (number, number, { [string]: Instance })
|
|
286
|
-
table.sort(instances, function(a, b)
|
|
287
|
-
return #a.path < #b.path
|
|
288
|
-
end)
|
|
289
|
-
|
|
290
|
-
local siblingOrdinalByPathClass: { [string]: number } = {}
|
|
291
|
-
local existingInstancesByGuid: { [string]: Instance } = {}
|
|
292
|
-
local resolvedInstancesByGuid: { [string]: Instance } = {}
|
|
293
|
-
local deferredPropertyApplications: { { instance: Instance, properties: { [string]: any }? } } = {}
|
|
294
|
-
|
|
295
|
-
local lockedContainerParents = {
|
|
296
|
-
StarterPlayerScripts = "StarterPlayer",
|
|
297
|
-
StarterCharacterScripts = "StarterPlayer",
|
|
298
|
-
StarterGear = "StarterPlayer",
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
local created = 0
|
|
302
|
-
local updated = 0
|
|
303
|
-
local guidMatchCount = 0
|
|
304
|
-
local fallbackMatchCount = 0
|
|
305
|
-
|
|
306
|
-
for _, existing in ipairs(game:GetDescendants()) do
|
|
307
|
-
local guid = existing:GetDebugId(0)
|
|
308
|
-
if type(guid) == "string" and guid ~= "" then existingInstancesByGuid[guid] = existing end
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
local function findInstanceByPath(pathSegments: { string }): Instance?
|
|
312
|
-
local current: Instance = game
|
|
313
|
-
for index, segment in ipairs(pathSegments) do
|
|
314
|
-
local nextNode: Instance? = nil
|
|
315
|
-
|
|
316
|
-
if index == 1 then
|
|
317
|
-
local okService, service = pcall(function()
|
|
318
|
-
return game:GetService(segment)
|
|
319
|
-
end)
|
|
320
|
-
if okService and service then nextNode = service end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
if not nextNode then nextNode = current:FindFirstChild(segment) end
|
|
324
|
-
if not nextNode then return nil end
|
|
325
|
-
current = nextNode
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
return current
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
for _, item in instances do
|
|
332
|
-
local adjustedPath = {}
|
|
333
|
-
for i, segment in ipairs(item.path) do
|
|
334
|
-
adjustedPath[i] = segment
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
local protectedContainerName: string? = nil
|
|
338
|
-
local first = adjustedPath[1]
|
|
339
|
-
local maybeParent = lockedContainerParents[first]
|
|
340
|
-
if maybeParent and maybeParent ~= first then
|
|
341
|
-
table.insert(adjustedPath, 1, maybeParent)
|
|
342
|
-
protectedContainerName = adjustedPath[2]
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
if not protectedContainerName and #adjustedPath >= 2 then
|
|
346
|
-
local second = adjustedPath[2]
|
|
347
|
-
if lockedContainerParents[second] == adjustedPath[1] then protectedContainerName = second end
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
local serviceCandidate: Instance? = nil
|
|
351
|
-
if #adjustedPath == 1 then
|
|
352
|
-
local okService, service = pcall(function()
|
|
353
|
-
return game:GetService(adjustedPath[1])
|
|
354
|
-
end)
|
|
355
|
-
if okService then serviceCandidate = service end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
local parentPath = {}
|
|
359
|
-
for i = 1, #adjustedPath - 1 do
|
|
360
|
-
parentPath[i] = adjustedPath[i]
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
local parent: Instance? = nil
|
|
364
|
-
if type(item.parentGuid) == "string" and item.parentGuid ~= "" and item.parentGuid ~= "root" then
|
|
365
|
-
parent = resolvedInstancesByGuid[item.parentGuid] or existingInstancesByGuid[item.parentGuid]
|
|
366
|
-
end
|
|
367
|
-
if not parent then parent = AzulService.getOrCreatePath(parentPath) end
|
|
368
|
-
local existing: Instance? = nil
|
|
369
|
-
if type(item.guid) == "string" and item.guid ~= "" then
|
|
370
|
-
existing = existingInstancesByGuid[item.guid]
|
|
371
|
-
if existing then
|
|
372
|
-
guidMatchCount += 1
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
if not existing then
|
|
377
|
-
existing = serviceCandidate
|
|
378
|
-
if existing then
|
|
379
|
-
fallbackMatchCount += 1
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
if not existing then
|
|
383
|
-
local targetName = adjustedPath[#adjustedPath]
|
|
384
|
-
local targetClassName = item.className
|
|
385
|
-
local occurrenceKey = table.concat(adjustedPath, "\u{1F}") .. "::" .. tostring(targetClassName)
|
|
386
|
-
local occurrenceOrdinal = (siblingOrdinalByPathClass[occurrenceKey] or 0) + 1
|
|
387
|
-
siblingOrdinalByPathClass[occurrenceKey] = occurrenceOrdinal
|
|
388
|
-
|
|
389
|
-
local sameNameChildren = {}
|
|
390
|
-
for _, child in ipairs((parent :: Instance):GetChildren()) do
|
|
391
|
-
if child.Name == targetName then table.insert(sameNameChildren, child) end
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
existing = sameNameChildren[occurrenceOrdinal]
|
|
395
|
-
if existing then
|
|
396
|
-
fallbackMatchCount += 1
|
|
397
|
-
end
|
|
398
|
-
end
|
|
399
|
-
local instance = existing
|
|
400
|
-
|
|
401
|
-
local function isScriptClass(className: string)
|
|
402
|
-
return className == "Script" or className == "LocalScript" or className == "ModuleScript"
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
if existing and existing.ClassName ~= item.className then
|
|
406
|
-
local targetIsScript = isScriptClass(item.className)
|
|
407
|
-
if targetIsScript then
|
|
408
|
-
if AzulService.isProtectedRobloxContainer(existing) then
|
|
409
|
-
instance = existing
|
|
410
|
-
else
|
|
411
|
-
existing:Destroy()
|
|
412
|
-
instance = nil
|
|
413
|
-
end
|
|
414
|
-
else
|
|
415
|
-
instance = existing
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
if not instance and protectedContainerName then
|
|
420
|
-
local protectedParentName = lockedContainerParents[protectedContainerName]
|
|
421
|
-
if protectedParentName then
|
|
422
|
-
local okParent, protectedParent = pcall(function()
|
|
423
|
-
return game:GetService(protectedParentName)
|
|
424
|
-
end)
|
|
425
|
-
if okParent and protectedParent then
|
|
426
|
-
local protectedExisting = protectedParent:FindFirstChild(protectedContainerName)
|
|
427
|
-
if protectedExisting then instance = protectedExisting end
|
|
428
|
-
end
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
if not instance then
|
|
433
|
-
local newInstance
|
|
434
|
-
local okNew, createdInstance = pcall(function()
|
|
435
|
-
return Instance.new(item.className)
|
|
436
|
-
end)
|
|
437
|
-
if okNew and createdInstance then
|
|
438
|
-
newInstance = createdInstance
|
|
439
|
-
else
|
|
440
|
-
newInstance = Instance.new("Folder")
|
|
441
|
-
debugPrint(`Failed to create instance of class "{item.className}", created Folder placeholder instead.`)
|
|
442
|
-
end
|
|
443
|
-
newInstance.Name = adjustedPath[#adjustedPath]
|
|
444
|
-
newInstance.Parent = parent :: Instance
|
|
445
|
-
instance = newInstance
|
|
446
|
-
created += 1
|
|
447
|
-
else
|
|
448
|
-
if parent and instance.Parent ~= parent and not AzulService.isProtectedRobloxContainer(instance) then
|
|
449
|
-
local okReparent = pcall(function()
|
|
450
|
-
instance.Parent = parent :: Instance
|
|
451
|
-
end)
|
|
452
|
-
if not okReparent then
|
|
453
|
-
debugPrint(`Failed to reparent instance "{instance.Name}" ({instance.ClassName}) to target parent.`)
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
if not serviceCandidate and not AzulService.isProtectedRobloxContainer(instance) then
|
|
458
|
-
instance.Name = adjustedPath[#adjustedPath]
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
if instance then
|
|
463
|
-
if type(item.guid) == "string" and item.guid ~= "" then
|
|
464
|
-
existingInstancesByGuid[item.guid] = instance
|
|
465
|
-
resolvedInstancesByGuid[item.guid] = instance
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
table.insert(deferredPropertyApplications, {
|
|
469
|
-
instance = instance,
|
|
470
|
-
properties = item.properties,
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
Serializer.applySerializedAttributes(instance, item.attributes)
|
|
474
|
-
Serializer.applySerializedTags(instance, item.tags)
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
if isScriptClass(item.className) and item.source and instance then
|
|
478
|
-
AzulService.setScriptSource(instance :: Script, item.source)
|
|
479
|
-
updated += 1
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
for _, pending in ipairs(deferredPropertyApplications) do
|
|
484
|
-
Serializer.applySerializedProperties(pending.instance, pending.properties, {
|
|
485
|
-
resolveInstanceByGuid = function(guid: string): Instance?
|
|
486
|
-
return resolvedInstancesByGuid[guid]
|
|
487
|
-
end,
|
|
488
|
-
resolveInstanceByPath = function(pathSegments: { string }): Instance?
|
|
489
|
-
return findInstanceByPath(pathSegments)
|
|
490
|
-
end,
|
|
491
|
-
})
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
debugPrint(
|
|
495
|
-
`Snapshot resolve summary: {guidMatchCount} guid matches, {fallbackMatchCount} fallback matches, {created} created, {updated} scripts updated`
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
return created, updated, resolvedInstancesByGuid
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
--- Find the push config ModuleScript
|
|
502
|
-
function AzulService.findPushConfigModule(pushConfigPath: { string }): ModuleScript?
|
|
503
|
-
local current: Instance = game
|
|
504
|
-
for index, segment in ipairs(pushConfigPath) do
|
|
505
|
-
if index == 1 then
|
|
506
|
-
local ok, service = pcall(function()
|
|
507
|
-
return game:GetService(segment)
|
|
508
|
-
end)
|
|
509
|
-
if not ok or not service then return nil end
|
|
510
|
-
current = service
|
|
511
|
-
else
|
|
512
|
-
local nextNode = current:FindFirstChild(segment)
|
|
513
|
-
if not nextNode then return nil end
|
|
514
|
-
current = nextNode
|
|
515
|
-
end
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
if current and current:IsA("ModuleScript") then return current :: ModuleScript end
|
|
519
|
-
|
|
520
|
-
return nil
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
--- Read and validate push config from ModuleScript
|
|
524
|
-
function AzulService.readPushConfig(pushConfigPath: { string }): ({ [string]: any }?, string?)
|
|
525
|
-
local module = AzulService.findPushConfigModule(pushConfigPath)
|
|
526
|
-
if not module then return nil, "Push config ModuleScript not found" end
|
|
527
|
-
|
|
528
|
-
-- Clone module to force require to refresh data
|
|
529
|
-
local moduleClone = module:Clone()
|
|
530
|
-
moduleClone.Parent = module.Parent
|
|
531
|
-
module:Destroy()
|
|
532
|
-
module = moduleClone
|
|
533
|
-
--
|
|
534
|
-
|
|
535
|
-
local ok, data = pcall(require, module)
|
|
536
|
-
if not ok then return nil, `Failed to require push config: {data}` end
|
|
537
|
-
|
|
538
|
-
if type(data) ~= "table" then return nil, "Push config is not a table" end
|
|
539
|
-
|
|
540
|
-
debugPrint(`Push config loaded from {module}: {HttpService:JSONEncode(data)}`)
|
|
541
|
-
|
|
542
|
-
local mappings = {}
|
|
543
|
-
if type(data.pushMappings) == "table" then
|
|
544
|
-
for _, entry in ipairs(data.pushMappings) do
|
|
545
|
-
if type(entry) == "table" and type(entry.source) == "string" and type(entry.destination) == "table" then
|
|
546
|
-
local dest = {}
|
|
547
|
-
for _, seg in ipairs(entry.destination) do
|
|
548
|
-
if type(seg) == "string" and seg ~= "" then table.insert(dest, seg) end
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
if #dest > 0 then
|
|
552
|
-
table.insert(mappings, {
|
|
553
|
-
source = entry.source,
|
|
554
|
-
destination = dest,
|
|
555
|
-
destructive = entry.destructive == true,
|
|
556
|
-
rojoMode = entry.rojoMode == true,
|
|
557
|
-
})
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
end
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
local configPayload = {
|
|
564
|
-
mappings = mappings,
|
|
565
|
-
port = data.port,
|
|
566
|
-
debugMode = data.debugMode,
|
|
567
|
-
deleteOrphansOnConnect = data.deleteOrphansOnConnect,
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
return configPayload, nil
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
return AzulService
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
local Enums = require("./Enums")
|
|
2
|
-
|
|
3
|
-
configTable = {
|
|
4
|
-
WS_URL = "ws://localhost:8080",
|
|
5
|
-
HEARTBEAT_INTERVAL = 30,
|
|
6
|
-
BATCH_IDLE_WINDOW = 0.1,
|
|
7
|
-
SCRIPT_CHANGE_DEBOUNCE = 0.35,
|
|
8
|
-
LIST_TYPE = Enums.listType.WHITELIST, -- or BLACKLIST
|
|
9
|
-
SERVICE_LIST = {
|
|
10
|
-
"Workspace",
|
|
11
|
-
"Lighting",
|
|
12
|
-
"ReplicatedFirst",
|
|
13
|
-
"ReplicatedStorage",
|
|
14
|
-
"ServerScriptService",
|
|
15
|
-
"ServerStorage",
|
|
16
|
-
"StarterGui",
|
|
17
|
-
"StarterPack",
|
|
18
|
-
"StarterPlayer",
|
|
19
|
-
"SoundService",
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
EXCLUDED_PARENTS = {
|
|
23
|
-
"ServerStorage.RecPlugins", -- Folder managed by "Eye" plugin. It updates the sourcemap thousands of times. We don't need to track this.
|
|
24
|
-
"Workspace.Surface Converter Storage", -- Folder managed by "Surface Converter" plugin. We don't need to track it.
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
DEBUG_MODE = false,
|
|
28
|
-
SILENT_MODE = false,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return configTable
|