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.
@@ -256,10 +256,445 @@ return "__DOMINUS_BRIDGE_TOKEN__"
256
256
  </Properties>
257
257
  </Item>
258
258
  <Item class="ModuleScript" referent="2">
259
+ <Properties>
260
+ <string name="Name">Building</string>
261
+ <string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
262
+ local Selection = game:GetService("Selection")
263
+
264
+ local InstanceRegistry = require(script.Parent.InstanceRegistry)
265
+ local ValueCodec = require(script.Parent.ValueCodec)
266
+
267
+ local Building = {}
268
+
269
+ local MAX_ITEMS = 200
270
+ local MAX_PROPERTIES = 100
271
+
272
+ local ALLOWED_PART_CLASSES = {
273
+ Part = true,
274
+ WedgePart = true,
275
+ CornerWedgePart = true,
276
+ TrussPart = true,
277
+ MeshPart = true,
278
+ SpawnLocation = true,
279
+ Seat = true,
280
+ VehicleSeat = true,
281
+ }
282
+
283
+ local function resolve(target, label)
284
+ local instance, err = InstanceRegistry.resolve(target)
285
+ if not instance then
286
+ error(label .. ": " .. tostring(err))
287
+ end
288
+ return instance
289
+ end
290
+
291
+ local function cleanup(instances)
292
+ for _, instance in instances do
293
+ pcall(function()
294
+ if instance.Parent == nil then
295
+ instance:Destroy()
296
+ end
297
+ end)
298
+ end
299
+ end
300
+
301
+ local function record(name, callback, created)
302
+ local recording = ChangeHistoryService:TryBeginRecording(name)
303
+ if not recording then
304
+ return { success = false, error = "Another plugin recording is already active" }
305
+ end
306
+
307
+ local ok, result = pcall(callback)
308
+ if not ok then
309
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
310
+ cleanup(created or {})
311
+ return { success = false, error = tostring(result), rolledBack = true }
312
+ end
313
+
314
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
315
+ result.success = true
316
+ return result
317
+ end
318
+
319
+ local function finiteNumber(value, label)
320
+ assert(type(value) == "number" and value == value and math.abs(value) < math.huge, label .. " must be finite")
321
+ return value
322
+ end
323
+
324
+ local function vector3(value, label, default)
325
+ if value == nil then
326
+ return default
327
+ end
328
+ assert(type(value) == "table", label .. " must be a Vector3 object")
329
+ return Vector3.new(
330
+ finiteNumber(value.X or value.x or value[1], label .. ".x"),
331
+ finiteNumber(value.Y or value.y or value[2], label .. ".y"),
332
+ finiteNumber(value.Z or value.z or value[3], label .. ".z")
333
+ )
334
+ end
335
+
336
+ local function rotation(value, label)
337
+ local degrees = vector3(value, label, Vector3.zero)
338
+ return CFrame.fromOrientation(math.rad(degrees.X), math.rad(degrees.Y), math.rad(degrees.Z))
339
+ end
340
+
341
+ local function setProperties(instance, properties)
342
+ if properties == nil then
343
+ return {}
344
+ end
345
+ assert(type(properties) == "table", "properties must be an object")
346
+ local count = 0
347
+ local applied = {}
348
+ for propertyName, value in properties do
349
+ count += 1
350
+ if count > MAX_PROPERTIES then
351
+ error("At most " .. MAX_PROPERTIES .. " properties are allowed per item")
352
+ end
353
+ applied[propertyName] = ValueCodec.setProperty(instance, propertyName, value)
354
+ end
355
+ return applied
356
+ end
357
+
358
+ local function validateItemCount(items, label, maximum)
359
+ assert(type(items) == "table" and #items > 0, label .. " must contain at least one item")
360
+ assert(#items <= maximum, label .. " is limited to " .. maximum .. " items")
361
+ end
362
+
363
+ local function assertMovable(instance, label)
364
+ assert(instance ~= game and instance.Parent ~= game, label .. " cannot be a DataModel service")
365
+ end
366
+
367
+ function Building.createParts(spec)
368
+ validateItemCount(spec.parts, "parts", MAX_ITEMS)
369
+ local parent = spec.parent and resolve(spec.parent, "parent") or workspace
370
+ local group = nil
371
+ local created = {}
372
+
373
+ return record("Dominus 2: Build parts", function()
374
+ if spec.groupName ~= nil then
375
+ assert(type(spec.groupName) == "string" and #spec.groupName > 0, "groupName must be a non-empty string")
376
+ local groupClass = spec.groupClass or "Model"
377
+ assert(groupClass == "Model" or groupClass == "Folder", "groupClass must be Model or Folder")
378
+ group = Instance.new(groupClass)
379
+ group.Name = spec.groupName
380
+ table.insert(created, group)
381
+ end
382
+
383
+ local parts = {}
384
+ for index, item in spec.parts do
385
+ assert(type(item) == "table", "Part " .. index .. " must be an object")
386
+ assert(type(item.name) == "string" and #item.name > 0, "Part " .. index .. ".name must be a non-empty string")
387
+ local className = item.className or "Part"
388
+ if item.shape == "Wedge" then
389
+ className = "WedgePart"
390
+ elseif item.shape == "CornerWedge" then
391
+ className = "CornerWedgePart"
392
+ end
393
+ assert(ALLOWED_PART_CLASSES[className] == true, "Part " .. index .. " uses an unsupported class: " .. tostring(className))
394
+
395
+ local part = Instance.new(className)
396
+ table.insert(created, part)
397
+ part.Name = item.name
398
+ if part:IsA("BasePart") then
399
+ part.Anchored = item.anchored ~= false
400
+ end
401
+ if item.position then
402
+ part.Position = vector3(item.position, "Part " .. index .. ".position")
403
+ end
404
+ if item.orientation then
405
+ part.Orientation = vector3(item.orientation, "Part " .. index .. ".orientation")
406
+ end
407
+ if item.size then
408
+ part.Size = vector3(item.size, "Part " .. index .. ".size")
409
+ end
410
+ if item.color then
411
+ ValueCodec.setProperty(part, "Color", item.color)
412
+ end
413
+ if item.material then
414
+ ValueCodec.setProperty(part, "Material", item.material)
415
+ end
416
+ if item.transparency ~= nil then
417
+ part.Transparency = finiteNumber(item.transparency, "Part " .. index .. ".transparency")
418
+ end
419
+ if item.canCollide ~= nil then
420
+ assert(type(item.canCollide) == "boolean", "Part " .. index .. ".canCollide must be a boolean")
421
+ part.CanCollide = item.canCollide
422
+ end
423
+ if item.shape and className == "Part" then
424
+ ValueCodec.setProperty(part, "Shape", item.shape)
425
+ end
426
+ setProperties(part, item.properties)
427
+ part.Parent = group or parent
428
+ table.insert(parts, part)
429
+ end
430
+
431
+ if group then
432
+ group.Parent = parent
433
+ end
434
+
435
+ local refs = {}
436
+ for _, part in parts do
437
+ table.insert(refs, InstanceRegistry.toRef(part))
438
+ end
439
+ return {
440
+ created = refs,
441
+ group = group and InstanceRegistry.toRef(group) or nil,
442
+ count = #refs,
443
+ }
444
+ end, created)
445
+ end
446
+
447
+ function Building.cloneInstances(spec)
448
+ validateItemCount(spec.requests, "requests", 100)
449
+ local created = {}
450
+
451
+ return record("Dominus 2: Clone instances", function()
452
+ local clones = {}
453
+ local totalCopies = 0
454
+ for requestIndex, request in spec.requests do
455
+ assert(type(request) == "table", "Clone request " .. requestIndex .. " must be an object")
456
+ local source = resolve(request.target, "Clone request " .. requestIndex .. " target")
457
+ assertMovable(source, "Clone source")
458
+ local parent = request.parent and resolve(request.parent, "Clone request " .. requestIndex .. " parent") or source.Parent
459
+ assert(parent ~= nil, "Clone request " .. requestIndex .. " source has no parent")
460
+ local count = request.count or 1
461
+ assert(type(count) == "number" and count % 1 == 0 and count >= 1 and count <= 50, "count must be an integer from 1 to 50")
462
+ totalCopies += count
463
+ assert(totalCopies <= MAX_ITEMS, "A clone batch is limited to " .. MAX_ITEMS .. " copies")
464
+ local offset = vector3(request.offset, "Clone request " .. requestIndex .. ".offset", Vector3.zero)
465
+ local stepOffset = vector3(request.stepOffset, "Clone request " .. requestIndex .. ".stepOffset", Vector3.zero)
466
+
467
+ for copyIndex = 1, count do
468
+ local clone = source:Clone()
469
+ assert(clone ~= nil, "Clone request " .. requestIndex .. " target is not Archivable")
470
+ table.insert(created, clone)
471
+ if request.name then
472
+ clone.Name = count == 1 and request.name or (request.name .. " " .. copyIndex)
473
+ end
474
+ setProperties(clone, request.properties)
475
+ if clone:IsA("PVInstance") then
476
+ local totalOffset = offset + stepOffset * (copyIndex - 1)
477
+ clone:PivotTo(clone:GetPivot() + totalOffset)
478
+ elseif offset.Magnitude > 0 or stepOffset.Magnitude > 0 then
479
+ error("Clone request " .. requestIndex .. " cannot offset a non-PVInstance")
480
+ end
481
+ clone.Parent = parent
482
+ table.insert(clones, InstanceRegistry.toRef(clone))
483
+ end
484
+ end
485
+ return { clones = clones, count = #clones }
486
+ end, created)
487
+ end
488
+
489
+ local function validateIndependentTargets(targets)
490
+ for leftIndex, left in targets do
491
+ for rightIndex = leftIndex + 1, #targets do
492
+ local right = targets[rightIndex]
493
+ assert(not left:IsAncestorOf(right) and not right:IsAncestorOf(left), "Targets " .. leftIndex .. " and " .. rightIndex .. " overlap")
494
+ end
495
+ end
496
+ end
497
+
498
+ function Building.groupInstances(spec)
499
+ validateItemCount(spec.targets, "targets", 100)
500
+ assert(type(spec.name) == "string" and #spec.name > 0, "name must be a non-empty string")
501
+ local className = spec.className or "Model"
502
+ assert(className == "Model" or className == "Folder", "className must be Model or Folder")
503
+ local targets = {}
504
+ for index, targetRef in spec.targets do
505
+ local target = resolve(targetRef, "Target " .. index)
506
+ assertMovable(target, "Target " .. index)
507
+ table.insert(targets, target)
508
+ end
509
+ validateIndependentTargets(targets)
510
+ local parent = spec.parent and resolve(spec.parent, "parent") or targets[1].Parent
511
+ assert(parent ~= nil, "The first target has no parent")
512
+ local created = {}
513
+
514
+ return record("Dominus 2: Group instances", function()
515
+ local group = Instance.new(className)
516
+ table.insert(created, group)
517
+ group.Name = spec.name
518
+ group.Parent = parent
519
+ for _, target in targets do
520
+ assert(not target:IsAncestorOf(group), "Cannot group an ancestor into its descendant")
521
+ target.Parent = group
522
+ end
523
+ return { group = InstanceRegistry.toRef(group), count = #targets }
524
+ end, created)
525
+ end
526
+
527
+ function Building.ungroupInstances(spec)
528
+ assert(spec.confirm == true, "confirm must be true")
529
+ validateItemCount(spec.targets, "targets", 20)
530
+
531
+ return record("Dominus 2: Ungroup instances", function()
532
+ local moved = {}
533
+ local removed = {}
534
+ for index, targetRef in spec.targets do
535
+ local group = resolve(targetRef, "Group " .. index)
536
+ assert(group:IsA("Model") or group:IsA("Folder"), "Group " .. index .. " must be a Model or Folder")
537
+ assertMovable(group, "Group " .. index)
538
+ local parent = spec.parent and resolve(spec.parent, "parent") or group.Parent
539
+ assert(parent ~= nil, "Group " .. index .. " has no parent")
540
+ for _, child in group:GetChildren() do
541
+ child.Parent = parent
542
+ table.insert(moved, InstanceRegistry.toRef(child))
543
+ end
544
+ table.insert(removed, { name = group.Name, className = group.ClassName })
545
+ group:Destroy()
546
+ end
547
+ return { moved = moved, removedGroups = removed, count = #moved }
548
+ end)
549
+ end
550
+
551
+ local function absolutePivot(current, operation)
552
+ if operation.cframe then
553
+ return ValueCodec.decodeForCurrent(current, operation.cframe)
554
+ end
555
+ local position = vector3(operation.position, "position", current.Position)
556
+ local x, y, z = current:ToOrientation()
557
+ if operation.orientation then
558
+ local degrees = vector3(operation.orientation, "orientation")
559
+ x, y, z = math.rad(degrees.X), math.rad(degrees.Y), math.rad(degrees.Z)
560
+ end
561
+ return CFrame.new(position) * CFrame.fromOrientation(x, y, z)
562
+ end
563
+
564
+ function Building.transformInstances(spec)
565
+ validateItemCount(spec.operations, "operations", 100)
566
+
567
+ return record("Dominus 2: Transform instances", function()
568
+ local results = {}
569
+ for index, operation in spec.operations do
570
+ local target = resolve(operation.target, "Operation " .. index .. " target")
571
+ assert(target:IsA("PVInstance"), "Operation " .. index .. " target must be a Model or BasePart")
572
+ local current = target:GetPivot()
573
+ local mode = operation.mode or "absolute"
574
+ local nextPivot
575
+ if mode == "absolute" then
576
+ nextPivot = absolutePivot(current, operation)
577
+ elseif mode == "relative" then
578
+ local offset = vector3(operation.offset, "Operation " .. index .. ".offset", Vector3.zero)
579
+ local rotate = rotation(operation.rotationOffset, "Operation " .. index .. ".rotationOffset")
580
+ if operation.space == "local" then
581
+ nextPivot = current * CFrame.new(offset) * rotate
582
+ else
583
+ nextPivot = CFrame.new(offset) * current * rotate
584
+ end
585
+ else
586
+ error("Operation " .. index .. ".mode must be absolute or relative")
587
+ end
588
+ target:PivotTo(nextPivot)
589
+ table.insert(results, { target = InstanceRegistry.toRef(target), pivot = ValueCodec.encode(target:GetPivot()) })
590
+ end
591
+ return { transformed = results, count = #results }
592
+ end)
593
+ end
594
+
595
+ function Building.createWelds(spec)
596
+ validateItemCount(spec.welds, "welds", MAX_ITEMS)
597
+ local created = {}
598
+
599
+ return record("Dominus 2: Create welds", function()
600
+ local welds = {}
601
+ for index, item in spec.welds do
602
+ local part0 = resolve(item.part0, "Weld " .. index .. " part0")
603
+ local part1 = resolve(item.part1, "Weld " .. index .. " part1")
604
+ assert(part0:IsA("BasePart") and part1:IsA("BasePart"), "Weld " .. index .. " endpoints must be BaseParts")
605
+ assert(part0 ~= part1, "Weld " .. index .. " endpoints must be different")
606
+ local parent = item.parent and resolve(item.parent, "Weld " .. index .. " parent") or part0
607
+ local weld = Instance.new("WeldConstraint")
608
+ table.insert(created, weld)
609
+ weld.Name = item.name or (part0.Name .. "_to_" .. part1.Name)
610
+ weld.Part0 = part0
611
+ weld.Part1 = part1
612
+ weld.Parent = parent
613
+ table.insert(welds, InstanceRegistry.toRef(weld))
614
+ end
615
+ return { welds = welds, count = #welds }
616
+ end, created)
617
+ end
618
+
619
+ local function overlapParams(spec)
620
+ local params = OverlapParams.new()
621
+ local maxParts = spec.maxParts or 200
622
+ assert(type(maxParts) == "number" and maxParts % 1 == 0 and maxParts >= 1 and maxParts <= 500, "maxParts must be an integer from 1 to 500")
623
+ params.MaxParts = maxParts
624
+ params.RespectCanCollide = spec.respectCanCollide == true
625
+ if spec.filter and #spec.filter > 0 then
626
+ local instances = {}
627
+ for index, targetRef in spec.filter do
628
+ table.insert(instances, resolve(targetRef, "Filter " .. index))
629
+ end
630
+ params.FilterDescendantsInstances = instances
631
+ params.FilterType = spec.filterType == "Include" and Enum.RaycastFilterType.Include or Enum.RaycastFilterType.Exclude
632
+ end
633
+ return params
634
+ end
635
+
636
+ function Building.queryParts(spec)
637
+ local root = spec.root and resolve(spec.root, "root") or workspace
638
+ assert(root:IsA("WorldRoot"), "root must be Workspace or a WorldModel")
639
+ local mode = spec.mode or "box"
640
+ local found
641
+ if mode == "box" then
642
+ local position = vector3(spec.position, "position")
643
+ local size = vector3(spec.size, "size")
644
+ assert(size.X > 0 and size.Y > 0 and size.Z > 0, "size components must be greater than zero")
645
+ local box = CFrame.new(position) * rotation(spec.orientation, "orientation")
646
+ found = root:GetPartBoundsInBox(box, size, overlapParams(spec))
647
+ elseif mode == "radius" then
648
+ local position = vector3(spec.position, "position")
649
+ local radius = finiteNumber(spec.radius, "radius")
650
+ assert(radius > 0, "radius must be greater than zero")
651
+ found = root:GetPartBoundsInRadius(position, radius, overlapParams(spec))
652
+ else
653
+ error("mode must be box or radius")
654
+ end
655
+
656
+ table.sort(found, function(left, right)
657
+ return InstanceRegistry.getDisplayPath(left) < InstanceRegistry.getDisplayPath(right)
658
+ end)
659
+ local parts = {}
660
+ for _, part in found do
661
+ table.insert(parts, {
662
+ name = part.Name,
663
+ className = part.ClassName,
664
+ ref = InstanceRegistry.toRef(part),
665
+ position = ValueCodec.encode(part.Position),
666
+ size = ValueCodec.encode(part.Size),
667
+ })
668
+ end
669
+ return { success = true, parts = parts, count = #parts, mode = mode }
670
+ end
671
+
672
+ function Building.setSelection(spec)
673
+ assert(type(spec.targets) == "table", "targets must be an array")
674
+ assert(#spec.targets <= 200, "At most 200 instances can be selected")
675
+ local targets = {}
676
+ for index, targetRef in spec.targets do
677
+ table.insert(targets, resolve(targetRef, "Target " .. index))
678
+ end
679
+ Selection:Set(targets)
680
+ local selected = {}
681
+ for _, target in targets do
682
+ table.insert(selected, InstanceRegistry.toRef(target))
683
+ end
684
+ return { success = true, selection = selected, count = #selected }
685
+ end
686
+
687
+ return Building
688
+ ]]></string>
689
+ </Properties>
690
+ </Item>
691
+ <Item class="ModuleScript" referent="3">
259
692
  <Properties>
260
693
  <string name="Name">CommandRouter</string>
261
- <string name="Source"><![CDATA[local Explorer = require(script.Parent.Explorer)
694
+ <string name="Source"><![CDATA[local Building = require(script.Parent.Building)
695
+ local Explorer = require(script.Parent.Explorer)
262
696
  local InstanceRegistry = require(script.Parent.InstanceRegistry)
697
+ local Metadata = require(script.Parent.Metadata)
263
698
  local Mutation = require(script.Parent.Mutation)
264
699
  local Properties = require(script.Parent.Properties)
265
700
  local Reflection = require(script.Parent.Reflection)
@@ -297,6 +732,45 @@ local handlers = {
297
732
  ["studio:v2:apply"] = function(payload)
298
733
  return Mutation.apply(payload)
299
734
  end,
735
+ ["studio:v2:create_parts"] = function(payload)
736
+ return Building.createParts(payload)
737
+ end,
738
+ ["studio:v2:clone_instances"] = function(payload)
739
+ return Building.cloneInstances(payload)
740
+ end,
741
+ ["studio:v2:group_instances"] = function(payload)
742
+ return Building.groupInstances(payload)
743
+ end,
744
+ ["studio:v2:ungroup_instances"] = function(payload)
745
+ return Building.ungroupInstances(payload)
746
+ end,
747
+ ["studio:v2:transform_instances"] = function(payload)
748
+ return Building.transformInstances(payload)
749
+ end,
750
+ ["studio:v2:create_welds"] = function(payload)
751
+ return Building.createWelds(payload)
752
+ end,
753
+ ["studio:v2:query_parts"] = function(payload)
754
+ return Building.queryParts(payload)
755
+ end,
756
+ ["studio:v2:set_selection"] = function(payload)
757
+ return Building.setSelection(payload)
758
+ end,
759
+ ["studio:v2:get_metadata"] = function(payload)
760
+ return Metadata.get(payload)
761
+ end,
762
+ ["studio:v2:set_attributes"] = function(payload)
763
+ return Metadata.setAttributes(payload)
764
+ end,
765
+ ["studio:v2:update_tags"] = function(payload)
766
+ return Metadata.updateTags(payload)
767
+ end,
768
+ ["studio:v2:list_callable_methods"] = function(payload)
769
+ return Metadata.listCallableMethods(payload)
770
+ end,
771
+ ["studio:v2:invoke_methods"] = function(payload)
772
+ return Metadata.invoke(payload)
773
+ end,
300
774
  ["studio:v2:delete"] = function(payload)
301
775
  return Mutation.delete(payload)
302
776
  end,
@@ -355,7 +829,7 @@ return CommandRouter
355
829
  ]]></string>
356
830
  </Properties>
357
831
  </Item>
358
- <Item class="ModuleScript" referent="3">
832
+ <Item class="ModuleScript" referent="4">
359
833
  <Properties>
360
834
  <string name="Name">Explorer</string>
361
835
  <string name="Source"><![CDATA[--[[
@@ -891,7 +1365,7 @@ return Explorer
891
1365
  ]]></string>
892
1366
  </Properties>
893
1367
  </Item>
894
- <Item class="ModuleScript" referent="4">
1368
+ <Item class="ModuleScript" referent="5">
895
1369
  <Properties>
896
1370
  <string name="Name">InstanceRegistry</string>
897
1371
  <string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
@@ -1019,12 +1493,430 @@ return InstanceRegistry
1019
1493
  ]]></string>
1020
1494
  </Properties>
1021
1495
  </Item>
1022
- <Item class="ModuleScript" referent="5">
1496
+ <Item class="ModuleScript" referent="6">
1497
+ <Properties>
1498
+ <string name="Name">Metadata</string>
1499
+ <string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
1500
+
1501
+ local InstanceRegistry = require(script.Parent.InstanceRegistry)
1502
+ local ValueCodec = require(script.Parent.ValueCodec)
1503
+
1504
+ local Metadata = {}
1505
+
1506
+ local MAX_TARGETS = 100
1507
+ local MAX_ITEMS_PER_TARGET = 100
1508
+ local MAX_METHOD_CALLS = 50
1509
+
1510
+ local ATTRIBUTE_TYPES = {
1511
+ string = true,
1512
+ boolean = true,
1513
+ number = true,
1514
+ UDim = true,
1515
+ UDim2 = true,
1516
+ BrickColor = true,
1517
+ Color3 = true,
1518
+ Vector2 = true,
1519
+ Vector3 = true,
1520
+ CFrame = true,
1521
+ NumberSequence = true,
1522
+ ColorSequence = true,
1523
+ NumberRange = true,
1524
+ Rect = true,
1525
+ Font = true,
1526
+ }
1527
+
1528
+ local METHOD_POLICY = {
1529
+ GetAttribute = { kind = "read", className = "Instance", params = { { type = "string" } } },
1530
+ GetAttributes = { kind = "read", className = "Instance", params = {} },
1531
+ GetTags = { kind = "read", className = "Instance", params = {} },
1532
+ HasTag = { kind = "read", className = "Instance", params = { { type = "string" } } },
1533
+ GetFullName = { kind = "read", className = "Instance", params = {} },
1534
+ IsA = { kind = "read", className = "Instance", params = { { type = "string" } } },
1535
+ IsAncestorOf = { kind = "read", className = "Instance", params = { { type = "Instance" } } },
1536
+ IsDescendantOf = { kind = "read", className = "Instance", params = { { type = "Instance" } } },
1537
+ FindFirstChild = {
1538
+ kind = "read",
1539
+ className = "Instance",
1540
+ params = { { type = "string" }, { type = "boolean", optional = true } },
1541
+ },
1542
+ FindFirstChildOfClass = { kind = "read", className = "Instance", params = { { type = "string" } } },
1543
+ FindFirstChildWhichIsA = {
1544
+ kind = "read",
1545
+ className = "Instance",
1546
+ params = { { type = "string" }, { type = "boolean", optional = true } },
1547
+ },
1548
+ FindFirstAncestor = { kind = "read", className = "Instance", params = { { type = "string" } } },
1549
+ FindFirstAncestorOfClass = { kind = "read", className = "Instance", params = { { type = "string" } } },
1550
+ FindFirstAncestorWhichIsA = { kind = "read", className = "Instance", params = { { type = "string" } } },
1551
+ GetChildren = { kind = "read", className = "Instance", params = {} },
1552
+ GetDescendants = { kind = "read", className = "Instance", params = {} },
1553
+ GetPivot = { kind = "read", className = "PVInstance", params = {} },
1554
+ GetScale = { kind = "read", className = "Model", params = {} },
1555
+ GetBoundingBox = { kind = "read", className = "Model", params = {} },
1556
+ AddTag = { kind = "write", className = "Instance", params = { { type = "string", tag = true } } },
1557
+ RemoveTag = { kind = "write", className = "Instance", params = { { type = "string", tag = true } } },
1558
+ SetAttribute = {
1559
+ kind = "write",
1560
+ className = "Instance",
1561
+ params = { { type = "string", attributeName = true }, { type = "Attribute", allowNil = true } },
1562
+ },
1563
+ PivotTo = { kind = "write", className = "PVInstance", params = { { type = "CFrame" } } },
1564
+ ScaleTo = { kind = "write", className = "Model", params = { { type = "number" } } },
1565
+ }
1566
+
1567
+ local function resolve(ref, label)
1568
+ local instance, err = InstanceRegistry.resolve(ref)
1569
+ if not instance then
1570
+ error(label .. ": " .. tostring(err))
1571
+ end
1572
+ return instance
1573
+ end
1574
+
1575
+ local function validateName(value, label)
1576
+ assert(type(value) == "string", label .. " must be a string")
1577
+ assert(#value > 0 and #value <= 100, label .. " must contain 1 to 100 characters")
1578
+ assert(not value:find("[%c]"), label .. " cannot contain control characters")
1579
+ return value
1580
+ end
1581
+
1582
+ local function validateTag(value)
1583
+ return validateName(value, "Tag")
1584
+ end
1585
+
1586
+ local function validateAttributeName(value)
1587
+ return validateName(value, "Attribute name")
1588
+ end
1589
+
1590
+ local function prepareAttributeChanges(set, remove, label)
1591
+ set = set or {}
1592
+ remove = remove or {}
1593
+ assert(type(set) == "table" and type(remove) == "table", "set and remove must be arrays")
1594
+ assert(#set + #remove > 0, label .. " has no attribute updates")
1595
+ assert(#set + #remove <= MAX_ITEMS_PER_TARGET, "Too many attribute updates in " .. label)
1596
+
1597
+ local changes = {}
1598
+ local seen = {}
1599
+ for itemIndex, item in set do
1600
+ assert(type(item) == "table" and type(item.value) == "table", "Set item " .. itemIndex .. " is invalid")
1601
+ local name = validateAttributeName(item.name)
1602
+ assert(not seen[name], "Attribute " .. name .. " appears more than once")
1603
+ local typeName = item.value.type
1604
+ assert(ATTRIBUTE_TYPES[typeName], "Unsupported attribute type: " .. tostring(typeName))
1605
+ seen[name] = true
1606
+ table.insert(changes, { name = name, value = ValueCodec.decodeTyped(typeName, item.value.value) })
1607
+ end
1608
+ for _, nameValue in remove do
1609
+ local name = validateAttributeName(nameValue)
1610
+ assert(not seen[name], "Attribute " .. name .. " cannot be set and removed together")
1611
+ seen[name] = true
1612
+ table.insert(changes, { name = name, remove = true })
1613
+ end
1614
+ return changes
1615
+ end
1616
+
1617
+ local function applyPreparedAttributeChanges(target, changes)
1618
+ for _, change in changes do
1619
+ if change.remove then
1620
+ target:SetAttribute(change.name, nil)
1621
+ else
1622
+ target:SetAttribute(change.name, change.value)
1623
+ end
1624
+ end
1625
+ end
1626
+
1627
+ local function prepareTagChanges(add, remove, label)
1628
+ add = add or {}
1629
+ remove = remove or {}
1630
+ assert(type(add) == "table" and type(remove) == "table", "add and remove must be arrays")
1631
+ assert(#add + #remove > 0, label .. " has no tag updates")
1632
+ assert(#add + #remove <= MAX_ITEMS_PER_TARGET, "Too many tag updates in " .. label)
1633
+
1634
+ local seen = {}
1635
+ for _, tagValue in add do
1636
+ local tag = validateTag(tagValue)
1637
+ assert(not seen[tag], "Tag " .. tag .. " appears more than once")
1638
+ seen[tag] = "add"
1639
+ end
1640
+ for _, tagValue in remove do
1641
+ local tag = validateTag(tagValue)
1642
+ assert(not seen[tag], "Tag " .. tag .. " cannot be added and removed together")
1643
+ seen[tag] = "remove"
1644
+ end
1645
+ return add, remove
1646
+ end
1647
+
1648
+ local function applyPreparedTagChanges(target, add, remove)
1649
+ for _, tag in add do
1650
+ target:AddTag(tag)
1651
+ end
1652
+ for _, tag in remove do
1653
+ target:RemoveTag(tag)
1654
+ end
1655
+ end
1656
+
1657
+ local function sortedTags(instance)
1658
+ local tags = instance:GetTags()
1659
+ table.sort(tags)
1660
+ return tags
1661
+ end
1662
+
1663
+ local function encodedAttributes(instance)
1664
+ local attributes = instance:GetAttributes()
1665
+ local names = {}
1666
+ for name in attributes do
1667
+ table.insert(names, name)
1668
+ end
1669
+ table.sort(names)
1670
+
1671
+ local encoded = {}
1672
+ for _, name in names do
1673
+ table.insert(encoded, {
1674
+ name = name,
1675
+ value = ValueCodec.encodeTyped(attributes[name]),
1676
+ })
1677
+ end
1678
+ return encoded
1679
+ end
1680
+
1681
+ local function metadataFor(instance)
1682
+ return {
1683
+ ref = InstanceRegistry.toRef(instance),
1684
+ name = instance.Name,
1685
+ className = instance.ClassName,
1686
+ tags = sortedTags(instance),
1687
+ attributes = encodedAttributes(instance),
1688
+ }
1689
+ end
1690
+
1691
+ local function beginRecording(name)
1692
+ local recording = ChangeHistoryService:TryBeginRecording(name)
1693
+ if not recording then
1694
+ error("Another plugin recording is already active")
1695
+ end
1696
+ return recording
1697
+ end
1698
+
1699
+ function Metadata.get(spec)
1700
+ assert(type(spec.targets) == "table" and #spec.targets > 0, "targets must contain at least one instance reference")
1701
+ assert(#spec.targets <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " instances can be inspected at once")
1702
+ local results = {}
1703
+ for index, targetRef in spec.targets do
1704
+ table.insert(results, metadataFor(resolve(targetRef, "Target " .. index)))
1705
+ end
1706
+ return { success = true, results = results }
1707
+ end
1708
+
1709
+ function Metadata.setAttributes(spec)
1710
+ assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one attribute update")
1711
+ assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " attribute operations are allowed")
1712
+
1713
+ local prepared = {}
1714
+ for index, operation in spec.operations do
1715
+ assert(type(operation) == "table", "Operation " .. index .. " must be an object")
1716
+ table.insert(prepared, {
1717
+ target = resolve(operation.target, "Operation " .. index .. " target"),
1718
+ changes = prepareAttributeChanges(operation.set, operation.remove, "Operation " .. index),
1719
+ })
1720
+ end
1721
+
1722
+ local recording = beginRecording("Dominus 2: Update attributes")
1723
+ local ok, err = pcall(function()
1724
+ for _, operation in prepared do
1725
+ applyPreparedAttributeChanges(operation.target, operation.changes)
1726
+ end
1727
+ end)
1728
+ if not ok then
1729
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
1730
+ return { success = false, error = tostring(err), rolledBack = true }
1731
+ end
1732
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
1733
+
1734
+ local results = {}
1735
+ for _, operation in prepared do
1736
+ table.insert(results, metadataFor(operation.target))
1737
+ end
1738
+ return { success = true, operationCount = #prepared, results = results }
1739
+ end
1740
+
1741
+ function Metadata.updateTags(spec)
1742
+ assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one tag update")
1743
+ assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " tag operations are allowed")
1744
+
1745
+ local prepared = {}
1746
+ for index, operation in spec.operations do
1747
+ assert(type(operation) == "table", "Operation " .. index .. " must be an object")
1748
+ local add, remove = prepareTagChanges(operation.add, operation.remove, "Operation " .. index)
1749
+ table.insert(prepared, {
1750
+ target = resolve(operation.target, "Operation " .. index .. " target"),
1751
+ add = add,
1752
+ remove = remove,
1753
+ })
1754
+ end
1755
+
1756
+ local recording = beginRecording("Dominus 2: Update tags")
1757
+ local ok, err = pcall(function()
1758
+ for _, operation in prepared do
1759
+ applyPreparedTagChanges(operation.target, operation.add, operation.remove)
1760
+ end
1761
+ end)
1762
+ if not ok then
1763
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
1764
+ return { success = false, error = tostring(err), rolledBack = true }
1765
+ end
1766
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
1767
+
1768
+ local results = {}
1769
+ for _, operation in prepared do
1770
+ table.insert(results, metadataFor(operation.target))
1771
+ end
1772
+ return { success = true, operationCount = #prepared, results = results }
1773
+ end
1774
+
1775
+ function Metadata.applyAttributeChangesWithinRecording(target, set, remove)
1776
+ local changes = prepareAttributeChanges(set, remove, "Mutation operation")
1777
+ applyPreparedAttributeChanges(target, changes)
1778
+ return encodedAttributes(target)
1779
+ end
1780
+
1781
+ function Metadata.applyTagChangesWithinRecording(target, add, remove)
1782
+ local preparedAdd, preparedRemove = prepareTagChanges(add, remove, "Mutation operation")
1783
+ applyPreparedTagChanges(target, preparedAdd, preparedRemove)
1784
+ return sortedTags(target)
1785
+ end
1786
+
1787
+ local function publicPolicy(methodName, policy)
1788
+ local params = {}
1789
+ for _, param in policy.params do
1790
+ table.insert(params, {
1791
+ type = param.type,
1792
+ optional = param.optional or false,
1793
+ })
1794
+ end
1795
+ return {
1796
+ name = methodName,
1797
+ kind = policy.kind,
1798
+ className = policy.className,
1799
+ parameters = params,
1800
+ }
1801
+ end
1802
+
1803
+ function Metadata.listCallableMethods(spec)
1804
+ local target = resolve(spec.target, "Target")
1805
+ local methods = {}
1806
+ for methodName, policy in METHOD_POLICY do
1807
+ if target:IsA(policy.className) then
1808
+ table.insert(methods, publicPolicy(methodName, policy))
1809
+ end
1810
+ end
1811
+ table.sort(methods, function(left, right)
1812
+ return left.name < right.name
1813
+ end)
1814
+ return {
1815
+ success = true,
1816
+ target = InstanceRegistry.toRef(target),
1817
+ methods = methods,
1818
+ blockedByDefault = true,
1819
+ }
1820
+ end
1821
+
1822
+ local function decodeArgument(argument, parameter, label)
1823
+ assert(type(argument) == "table" and type(argument.type) == "string", label .. " must be a typed value")
1824
+ if parameter.type == "Attribute" then
1825
+ if argument.type == "nil" and parameter.allowNil then
1826
+ return nil
1827
+ end
1828
+ assert(ATTRIBUTE_TYPES[argument.type], label .. " must be a supported Roblox attribute type")
1829
+ return ValueCodec.decodeTyped(argument.type, argument.value)
1830
+ end
1831
+ assert(argument.type == parameter.type, label .. " must have type " .. parameter.type)
1832
+ local decoded = ValueCodec.decodeTyped(argument.type, argument.value)
1833
+ if parameter.tag then
1834
+ validateTag(decoded)
1835
+ elseif parameter.attributeName then
1836
+ validateAttributeName(decoded)
1837
+ end
1838
+ return decoded
1839
+ end
1840
+
1841
+ function Metadata.invoke(spec)
1842
+ assert(type(spec.calls) == "table" and #spec.calls > 0, "calls must contain at least one method call")
1843
+ assert(#spec.calls <= MAX_METHOD_CALLS, "At most " .. MAX_METHOD_CALLS .. " method calls are allowed")
1844
+
1845
+ local prepared = {}
1846
+ local hasWrites = false
1847
+ for index, call in spec.calls do
1848
+ assert(type(call) == "table", "Call " .. index .. " must be an object")
1849
+ local methodName = call.method
1850
+ local policy = METHOD_POLICY[methodName]
1851
+ assert(policy, "Method is not allowed by Dominus: " .. tostring(methodName))
1852
+ local target = resolve(call.target, "Call " .. index .. " target")
1853
+ assert(target:IsA(policy.className), methodName .. " requires " .. policy.className .. ", got " .. target.ClassName)
1854
+ local arguments = call.arguments or {}
1855
+ assert(type(arguments) == "table", "Call " .. index .. " arguments must be an array")
1856
+ local minimum = 0
1857
+ for _, parameter in policy.params do
1858
+ if not parameter.optional then
1859
+ minimum += 1
1860
+ end
1861
+ end
1862
+ assert(#arguments >= minimum and #arguments <= #policy.params, methodName .. " received the wrong number of arguments")
1863
+
1864
+ local decoded = {}
1865
+ for argumentIndex, argument in arguments do
1866
+ decoded[argumentIndex] = decodeArgument(argument, policy.params[argumentIndex], "Argument " .. argumentIndex)
1867
+ end
1868
+ table.insert(prepared, {
1869
+ target = target,
1870
+ method = methodName,
1871
+ policy = policy,
1872
+ arguments = decoded,
1873
+ argumentCount = #arguments,
1874
+ })
1875
+ hasWrites = hasWrites or policy.kind == "write"
1876
+ end
1877
+
1878
+ local recording = hasWrites and beginRecording("Dominus 2: Invoke safe instance methods") or nil
1879
+ local results = {}
1880
+ local ok, err = pcall(function()
1881
+ for _, call in prepared do
1882
+ local method = call.target[call.method]
1883
+ assert(type(method) == "function", call.method .. " is unavailable on " .. call.target.ClassName)
1884
+ local returned = table.pack(method(call.target, table.unpack(call.arguments, 1, call.argumentCount)))
1885
+ local values = {}
1886
+ for resultIndex = 1, returned.n do
1887
+ table.insert(values, ValueCodec.encodeTyped(returned[resultIndex]))
1888
+ end
1889
+ table.insert(results, {
1890
+ target = InstanceRegistry.toRef(call.target),
1891
+ method = call.method,
1892
+ kind = call.policy.kind,
1893
+ results = values,
1894
+ })
1895
+ end
1896
+ end)
1897
+ if not ok then
1898
+ if recording then
1899
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
1900
+ end
1901
+ return { success = false, error = tostring(err), rolledBack = recording ~= nil }
1902
+ end
1903
+ if recording then
1904
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
1905
+ end
1906
+ return { success = true, callCount = #results, mutationRecorded = recording ~= nil, results = results }
1907
+ end
1908
+
1909
+ return Metadata
1910
+ ]]></string>
1911
+ </Properties>
1912
+ </Item>
1913
+ <Item class="ModuleScript" referent="7">
1023
1914
  <Properties>
1024
1915
  <string name="Name">Mutation</string>
1025
1916
  <string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
1026
1917
 
1027
1918
  local InstanceRegistry = require(script.Parent.InstanceRegistry)
1919
+ local Metadata = require(script.Parent.Metadata)
1028
1920
  local ValueCodec = require(script.Parent.ValueCodec)
1029
1921
 
1030
1922
  local Mutation = {}
@@ -1068,6 +1960,7 @@ function Mutation.apply(spec)
1068
1960
  end
1069
1961
 
1070
1962
  local results = {}
1963
+ local created = {}
1071
1964
  local ok, err = pcall(function()
1072
1965
  for index, operation in operations do
1073
1966
  assert(type(operation) == "table", "Operation " .. index .. " must be an object")
@@ -1078,17 +1971,53 @@ function Mutation.apply(spec)
1078
1971
  target = InstanceRegistry.toRef(target),
1079
1972
  properties = setProperties(target, operation.properties),
1080
1973
  })
1974
+ elseif operation.op == "setAttributes" then
1975
+ local target = resolve(operation.target, "Operation " .. index .. " target")
1976
+ table.insert(results, {
1977
+ op = operation.op,
1978
+ target = InstanceRegistry.toRef(target),
1979
+ attributes = Metadata.applyAttributeChangesWithinRecording(target, operation.set, operation.remove),
1980
+ })
1981
+ elseif operation.op == "updateTags" then
1982
+ local target = resolve(operation.target, "Operation " .. index .. " target")
1983
+ table.insert(results, {
1984
+ op = operation.op,
1985
+ target = InstanceRegistry.toRef(target),
1986
+ tags = Metadata.applyTagChangesWithinRecording(target, operation.add, operation.remove),
1987
+ })
1081
1988
  elseif operation.op == "create" then
1082
1989
  local parent = resolve(operation.parent, "Operation " .. index .. " parent")
1083
1990
  assert(type(operation.className) == "string", "create.className must be a string")
1084
1991
  assert(type(operation.name) == "string", "create.name must be a string")
1085
1992
  local instance = Instance.new(operation.className)
1993
+ table.insert(created, instance)
1086
1994
  instance.Name = operation.name
1087
1995
  if operation.properties then
1088
1996
  setProperties(instance, operation.properties)
1089
1997
  end
1090
1998
  instance.Parent = parent
1091
1999
  table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(instance) })
2000
+ elseif operation.op == "clone" then
2001
+ local target = resolve(operation.target, "Operation " .. index .. " target")
2002
+ assert(target ~= game and target.Parent ~= game, "Services cannot be cloned")
2003
+ local parent = operation.parent and resolve(operation.parent, "Operation " .. index .. " parent") or target.Parent
2004
+ assert(parent ~= nil, "Clone target has no parent")
2005
+ local clone = target:Clone()
2006
+ assert(clone ~= nil, "Clone target is not Archivable")
2007
+ table.insert(created, clone)
2008
+ if operation.name then
2009
+ clone.Name = operation.name
2010
+ end
2011
+ if operation.properties then
2012
+ setProperties(clone, operation.properties)
2013
+ end
2014
+ if operation.offset then
2015
+ assert(clone:IsA("PVInstance"), "Only Models and BaseParts can be spatially offset")
2016
+ local offset = ValueCodec.decodeForCurrent(Vector3.zero, operation.offset)
2017
+ clone:PivotTo(clone:GetPivot() + offset)
2018
+ end
2019
+ clone.Parent = parent
2020
+ table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(clone) })
1092
2021
  elseif operation.op == "move" then
1093
2022
  local target = resolve(operation.target, "Operation " .. index .. " target")
1094
2023
  local parent = resolve(operation.parent, "Operation " .. index .. " parent")
@@ -1103,6 +2032,13 @@ function Mutation.apply(spec)
1103
2032
 
1104
2033
  if not ok then
1105
2034
  ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
2035
+ for _, instance in created do
2036
+ pcall(function()
2037
+ if instance.Parent == nil then
2038
+ instance:Destroy()
2039
+ end
2040
+ end)
2041
+ end
1106
2042
  return { success = false, error = tostring(err), rolledBack = true }
1107
2043
  end
1108
2044
 
@@ -1160,7 +2096,7 @@ return Mutation
1160
2096
  ]]></string>
1161
2097
  </Properties>
1162
2098
  </Item>
1163
- <Item class="ModuleScript" referent="6">
2099
+ <Item class="ModuleScript" referent="8">
1164
2100
  <Properties>
1165
2101
  <string name="Name">Properties</string>
1166
2102
  <string name="Source"><![CDATA[--[[
@@ -2025,7 +2961,7 @@ return Properties
2025
2961
  ]]></string>
2026
2962
  </Properties>
2027
2963
  </Item>
2028
- <Item class="ModuleScript" referent="7">
2964
+ <Item class="ModuleScript" referent="9">
2029
2965
  <Properties>
2030
2966
  <string name="Name">Protocol</string>
2031
2967
  <string name="Source"><![CDATA[--[[
@@ -2061,7 +2997,7 @@ return Protocol
2061
2997
  ]]></string>
2062
2998
  </Properties>
2063
2999
  </Item>
2064
- <Item class="ModuleScript" referent="8">
3000
+ <Item class="ModuleScript" referent="10">
2065
3001
  <Properties>
2066
3002
  <string name="Name">Reflection</string>
2067
3003
  <string name="Source"><![CDATA[local ReflectionService = game:GetService("ReflectionService")
@@ -2236,7 +3172,7 @@ return Reflection
2236
3172
  ]]></string>
2237
3173
  </Properties>
2238
3174
  </Item>
2239
- <Item class="ModuleScript" referent="9">
3175
+ <Item class="ModuleScript" referent="11">
2240
3176
  <Properties>
2241
3177
  <string name="Name">TestRunner</string>
2242
3178
  <string name="Source"><![CDATA[local StudioTestService = game:GetService("StudioTestService")
@@ -2311,7 +3247,7 @@ return TestRunner
2311
3247
  ]]></string>
2312
3248
  </Properties>
2313
3249
  </Item>
2314
- <Item class="ModuleScript" referent="10">
3250
+ <Item class="ModuleScript" referent="12">
2315
3251
  <Properties>
2316
3252
  <string name="Name">UIBuilder</string>
2317
3253
  <string name="Source"><![CDATA[--[[
@@ -3174,7 +4110,7 @@ return UIBuilder
3174
4110
  ]]></string>
3175
4111
  </Properties>
3176
4112
  </Item>
3177
- <Item class="ModuleScript" referent="11">
4113
+ <Item class="ModuleScript" referent="13">
3178
4114
  <Properties>
3179
4115
  <string name="Name">ValueCodec</string>
3180
4116
  <string name="Source"><![CDATA[local InstanceRegistry = require(script.Parent.InstanceRegistry)
@@ -3387,6 +4323,122 @@ function ValueCodec.encode(value)
3387
4323
  return tostring(value)
3388
4324
  end
3389
4325
 
4326
+ local TYPED_DEFAULTS = {
4327
+ UDim = function()
4328
+ return UDim.new()
4329
+ end,
4330
+ UDim2 = function()
4331
+ return UDim2.new()
4332
+ end,
4333
+ Vector2 = function()
4334
+ return Vector2.zero
4335
+ end,
4336
+ Vector3 = function()
4337
+ return Vector3.zero
4338
+ end,
4339
+ Color3 = function()
4340
+ return Color3.new()
4341
+ end,
4342
+ BrickColor = function()
4343
+ return BrickColor.new("Medium stone grey")
4344
+ end,
4345
+ Rect = function()
4346
+ return Rect.new()
4347
+ end,
4348
+ NumberRange = function()
4349
+ return NumberRange.new(0)
4350
+ end,
4351
+ CFrame = function()
4352
+ return CFrame.new()
4353
+ end,
4354
+ NumberSequence = function()
4355
+ return NumberSequence.new(0)
4356
+ end,
4357
+ ColorSequence = function()
4358
+ return ColorSequence.new(Color3.new())
4359
+ end,
4360
+ Font = function()
4361
+ return Font.new("rbxasset://fonts/families/SourceSansPro.json")
4362
+ end,
4363
+ }
4364
+
4365
+ function ValueCodec.decodeTyped(typeName, value)
4366
+ assert(type(typeName) == "string", "Typed value requires a type")
4367
+ if typeName == "nil" then
4368
+ return nil
4369
+ end
4370
+ if typeName == "string" or typeName == "number" or typeName == "boolean" then
4371
+ return ValueCodec.decodeForCurrent(typeName == "string" and "" or typeName == "number" and 0 or false, value)
4372
+ end
4373
+ if typeName == "Instance" then
4374
+ local resolved, err = InstanceRegistry.resolve(value)
4375
+ if not resolved then
4376
+ error(err)
4377
+ end
4378
+ return resolved
4379
+ end
4380
+ local createDefault = TYPED_DEFAULTS[typeName]
4381
+ if not createDefault then
4382
+ error("Unsupported typed value: " .. typeName)
4383
+ end
4384
+ return ValueCodec.decodeForCurrent(createDefault(), value)
4385
+ end
4386
+
4387
+ local function encodeSerializable(value, depth, state, seen)
4388
+ if depth > 5 then
4389
+ return "<maximum depth reached>"
4390
+ end
4391
+ if type(value) ~= "table" then
4392
+ return ValueCodec.encode(value)
4393
+ end
4394
+ if seen[value] then
4395
+ return "<cycle>"
4396
+ end
4397
+ seen[value] = true
4398
+
4399
+ local count = 0
4400
+ local maxIndex = 0
4401
+ local isArray = true
4402
+ for key in value do
4403
+ count += 1
4404
+ state.count += 1
4405
+ if state.count > state.limit then
4406
+ seen[value] = nil
4407
+ return "<result truncated>"
4408
+ end
4409
+ if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
4410
+ isArray = false
4411
+ else
4412
+ maxIndex = math.max(maxIndex, key)
4413
+ end
4414
+ end
4415
+
4416
+ local result = {}
4417
+ if isArray and maxIndex == count then
4418
+ for index = 1, maxIndex do
4419
+ result[index] = encodeSerializable(value[index], depth + 1, state, seen)
4420
+ end
4421
+ else
4422
+ for key, child in value do
4423
+ result[tostring(key)] = encodeSerializable(child, depth + 1, state, seen)
4424
+ end
4425
+ end
4426
+ seen[value] = nil
4427
+ return result
4428
+ end
4429
+
4430
+ function ValueCodec.encodeSerializable(value, limit)
4431
+ return encodeSerializable(value, 0, { count = 0, limit = limit or 500 }, {})
4432
+ end
4433
+
4434
+ function ValueCodec.encodeTyped(value, limit)
4435
+ local encoded = { type = typeof(value) }
4436
+ if value ~= nil then
4437
+ encoded.value = ValueCodec.encodeSerializable(value, limit)
4438
+ end
4439
+ return encoded
4440
+ end
4441
+
3390
4442
  function ValueCodec.setProperty(instance, propertyName, value)
3391
4443
  if propertyName == "Parent" or propertyName == "ClassName" or propertyName == "Source" then
3392
4444
  error("Property cannot be changed through setProperties: " .. propertyName)
@@ -3411,7 +4463,7 @@ return ValueCodec
3411
4463
  ]]></string>
3412
4464
  </Properties>
3413
4465
  </Item>
3414
- <Item class="ModuleScript" referent="12">
4466
+ <Item class="ModuleScript" referent="14">
3415
4467
  <Properties>
3416
4468
  <string name="Name">Watcher</string>
3417
4469
  <string name="Source"><![CDATA[--[[
@@ -3502,7 +4554,7 @@ return Watcher
3502
4554
  ]]></string>
3503
4555
  </Properties>
3504
4556
  </Item>
3505
- <Item class="ModuleScript" referent="13">
4557
+ <Item class="ModuleScript" referent="15">
3506
4558
  <Properties>
3507
4559
  <string name="Name">WsClient</string>
3508
4560
  <string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")