dominus-cli 0.2.1 → 0.5.0
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/index.js +423 -70
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +18 -3
- package/dist/mcp.js.map +1 -1
- package/package.json +17 -16
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/Executor.lua +1 -0
- package/plugin/src/Explorer.lua +123 -1
- package/plugin/src/Properties.lua +213 -28
- package/plugin/src/UIBuilder.lua +494 -32
- package/plugin/src/init.server.lua +111 -22
package/plugin/src/Explorer.lua
CHANGED
|
@@ -209,7 +209,9 @@ function Explorer.searchScripts(query, maxResults)
|
|
|
209
209
|
end)
|
|
210
210
|
|
|
211
211
|
if not ok then
|
|
212
|
-
ok, source = pcall(function()
|
|
212
|
+
ok, source = pcall(function()
|
|
213
|
+
return instance.Source
|
|
214
|
+
end)
|
|
213
215
|
end
|
|
214
216
|
|
|
215
217
|
if ok and source then
|
|
@@ -248,4 +250,124 @@ function Explorer.searchScripts(query, maxResults)
|
|
|
248
250
|
return { results = results }
|
|
249
251
|
end
|
|
250
252
|
|
|
253
|
+
function Explorer.findReplaceScripts(spec)
|
|
254
|
+
local find = spec.find
|
|
255
|
+
local replace = spec.replace
|
|
256
|
+
local usePattern = spec.usePattern or false
|
|
257
|
+
local dryRun = spec.dryRun or false
|
|
258
|
+
|
|
259
|
+
if not find or find == "" then
|
|
260
|
+
return { success = false, error = "Missing 'find' parameter" }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
local root = game
|
|
264
|
+
if spec.root and spec.root ~= "" then
|
|
265
|
+
root = Explorer.resolveInstance(spec.root)
|
|
266
|
+
if not root then
|
|
267
|
+
local ok, svc = pcall(function()
|
|
268
|
+
return game:GetService(spec.root)
|
|
269
|
+
end)
|
|
270
|
+
if ok then
|
|
271
|
+
root = svc
|
|
272
|
+
else
|
|
273
|
+
return { success = false, error = "Root not found: " .. spec.root }
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
279
|
+
local recording = nil
|
|
280
|
+
if not dryRun then
|
|
281
|
+
recording = ChangeHistoryService:TryBeginRecording("Dominus: Find/replace in scripts")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
local modified = {}
|
|
285
|
+
local totalMatches = 0
|
|
286
|
+
local totalFiles = 0
|
|
287
|
+
|
|
288
|
+
local function processScript(instance)
|
|
289
|
+
if not SCRIPT_CLASSES[instance.ClassName] then
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
local ok, source = pcall(function()
|
|
294
|
+
return ScriptEditorService:GetEditorSource(instance)
|
|
295
|
+
end)
|
|
296
|
+
if not ok then
|
|
297
|
+
ok, source = pcall(function()
|
|
298
|
+
return instance.Source
|
|
299
|
+
end)
|
|
300
|
+
end
|
|
301
|
+
if not ok or not source then
|
|
302
|
+
return
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
local count = 0
|
|
306
|
+
if usePattern then
|
|
307
|
+
_, count = source:gsub(find, "")
|
|
308
|
+
else
|
|
309
|
+
_, count = source:gsub(find, "", nil)
|
|
310
|
+
-- plain text: count occurrences with plain find
|
|
311
|
+
count = 0
|
|
312
|
+
local startPos = 1
|
|
313
|
+
while true do
|
|
314
|
+
local foundPos = source:find(find, startPos, true)
|
|
315
|
+
if not foundPos then
|
|
316
|
+
break
|
|
317
|
+
end
|
|
318
|
+
count = count + 1
|
|
319
|
+
startPos = foundPos + #find
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
if count > 0 then
|
|
324
|
+
totalMatches = totalMatches + count
|
|
325
|
+
totalFiles = totalFiles + 1
|
|
326
|
+
|
|
327
|
+
local path = Explorer.getPath(instance)
|
|
328
|
+
table.insert(modified, { path = path, matches = count })
|
|
329
|
+
|
|
330
|
+
if not dryRun then
|
|
331
|
+
local newSource
|
|
332
|
+
if usePattern then
|
|
333
|
+
newSource = source:gsub(find, replace)
|
|
334
|
+
else
|
|
335
|
+
newSource =
|
|
336
|
+
source:gsub(find:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"), replace:gsub("%%", "%%%%"))
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
pcall(function()
|
|
340
|
+
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
341
|
+
return newSource
|
|
342
|
+
end)
|
|
343
|
+
end)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
local function walk(instance)
|
|
349
|
+
processScript(instance)
|
|
350
|
+
for _, child in instance:GetChildren() do
|
|
351
|
+
pcall(walk, child)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
pcall(walk, root)
|
|
356
|
+
|
|
357
|
+
if recording then
|
|
358
|
+
if totalFiles > 0 then
|
|
359
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
360
|
+
else
|
|
361
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
success = true,
|
|
367
|
+
modified = modified,
|
|
368
|
+
totalMatches = totalMatches,
|
|
369
|
+
totalFiles = totalFiles,
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
|
|
251
373
|
return Explorer
|
|
@@ -18,7 +18,9 @@ local IGNORED_PROPERTIES = {
|
|
|
18
18
|
local typeCache = {}
|
|
19
19
|
|
|
20
20
|
local function getPropertyTypeMap(className)
|
|
21
|
-
if typeCache[className] then
|
|
21
|
+
if typeCache[className] then
|
|
22
|
+
return typeCache[className]
|
|
23
|
+
end
|
|
22
24
|
|
|
23
25
|
local typeMap = {}
|
|
24
26
|
local ok, props = pcall(function()
|
|
@@ -67,7 +69,8 @@ function Properties.get(path)
|
|
|
67
69
|
if ok then
|
|
68
70
|
properties = apiData
|
|
69
71
|
else
|
|
70
|
-
local commonProps =
|
|
72
|
+
local commonProps =
|
|
73
|
+
{ "Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide" }
|
|
71
74
|
for _, propName in commonProps do
|
|
72
75
|
local valOk, val = pcall(function()
|
|
73
76
|
return instance[propName]
|
|
@@ -85,6 +88,80 @@ function Properties.get(path)
|
|
|
85
88
|
return { properties = properties }
|
|
86
89
|
end
|
|
87
90
|
|
|
91
|
+
function Properties.getDescendants(path)
|
|
92
|
+
local rootInstance = Explorer.resolveInstance(path)
|
|
93
|
+
if not rootInstance then
|
|
94
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
local function getProps(instance)
|
|
98
|
+
local properties = {}
|
|
99
|
+
|
|
100
|
+
local ok, apiData = pcall(function()
|
|
101
|
+
local info = {}
|
|
102
|
+
local propData = ReflectionService:GetPropertiesOfClass(instance.ClassName)
|
|
103
|
+
for _, prop in propData do
|
|
104
|
+
if not IGNORED_PROPERTIES[prop.Name] then
|
|
105
|
+
local valueOk, value = pcall(function()
|
|
106
|
+
return instance[prop.Name]
|
|
107
|
+
end)
|
|
108
|
+
if valueOk then
|
|
109
|
+
table.insert(info, {
|
|
110
|
+
name = prop.Name,
|
|
111
|
+
value = tostring(value),
|
|
112
|
+
type = prop.ValueType and tostring(prop.ValueType) or typeof(value),
|
|
113
|
+
category = prop.Category or "Other",
|
|
114
|
+
})
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
return info
|
|
119
|
+
end)
|
|
120
|
+
|
|
121
|
+
if ok then
|
|
122
|
+
properties = apiData
|
|
123
|
+
else
|
|
124
|
+
local commonProps =
|
|
125
|
+
{ "Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide" }
|
|
126
|
+
for _, propName in commonProps do
|
|
127
|
+
local valOk, val = pcall(function()
|
|
128
|
+
return instance[propName]
|
|
129
|
+
end)
|
|
130
|
+
if valOk and val ~= nil then
|
|
131
|
+
table.insert(properties, {
|
|
132
|
+
name = propName,
|
|
133
|
+
value = tostring(val),
|
|
134
|
+
type = typeof(val),
|
|
135
|
+
})
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return properties
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local results = {}
|
|
144
|
+
|
|
145
|
+
-- Include the root instance itself
|
|
146
|
+
table.insert(results, {
|
|
147
|
+
name = rootInstance.Name,
|
|
148
|
+
className = rootInstance.ClassName,
|
|
149
|
+
path = Explorer.getPath(rootInstance),
|
|
150
|
+
properties = getProps(rootInstance),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
for _, descendant in rootInstance:GetDescendants() do
|
|
154
|
+
table.insert(results, {
|
|
155
|
+
name = descendant.Name,
|
|
156
|
+
className = descendant.ClassName,
|
|
157
|
+
path = Explorer.getPath(descendant),
|
|
158
|
+
properties = getProps(descendant),
|
|
159
|
+
})
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
return { success = true, descendants = results }
|
|
163
|
+
end
|
|
164
|
+
|
|
88
165
|
--[[
|
|
89
166
|
Smart type coercion: converts JSON-friendly values into Roblox types.
|
|
90
167
|
Uses the expected property type from ReflectionService when available.
|
|
@@ -96,7 +173,9 @@ local function coerceValue(instance, key, value)
|
|
|
96
173
|
|
|
97
174
|
-- If we don't know the expected type, try reading the current value
|
|
98
175
|
if not expectedType then
|
|
99
|
-
local curOk, curVal = pcall(function()
|
|
176
|
+
local curOk, curVal = pcall(function()
|
|
177
|
+
return instance[key]
|
|
178
|
+
end)
|
|
100
179
|
if curOk and curVal ~= nil then
|
|
101
180
|
expectedType = typeof(curVal)
|
|
102
181
|
end
|
|
@@ -135,7 +214,9 @@ local function coerceValue(instance, key, value)
|
|
|
135
214
|
if type(value) == "string" then
|
|
136
215
|
-- "0.5, 0, 0.5, 0" format
|
|
137
216
|
local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
|
|
138
|
-
if a then
|
|
217
|
+
if a then
|
|
218
|
+
return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d))
|
|
219
|
+
end
|
|
139
220
|
end
|
|
140
221
|
end
|
|
141
222
|
|
|
@@ -169,10 +250,7 @@ local function coerceValue(instance, key, value)
|
|
|
169
250
|
-- ═══════════════════════════════════════════
|
|
170
251
|
if expectedType == "Vector2" then
|
|
171
252
|
if type(value) == "table" then
|
|
172
|
-
return Vector2.new(
|
|
173
|
-
value.X or value.x or value[1] or 0,
|
|
174
|
-
value.Y or value.y or value[2] or 0
|
|
175
|
-
)
|
|
253
|
+
return Vector2.new(value.X or value.x or value[1] or 0, value.Y or value.y or value[2] or 0)
|
|
176
254
|
end
|
|
177
255
|
end
|
|
178
256
|
|
|
@@ -186,10 +264,14 @@ local function coerceValue(instance, key, value)
|
|
|
186
264
|
end
|
|
187
265
|
if value:match("^rgb") then
|
|
188
266
|
local r, g, b = value:match("(%d+)%D+(%d+)%D+(%d+)")
|
|
189
|
-
if r then
|
|
267
|
+
if r then
|
|
268
|
+
return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
|
|
269
|
+
end
|
|
190
270
|
end
|
|
191
271
|
local ok, bc = pcall(BrickColor.new, value)
|
|
192
|
-
if ok then
|
|
272
|
+
if ok then
|
|
273
|
+
return bc.Color
|
|
274
|
+
end
|
|
193
275
|
end
|
|
194
276
|
if type(value) == "table" then
|
|
195
277
|
-- {R, G, B} 0-1 or {r, g, b}
|
|
@@ -198,7 +280,11 @@ local function coerceValue(instance, key, value)
|
|
|
198
280
|
end
|
|
199
281
|
-- {red, green, blue} 0-255
|
|
200
282
|
if value.red or value.Red then
|
|
201
|
-
return Color3.fromRGB(
|
|
283
|
+
return Color3.fromRGB(
|
|
284
|
+
value.red or value.Red or 0,
|
|
285
|
+
value.green or value.Green or 0,
|
|
286
|
+
value.blue or value.Blue or 0
|
|
287
|
+
)
|
|
202
288
|
end
|
|
203
289
|
-- Array [r, g, b]
|
|
204
290
|
if value[1] then
|
|
@@ -229,14 +315,19 @@ local function coerceValue(instance, key, value)
|
|
|
229
315
|
if type(value) == "table" then
|
|
230
316
|
if value.Position or value.position then
|
|
231
317
|
local pos = value.Position or value.position
|
|
232
|
-
local cf = CFrame.new(
|
|
318
|
+
local cf = CFrame.new(
|
|
319
|
+
pos.x or pos.X or pos[1] or 0,
|
|
320
|
+
pos.y or pos.Y or pos[2] or 0,
|
|
321
|
+
pos.z or pos.Z or pos[3] or 0
|
|
322
|
+
)
|
|
233
323
|
if value.Rotation or value.rotation then
|
|
234
324
|
local rot = value.Rotation or value.rotation
|
|
235
|
-
cf = cf
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
325
|
+
cf = cf
|
|
326
|
+
* CFrame.Angles(
|
|
327
|
+
math.rad(rot.x or rot.X or rot[1] or 0),
|
|
328
|
+
math.rad(rot.y or rot.Y or rot[2] or 0),
|
|
329
|
+
math.rad(rot.z or rot.Z or rot[3] or 0)
|
|
330
|
+
)
|
|
240
331
|
end
|
|
241
332
|
return cf
|
|
242
333
|
end
|
|
@@ -252,8 +343,12 @@ local function coerceValue(instance, key, value)
|
|
|
252
343
|
-- ═══════════════════════════════════════════
|
|
253
344
|
if expectedType == "Rect" then
|
|
254
345
|
if type(value) == "table" then
|
|
255
|
-
return Rect.new(
|
|
256
|
-
value[
|
|
346
|
+
return Rect.new(
|
|
347
|
+
value[1] or value.Min and value.Min[1] or 0,
|
|
348
|
+
value[2] or value.Min and value.Min[2] or 0,
|
|
349
|
+
value[3] or value.Max and value.Max[1] or 0,
|
|
350
|
+
value[4] or value.Max and value.Max[2] or 0
|
|
351
|
+
)
|
|
257
352
|
end
|
|
258
353
|
end
|
|
259
354
|
|
|
@@ -277,7 +372,14 @@ local function coerceValue(instance, key, value)
|
|
|
277
372
|
if value[1] and type(value[1]) == "table" then
|
|
278
373
|
local keypoints = {}
|
|
279
374
|
for _, kp in value do
|
|
280
|
-
table.insert(
|
|
375
|
+
table.insert(
|
|
376
|
+
keypoints,
|
|
377
|
+
NumberSequenceKeypoint.new(
|
|
378
|
+
kp.Time or kp[1] or 0,
|
|
379
|
+
kp.Value or kp[2] or 0,
|
|
380
|
+
kp.Envelope or kp[3] or 0
|
|
381
|
+
)
|
|
382
|
+
)
|
|
281
383
|
end
|
|
282
384
|
return NumberSequence.new(keypoints)
|
|
283
385
|
end
|
|
@@ -300,7 +402,7 @@ local function coerceValue(instance, key, value)
|
|
|
300
402
|
for _, kp in value do
|
|
301
403
|
local color = Color3.new(1, 1, 1)
|
|
302
404
|
if kp.Color then
|
|
303
|
-
if type(kp.Color) == "string" and kp.Color:sub(1,1) == "#" then
|
|
405
|
+
if type(kp.Color) == "string" and kp.Color:sub(1, 1) == "#" then
|
|
304
406
|
color = Color3.fromHex(kp.Color)
|
|
305
407
|
elseif type(kp.Color) == "table" then
|
|
306
408
|
color = Color3.new(kp.Color[1] or 0, kp.Color[2] or 0, kp.Color[3] or 0)
|
|
@@ -322,10 +424,14 @@ local function coerceValue(instance, key, value)
|
|
|
322
424
|
local weight = Enum.FontWeight.Regular
|
|
323
425
|
local style = Enum.FontStyle.Normal
|
|
324
426
|
if value.Weight or value.weight then
|
|
325
|
-
pcall(function()
|
|
427
|
+
pcall(function()
|
|
428
|
+
weight = Enum.FontWeight[value.Weight or value.weight]
|
|
429
|
+
end)
|
|
326
430
|
end
|
|
327
431
|
if value.Style or value.style then
|
|
328
|
-
pcall(function()
|
|
432
|
+
pcall(function()
|
|
433
|
+
style = Enum.FontStyle[value.Style or value.style]
|
|
434
|
+
end)
|
|
329
435
|
end
|
|
330
436
|
return Font.new(family, weight, style)
|
|
331
437
|
end
|
|
@@ -339,13 +445,17 @@ local function coerceValue(instance, key, value)
|
|
|
339
445
|
-- ═══════════════════════════════════════════
|
|
340
446
|
if type(value) == "string" then
|
|
341
447
|
local currentVal = nil
|
|
342
|
-
pcall(function()
|
|
448
|
+
pcall(function()
|
|
449
|
+
currentVal = instance[key]
|
|
450
|
+
end)
|
|
343
451
|
if typeof(currentVal) == "EnumItem" then
|
|
344
452
|
local enumType = tostring(currentVal.EnumType)
|
|
345
453
|
local enumOk, enumVal = pcall(function()
|
|
346
454
|
return Enum[enumType][value]
|
|
347
455
|
end)
|
|
348
|
-
if enumOk then
|
|
456
|
+
if enumOk then
|
|
457
|
+
return enumVal
|
|
458
|
+
end
|
|
349
459
|
end
|
|
350
460
|
end
|
|
351
461
|
|
|
@@ -356,8 +466,10 @@ local function coerceValue(instance, key, value)
|
|
|
356
466
|
-- UDim2 guess: has XScale/YScale keys
|
|
357
467
|
if value.XScale or value.xScale then
|
|
358
468
|
return UDim2.new(
|
|
359
|
-
value.XScale or value.xScale or 0,
|
|
360
|
-
value.
|
|
469
|
+
value.XScale or value.xScale or 0,
|
|
470
|
+
value.XOffset or value.xOffset or 0,
|
|
471
|
+
value.YScale or value.yScale or 0,
|
|
472
|
+
value.YOffset or value.yOffset or 0
|
|
361
473
|
)
|
|
362
474
|
end
|
|
363
475
|
-- Vector3 guess: 3 number elements or X/Y/Z keys
|
|
@@ -383,7 +495,9 @@ local function coerceValue(instance, key, value)
|
|
|
383
495
|
return Color3.fromHex(value)
|
|
384
496
|
end
|
|
385
497
|
local ok, bc = pcall(BrickColor.new, value)
|
|
386
|
-
if ok then
|
|
498
|
+
if ok then
|
|
499
|
+
return bc.Color
|
|
500
|
+
end
|
|
387
501
|
end
|
|
388
502
|
|
|
389
503
|
if type(value) == "string" and key == "BrickColor" then
|
|
@@ -421,4 +535,75 @@ function Properties.set(path, propertiesToSet)
|
|
|
421
535
|
return { success = true, set = set }
|
|
422
536
|
end
|
|
423
537
|
|
|
538
|
+
function Properties.bulkSet(spec)
|
|
539
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
540
|
+
local recording = ChangeHistoryService:TryBeginRecording("Dominus: Bulk set properties")
|
|
541
|
+
|
|
542
|
+
local modified = 0
|
|
543
|
+
local errors = {}
|
|
544
|
+
|
|
545
|
+
if spec.targets and type(spec.targets) == "table" then
|
|
546
|
+
-- Explicit mode: array of {path, properties}
|
|
547
|
+
for _, target in ipairs(spec.targets) do
|
|
548
|
+
local instance = Explorer.resolveInstance(target.path)
|
|
549
|
+
if not instance then
|
|
550
|
+
table.insert(errors, "Not found: " .. tostring(target.path))
|
|
551
|
+
else
|
|
552
|
+
for key, value in target.properties do
|
|
553
|
+
local coerced = coerceValue(instance, key, value)
|
|
554
|
+
local ok, err = pcall(function()
|
|
555
|
+
instance[key] = coerced
|
|
556
|
+
end)
|
|
557
|
+
if not ok then
|
|
558
|
+
table.insert(errors, target.path .. "." .. key .. ": " .. tostring(err))
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
modified = modified + 1
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
elseif spec.root and spec.properties then
|
|
565
|
+
-- Filter mode: apply to all matching descendants
|
|
566
|
+
local root = Explorer.resolveInstance(spec.root)
|
|
567
|
+
if not root then
|
|
568
|
+
if recording then
|
|
569
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
570
|
+
end
|
|
571
|
+
return { success = false, error = "Root not found: " .. tostring(spec.root) }
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
local function processInstance(inst)
|
|
575
|
+
if spec.className and inst.ClassName ~= spec.className then
|
|
576
|
+
return
|
|
577
|
+
end
|
|
578
|
+
for key, value in spec.properties do
|
|
579
|
+
local coerced = coerceValue(inst, key, value)
|
|
580
|
+
local ok, err = pcall(function()
|
|
581
|
+
inst[key] = coerced
|
|
582
|
+
end)
|
|
583
|
+
if not ok then
|
|
584
|
+
table.insert(errors, Explorer.getPath(inst) .. "." .. key .. ": " .. tostring(err))
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
modified = modified + 1
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
-- Include root itself if it matches
|
|
591
|
+
processInstance(root)
|
|
592
|
+
for _, desc in root:GetDescendants() do
|
|
593
|
+
processInstance(desc)
|
|
594
|
+
end
|
|
595
|
+
else
|
|
596
|
+
if recording then
|
|
597
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
598
|
+
end
|
|
599
|
+
return { success = false, error = "Provide either 'targets' array or 'root' + 'properties'" }
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
if recording then
|
|
603
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
return { success = true, modified = modified, errors = errors }
|
|
607
|
+
end
|
|
608
|
+
|
|
424
609
|
return Properties
|