dominus-cli 2.3.0 → 2.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dominus-cli",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "Dominus 2: a secure, MCP-native Roblox Studio engineering agent bridge.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,10 @@
8
8
  "dominus-install-plugin": "dist/install-plugin.js"
9
9
  },
10
10
  "main": "dist/mcp.js",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "registry": "https://registry.npmjs.org/"
14
+ },
11
15
  "scripts": {
12
16
  "dev": "tsx src/mcp/index.ts",
13
17
  "dev:mcp": "tsx src/mcp/index.ts",
@@ -76,6 +80,6 @@
76
80
  "license": "MIT",
77
81
  "repository": {
78
82
  "type": "git",
79
- "url": "git+https://github.com/eli/dominus.git"
83
+ "url": "git+https://github.com/rbxrootx/dominus.git"
80
84
  }
81
85
  }
@@ -694,6 +694,7 @@ return Building
694
694
  <string name="Source"><![CDATA[local Building = require(script.Parent.Building)
695
695
  local Explorer = require(script.Parent.Explorer)
696
696
  local InstanceRegistry = require(script.Parent.InstanceRegistry)
697
+ local Metadata = require(script.Parent.Metadata)
697
698
  local Mutation = require(script.Parent.Mutation)
698
699
  local Properties = require(script.Parent.Properties)
699
700
  local Reflection = require(script.Parent.Reflection)
@@ -755,6 +756,21 @@ local handlers = {
755
756
  ["studio:v2:set_selection"] = function(payload)
756
757
  return Building.setSelection(payload)
757
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,
758
774
  ["studio:v2:delete"] = function(payload)
759
775
  return Mutation.delete(payload)
760
776
  end,
@@ -1478,11 +1494,429 @@ return InstanceRegistry
1478
1494
  </Properties>
1479
1495
  </Item>
1480
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">
1481
1914
  <Properties>
1482
1915
  <string name="Name">Mutation</string>
1483
1916
  <string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
1484
1917
 
1485
1918
  local InstanceRegistry = require(script.Parent.InstanceRegistry)
1919
+ local Metadata = require(script.Parent.Metadata)
1486
1920
  local ValueCodec = require(script.Parent.ValueCodec)
1487
1921
 
1488
1922
  local Mutation = {}
@@ -1537,6 +1971,20 @@ function Mutation.apply(spec)
1537
1971
  target = InstanceRegistry.toRef(target),
1538
1972
  properties = setProperties(target, operation.properties),
1539
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
+ })
1540
1988
  elseif operation.op == "create" then
1541
1989
  local parent = resolve(operation.parent, "Operation " .. index .. " parent")
1542
1990
  assert(type(operation.className) == "string", "create.className must be a string")
@@ -1648,7 +2096,7 @@ return Mutation
1648
2096
  ]]></string>
1649
2097
  </Properties>
1650
2098
  </Item>
1651
- <Item class="ModuleScript" referent="7">
2099
+ <Item class="ModuleScript" referent="8">
1652
2100
  <Properties>
1653
2101
  <string name="Name">Properties</string>
1654
2102
  <string name="Source"><![CDATA[--[[
@@ -2513,7 +2961,7 @@ return Properties
2513
2961
  ]]></string>
2514
2962
  </Properties>
2515
2963
  </Item>
2516
- <Item class="ModuleScript" referent="8">
2964
+ <Item class="ModuleScript" referent="9">
2517
2965
  <Properties>
2518
2966
  <string name="Name">Protocol</string>
2519
2967
  <string name="Source"><![CDATA[--[[
@@ -2549,7 +2997,7 @@ return Protocol
2549
2997
  ]]></string>
2550
2998
  </Properties>
2551
2999
  </Item>
2552
- <Item class="ModuleScript" referent="9">
3000
+ <Item class="ModuleScript" referent="10">
2553
3001
  <Properties>
2554
3002
  <string name="Name">Reflection</string>
2555
3003
  <string name="Source"><![CDATA[local ReflectionService = game:GetService("ReflectionService")
@@ -2724,7 +3172,7 @@ return Reflection
2724
3172
  ]]></string>
2725
3173
  </Properties>
2726
3174
  </Item>
2727
- <Item class="ModuleScript" referent="10">
3175
+ <Item class="ModuleScript" referent="11">
2728
3176
  <Properties>
2729
3177
  <string name="Name">TestRunner</string>
2730
3178
  <string name="Source"><![CDATA[local StudioTestService = game:GetService("StudioTestService")
@@ -2799,7 +3247,7 @@ return TestRunner
2799
3247
  ]]></string>
2800
3248
  </Properties>
2801
3249
  </Item>
2802
- <Item class="ModuleScript" referent="11">
3250
+ <Item class="ModuleScript" referent="12">
2803
3251
  <Properties>
2804
3252
  <string name="Name">UIBuilder</string>
2805
3253
  <string name="Source"><![CDATA[--[[
@@ -3662,7 +4110,7 @@ return UIBuilder
3662
4110
  ]]></string>
3663
4111
  </Properties>
3664
4112
  </Item>
3665
- <Item class="ModuleScript" referent="12">
4113
+ <Item class="ModuleScript" referent="13">
3666
4114
  <Properties>
3667
4115
  <string name="Name">ValueCodec</string>
3668
4116
  <string name="Source"><![CDATA[local InstanceRegistry = require(script.Parent.InstanceRegistry)
@@ -3875,6 +4323,122 @@ function ValueCodec.encode(value)
3875
4323
  return tostring(value)
3876
4324
  end
3877
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
+
3878
4442
  function ValueCodec.setProperty(instance, propertyName, value)
3879
4443
  if propertyName == "Parent" or propertyName == "ClassName" or propertyName == "Source" then
3880
4444
  error("Property cannot be changed through setProperties: " .. propertyName)
@@ -3899,7 +4463,7 @@ return ValueCodec
3899
4463
  ]]></string>
3900
4464
  </Properties>
3901
4465
  </Item>
3902
- <Item class="ModuleScript" referent="13">
4466
+ <Item class="ModuleScript" referent="14">
3903
4467
  <Properties>
3904
4468
  <string name="Name">Watcher</string>
3905
4469
  <string name="Source"><![CDATA[--[[
@@ -3990,7 +4554,7 @@ return Watcher
3990
4554
  ]]></string>
3991
4555
  </Properties>
3992
4556
  </Item>
3993
- <Item class="ModuleScript" referent="14">
4557
+ <Item class="ModuleScript" referent="15">
3994
4558
  <Properties>
3995
4559
  <string name="Name">WsClient</string>
3996
4560
  <string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")