dominus-cli 2.2.1 → 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.
@@ -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
@@ -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 = {}
@@ -44,6 +45,7 @@ function Mutation.apply(spec)
44
45
  end
45
46
 
46
47
  local results = {}
48
+ local created = {}
47
49
  local ok, err = pcall(function()
48
50
  for index, operation in operations do
49
51
  assert(type(operation) == "table", "Operation " .. index .. " must be an object")
@@ -54,17 +56,53 @@ function Mutation.apply(spec)
54
56
  target = InstanceRegistry.toRef(target),
55
57
  properties = setProperties(target, operation.properties),
56
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
+ })
57
73
  elseif operation.op == "create" then
58
74
  local parent = resolve(operation.parent, "Operation " .. index .. " parent")
59
75
  assert(type(operation.className) == "string", "create.className must be a string")
60
76
  assert(type(operation.name) == "string", "create.name must be a string")
61
77
  local instance = Instance.new(operation.className)
78
+ table.insert(created, instance)
62
79
  instance.Name = operation.name
63
80
  if operation.properties then
64
81
  setProperties(instance, operation.properties)
65
82
  end
66
83
  instance.Parent = parent
67
84
  table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(instance) })
85
+ elseif operation.op == "clone" then
86
+ local target = resolve(operation.target, "Operation " .. index .. " target")
87
+ assert(target ~= game and target.Parent ~= game, "Services cannot be cloned")
88
+ local parent = operation.parent and resolve(operation.parent, "Operation " .. index .. " parent") or target.Parent
89
+ assert(parent ~= nil, "Clone target has no parent")
90
+ local clone = target:Clone()
91
+ assert(clone ~= nil, "Clone target is not Archivable")
92
+ table.insert(created, clone)
93
+ if operation.name then
94
+ clone.Name = operation.name
95
+ end
96
+ if operation.properties then
97
+ setProperties(clone, operation.properties)
98
+ end
99
+ if operation.offset then
100
+ assert(clone:IsA("PVInstance"), "Only Models and BaseParts can be spatially offset")
101
+ local offset = ValueCodec.decodeForCurrent(Vector3.zero, operation.offset)
102
+ clone:PivotTo(clone:GetPivot() + offset)
103
+ end
104
+ clone.Parent = parent
105
+ table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(clone) })
68
106
  elseif operation.op == "move" then
69
107
  local target = resolve(operation.target, "Operation " .. index .. " target")
70
108
  local parent = resolve(operation.parent, "Operation " .. index .. " parent")
@@ -79,6 +117,13 @@ function Mutation.apply(spec)
79
117
 
80
118
  if not ok then
81
119
  ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
120
+ for _, instance in created do
121
+ pcall(function()
122
+ if instance.Parent == nil then
123
+ instance:Destroy()
124
+ end
125
+ end)
126
+ end
82
127
  return { success = false, error = tostring(err), rolledBack = true }
83
128
  end
84
129
 
@@ -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)