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/CHANGELOG.md +42 -1
- package/README.md +18 -3
- package/dist/bridge-daemon.js +9 -0
- package/dist/bridge-daemon.js.map +1 -1
- package/dist/install-plugin.js +84 -5
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +470 -32
- package/dist/mcp.js.map +1 -1
- package/package.json +6 -2
- package/plugin/Dominus.rbxmx +572 -8
- package/plugin/src/CommandRouter.lua +16 -0
- package/plugin/src/Metadata.lua +411 -0
- package/plugin/src/Mutation.lua +15 -0
- package/plugin/src/ValueCodec.lua +116 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dominus-cli",
|
|
3
|
-
"version": "2.
|
|
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/
|
|
83
|
+
"url": "git+https://github.com/rbxrootx/dominus.git"
|
|
80
84
|
}
|
|
81
85
|
}
|
package/plugin/Dominus.rbxmx
CHANGED
|
@@ -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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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")
|