dominus-cli 2.3.0 → 2.4.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/CHANGELOG.md +29 -1
- package/README.md +18 -3
- package/dist/bridge-daemon.js.map +1 -1
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +364 -9
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/plugin/Dominus.rbxmx +572 -8
- package/plugin/src/CommandRouter.lua +16 -0
- package/plugin/src/Metadata.lua +411 -0
- package/plugin/src/Mutation.lua +15 -0
- package/plugin/src/ValueCodec.lua +116 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
local Building = require(script.Parent.Building)
|
|
2
2
|
local Explorer = require(script.Parent.Explorer)
|
|
3
3
|
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
4
|
+
local Metadata = require(script.Parent.Metadata)
|
|
4
5
|
local Mutation = require(script.Parent.Mutation)
|
|
5
6
|
local Properties = require(script.Parent.Properties)
|
|
6
7
|
local Reflection = require(script.Parent.Reflection)
|
|
@@ -62,6 +63,21 @@ local handlers = {
|
|
|
62
63
|
["studio:v2:set_selection"] = function(payload)
|
|
63
64
|
return Building.setSelection(payload)
|
|
64
65
|
end,
|
|
66
|
+
["studio:v2:get_metadata"] = function(payload)
|
|
67
|
+
return Metadata.get(payload)
|
|
68
|
+
end,
|
|
69
|
+
["studio:v2:set_attributes"] = function(payload)
|
|
70
|
+
return Metadata.setAttributes(payload)
|
|
71
|
+
end,
|
|
72
|
+
["studio:v2:update_tags"] = function(payload)
|
|
73
|
+
return Metadata.updateTags(payload)
|
|
74
|
+
end,
|
|
75
|
+
["studio:v2:list_callable_methods"] = function(payload)
|
|
76
|
+
return Metadata.listCallableMethods(payload)
|
|
77
|
+
end,
|
|
78
|
+
["studio:v2:invoke_methods"] = function(payload)
|
|
79
|
+
return Metadata.invoke(payload)
|
|
80
|
+
end,
|
|
65
81
|
["studio:v2:delete"] = function(payload)
|
|
66
82
|
return Mutation.delete(payload)
|
|
67
83
|
end,
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
2
|
+
|
|
3
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
4
|
+
local ValueCodec = require(script.Parent.ValueCodec)
|
|
5
|
+
|
|
6
|
+
local Metadata = {}
|
|
7
|
+
|
|
8
|
+
local MAX_TARGETS = 100
|
|
9
|
+
local MAX_ITEMS_PER_TARGET = 100
|
|
10
|
+
local MAX_METHOD_CALLS = 50
|
|
11
|
+
|
|
12
|
+
local ATTRIBUTE_TYPES = {
|
|
13
|
+
string = true,
|
|
14
|
+
boolean = true,
|
|
15
|
+
number = true,
|
|
16
|
+
UDim = true,
|
|
17
|
+
UDim2 = true,
|
|
18
|
+
BrickColor = true,
|
|
19
|
+
Color3 = true,
|
|
20
|
+
Vector2 = true,
|
|
21
|
+
Vector3 = true,
|
|
22
|
+
CFrame = true,
|
|
23
|
+
NumberSequence = true,
|
|
24
|
+
ColorSequence = true,
|
|
25
|
+
NumberRange = true,
|
|
26
|
+
Rect = true,
|
|
27
|
+
Font = true,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
local METHOD_POLICY = {
|
|
31
|
+
GetAttribute = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
32
|
+
GetAttributes = { kind = "read", className = "Instance", params = {} },
|
|
33
|
+
GetTags = { kind = "read", className = "Instance", params = {} },
|
|
34
|
+
HasTag = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
35
|
+
GetFullName = { kind = "read", className = "Instance", params = {} },
|
|
36
|
+
IsA = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
37
|
+
IsAncestorOf = { kind = "read", className = "Instance", params = { { type = "Instance" } } },
|
|
38
|
+
IsDescendantOf = { kind = "read", className = "Instance", params = { { type = "Instance" } } },
|
|
39
|
+
FindFirstChild = {
|
|
40
|
+
kind = "read",
|
|
41
|
+
className = "Instance",
|
|
42
|
+
params = { { type = "string" }, { type = "boolean", optional = true } },
|
|
43
|
+
},
|
|
44
|
+
FindFirstChildOfClass = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
45
|
+
FindFirstChildWhichIsA = {
|
|
46
|
+
kind = "read",
|
|
47
|
+
className = "Instance",
|
|
48
|
+
params = { { type = "string" }, { type = "boolean", optional = true } },
|
|
49
|
+
},
|
|
50
|
+
FindFirstAncestor = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
51
|
+
FindFirstAncestorOfClass = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
52
|
+
FindFirstAncestorWhichIsA = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
53
|
+
GetChildren = { kind = "read", className = "Instance", params = {} },
|
|
54
|
+
GetDescendants = { kind = "read", className = "Instance", params = {} },
|
|
55
|
+
GetPivot = { kind = "read", className = "PVInstance", params = {} },
|
|
56
|
+
GetScale = { kind = "read", className = "Model", params = {} },
|
|
57
|
+
GetBoundingBox = { kind = "read", className = "Model", params = {} },
|
|
58
|
+
AddTag = { kind = "write", className = "Instance", params = { { type = "string", tag = true } } },
|
|
59
|
+
RemoveTag = { kind = "write", className = "Instance", params = { { type = "string", tag = true } } },
|
|
60
|
+
SetAttribute = {
|
|
61
|
+
kind = "write",
|
|
62
|
+
className = "Instance",
|
|
63
|
+
params = { { type = "string", attributeName = true }, { type = "Attribute", allowNil = true } },
|
|
64
|
+
},
|
|
65
|
+
PivotTo = { kind = "write", className = "PVInstance", params = { { type = "CFrame" } } },
|
|
66
|
+
ScaleTo = { kind = "write", className = "Model", params = { { type = "number" } } },
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
local function resolve(ref, label)
|
|
70
|
+
local instance, err = InstanceRegistry.resolve(ref)
|
|
71
|
+
if not instance then
|
|
72
|
+
error(label .. ": " .. tostring(err))
|
|
73
|
+
end
|
|
74
|
+
return instance
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
local function validateName(value, label)
|
|
78
|
+
assert(type(value) == "string", label .. " must be a string")
|
|
79
|
+
assert(#value > 0 and #value <= 100, label .. " must contain 1 to 100 characters")
|
|
80
|
+
assert(not value:find("[%c]"), label .. " cannot contain control characters")
|
|
81
|
+
return value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
local function validateTag(value)
|
|
85
|
+
return validateName(value, "Tag")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
local function validateAttributeName(value)
|
|
89
|
+
return validateName(value, "Attribute name")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
local function prepareAttributeChanges(set, remove, label)
|
|
93
|
+
set = set or {}
|
|
94
|
+
remove = remove or {}
|
|
95
|
+
assert(type(set) == "table" and type(remove) == "table", "set and remove must be arrays")
|
|
96
|
+
assert(#set + #remove > 0, label .. " has no attribute updates")
|
|
97
|
+
assert(#set + #remove <= MAX_ITEMS_PER_TARGET, "Too many attribute updates in " .. label)
|
|
98
|
+
|
|
99
|
+
local changes = {}
|
|
100
|
+
local seen = {}
|
|
101
|
+
for itemIndex, item in set do
|
|
102
|
+
assert(type(item) == "table" and type(item.value) == "table", "Set item " .. itemIndex .. " is invalid")
|
|
103
|
+
local name = validateAttributeName(item.name)
|
|
104
|
+
assert(not seen[name], "Attribute " .. name .. " appears more than once")
|
|
105
|
+
local typeName = item.value.type
|
|
106
|
+
assert(ATTRIBUTE_TYPES[typeName], "Unsupported attribute type: " .. tostring(typeName))
|
|
107
|
+
seen[name] = true
|
|
108
|
+
table.insert(changes, { name = name, value = ValueCodec.decodeTyped(typeName, item.value.value) })
|
|
109
|
+
end
|
|
110
|
+
for _, nameValue in remove do
|
|
111
|
+
local name = validateAttributeName(nameValue)
|
|
112
|
+
assert(not seen[name], "Attribute " .. name .. " cannot be set and removed together")
|
|
113
|
+
seen[name] = true
|
|
114
|
+
table.insert(changes, { name = name, remove = true })
|
|
115
|
+
end
|
|
116
|
+
return changes
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
local function applyPreparedAttributeChanges(target, changes)
|
|
120
|
+
for _, change in changes do
|
|
121
|
+
if change.remove then
|
|
122
|
+
target:SetAttribute(change.name, nil)
|
|
123
|
+
else
|
|
124
|
+
target:SetAttribute(change.name, change.value)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
local function prepareTagChanges(add, remove, label)
|
|
130
|
+
add = add or {}
|
|
131
|
+
remove = remove or {}
|
|
132
|
+
assert(type(add) == "table" and type(remove) == "table", "add and remove must be arrays")
|
|
133
|
+
assert(#add + #remove > 0, label .. " has no tag updates")
|
|
134
|
+
assert(#add + #remove <= MAX_ITEMS_PER_TARGET, "Too many tag updates in " .. label)
|
|
135
|
+
|
|
136
|
+
local seen = {}
|
|
137
|
+
for _, tagValue in add do
|
|
138
|
+
local tag = validateTag(tagValue)
|
|
139
|
+
assert(not seen[tag], "Tag " .. tag .. " appears more than once")
|
|
140
|
+
seen[tag] = "add"
|
|
141
|
+
end
|
|
142
|
+
for _, tagValue in remove do
|
|
143
|
+
local tag = validateTag(tagValue)
|
|
144
|
+
assert(not seen[tag], "Tag " .. tag .. " cannot be added and removed together")
|
|
145
|
+
seen[tag] = "remove"
|
|
146
|
+
end
|
|
147
|
+
return add, remove
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
local function applyPreparedTagChanges(target, add, remove)
|
|
151
|
+
for _, tag in add do
|
|
152
|
+
target:AddTag(tag)
|
|
153
|
+
end
|
|
154
|
+
for _, tag in remove do
|
|
155
|
+
target:RemoveTag(tag)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
local function sortedTags(instance)
|
|
160
|
+
local tags = instance:GetTags()
|
|
161
|
+
table.sort(tags)
|
|
162
|
+
return tags
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
local function encodedAttributes(instance)
|
|
166
|
+
local attributes = instance:GetAttributes()
|
|
167
|
+
local names = {}
|
|
168
|
+
for name in attributes do
|
|
169
|
+
table.insert(names, name)
|
|
170
|
+
end
|
|
171
|
+
table.sort(names)
|
|
172
|
+
|
|
173
|
+
local encoded = {}
|
|
174
|
+
for _, name in names do
|
|
175
|
+
table.insert(encoded, {
|
|
176
|
+
name = name,
|
|
177
|
+
value = ValueCodec.encodeTyped(attributes[name]),
|
|
178
|
+
})
|
|
179
|
+
end
|
|
180
|
+
return encoded
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
local function metadataFor(instance)
|
|
184
|
+
return {
|
|
185
|
+
ref = InstanceRegistry.toRef(instance),
|
|
186
|
+
name = instance.Name,
|
|
187
|
+
className = instance.ClassName,
|
|
188
|
+
tags = sortedTags(instance),
|
|
189
|
+
attributes = encodedAttributes(instance),
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
local function beginRecording(name)
|
|
194
|
+
local recording = ChangeHistoryService:TryBeginRecording(name)
|
|
195
|
+
if not recording then
|
|
196
|
+
error("Another plugin recording is already active")
|
|
197
|
+
end
|
|
198
|
+
return recording
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
function Metadata.get(spec)
|
|
202
|
+
assert(type(spec.targets) == "table" and #spec.targets > 0, "targets must contain at least one instance reference")
|
|
203
|
+
assert(#spec.targets <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " instances can be inspected at once")
|
|
204
|
+
local results = {}
|
|
205
|
+
for index, targetRef in spec.targets do
|
|
206
|
+
table.insert(results, metadataFor(resolve(targetRef, "Target " .. index)))
|
|
207
|
+
end
|
|
208
|
+
return { success = true, results = results }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
function Metadata.setAttributes(spec)
|
|
212
|
+
assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one attribute update")
|
|
213
|
+
assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " attribute operations are allowed")
|
|
214
|
+
|
|
215
|
+
local prepared = {}
|
|
216
|
+
for index, operation in spec.operations do
|
|
217
|
+
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
218
|
+
table.insert(prepared, {
|
|
219
|
+
target = resolve(operation.target, "Operation " .. index .. " target"),
|
|
220
|
+
changes = prepareAttributeChanges(operation.set, operation.remove, "Operation " .. index),
|
|
221
|
+
})
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
local recording = beginRecording("Dominus 2: Update attributes")
|
|
225
|
+
local ok, err = pcall(function()
|
|
226
|
+
for _, operation in prepared do
|
|
227
|
+
applyPreparedAttributeChanges(operation.target, operation.changes)
|
|
228
|
+
end
|
|
229
|
+
end)
|
|
230
|
+
if not ok then
|
|
231
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
232
|
+
return { success = false, error = tostring(err), rolledBack = true }
|
|
233
|
+
end
|
|
234
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
235
|
+
|
|
236
|
+
local results = {}
|
|
237
|
+
for _, operation in prepared do
|
|
238
|
+
table.insert(results, metadataFor(operation.target))
|
|
239
|
+
end
|
|
240
|
+
return { success = true, operationCount = #prepared, results = results }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
function Metadata.updateTags(spec)
|
|
244
|
+
assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one tag update")
|
|
245
|
+
assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " tag operations are allowed")
|
|
246
|
+
|
|
247
|
+
local prepared = {}
|
|
248
|
+
for index, operation in spec.operations do
|
|
249
|
+
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
250
|
+
local add, remove = prepareTagChanges(operation.add, operation.remove, "Operation " .. index)
|
|
251
|
+
table.insert(prepared, {
|
|
252
|
+
target = resolve(operation.target, "Operation " .. index .. " target"),
|
|
253
|
+
add = add,
|
|
254
|
+
remove = remove,
|
|
255
|
+
})
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
local recording = beginRecording("Dominus 2: Update tags")
|
|
259
|
+
local ok, err = pcall(function()
|
|
260
|
+
for _, operation in prepared do
|
|
261
|
+
applyPreparedTagChanges(operation.target, operation.add, operation.remove)
|
|
262
|
+
end
|
|
263
|
+
end)
|
|
264
|
+
if not ok then
|
|
265
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
266
|
+
return { success = false, error = tostring(err), rolledBack = true }
|
|
267
|
+
end
|
|
268
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
269
|
+
|
|
270
|
+
local results = {}
|
|
271
|
+
for _, operation in prepared do
|
|
272
|
+
table.insert(results, metadataFor(operation.target))
|
|
273
|
+
end
|
|
274
|
+
return { success = true, operationCount = #prepared, results = results }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
function Metadata.applyAttributeChangesWithinRecording(target, set, remove)
|
|
278
|
+
local changes = prepareAttributeChanges(set, remove, "Mutation operation")
|
|
279
|
+
applyPreparedAttributeChanges(target, changes)
|
|
280
|
+
return encodedAttributes(target)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
function Metadata.applyTagChangesWithinRecording(target, add, remove)
|
|
284
|
+
local preparedAdd, preparedRemove = prepareTagChanges(add, remove, "Mutation operation")
|
|
285
|
+
applyPreparedTagChanges(target, preparedAdd, preparedRemove)
|
|
286
|
+
return sortedTags(target)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
local function publicPolicy(methodName, policy)
|
|
290
|
+
local params = {}
|
|
291
|
+
for _, param in policy.params do
|
|
292
|
+
table.insert(params, {
|
|
293
|
+
type = param.type,
|
|
294
|
+
optional = param.optional or false,
|
|
295
|
+
})
|
|
296
|
+
end
|
|
297
|
+
return {
|
|
298
|
+
name = methodName,
|
|
299
|
+
kind = policy.kind,
|
|
300
|
+
className = policy.className,
|
|
301
|
+
parameters = params,
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
function Metadata.listCallableMethods(spec)
|
|
306
|
+
local target = resolve(spec.target, "Target")
|
|
307
|
+
local methods = {}
|
|
308
|
+
for methodName, policy in METHOD_POLICY do
|
|
309
|
+
if target:IsA(policy.className) then
|
|
310
|
+
table.insert(methods, publicPolicy(methodName, policy))
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
table.sort(methods, function(left, right)
|
|
314
|
+
return left.name < right.name
|
|
315
|
+
end)
|
|
316
|
+
return {
|
|
317
|
+
success = true,
|
|
318
|
+
target = InstanceRegistry.toRef(target),
|
|
319
|
+
methods = methods,
|
|
320
|
+
blockedByDefault = true,
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
local function decodeArgument(argument, parameter, label)
|
|
325
|
+
assert(type(argument) == "table" and type(argument.type) == "string", label .. " must be a typed value")
|
|
326
|
+
if parameter.type == "Attribute" then
|
|
327
|
+
if argument.type == "nil" and parameter.allowNil then
|
|
328
|
+
return nil
|
|
329
|
+
end
|
|
330
|
+
assert(ATTRIBUTE_TYPES[argument.type], label .. " must be a supported Roblox attribute type")
|
|
331
|
+
return ValueCodec.decodeTyped(argument.type, argument.value)
|
|
332
|
+
end
|
|
333
|
+
assert(argument.type == parameter.type, label .. " must have type " .. parameter.type)
|
|
334
|
+
local decoded = ValueCodec.decodeTyped(argument.type, argument.value)
|
|
335
|
+
if parameter.tag then
|
|
336
|
+
validateTag(decoded)
|
|
337
|
+
elseif parameter.attributeName then
|
|
338
|
+
validateAttributeName(decoded)
|
|
339
|
+
end
|
|
340
|
+
return decoded
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
function Metadata.invoke(spec)
|
|
344
|
+
assert(type(spec.calls) == "table" and #spec.calls > 0, "calls must contain at least one method call")
|
|
345
|
+
assert(#spec.calls <= MAX_METHOD_CALLS, "At most " .. MAX_METHOD_CALLS .. " method calls are allowed")
|
|
346
|
+
|
|
347
|
+
local prepared = {}
|
|
348
|
+
local hasWrites = false
|
|
349
|
+
for index, call in spec.calls do
|
|
350
|
+
assert(type(call) == "table", "Call " .. index .. " must be an object")
|
|
351
|
+
local methodName = call.method
|
|
352
|
+
local policy = METHOD_POLICY[methodName]
|
|
353
|
+
assert(policy, "Method is not allowed by Dominus: " .. tostring(methodName))
|
|
354
|
+
local target = resolve(call.target, "Call " .. index .. " target")
|
|
355
|
+
assert(target:IsA(policy.className), methodName .. " requires " .. policy.className .. ", got " .. target.ClassName)
|
|
356
|
+
local arguments = call.arguments or {}
|
|
357
|
+
assert(type(arguments) == "table", "Call " .. index .. " arguments must be an array")
|
|
358
|
+
local minimum = 0
|
|
359
|
+
for _, parameter in policy.params do
|
|
360
|
+
if not parameter.optional then
|
|
361
|
+
minimum += 1
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
assert(#arguments >= minimum and #arguments <= #policy.params, methodName .. " received the wrong number of arguments")
|
|
365
|
+
|
|
366
|
+
local decoded = {}
|
|
367
|
+
for argumentIndex, argument in arguments do
|
|
368
|
+
decoded[argumentIndex] = decodeArgument(argument, policy.params[argumentIndex], "Argument " .. argumentIndex)
|
|
369
|
+
end
|
|
370
|
+
table.insert(prepared, {
|
|
371
|
+
target = target,
|
|
372
|
+
method = methodName,
|
|
373
|
+
policy = policy,
|
|
374
|
+
arguments = decoded,
|
|
375
|
+
argumentCount = #arguments,
|
|
376
|
+
})
|
|
377
|
+
hasWrites = hasWrites or policy.kind == "write"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
local recording = hasWrites and beginRecording("Dominus 2: Invoke safe instance methods") or nil
|
|
381
|
+
local results = {}
|
|
382
|
+
local ok, err = pcall(function()
|
|
383
|
+
for _, call in prepared do
|
|
384
|
+
local method = call.target[call.method]
|
|
385
|
+
assert(type(method) == "function", call.method .. " is unavailable on " .. call.target.ClassName)
|
|
386
|
+
local returned = table.pack(method(call.target, table.unpack(call.arguments, 1, call.argumentCount)))
|
|
387
|
+
local values = {}
|
|
388
|
+
for resultIndex = 1, returned.n do
|
|
389
|
+
table.insert(values, ValueCodec.encodeTyped(returned[resultIndex]))
|
|
390
|
+
end
|
|
391
|
+
table.insert(results, {
|
|
392
|
+
target = InstanceRegistry.toRef(call.target),
|
|
393
|
+
method = call.method,
|
|
394
|
+
kind = call.policy.kind,
|
|
395
|
+
results = values,
|
|
396
|
+
})
|
|
397
|
+
end
|
|
398
|
+
end)
|
|
399
|
+
if not ok then
|
|
400
|
+
if recording then
|
|
401
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
402
|
+
end
|
|
403
|
+
return { success = false, error = tostring(err), rolledBack = recording ~= nil }
|
|
404
|
+
end
|
|
405
|
+
if recording then
|
|
406
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
407
|
+
end
|
|
408
|
+
return { success = true, callCount = #results, mutationRecorded = recording ~= nil, results = results }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
return Metadata
|
package/plugin/src/Mutation.lua
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
2
2
|
|
|
3
3
|
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
4
|
+
local Metadata = require(script.Parent.Metadata)
|
|
4
5
|
local ValueCodec = require(script.Parent.ValueCodec)
|
|
5
6
|
|
|
6
7
|
local Mutation = {}
|
|
@@ -55,6 +56,20 @@ function Mutation.apply(spec)
|
|
|
55
56
|
target = InstanceRegistry.toRef(target),
|
|
56
57
|
properties = setProperties(target, operation.properties),
|
|
57
58
|
})
|
|
59
|
+
elseif operation.op == "setAttributes" then
|
|
60
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
61
|
+
table.insert(results, {
|
|
62
|
+
op = operation.op,
|
|
63
|
+
target = InstanceRegistry.toRef(target),
|
|
64
|
+
attributes = Metadata.applyAttributeChangesWithinRecording(target, operation.set, operation.remove),
|
|
65
|
+
})
|
|
66
|
+
elseif operation.op == "updateTags" then
|
|
67
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
68
|
+
table.insert(results, {
|
|
69
|
+
op = operation.op,
|
|
70
|
+
target = InstanceRegistry.toRef(target),
|
|
71
|
+
tags = Metadata.applyTagChangesWithinRecording(target, operation.add, operation.remove),
|
|
72
|
+
})
|
|
58
73
|
elseif operation.op == "create" then
|
|
59
74
|
local parent = resolve(operation.parent, "Operation " .. index .. " parent")
|
|
60
75
|
assert(type(operation.className) == "string", "create.className must be a string")
|
|
@@ -208,6 +208,122 @@ function ValueCodec.encode(value)
|
|
|
208
208
|
return tostring(value)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
+
local TYPED_DEFAULTS = {
|
|
212
|
+
UDim = function()
|
|
213
|
+
return UDim.new()
|
|
214
|
+
end,
|
|
215
|
+
UDim2 = function()
|
|
216
|
+
return UDim2.new()
|
|
217
|
+
end,
|
|
218
|
+
Vector2 = function()
|
|
219
|
+
return Vector2.zero
|
|
220
|
+
end,
|
|
221
|
+
Vector3 = function()
|
|
222
|
+
return Vector3.zero
|
|
223
|
+
end,
|
|
224
|
+
Color3 = function()
|
|
225
|
+
return Color3.new()
|
|
226
|
+
end,
|
|
227
|
+
BrickColor = function()
|
|
228
|
+
return BrickColor.new("Medium stone grey")
|
|
229
|
+
end,
|
|
230
|
+
Rect = function()
|
|
231
|
+
return Rect.new()
|
|
232
|
+
end,
|
|
233
|
+
NumberRange = function()
|
|
234
|
+
return NumberRange.new(0)
|
|
235
|
+
end,
|
|
236
|
+
CFrame = function()
|
|
237
|
+
return CFrame.new()
|
|
238
|
+
end,
|
|
239
|
+
NumberSequence = function()
|
|
240
|
+
return NumberSequence.new(0)
|
|
241
|
+
end,
|
|
242
|
+
ColorSequence = function()
|
|
243
|
+
return ColorSequence.new(Color3.new())
|
|
244
|
+
end,
|
|
245
|
+
Font = function()
|
|
246
|
+
return Font.new("rbxasset://fonts/families/SourceSansPro.json")
|
|
247
|
+
end,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function ValueCodec.decodeTyped(typeName, value)
|
|
251
|
+
assert(type(typeName) == "string", "Typed value requires a type")
|
|
252
|
+
if typeName == "nil" then
|
|
253
|
+
return nil
|
|
254
|
+
end
|
|
255
|
+
if typeName == "string" or typeName == "number" or typeName == "boolean" then
|
|
256
|
+
return ValueCodec.decodeForCurrent(typeName == "string" and "" or typeName == "number" and 0 or false, value)
|
|
257
|
+
end
|
|
258
|
+
if typeName == "Instance" then
|
|
259
|
+
local resolved, err = InstanceRegistry.resolve(value)
|
|
260
|
+
if not resolved then
|
|
261
|
+
error(err)
|
|
262
|
+
end
|
|
263
|
+
return resolved
|
|
264
|
+
end
|
|
265
|
+
local createDefault = TYPED_DEFAULTS[typeName]
|
|
266
|
+
if not createDefault then
|
|
267
|
+
error("Unsupported typed value: " .. typeName)
|
|
268
|
+
end
|
|
269
|
+
return ValueCodec.decodeForCurrent(createDefault(), value)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
local function encodeSerializable(value, depth, state, seen)
|
|
273
|
+
if depth > 5 then
|
|
274
|
+
return "<maximum depth reached>"
|
|
275
|
+
end
|
|
276
|
+
if type(value) ~= "table" then
|
|
277
|
+
return ValueCodec.encode(value)
|
|
278
|
+
end
|
|
279
|
+
if seen[value] then
|
|
280
|
+
return "<cycle>"
|
|
281
|
+
end
|
|
282
|
+
seen[value] = true
|
|
283
|
+
|
|
284
|
+
local count = 0
|
|
285
|
+
local maxIndex = 0
|
|
286
|
+
local isArray = true
|
|
287
|
+
for key in value do
|
|
288
|
+
count += 1
|
|
289
|
+
state.count += 1
|
|
290
|
+
if state.count > state.limit then
|
|
291
|
+
seen[value] = nil
|
|
292
|
+
return "<result truncated>"
|
|
293
|
+
end
|
|
294
|
+
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
|
|
295
|
+
isArray = false
|
|
296
|
+
else
|
|
297
|
+
maxIndex = math.max(maxIndex, key)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
local result = {}
|
|
302
|
+
if isArray and maxIndex == count then
|
|
303
|
+
for index = 1, maxIndex do
|
|
304
|
+
result[index] = encodeSerializable(value[index], depth + 1, state, seen)
|
|
305
|
+
end
|
|
306
|
+
else
|
|
307
|
+
for key, child in value do
|
|
308
|
+
result[tostring(key)] = encodeSerializable(child, depth + 1, state, seen)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
seen[value] = nil
|
|
312
|
+
return result
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
function ValueCodec.encodeSerializable(value, limit)
|
|
316
|
+
return encodeSerializable(value, 0, { count = 0, limit = limit or 500 }, {})
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
function ValueCodec.encodeTyped(value, limit)
|
|
320
|
+
local encoded = { type = typeof(value) }
|
|
321
|
+
if value ~= nil then
|
|
322
|
+
encoded.value = ValueCodec.encodeSerializable(value, limit)
|
|
323
|
+
end
|
|
324
|
+
return encoded
|
|
325
|
+
end
|
|
326
|
+
|
|
211
327
|
function ValueCodec.setProperty(instance, propertyName, value)
|
|
212
328
|
if propertyName == "Parent" or propertyName == "ClassName" or propertyName == "Source" then
|
|
213
329
|
error("Property cannot be changed through setProperties: " .. propertyName)
|