@chrrxs/robloxstudio-mcp-inspector 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1508 -64
- package/package.json +2 -2
- package/studio-plugin/INSTALLATION.md +1 -0
- package/studio-plugin/MCPInspectorPlugin.rbxmx +1312 -325
- package/studio-plugin/MCPPlugin.rbxmx +705 -97
- package/studio-plugin/src/modules/ClientBroker.ts +91 -1
- package/studio-plugin/src/modules/Communication.ts +22 -0
- package/studio-plugin/src/modules/EvalBridges.ts +60 -11
- package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +45 -3
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +100 -39
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +257 -18
- package/studio-plugin/src/server/index.server.ts +6 -0
package/dist/index.js
CHANGED
|
@@ -43,18 +43,25 @@ var init_bridge_service = __esm({
|
|
|
43
43
|
requestTimeout = 3e4;
|
|
44
44
|
registerInstance(input) {
|
|
45
45
|
const { pluginSessionId, instanceId, role } = input;
|
|
46
|
+
const prior = this.instances.get(pluginSessionId);
|
|
46
47
|
let assignedRole = role;
|
|
47
48
|
if (role === "client") {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if (prior && prior.instanceId === instanceId && prior.role.match(/^client-\d+$/)) {
|
|
50
|
+
assignedRole = prior.role;
|
|
51
|
+
} else {
|
|
52
|
+
const used = /* @__PURE__ */ new Set();
|
|
53
|
+
for (const inst of this.instances.values()) {
|
|
54
|
+
if (inst.instanceId !== instanceId || inst.pluginSessionId === pluginSessionId)
|
|
55
|
+
continue;
|
|
56
|
+
const match = inst.role.match(/^client-(\d+)$/);
|
|
57
|
+
if (match)
|
|
58
|
+
used.add(Number(match[1]));
|
|
59
|
+
}
|
|
60
|
+
let idx = 1;
|
|
61
|
+
while (used.has(idx))
|
|
62
|
+
idx++;
|
|
63
|
+
assignedRole = `client-${idx}`;
|
|
53
64
|
}
|
|
54
|
-
let idx = 1;
|
|
55
|
-
while (used.has(idx))
|
|
56
|
-
idx++;
|
|
57
|
-
assignedRole = `client-${idx}`;
|
|
58
65
|
}
|
|
59
66
|
const existing = Array.from(this.instances.values()).find((i) => i.instanceId === instanceId && i.role === assignedRole && i.pluginSessionId !== pluginSessionId);
|
|
60
67
|
if (existing) {
|
|
@@ -76,7 +83,7 @@ var init_bridge_service = __esm({
|
|
|
76
83
|
dataModelName: input.dataModelName ?? "",
|
|
77
84
|
isRunning: input.isRunning ?? false,
|
|
78
85
|
lastActivity: Date.now(),
|
|
79
|
-
connectedAt: Date.now()
|
|
86
|
+
connectedAt: prior?.connectedAt ?? Date.now()
|
|
80
87
|
});
|
|
81
88
|
return { ok: true, assignedRole, instanceId };
|
|
82
89
|
}
|
|
@@ -214,7 +221,7 @@ var init_bridge_service = __esm({
|
|
|
214
221
|
}
|
|
215
222
|
if (distinctInstanceIds.size > 1) {
|
|
216
223
|
const errorCode = role ? "ambiguous_target" : "multiple_instances_connected";
|
|
217
|
-
const msg = role ? `target=${role} is ambiguous
|
|
224
|
+
const msg = role ? `target=${role} is ambiguous because multiple Studio places are connected. Pass instance_id to choose a place.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
|
|
218
225
|
return { ok: false, error: { code: errorCode, message: msg, data: errorData } };
|
|
219
226
|
}
|
|
220
227
|
const onlyInstanceId = instances[0].instanceId;
|
|
@@ -236,6 +243,7 @@ var init_bridge_service = __esm({
|
|
|
236
243
|
targetInstanceId,
|
|
237
244
|
targetRole,
|
|
238
245
|
timestamp: Date.now(),
|
|
246
|
+
inFlight: false,
|
|
239
247
|
resolve: resolve2,
|
|
240
248
|
reject,
|
|
241
249
|
timeoutId
|
|
@@ -250,11 +258,14 @@ var init_bridge_service = __esm({
|
|
|
250
258
|
continue;
|
|
251
259
|
if (request.targetRole !== callerRole)
|
|
252
260
|
continue;
|
|
261
|
+
if (request.inFlight)
|
|
262
|
+
continue;
|
|
253
263
|
if (!oldestRequest || request.timestamp < oldestRequest.timestamp) {
|
|
254
264
|
oldestRequest = request;
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
if (oldestRequest) {
|
|
268
|
+
oldestRequest.inFlight = true;
|
|
258
269
|
return {
|
|
259
270
|
requestId: oldestRequest.id,
|
|
260
271
|
request: {
|
|
@@ -677,6 +688,11 @@ var init_http_server = __esm({
|
|
|
677
688
|
start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
|
|
678
689
|
stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
|
|
679
690
|
get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target, body.instance_id),
|
|
691
|
+
multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
|
|
692
|
+
multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
|
|
693
|
+
multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
|
|
694
|
+
multiplayer_test_leave_client: (tools, body) => tools.multiplayerTestLeaveClient(body.target, body.timeout, body.instance_id),
|
|
695
|
+
multiplayer_test_end: (tools, body) => tools.multiplayerTestEnd(body.value, body.timeout, body.instance_id),
|
|
680
696
|
get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.target, body.since, body.tail, body.filter, body.instance_id),
|
|
681
697
|
get_connected_instances: (tools) => tools.getConnectedInstances(),
|
|
682
698
|
export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style, body.instance_id),
|
|
@@ -700,9 +716,9 @@ var init_http_server = __esm({
|
|
|
700
716
|
compare_instances: (tools, body) => tools.compareInstances(body.instancePathA, body.instancePathB, body.instance_id),
|
|
701
717
|
get_output_log: (tools, body) => tools.getOutputLog(body.maxEntries, body.messageType, body.instance_id),
|
|
702
718
|
bulk_set_attributes: (tools, body) => tools.bulkSetAttributes(body.instancePath, body.attributes, body.instance_id),
|
|
703
|
-
capture_screenshot: (tools, body) => tools.captureScreenshot(body.instance_id),
|
|
719
|
+
capture_screenshot: (tools, body) => tools.captureScreenshot(body.instance_id, body.format, body.quality),
|
|
704
720
|
simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target, body.instance_id),
|
|
705
|
-
simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.target, body.instance_id),
|
|
721
|
+
simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.text, body.target, body.instance_id),
|
|
706
722
|
character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target, body.instance_id),
|
|
707
723
|
get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags, body.instance_id),
|
|
708
724
|
export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target, body.instance_id),
|
|
@@ -1468,6 +1484,963 @@ var init_roblox_cookie_client = __esm({
|
|
|
1468
1484
|
}
|
|
1469
1485
|
});
|
|
1470
1486
|
|
|
1487
|
+
// ../core/dist/jpeg-encoder.js
|
|
1488
|
+
function rgbaToJpeg(rgba, width, height, quality = 80) {
|
|
1489
|
+
if (width <= 0 || height <= 0)
|
|
1490
|
+
throw new Error(`Invalid JPEG dimensions: ${width}x${height}`);
|
|
1491
|
+
const expected = width * height * 4;
|
|
1492
|
+
if (rgba.length < expected)
|
|
1493
|
+
throw new Error(`Buffer too small: got ${rgba.length}, need ${expected}`);
|
|
1494
|
+
const encoder = new JpegEncoder(quality);
|
|
1495
|
+
return encoder.encode(rgba, width, height);
|
|
1496
|
+
}
|
|
1497
|
+
var ZIGZAG, STD_DC_LUMINANCE_NRCODES, STD_DC_LUMINANCE_VALUES, STD_AC_LUMINANCE_NRCODES, STD_AC_LUMINANCE_VALUES, STD_DC_CHROMINANCE_NRCODES, STD_DC_CHROMINANCE_VALUES, STD_AC_CHROMINANCE_NRCODES, STD_AC_CHROMINANCE_VALUES, JpegEncoder;
|
|
1498
|
+
var init_jpeg_encoder = __esm({
|
|
1499
|
+
"../core/dist/jpeg-encoder.js"() {
|
|
1500
|
+
"use strict";
|
|
1501
|
+
ZIGZAG = [
|
|
1502
|
+
0,
|
|
1503
|
+
1,
|
|
1504
|
+
5,
|
|
1505
|
+
6,
|
|
1506
|
+
14,
|
|
1507
|
+
15,
|
|
1508
|
+
27,
|
|
1509
|
+
28,
|
|
1510
|
+
2,
|
|
1511
|
+
4,
|
|
1512
|
+
7,
|
|
1513
|
+
13,
|
|
1514
|
+
16,
|
|
1515
|
+
26,
|
|
1516
|
+
29,
|
|
1517
|
+
42,
|
|
1518
|
+
3,
|
|
1519
|
+
8,
|
|
1520
|
+
12,
|
|
1521
|
+
17,
|
|
1522
|
+
25,
|
|
1523
|
+
30,
|
|
1524
|
+
41,
|
|
1525
|
+
43,
|
|
1526
|
+
9,
|
|
1527
|
+
11,
|
|
1528
|
+
18,
|
|
1529
|
+
24,
|
|
1530
|
+
31,
|
|
1531
|
+
40,
|
|
1532
|
+
44,
|
|
1533
|
+
53,
|
|
1534
|
+
10,
|
|
1535
|
+
19,
|
|
1536
|
+
23,
|
|
1537
|
+
32,
|
|
1538
|
+
39,
|
|
1539
|
+
45,
|
|
1540
|
+
52,
|
|
1541
|
+
54,
|
|
1542
|
+
20,
|
|
1543
|
+
22,
|
|
1544
|
+
33,
|
|
1545
|
+
38,
|
|
1546
|
+
46,
|
|
1547
|
+
51,
|
|
1548
|
+
55,
|
|
1549
|
+
60,
|
|
1550
|
+
21,
|
|
1551
|
+
34,
|
|
1552
|
+
37,
|
|
1553
|
+
47,
|
|
1554
|
+
50,
|
|
1555
|
+
56,
|
|
1556
|
+
59,
|
|
1557
|
+
61,
|
|
1558
|
+
35,
|
|
1559
|
+
36,
|
|
1560
|
+
48,
|
|
1561
|
+
49,
|
|
1562
|
+
57,
|
|
1563
|
+
58,
|
|
1564
|
+
62,
|
|
1565
|
+
63
|
|
1566
|
+
];
|
|
1567
|
+
STD_DC_LUMINANCE_NRCODES = [0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0];
|
|
1568
|
+
STD_DC_LUMINANCE_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
1569
|
+
STD_AC_LUMINANCE_NRCODES = [0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125];
|
|
1570
|
+
STD_AC_LUMINANCE_VALUES = [
|
|
1571
|
+
1,
|
|
1572
|
+
2,
|
|
1573
|
+
3,
|
|
1574
|
+
0,
|
|
1575
|
+
4,
|
|
1576
|
+
17,
|
|
1577
|
+
5,
|
|
1578
|
+
18,
|
|
1579
|
+
33,
|
|
1580
|
+
49,
|
|
1581
|
+
65,
|
|
1582
|
+
6,
|
|
1583
|
+
19,
|
|
1584
|
+
81,
|
|
1585
|
+
97,
|
|
1586
|
+
7,
|
|
1587
|
+
34,
|
|
1588
|
+
113,
|
|
1589
|
+
20,
|
|
1590
|
+
50,
|
|
1591
|
+
129,
|
|
1592
|
+
145,
|
|
1593
|
+
161,
|
|
1594
|
+
8,
|
|
1595
|
+
35,
|
|
1596
|
+
66,
|
|
1597
|
+
177,
|
|
1598
|
+
193,
|
|
1599
|
+
21,
|
|
1600
|
+
82,
|
|
1601
|
+
209,
|
|
1602
|
+
240,
|
|
1603
|
+
36,
|
|
1604
|
+
51,
|
|
1605
|
+
98,
|
|
1606
|
+
114,
|
|
1607
|
+
130,
|
|
1608
|
+
9,
|
|
1609
|
+
10,
|
|
1610
|
+
22,
|
|
1611
|
+
23,
|
|
1612
|
+
24,
|
|
1613
|
+
25,
|
|
1614
|
+
26,
|
|
1615
|
+
37,
|
|
1616
|
+
38,
|
|
1617
|
+
39,
|
|
1618
|
+
40,
|
|
1619
|
+
41,
|
|
1620
|
+
42,
|
|
1621
|
+
52,
|
|
1622
|
+
53,
|
|
1623
|
+
54,
|
|
1624
|
+
55,
|
|
1625
|
+
56,
|
|
1626
|
+
57,
|
|
1627
|
+
58,
|
|
1628
|
+
67,
|
|
1629
|
+
68,
|
|
1630
|
+
69,
|
|
1631
|
+
70,
|
|
1632
|
+
71,
|
|
1633
|
+
72,
|
|
1634
|
+
73,
|
|
1635
|
+
74,
|
|
1636
|
+
83,
|
|
1637
|
+
84,
|
|
1638
|
+
85,
|
|
1639
|
+
86,
|
|
1640
|
+
87,
|
|
1641
|
+
88,
|
|
1642
|
+
89,
|
|
1643
|
+
90,
|
|
1644
|
+
99,
|
|
1645
|
+
100,
|
|
1646
|
+
101,
|
|
1647
|
+
102,
|
|
1648
|
+
103,
|
|
1649
|
+
104,
|
|
1650
|
+
105,
|
|
1651
|
+
106,
|
|
1652
|
+
115,
|
|
1653
|
+
116,
|
|
1654
|
+
117,
|
|
1655
|
+
118,
|
|
1656
|
+
119,
|
|
1657
|
+
120,
|
|
1658
|
+
121,
|
|
1659
|
+
122,
|
|
1660
|
+
131,
|
|
1661
|
+
132,
|
|
1662
|
+
133,
|
|
1663
|
+
134,
|
|
1664
|
+
135,
|
|
1665
|
+
136,
|
|
1666
|
+
137,
|
|
1667
|
+
138,
|
|
1668
|
+
146,
|
|
1669
|
+
147,
|
|
1670
|
+
148,
|
|
1671
|
+
149,
|
|
1672
|
+
150,
|
|
1673
|
+
151,
|
|
1674
|
+
152,
|
|
1675
|
+
153,
|
|
1676
|
+
154,
|
|
1677
|
+
162,
|
|
1678
|
+
163,
|
|
1679
|
+
164,
|
|
1680
|
+
165,
|
|
1681
|
+
166,
|
|
1682
|
+
167,
|
|
1683
|
+
168,
|
|
1684
|
+
169,
|
|
1685
|
+
170,
|
|
1686
|
+
178,
|
|
1687
|
+
179,
|
|
1688
|
+
180,
|
|
1689
|
+
181,
|
|
1690
|
+
182,
|
|
1691
|
+
183,
|
|
1692
|
+
184,
|
|
1693
|
+
185,
|
|
1694
|
+
186,
|
|
1695
|
+
194,
|
|
1696
|
+
195,
|
|
1697
|
+
196,
|
|
1698
|
+
197,
|
|
1699
|
+
198,
|
|
1700
|
+
199,
|
|
1701
|
+
200,
|
|
1702
|
+
201,
|
|
1703
|
+
202,
|
|
1704
|
+
210,
|
|
1705
|
+
211,
|
|
1706
|
+
212,
|
|
1707
|
+
213,
|
|
1708
|
+
214,
|
|
1709
|
+
215,
|
|
1710
|
+
216,
|
|
1711
|
+
217,
|
|
1712
|
+
218,
|
|
1713
|
+
225,
|
|
1714
|
+
226,
|
|
1715
|
+
227,
|
|
1716
|
+
228,
|
|
1717
|
+
229,
|
|
1718
|
+
230,
|
|
1719
|
+
231,
|
|
1720
|
+
232,
|
|
1721
|
+
233,
|
|
1722
|
+
234,
|
|
1723
|
+
241,
|
|
1724
|
+
242,
|
|
1725
|
+
243,
|
|
1726
|
+
244,
|
|
1727
|
+
245,
|
|
1728
|
+
246,
|
|
1729
|
+
247,
|
|
1730
|
+
248,
|
|
1731
|
+
249,
|
|
1732
|
+
250
|
|
1733
|
+
];
|
|
1734
|
+
STD_DC_CHROMINANCE_NRCODES = [0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0];
|
|
1735
|
+
STD_DC_CHROMINANCE_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
1736
|
+
STD_AC_CHROMINANCE_NRCODES = [0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119];
|
|
1737
|
+
STD_AC_CHROMINANCE_VALUES = [
|
|
1738
|
+
0,
|
|
1739
|
+
1,
|
|
1740
|
+
2,
|
|
1741
|
+
3,
|
|
1742
|
+
17,
|
|
1743
|
+
4,
|
|
1744
|
+
5,
|
|
1745
|
+
33,
|
|
1746
|
+
49,
|
|
1747
|
+
6,
|
|
1748
|
+
18,
|
|
1749
|
+
65,
|
|
1750
|
+
81,
|
|
1751
|
+
7,
|
|
1752
|
+
97,
|
|
1753
|
+
113,
|
|
1754
|
+
19,
|
|
1755
|
+
34,
|
|
1756
|
+
50,
|
|
1757
|
+
129,
|
|
1758
|
+
8,
|
|
1759
|
+
20,
|
|
1760
|
+
66,
|
|
1761
|
+
145,
|
|
1762
|
+
161,
|
|
1763
|
+
177,
|
|
1764
|
+
193,
|
|
1765
|
+
9,
|
|
1766
|
+
35,
|
|
1767
|
+
51,
|
|
1768
|
+
82,
|
|
1769
|
+
240,
|
|
1770
|
+
21,
|
|
1771
|
+
98,
|
|
1772
|
+
114,
|
|
1773
|
+
209,
|
|
1774
|
+
10,
|
|
1775
|
+
22,
|
|
1776
|
+
36,
|
|
1777
|
+
52,
|
|
1778
|
+
225,
|
|
1779
|
+
37,
|
|
1780
|
+
241,
|
|
1781
|
+
23,
|
|
1782
|
+
24,
|
|
1783
|
+
25,
|
|
1784
|
+
26,
|
|
1785
|
+
38,
|
|
1786
|
+
39,
|
|
1787
|
+
40,
|
|
1788
|
+
41,
|
|
1789
|
+
42,
|
|
1790
|
+
53,
|
|
1791
|
+
54,
|
|
1792
|
+
55,
|
|
1793
|
+
56,
|
|
1794
|
+
57,
|
|
1795
|
+
58,
|
|
1796
|
+
67,
|
|
1797
|
+
68,
|
|
1798
|
+
69,
|
|
1799
|
+
70,
|
|
1800
|
+
71,
|
|
1801
|
+
72,
|
|
1802
|
+
73,
|
|
1803
|
+
74,
|
|
1804
|
+
83,
|
|
1805
|
+
84,
|
|
1806
|
+
85,
|
|
1807
|
+
86,
|
|
1808
|
+
87,
|
|
1809
|
+
88,
|
|
1810
|
+
89,
|
|
1811
|
+
90,
|
|
1812
|
+
99,
|
|
1813
|
+
100,
|
|
1814
|
+
101,
|
|
1815
|
+
102,
|
|
1816
|
+
103,
|
|
1817
|
+
104,
|
|
1818
|
+
105,
|
|
1819
|
+
106,
|
|
1820
|
+
115,
|
|
1821
|
+
116,
|
|
1822
|
+
117,
|
|
1823
|
+
118,
|
|
1824
|
+
119,
|
|
1825
|
+
120,
|
|
1826
|
+
121,
|
|
1827
|
+
122,
|
|
1828
|
+
130,
|
|
1829
|
+
131,
|
|
1830
|
+
132,
|
|
1831
|
+
133,
|
|
1832
|
+
134,
|
|
1833
|
+
135,
|
|
1834
|
+
136,
|
|
1835
|
+
137,
|
|
1836
|
+
138,
|
|
1837
|
+
146,
|
|
1838
|
+
147,
|
|
1839
|
+
148,
|
|
1840
|
+
149,
|
|
1841
|
+
150,
|
|
1842
|
+
151,
|
|
1843
|
+
152,
|
|
1844
|
+
153,
|
|
1845
|
+
154,
|
|
1846
|
+
162,
|
|
1847
|
+
163,
|
|
1848
|
+
164,
|
|
1849
|
+
165,
|
|
1850
|
+
166,
|
|
1851
|
+
167,
|
|
1852
|
+
168,
|
|
1853
|
+
169,
|
|
1854
|
+
170,
|
|
1855
|
+
178,
|
|
1856
|
+
179,
|
|
1857
|
+
180,
|
|
1858
|
+
181,
|
|
1859
|
+
182,
|
|
1860
|
+
183,
|
|
1861
|
+
184,
|
|
1862
|
+
185,
|
|
1863
|
+
186,
|
|
1864
|
+
194,
|
|
1865
|
+
195,
|
|
1866
|
+
196,
|
|
1867
|
+
197,
|
|
1868
|
+
198,
|
|
1869
|
+
199,
|
|
1870
|
+
200,
|
|
1871
|
+
201,
|
|
1872
|
+
202,
|
|
1873
|
+
210,
|
|
1874
|
+
211,
|
|
1875
|
+
212,
|
|
1876
|
+
213,
|
|
1877
|
+
214,
|
|
1878
|
+
215,
|
|
1879
|
+
216,
|
|
1880
|
+
217,
|
|
1881
|
+
218,
|
|
1882
|
+
226,
|
|
1883
|
+
227,
|
|
1884
|
+
228,
|
|
1885
|
+
229,
|
|
1886
|
+
230,
|
|
1887
|
+
231,
|
|
1888
|
+
232,
|
|
1889
|
+
233,
|
|
1890
|
+
234,
|
|
1891
|
+
242,
|
|
1892
|
+
243,
|
|
1893
|
+
244,
|
|
1894
|
+
245,
|
|
1895
|
+
246,
|
|
1896
|
+
247,
|
|
1897
|
+
248,
|
|
1898
|
+
249,
|
|
1899
|
+
250
|
|
1900
|
+
];
|
|
1901
|
+
JpegEncoder = class {
|
|
1902
|
+
YTable = new Int32Array(64);
|
|
1903
|
+
UVTable = new Int32Array(64);
|
|
1904
|
+
fdtbl_Y = new Float32Array(64);
|
|
1905
|
+
fdtbl_UV = new Float32Array(64);
|
|
1906
|
+
YDC_HT;
|
|
1907
|
+
UVDC_HT;
|
|
1908
|
+
YAC_HT;
|
|
1909
|
+
UVAC_HT;
|
|
1910
|
+
bitcode = new Array(65535);
|
|
1911
|
+
category = new Int32Array(65535);
|
|
1912
|
+
outputfDCTQuant = new Float32Array(64);
|
|
1913
|
+
DU = new Int32Array(64);
|
|
1914
|
+
// RGB->YUV lookup tables
|
|
1915
|
+
RGB_YUV_TABLE = new Int32Array(2048);
|
|
1916
|
+
byteout = [];
|
|
1917
|
+
bytenew = 0;
|
|
1918
|
+
bytepos = 7;
|
|
1919
|
+
constructor(quality) {
|
|
1920
|
+
this.initHuffmanTbl();
|
|
1921
|
+
this.initCategoryNumber();
|
|
1922
|
+
this.initRGBYUVTable();
|
|
1923
|
+
this.setQuality(quality);
|
|
1924
|
+
}
|
|
1925
|
+
initQuantTables(sf) {
|
|
1926
|
+
const YQT = [
|
|
1927
|
+
16,
|
|
1928
|
+
11,
|
|
1929
|
+
10,
|
|
1930
|
+
16,
|
|
1931
|
+
24,
|
|
1932
|
+
40,
|
|
1933
|
+
51,
|
|
1934
|
+
61,
|
|
1935
|
+
12,
|
|
1936
|
+
12,
|
|
1937
|
+
14,
|
|
1938
|
+
19,
|
|
1939
|
+
26,
|
|
1940
|
+
58,
|
|
1941
|
+
60,
|
|
1942
|
+
55,
|
|
1943
|
+
14,
|
|
1944
|
+
13,
|
|
1945
|
+
16,
|
|
1946
|
+
24,
|
|
1947
|
+
40,
|
|
1948
|
+
57,
|
|
1949
|
+
69,
|
|
1950
|
+
56,
|
|
1951
|
+
14,
|
|
1952
|
+
17,
|
|
1953
|
+
22,
|
|
1954
|
+
29,
|
|
1955
|
+
51,
|
|
1956
|
+
87,
|
|
1957
|
+
80,
|
|
1958
|
+
62,
|
|
1959
|
+
18,
|
|
1960
|
+
22,
|
|
1961
|
+
37,
|
|
1962
|
+
56,
|
|
1963
|
+
68,
|
|
1964
|
+
109,
|
|
1965
|
+
103,
|
|
1966
|
+
77,
|
|
1967
|
+
24,
|
|
1968
|
+
35,
|
|
1969
|
+
55,
|
|
1970
|
+
64,
|
|
1971
|
+
81,
|
|
1972
|
+
104,
|
|
1973
|
+
113,
|
|
1974
|
+
92,
|
|
1975
|
+
49,
|
|
1976
|
+
64,
|
|
1977
|
+
78,
|
|
1978
|
+
87,
|
|
1979
|
+
103,
|
|
1980
|
+
121,
|
|
1981
|
+
120,
|
|
1982
|
+
101,
|
|
1983
|
+
72,
|
|
1984
|
+
92,
|
|
1985
|
+
95,
|
|
1986
|
+
98,
|
|
1987
|
+
112,
|
|
1988
|
+
100,
|
|
1989
|
+
103,
|
|
1990
|
+
99
|
|
1991
|
+
];
|
|
1992
|
+
for (let i = 0; i < 64; i++) {
|
|
1993
|
+
let t = Math.floor((YQT[i] * sf + 50) / 100);
|
|
1994
|
+
if (t < 1)
|
|
1995
|
+
t = 1;
|
|
1996
|
+
else if (t > 255)
|
|
1997
|
+
t = 255;
|
|
1998
|
+
this.YTable[ZIGZAG[i]] = t;
|
|
1999
|
+
}
|
|
2000
|
+
const UVQT = [
|
|
2001
|
+
17,
|
|
2002
|
+
18,
|
|
2003
|
+
24,
|
|
2004
|
+
47,
|
|
2005
|
+
99,
|
|
2006
|
+
99,
|
|
2007
|
+
99,
|
|
2008
|
+
99,
|
|
2009
|
+
18,
|
|
2010
|
+
21,
|
|
2011
|
+
26,
|
|
2012
|
+
66,
|
|
2013
|
+
99,
|
|
2014
|
+
99,
|
|
2015
|
+
99,
|
|
2016
|
+
99,
|
|
2017
|
+
24,
|
|
2018
|
+
26,
|
|
2019
|
+
56,
|
|
2020
|
+
99,
|
|
2021
|
+
99,
|
|
2022
|
+
99,
|
|
2023
|
+
99,
|
|
2024
|
+
99,
|
|
2025
|
+
47,
|
|
2026
|
+
66,
|
|
2027
|
+
99,
|
|
2028
|
+
99,
|
|
2029
|
+
99,
|
|
2030
|
+
99,
|
|
2031
|
+
99,
|
|
2032
|
+
99,
|
|
2033
|
+
99,
|
|
2034
|
+
99,
|
|
2035
|
+
99,
|
|
2036
|
+
99,
|
|
2037
|
+
99,
|
|
2038
|
+
99,
|
|
2039
|
+
99,
|
|
2040
|
+
99,
|
|
2041
|
+
99,
|
|
2042
|
+
99,
|
|
2043
|
+
99,
|
|
2044
|
+
99,
|
|
2045
|
+
99,
|
|
2046
|
+
99,
|
|
2047
|
+
99,
|
|
2048
|
+
99,
|
|
2049
|
+
99,
|
|
2050
|
+
99,
|
|
2051
|
+
99,
|
|
2052
|
+
99,
|
|
2053
|
+
99,
|
|
2054
|
+
99,
|
|
2055
|
+
99,
|
|
2056
|
+
99,
|
|
2057
|
+
99,
|
|
2058
|
+
99,
|
|
2059
|
+
99,
|
|
2060
|
+
99,
|
|
2061
|
+
99,
|
|
2062
|
+
99,
|
|
2063
|
+
99,
|
|
2064
|
+
99
|
|
2065
|
+
];
|
|
2066
|
+
for (let j = 0; j < 64; j++) {
|
|
2067
|
+
let u = Math.floor((UVQT[j] * sf + 50) / 100);
|
|
2068
|
+
if (u < 1)
|
|
2069
|
+
u = 1;
|
|
2070
|
+
else if (u > 255)
|
|
2071
|
+
u = 255;
|
|
2072
|
+
this.UVTable[ZIGZAG[j]] = u;
|
|
2073
|
+
}
|
|
2074
|
+
const aasf = [
|
|
2075
|
+
1,
|
|
2076
|
+
1.387039845,
|
|
2077
|
+
1.306562965,
|
|
2078
|
+
1.175875602,
|
|
2079
|
+
1,
|
|
2080
|
+
0.785694958,
|
|
2081
|
+
0.5411961,
|
|
2082
|
+
0.275899379
|
|
2083
|
+
];
|
|
2084
|
+
let k = 0;
|
|
2085
|
+
for (let row = 0; row < 8; row++) {
|
|
2086
|
+
for (let col = 0; col < 8; col++) {
|
|
2087
|
+
this.fdtbl_Y[k] = 1 / (this.YTable[ZIGZAG[k]] * aasf[row] * aasf[col] * 8);
|
|
2088
|
+
this.fdtbl_UV[k] = 1 / (this.UVTable[ZIGZAG[k]] * aasf[row] * aasf[col] * 8);
|
|
2089
|
+
k++;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
computeHuffmanTbl(nrcodes, std_table) {
|
|
2094
|
+
let codevalue = 0;
|
|
2095
|
+
let pos_in_table = 0;
|
|
2096
|
+
const HT = [];
|
|
2097
|
+
for (let k = 1; k <= 16; k++) {
|
|
2098
|
+
for (let j = 1; j <= nrcodes[k]; j++) {
|
|
2099
|
+
HT[std_table[pos_in_table]] = { code: codevalue, length: k };
|
|
2100
|
+
pos_in_table++;
|
|
2101
|
+
codevalue++;
|
|
2102
|
+
}
|
|
2103
|
+
codevalue *= 2;
|
|
2104
|
+
}
|
|
2105
|
+
return HT;
|
|
2106
|
+
}
|
|
2107
|
+
initHuffmanTbl() {
|
|
2108
|
+
this.YDC_HT = this.computeHuffmanTbl(STD_DC_LUMINANCE_NRCODES, STD_DC_LUMINANCE_VALUES);
|
|
2109
|
+
this.UVDC_HT = this.computeHuffmanTbl(STD_DC_CHROMINANCE_NRCODES, STD_DC_CHROMINANCE_VALUES);
|
|
2110
|
+
this.YAC_HT = this.computeHuffmanTbl(STD_AC_LUMINANCE_NRCODES, STD_AC_LUMINANCE_VALUES);
|
|
2111
|
+
this.UVAC_HT = this.computeHuffmanTbl(STD_AC_CHROMINANCE_NRCODES, STD_AC_CHROMINANCE_VALUES);
|
|
2112
|
+
}
|
|
2113
|
+
initCategoryNumber() {
|
|
2114
|
+
let nrlower = 1;
|
|
2115
|
+
let nrupper = 2;
|
|
2116
|
+
for (let cat = 1; cat <= 15; cat++) {
|
|
2117
|
+
for (let nr = nrlower; nr < nrupper; nr++) {
|
|
2118
|
+
this.category[32767 + nr] = cat;
|
|
2119
|
+
this.bitcode[32767 + nr] = { length: cat, code: nr };
|
|
2120
|
+
}
|
|
2121
|
+
for (let nrneg = -(nrupper - 1); nrneg <= -nrlower; nrneg++) {
|
|
2122
|
+
this.category[32767 + nrneg] = cat;
|
|
2123
|
+
this.bitcode[32767 + nrneg] = { length: cat, code: nrupper - 1 + nrneg };
|
|
2124
|
+
}
|
|
2125
|
+
nrlower <<= 1;
|
|
2126
|
+
nrupper <<= 1;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
initRGBYUVTable() {
|
|
2130
|
+
for (let i = 0; i < 256; i++) {
|
|
2131
|
+
this.RGB_YUV_TABLE[i] = 19595 * i;
|
|
2132
|
+
this.RGB_YUV_TABLE[i + 256 >> 0] = 38470 * i;
|
|
2133
|
+
this.RGB_YUV_TABLE[i + 512 >> 0] = 7471 * i + 32768;
|
|
2134
|
+
this.RGB_YUV_TABLE[i + 768 >> 0] = -11059 * i;
|
|
2135
|
+
this.RGB_YUV_TABLE[i + 1024 >> 0] = -21709 * i;
|
|
2136
|
+
this.RGB_YUV_TABLE[i + 1280 >> 0] = 32768 * i + 8421375;
|
|
2137
|
+
this.RGB_YUV_TABLE[i + 1536 >> 0] = -27439 * i;
|
|
2138
|
+
this.RGB_YUV_TABLE[i + 1792 >> 0] = -5329 * i;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
setQuality(quality) {
|
|
2142
|
+
let q = quality;
|
|
2143
|
+
if (q <= 0)
|
|
2144
|
+
q = 1;
|
|
2145
|
+
if (q > 100)
|
|
2146
|
+
q = 100;
|
|
2147
|
+
const sf = q < 50 ? Math.floor(5e3 / q) : Math.floor(200 - q * 2);
|
|
2148
|
+
this.initQuantTables(sf);
|
|
2149
|
+
}
|
|
2150
|
+
writeBits(bs) {
|
|
2151
|
+
const value = bs.code;
|
|
2152
|
+
let posval = bs.length - 1;
|
|
2153
|
+
while (posval >= 0) {
|
|
2154
|
+
if (value & 1 << posval) {
|
|
2155
|
+
this.bytenew |= 1 << this.bytepos;
|
|
2156
|
+
}
|
|
2157
|
+
posval--;
|
|
2158
|
+
this.bytepos--;
|
|
2159
|
+
if (this.bytepos < 0) {
|
|
2160
|
+
if (this.bytenew === 255) {
|
|
2161
|
+
this.writeByte(255);
|
|
2162
|
+
this.writeByte(0);
|
|
2163
|
+
} else {
|
|
2164
|
+
this.writeByte(this.bytenew);
|
|
2165
|
+
}
|
|
2166
|
+
this.bytepos = 7;
|
|
2167
|
+
this.bytenew = 0;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
writeByte(value) {
|
|
2172
|
+
this.byteout.push(value & 255);
|
|
2173
|
+
}
|
|
2174
|
+
writeWord(value) {
|
|
2175
|
+
this.writeByte(value >> 8 & 255);
|
|
2176
|
+
this.writeByte(value & 255);
|
|
2177
|
+
}
|
|
2178
|
+
fDCTQuant(data, fdtbl) {
|
|
2179
|
+
let d0, d1, d2, d3, d4, d5, d6, d7;
|
|
2180
|
+
let dataOff = 0;
|
|
2181
|
+
const I8 = 8;
|
|
2182
|
+
const I64 = 64;
|
|
2183
|
+
for (let i = 0; i < I8; ++i) {
|
|
2184
|
+
d0 = data[dataOff];
|
|
2185
|
+
d1 = data[dataOff + 1];
|
|
2186
|
+
d2 = data[dataOff + 2];
|
|
2187
|
+
d3 = data[dataOff + 3];
|
|
2188
|
+
d4 = data[dataOff + 4];
|
|
2189
|
+
d5 = data[dataOff + 5];
|
|
2190
|
+
d6 = data[dataOff + 6];
|
|
2191
|
+
d7 = data[dataOff + 7];
|
|
2192
|
+
const tmp0 = d0 + d7;
|
|
2193
|
+
const tmp7 = d0 - d7;
|
|
2194
|
+
const tmp1 = d1 + d6;
|
|
2195
|
+
const tmp6 = d1 - d6;
|
|
2196
|
+
const tmp2 = d2 + d5;
|
|
2197
|
+
const tmp5 = d2 - d5;
|
|
2198
|
+
const tmp3 = d3 + d4;
|
|
2199
|
+
const tmp4 = d3 - d4;
|
|
2200
|
+
let tmp10 = tmp0 + tmp3;
|
|
2201
|
+
const tmp13 = tmp0 - tmp3;
|
|
2202
|
+
let tmp11 = tmp1 + tmp2;
|
|
2203
|
+
let tmp12 = tmp1 - tmp2;
|
|
2204
|
+
data[dataOff] = tmp10 + tmp11;
|
|
2205
|
+
data[dataOff + 4] = tmp10 - tmp11;
|
|
2206
|
+
const z1 = (tmp12 + tmp13) * 0.707106781;
|
|
2207
|
+
data[dataOff + 2] = tmp13 + z1;
|
|
2208
|
+
data[dataOff + 6] = tmp13 - z1;
|
|
2209
|
+
tmp10 = tmp4 + tmp5;
|
|
2210
|
+
tmp11 = tmp5 + tmp6;
|
|
2211
|
+
tmp12 = tmp6 + tmp7;
|
|
2212
|
+
const z5 = (tmp10 - tmp12) * 0.382683433;
|
|
2213
|
+
const z2 = 0.5411961 * tmp10 + z5;
|
|
2214
|
+
const z4 = 1.306562965 * tmp12 + z5;
|
|
2215
|
+
const z3 = tmp11 * 0.707106781;
|
|
2216
|
+
const z11 = tmp7 + z3;
|
|
2217
|
+
const z13 = tmp7 - z3;
|
|
2218
|
+
data[dataOff + 5] = z13 + z2;
|
|
2219
|
+
data[dataOff + 3] = z13 - z2;
|
|
2220
|
+
data[dataOff + 1] = z11 + z4;
|
|
2221
|
+
data[dataOff + 7] = z11 - z4;
|
|
2222
|
+
dataOff += 8;
|
|
2223
|
+
}
|
|
2224
|
+
dataOff = 0;
|
|
2225
|
+
for (let i = 0; i < I8; ++i) {
|
|
2226
|
+
d0 = data[dataOff];
|
|
2227
|
+
d1 = data[dataOff + 8];
|
|
2228
|
+
d2 = data[dataOff + 16];
|
|
2229
|
+
d3 = data[dataOff + 24];
|
|
2230
|
+
d4 = data[dataOff + 32];
|
|
2231
|
+
d5 = data[dataOff + 40];
|
|
2232
|
+
d6 = data[dataOff + 48];
|
|
2233
|
+
d7 = data[dataOff + 56];
|
|
2234
|
+
const tmp0p2 = d0 + d7;
|
|
2235
|
+
const tmp7p2 = d0 - d7;
|
|
2236
|
+
const tmp1p2 = d1 + d6;
|
|
2237
|
+
const tmp6p2 = d1 - d6;
|
|
2238
|
+
const tmp2p2 = d2 + d5;
|
|
2239
|
+
const tmp5p2 = d2 - d5;
|
|
2240
|
+
const tmp3p2 = d3 + d4;
|
|
2241
|
+
const tmp4p2 = d3 - d4;
|
|
2242
|
+
let tmp10p2 = tmp0p2 + tmp3p2;
|
|
2243
|
+
const tmp13p2 = tmp0p2 - tmp3p2;
|
|
2244
|
+
let tmp11p2 = tmp1p2 + tmp2p2;
|
|
2245
|
+
let tmp12p2 = tmp1p2 - tmp2p2;
|
|
2246
|
+
data[dataOff] = tmp10p2 + tmp11p2;
|
|
2247
|
+
data[dataOff + 32] = tmp10p2 - tmp11p2;
|
|
2248
|
+
const z1p2 = (tmp12p2 + tmp13p2) * 0.707106781;
|
|
2249
|
+
data[dataOff + 16] = tmp13p2 + z1p2;
|
|
2250
|
+
data[dataOff + 48] = tmp13p2 - z1p2;
|
|
2251
|
+
tmp10p2 = tmp4p2 + tmp5p2;
|
|
2252
|
+
tmp11p2 = tmp5p2 + tmp6p2;
|
|
2253
|
+
tmp12p2 = tmp6p2 + tmp7p2;
|
|
2254
|
+
const z5p2 = (tmp10p2 - tmp12p2) * 0.382683433;
|
|
2255
|
+
const z2p2 = 0.5411961 * tmp10p2 + z5p2;
|
|
2256
|
+
const z4p2 = 1.306562965 * tmp12p2 + z5p2;
|
|
2257
|
+
const z3p2 = tmp11p2 * 0.707106781;
|
|
2258
|
+
const z11p2 = tmp7p2 + z3p2;
|
|
2259
|
+
const z13p2 = tmp7p2 - z3p2;
|
|
2260
|
+
data[dataOff + 40] = z13p2 + z2p2;
|
|
2261
|
+
data[dataOff + 24] = z13p2 - z2p2;
|
|
2262
|
+
data[dataOff + 8] = z11p2 + z4p2;
|
|
2263
|
+
data[dataOff + 56] = z11p2 - z4p2;
|
|
2264
|
+
dataOff++;
|
|
2265
|
+
}
|
|
2266
|
+
for (let i = 0; i < I64; ++i) {
|
|
2267
|
+
const fDCTVal = data[i] * fdtbl[i];
|
|
2268
|
+
this.outputfDCTQuant[i] = fDCTVal > 0 ? fDCTVal + 0.5 | 0 : fDCTVal - 0.5 | 0;
|
|
2269
|
+
}
|
|
2270
|
+
return this.outputfDCTQuant;
|
|
2271
|
+
}
|
|
2272
|
+
processDU(CDU, fdtbl, DC, HTDC, HTAC) {
|
|
2273
|
+
const EOB = HTAC[0];
|
|
2274
|
+
const M16zeroes = HTAC[240];
|
|
2275
|
+
let pos;
|
|
2276
|
+
const I16 = 16;
|
|
2277
|
+
const I63 = 63;
|
|
2278
|
+
const I64 = 64;
|
|
2279
|
+
const DU_DCT = this.fDCTQuant(CDU, fdtbl);
|
|
2280
|
+
for (let j = 0; j < I64; ++j) {
|
|
2281
|
+
this.DU[ZIGZAG[j]] = DU_DCT[j];
|
|
2282
|
+
}
|
|
2283
|
+
const Diff = this.DU[0] - DC;
|
|
2284
|
+
DC = this.DU[0];
|
|
2285
|
+
if (Diff === 0) {
|
|
2286
|
+
this.writeBits(HTDC[0]);
|
|
2287
|
+
} else {
|
|
2288
|
+
pos = 32767 + Diff;
|
|
2289
|
+
this.writeBits(HTDC[this.category[pos]]);
|
|
2290
|
+
this.writeBits(this.bitcode[pos]);
|
|
2291
|
+
}
|
|
2292
|
+
let end0pos = 63;
|
|
2293
|
+
for (; end0pos > 0 && this.DU[end0pos] === 0; end0pos--) {
|
|
2294
|
+
}
|
|
2295
|
+
if (end0pos === 0) {
|
|
2296
|
+
this.writeBits(EOB);
|
|
2297
|
+
return DC;
|
|
2298
|
+
}
|
|
2299
|
+
let i = 1;
|
|
2300
|
+
let lng;
|
|
2301
|
+
while (i <= end0pos) {
|
|
2302
|
+
const startpos = i;
|
|
2303
|
+
for (; this.DU[i] === 0 && i <= end0pos; ++i) {
|
|
2304
|
+
}
|
|
2305
|
+
let nrzeroes = i - startpos;
|
|
2306
|
+
if (nrzeroes >= I16) {
|
|
2307
|
+
lng = nrzeroes >> 4;
|
|
2308
|
+
for (let nrmarker = 1; nrmarker <= lng; ++nrmarker)
|
|
2309
|
+
this.writeBits(M16zeroes);
|
|
2310
|
+
nrzeroes = nrzeroes & 15;
|
|
2311
|
+
}
|
|
2312
|
+
pos = 32767 + this.DU[i];
|
|
2313
|
+
this.writeBits(HTAC[(nrzeroes << 4) + this.category[pos]]);
|
|
2314
|
+
this.writeBits(this.bitcode[pos]);
|
|
2315
|
+
i++;
|
|
2316
|
+
}
|
|
2317
|
+
if (end0pos !== I63) {
|
|
2318
|
+
this.writeBits(EOB);
|
|
2319
|
+
}
|
|
2320
|
+
return DC;
|
|
2321
|
+
}
|
|
2322
|
+
encode(rgba, width, height) {
|
|
2323
|
+
this.byteout = [];
|
|
2324
|
+
this.bytenew = 0;
|
|
2325
|
+
this.bytepos = 7;
|
|
2326
|
+
this.writeWord(65496);
|
|
2327
|
+
this.writeWord(65504);
|
|
2328
|
+
this.writeWord(16);
|
|
2329
|
+
this.writeByte(74);
|
|
2330
|
+
this.writeByte(70);
|
|
2331
|
+
this.writeByte(73);
|
|
2332
|
+
this.writeByte(70);
|
|
2333
|
+
this.writeByte(0);
|
|
2334
|
+
this.writeByte(1);
|
|
2335
|
+
this.writeByte(1);
|
|
2336
|
+
this.writeByte(0);
|
|
2337
|
+
this.writeWord(1);
|
|
2338
|
+
this.writeWord(1);
|
|
2339
|
+
this.writeByte(0);
|
|
2340
|
+
this.writeByte(0);
|
|
2341
|
+
this.writeWord(65499);
|
|
2342
|
+
this.writeWord(132);
|
|
2343
|
+
this.writeByte(0);
|
|
2344
|
+
for (let i = 0; i < 64; i++)
|
|
2345
|
+
this.writeByte(this.YTable[i]);
|
|
2346
|
+
this.writeByte(1);
|
|
2347
|
+
for (let j = 0; j < 64; j++)
|
|
2348
|
+
this.writeByte(this.UVTable[j]);
|
|
2349
|
+
this.writeWord(65472);
|
|
2350
|
+
this.writeWord(17);
|
|
2351
|
+
this.writeByte(8);
|
|
2352
|
+
this.writeWord(height);
|
|
2353
|
+
this.writeWord(width);
|
|
2354
|
+
this.writeByte(3);
|
|
2355
|
+
this.writeByte(1);
|
|
2356
|
+
this.writeByte(17);
|
|
2357
|
+
this.writeByte(0);
|
|
2358
|
+
this.writeByte(2);
|
|
2359
|
+
this.writeByte(17);
|
|
2360
|
+
this.writeByte(1);
|
|
2361
|
+
this.writeByte(3);
|
|
2362
|
+
this.writeByte(17);
|
|
2363
|
+
this.writeByte(1);
|
|
2364
|
+
this.writeWord(65476);
|
|
2365
|
+
this.writeWord(418);
|
|
2366
|
+
this.writeByte(0);
|
|
2367
|
+
for (let i = 0; i < 16; i++)
|
|
2368
|
+
this.writeByte(STD_DC_LUMINANCE_NRCODES[i + 1]);
|
|
2369
|
+
for (let i = 0; i <= 11; i++)
|
|
2370
|
+
this.writeByte(STD_DC_LUMINANCE_VALUES[i]);
|
|
2371
|
+
this.writeByte(16);
|
|
2372
|
+
for (let i = 0; i < 16; i++)
|
|
2373
|
+
this.writeByte(STD_AC_LUMINANCE_NRCODES[i + 1]);
|
|
2374
|
+
for (let i = 0; i <= 161; i++)
|
|
2375
|
+
this.writeByte(STD_AC_LUMINANCE_VALUES[i]);
|
|
2376
|
+
this.writeByte(1);
|
|
2377
|
+
for (let i = 0; i < 16; i++)
|
|
2378
|
+
this.writeByte(STD_DC_CHROMINANCE_NRCODES[i + 1]);
|
|
2379
|
+
for (let i = 0; i <= 11; i++)
|
|
2380
|
+
this.writeByte(STD_DC_CHROMINANCE_VALUES[i]);
|
|
2381
|
+
this.writeByte(17);
|
|
2382
|
+
for (let i = 0; i < 16; i++)
|
|
2383
|
+
this.writeByte(STD_AC_CHROMINANCE_NRCODES[i + 1]);
|
|
2384
|
+
for (let i = 0; i <= 161; i++)
|
|
2385
|
+
this.writeByte(STD_AC_CHROMINANCE_VALUES[i]);
|
|
2386
|
+
this.writeWord(65498);
|
|
2387
|
+
this.writeWord(12);
|
|
2388
|
+
this.writeByte(3);
|
|
2389
|
+
this.writeByte(1);
|
|
2390
|
+
this.writeByte(0);
|
|
2391
|
+
this.writeByte(2);
|
|
2392
|
+
this.writeByte(17);
|
|
2393
|
+
this.writeByte(3);
|
|
2394
|
+
this.writeByte(17);
|
|
2395
|
+
this.writeByte(0);
|
|
2396
|
+
this.writeByte(63);
|
|
2397
|
+
this.writeByte(0);
|
|
2398
|
+
let DCY = 0;
|
|
2399
|
+
let DCU = 0;
|
|
2400
|
+
let DCV = 0;
|
|
2401
|
+
this.bytenew = 0;
|
|
2402
|
+
this.bytepos = 7;
|
|
2403
|
+
const YDU = new Float32Array(64);
|
|
2404
|
+
const UDU = new Float32Array(64);
|
|
2405
|
+
const VDU = new Float32Array(64);
|
|
2406
|
+
const quadWidth = width * 4;
|
|
2407
|
+
const fdtbl_Y = this.fdtbl_Y;
|
|
2408
|
+
const fdtbl_UV = this.fdtbl_UV;
|
|
2409
|
+
const RGB_YUV_TABLE = this.RGB_YUV_TABLE;
|
|
2410
|
+
for (let y = 0; y < height; y += 8) {
|
|
2411
|
+
for (let x = 0; x < width; x += 8) {
|
|
2412
|
+
let start = quadWidth * y + x * 4;
|
|
2413
|
+
for (let pos = 0; pos < 64; pos++) {
|
|
2414
|
+
const row = pos >> 3;
|
|
2415
|
+
const col = (pos & 7) * 4;
|
|
2416
|
+
let p = start + row * quadWidth + col;
|
|
2417
|
+
if (y + row >= height)
|
|
2418
|
+
p -= quadWidth * (y + row - height + 1);
|
|
2419
|
+
if (x + (col >> 2) >= width)
|
|
2420
|
+
p -= 4 * (x + (col >> 2) - width + 1);
|
|
2421
|
+
const r = rgba[p];
|
|
2422
|
+
const g = rgba[p + 1];
|
|
2423
|
+
const b = rgba[p + 2];
|
|
2424
|
+
YDU[pos] = (RGB_YUV_TABLE[r] + RGB_YUV_TABLE[g + 256] + RGB_YUV_TABLE[b + 512] >> 16) - 128;
|
|
2425
|
+
UDU[pos] = (RGB_YUV_TABLE[r + 768] + RGB_YUV_TABLE[g + 1024] + RGB_YUV_TABLE[b + 1280] >> 16) - 128;
|
|
2426
|
+
VDU[pos] = (RGB_YUV_TABLE[r + 1280] + RGB_YUV_TABLE[g + 1536] + RGB_YUV_TABLE[b + 1792] >> 16) - 128;
|
|
2427
|
+
}
|
|
2428
|
+
DCY = this.processDU(YDU, fdtbl_Y, DCY, this.YDC_HT, this.YAC_HT);
|
|
2429
|
+
DCU = this.processDU(UDU, fdtbl_UV, DCU, this.UVDC_HT, this.UVAC_HT);
|
|
2430
|
+
DCV = this.processDU(VDU, fdtbl_UV, DCV, this.UVDC_HT, this.UVAC_HT);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
if (this.bytepos >= 0) {
|
|
2434
|
+
const fillbits = { length: this.bytepos + 1, code: (1 << this.bytepos + 1) - 1 };
|
|
2435
|
+
this.writeBits(fillbits);
|
|
2436
|
+
}
|
|
2437
|
+
this.writeWord(65497);
|
|
2438
|
+
return Buffer.from(this.byteout);
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
|
|
1471
2444
|
// ../core/dist/png-encoder.js
|
|
1472
2445
|
import { deflateSync } from "zlib";
|
|
1473
2446
|
function crc32(buf) {
|
|
@@ -1531,12 +2504,18 @@ var init_png_encoder = __esm({
|
|
|
1531
2504
|
import * as fs from "fs";
|
|
1532
2505
|
import * as os from "os";
|
|
1533
2506
|
import * as path from "path";
|
|
1534
|
-
function
|
|
2507
|
+
function encodeImageFromRgbaResponse(response, format, quality) {
|
|
1535
2508
|
if (!response.data || response.width === void 0 || response.height === void 0) {
|
|
1536
2509
|
throw new Error("Render response missing data, width, or height");
|
|
1537
2510
|
}
|
|
1538
2511
|
const rgbaBuffer = Buffer.from(response.data, "base64");
|
|
1539
|
-
|
|
2512
|
+
if (format === "png") {
|
|
2513
|
+
return { buffer: rgbaToPng(rgbaBuffer, response.width, response.height), mimeType: "image/png" };
|
|
2514
|
+
}
|
|
2515
|
+
return {
|
|
2516
|
+
buffer: rgbaToJpeg(rgbaBuffer, response.width, response.height, quality),
|
|
2517
|
+
mimeType: "image/jpeg"
|
|
2518
|
+
};
|
|
1540
2519
|
}
|
|
1541
2520
|
function luaLongQuote(s) {
|
|
1542
2521
|
let level = 0;
|
|
@@ -1751,6 +2730,9 @@ function parseBridgeResponse(response) {
|
|
|
1751
2730
|
}
|
|
1752
2731
|
return JSON.stringify(response);
|
|
1753
2732
|
}
|
|
2733
|
+
function sleep(ms) {
|
|
2734
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2735
|
+
}
|
|
1754
2736
|
var SERVER_LOCAL_NAME, CLIENT_LOCAL_NAME, EVAL_WRAPPER_LINE_OFFSET, RobloxStudioTools;
|
|
1755
2737
|
var init_tools = __esm({
|
|
1756
2738
|
"../core/dist/tools/index.js"() {
|
|
@@ -1760,6 +2742,7 @@ var init_tools = __esm({
|
|
|
1760
2742
|
init_build_executor();
|
|
1761
2743
|
init_opencloud_client();
|
|
1762
2744
|
init_roblox_cookie_client();
|
|
2745
|
+
init_jpeg_encoder();
|
|
1763
2746
|
init_png_encoder();
|
|
1764
2747
|
SERVER_LOCAL_NAME = "__MCP_ServerEvalLocal";
|
|
1765
2748
|
CLIENT_LOCAL_NAME = "__MCP_ClientEvalBridge";
|
|
@@ -1796,6 +2779,111 @@ var init_tools = __esm({
|
|
|
1796
2779
|
}
|
|
1797
2780
|
return this.client.request(endpoint, data, r.targetInstanceId, r.targetRole);
|
|
1798
2781
|
}
|
|
2782
|
+
// Resolves which connected place a tool should target and whether a playtest
|
|
2783
|
+
// CLIENT peer is present on it. Used by capture/input to auto-route to the
|
|
2784
|
+
// running client (where the live viewport + input pipeline are) without the
|
|
2785
|
+
// caller having to pass target. Throws RoutingFailure with the standard
|
|
2786
|
+
// instance list if the place is ambiguous (multiple connected, no instance_id).
|
|
2787
|
+
_resolveRuntime(instance_id) {
|
|
2788
|
+
const r = this.bridge.resolveTarget({ instance_id, target: void 0 });
|
|
2789
|
+
if (!r.ok)
|
|
2790
|
+
throw new RoutingFailure(r.error);
|
|
2791
|
+
const resolvedId = r.targetInstanceId;
|
|
2792
|
+
const roles = this.bridge.getInstances().filter((i) => i.instanceId === resolvedId).map((i) => i.role);
|
|
2793
|
+
const clientRoles = roles.filter((role) => role.startsWith("client")).sort();
|
|
2794
|
+
return { instanceId: resolvedId, clientRole: clientRoles[0] };
|
|
2795
|
+
}
|
|
2796
|
+
_resolveInstanceIdOnly(instance_id) {
|
|
2797
|
+
const instances = this.bridge.getInstances();
|
|
2798
|
+
const publicList = this.bridge.getPublicInstances();
|
|
2799
|
+
const errorData = { instances: publicList, count: publicList.length };
|
|
2800
|
+
if (instance_id !== void 0) {
|
|
2801
|
+
if (!instances.some((i) => i.instanceId === instance_id)) {
|
|
2802
|
+
throw new RoutingFailure({
|
|
2803
|
+
code: "unrecognized_instance_id",
|
|
2804
|
+
message: `instance_id "${instance_id}" is not connected. Pass one from data.instances.`,
|
|
2805
|
+
data: errorData
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
return instance_id;
|
|
2809
|
+
}
|
|
2810
|
+
const distinct = Array.from(new Set(instances.map((i) => i.instanceId)));
|
|
2811
|
+
if (distinct.length === 0) {
|
|
2812
|
+
throw new RoutingFailure({
|
|
2813
|
+
code: "unrecognized_instance_id",
|
|
2814
|
+
message: "No Studio plugin is connected.",
|
|
2815
|
+
data: errorData
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
if (distinct.length > 1) {
|
|
2819
|
+
throw new RoutingFailure({
|
|
2820
|
+
code: "multiple_instances_connected",
|
|
2821
|
+
message: "Multiple Studio places are connected. Pass instance_id to disambiguate.",
|
|
2822
|
+
data: errorData
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
return distinct[0];
|
|
2826
|
+
}
|
|
2827
|
+
_resolveSingleTarget(target, instance_id) {
|
|
2828
|
+
const resolved = this.bridge.resolveTarget({ instance_id, target });
|
|
2829
|
+
if (!resolved.ok)
|
|
2830
|
+
throw new RoutingFailure(resolved.error);
|
|
2831
|
+
if (resolved.mode !== "single") {
|
|
2832
|
+
throw new RoutingFailure({
|
|
2833
|
+
code: "target_role_not_present_on_instance",
|
|
2834
|
+
message: "Pick a specific target role for this tool.",
|
|
2835
|
+
data: {
|
|
2836
|
+
instances: this.bridge.getPublicInstances(),
|
|
2837
|
+
count: this.bridge.getInstances().length
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
return { instanceId: resolved.targetInstanceId, role: resolved.targetRole };
|
|
2842
|
+
}
|
|
2843
|
+
_rolesForInstance(instanceId) {
|
|
2844
|
+
return this.bridge.getInstances().filter((i) => i.instanceId === instanceId).map((i) => i.role);
|
|
2845
|
+
}
|
|
2846
|
+
_clientRolesForInstance(instanceId) {
|
|
2847
|
+
return this._rolesForInstance(instanceId).filter((role) => /^client-\d+$/.test(role)).sort((a, b) => Number(a.slice("client-".length)) - Number(b.slice("client-".length)));
|
|
2848
|
+
}
|
|
2849
|
+
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
|
|
2850
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
2851
|
+
while (Date.now() < deadline) {
|
|
2852
|
+
const roles = this._rolesForInstance(instanceId);
|
|
2853
|
+
const hasServer = !opts.server || roles.includes("server");
|
|
2854
|
+
const hasClients = opts.clientCount === void 0 || this._clientRolesForInstance(instanceId).length >= opts.clientCount;
|
|
2855
|
+
const absent = opts.absentRole === void 0 || !roles.includes(opts.absentRole);
|
|
2856
|
+
const runtimeAbsent = !opts.noRuntime || !roles.some((role) => role === "server" || /^client-\d+$/.test(role));
|
|
2857
|
+
if (hasServer && hasClients && absent && runtimeAbsent) {
|
|
2858
|
+
return { ok: true, roles, timedOut: false };
|
|
2859
|
+
}
|
|
2860
|
+
await sleep(250);
|
|
2861
|
+
}
|
|
2862
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
2863
|
+
}
|
|
2864
|
+
async _waitForExactClientCount(instanceId, expectedClientCount, timeoutSec = 30, stableMs = 3e3) {
|
|
2865
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
2866
|
+
let exactSince;
|
|
2867
|
+
while (Date.now() < deadline) {
|
|
2868
|
+
const roles2 = this._rolesForInstance(instanceId);
|
|
2869
|
+
const clientCount2 = this._clientRolesForInstance(instanceId).length;
|
|
2870
|
+
if (clientCount2 > expectedClientCount) {
|
|
2871
|
+
return { ok: false, roles: roles2, timedOut: false, extraClients: true, clientCount: clientCount2 };
|
|
2872
|
+
}
|
|
2873
|
+
if (roles2.includes("server") && clientCount2 === expectedClientCount) {
|
|
2874
|
+
exactSince ??= Date.now();
|
|
2875
|
+
if (Date.now() - exactSince >= stableMs) {
|
|
2876
|
+
return { ok: true, roles: roles2, timedOut: false, extraClients: false, clientCount: clientCount2 };
|
|
2877
|
+
}
|
|
2878
|
+
} else {
|
|
2879
|
+
exactSince = void 0;
|
|
2880
|
+
}
|
|
2881
|
+
await sleep(250);
|
|
2882
|
+
}
|
|
2883
|
+
const roles = this._rolesForInstance(instanceId);
|
|
2884
|
+
const clientCount = this._clientRolesForInstance(instanceId).length;
|
|
2885
|
+
return { ok: false, roles, timedOut: true, extraClients: clientCount > expectedClientCount, clientCount };
|
|
2886
|
+
}
|
|
1799
2887
|
async getFileTree(path2 = "", instance_id) {
|
|
1800
2888
|
const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
|
|
1801
2889
|
return {
|
|
@@ -2325,7 +3413,7 @@ ${code}`
|
|
|
2325
3413
|
const wrapper = buildModuleScriptInvokeWrapper({
|
|
2326
3414
|
service: "ServerScriptService",
|
|
2327
3415
|
bridgeName: SERVER_LOCAL_NAME,
|
|
2328
|
-
missingError: "ServerEvalBridge not
|
|
3416
|
+
missingError: "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically (including for manually-started playtests); if a playtest is running and you still see this, reconnect the plugin in the edit window so the bridge reinstalls, then start the playtest again.",
|
|
2329
3417
|
userCode: code
|
|
2330
3418
|
});
|
|
2331
3419
|
const response = await this._callSingle("/api/execute-luau", { code: wrapper }, "server", instance_id);
|
|
@@ -2349,7 +3437,7 @@ ${code}`
|
|
|
2349
3437
|
const wrapper = buildModuleScriptInvokeWrapper({
|
|
2350
3438
|
service: "ReplicatedStorage",
|
|
2351
3439
|
bridgeName: CLIENT_LOCAL_NAME,
|
|
2352
|
-
missingError: "ClientEvalBridge not
|
|
3440
|
+
missingError: "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically (including for manually-started playtests); if a playtest is running and you still see this, reconnect the plugin in the edit window so the bridge reinstalls, then start the playtest again.",
|
|
2353
3441
|
userCode: code
|
|
2354
3442
|
});
|
|
2355
3443
|
const response = await this._callSingle("/api/execute-luau", { code: wrapper }, clientTarget, instance_id);
|
|
@@ -2440,10 +3528,10 @@ ${code}`
|
|
|
2440
3528
|
if (mode !== "play" && mode !== "run") {
|
|
2441
3529
|
throw new Error('mode must be "play" or "run"');
|
|
2442
3530
|
}
|
|
2443
|
-
const data = { mode };
|
|
2444
3531
|
if (numPlayers !== void 0) {
|
|
2445
|
-
|
|
3532
|
+
throw new Error("start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions.");
|
|
2446
3533
|
}
|
|
3534
|
+
const data = { mode };
|
|
2447
3535
|
const response = await this._callSingle("/api/start-playtest", data, void 0, instance_id);
|
|
2448
3536
|
return {
|
|
2449
3537
|
content: [
|
|
@@ -2471,6 +3559,196 @@ ${code}`
|
|
|
2471
3559
|
]
|
|
2472
3560
|
};
|
|
2473
3561
|
}
|
|
3562
|
+
async _buildMultiplayerState(instanceId) {
|
|
3563
|
+
const peers = this.bridge.getPublicInstances().filter((i) => i.instanceId === instanceId).sort((a, b) => a.role.localeCompare(b.role));
|
|
3564
|
+
const body = {
|
|
3565
|
+
instanceId,
|
|
3566
|
+
peers,
|
|
3567
|
+
peerCount: peers.length
|
|
3568
|
+
};
|
|
3569
|
+
const edit = peers.find((p) => p.role === "edit");
|
|
3570
|
+
const server = peers.find((p) => p.role === "server");
|
|
3571
|
+
let editState;
|
|
3572
|
+
let serverState;
|
|
3573
|
+
if (edit) {
|
|
3574
|
+
try {
|
|
3575
|
+
editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3576
|
+
body.edit = editState;
|
|
3577
|
+
} catch (err) {
|
|
3578
|
+
body.edit = { error: err instanceof Error ? err.message : String(err) };
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
if (server) {
|
|
3582
|
+
try {
|
|
3583
|
+
serverState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "server");
|
|
3584
|
+
body.server = serverState;
|
|
3585
|
+
} catch (err) {
|
|
3586
|
+
body.server = { error: err instanceof Error ? err.message : String(err) };
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
const session = editState?.session;
|
|
3590
|
+
const rawPhase = typeof session?.phase === "string" ? session.phase : void 0;
|
|
3591
|
+
const hasRuntime = peers.some((p) => p.role === "server" || p.role.startsWith("client-"));
|
|
3592
|
+
body.phase = rawPhase === "starting" && hasRuntime ? "running" : rawPhase ?? (hasRuntime ? "running" : "idle");
|
|
3593
|
+
body.testId = session?.testId;
|
|
3594
|
+
body.numPlayers = session?.numPlayers;
|
|
3595
|
+
body.testArgs = session?.testArgs ?? serverState?.testArgs;
|
|
3596
|
+
body.result = session?.result;
|
|
3597
|
+
body.error = session?.error;
|
|
3598
|
+
body.players = serverState?.players ?? [];
|
|
3599
|
+
body.playerCount = serverState?.playerCount ?? 0;
|
|
3600
|
+
body.clientRoles = this._clientRolesForInstance(instanceId);
|
|
3601
|
+
return body;
|
|
3602
|
+
}
|
|
3603
|
+
async _waitForMultiplayerEditDone(instanceId, timeoutSec = 30) {
|
|
3604
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3605
|
+
while (Date.now() < deadline) {
|
|
3606
|
+
if (!this._rolesForInstance(instanceId).includes("edit"))
|
|
3607
|
+
return false;
|
|
3608
|
+
try {
|
|
3609
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3610
|
+
const phase = editState?.session?.phase;
|
|
3611
|
+
if (phase === "completed" || phase === "failed")
|
|
3612
|
+
return true;
|
|
3613
|
+
} catch {
|
|
3614
|
+
}
|
|
3615
|
+
await sleep(250);
|
|
3616
|
+
}
|
|
3617
|
+
return false;
|
|
3618
|
+
}
|
|
3619
|
+
async _isMultiplayerTestRunning(instanceId) {
|
|
3620
|
+
if (!this._rolesForInstance(instanceId).includes("edit"))
|
|
3621
|
+
return false;
|
|
3622
|
+
try {
|
|
3623
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3624
|
+
const phase = editState?.session?.phase;
|
|
3625
|
+
return phase === "starting" || phase === "running";
|
|
3626
|
+
} catch {
|
|
3627
|
+
return false;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30) {
|
|
3631
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3632
|
+
while (Date.now() < deadline) {
|
|
3633
|
+
const exact = await this._waitForExactClientCount(instanceId, clientCount, 0.25, 0);
|
|
3634
|
+
if (exact.ok || exact.extraClients) {
|
|
3635
|
+
return { ok: exact.ok, roles: exact.roles, timedOut: false, error: exact.extraClients ? `Expected ${clientCount} client(s), but Studio registered ${exact.clientCount}.` : void 0 };
|
|
3636
|
+
}
|
|
3637
|
+
try {
|
|
3638
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
3639
|
+
const session = editState?.session;
|
|
3640
|
+
if (session?.phase === "failed" || session?.phase === "completed") {
|
|
3641
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: false, phase: session.phase, error: session.error };
|
|
3642
|
+
}
|
|
3643
|
+
} catch {
|
|
3644
|
+
}
|
|
3645
|
+
await sleep(250);
|
|
3646
|
+
}
|
|
3647
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
3648
|
+
}
|
|
3649
|
+
async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
|
|
3650
|
+
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
3651
|
+
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
3652
|
+
}
|
|
3653
|
+
const editTarget = this._resolveSingleTarget("edit", instance_id);
|
|
3654
|
+
const response = await this.client.request("/api/multiplayer-test-start", { numPlayers, testArgs: testArgs ?? {} }, editTarget.instanceId, editTarget.role);
|
|
3655
|
+
if (response?.error) {
|
|
3656
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3657
|
+
}
|
|
3658
|
+
const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ?? 30);
|
|
3659
|
+
const state = await this._buildMultiplayerState(editTarget.instanceId);
|
|
3660
|
+
return {
|
|
3661
|
+
content: [{
|
|
3662
|
+
type: "text",
|
|
3663
|
+
text: JSON.stringify({
|
|
3664
|
+
...response,
|
|
3665
|
+
ready: wait.ok,
|
|
3666
|
+
timedOut: wait.timedOut,
|
|
3667
|
+
wait,
|
|
3668
|
+
roles: wait.roles,
|
|
3669
|
+
state
|
|
3670
|
+
})
|
|
3671
|
+
}]
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
async multiplayerTestState(instance_id) {
|
|
3675
|
+
const instanceId = this._resolveInstanceIdOnly(instance_id);
|
|
3676
|
+
const state = await this._buildMultiplayerState(instanceId);
|
|
3677
|
+
return { content: [{ type: "text", text: JSON.stringify(state) }] };
|
|
3678
|
+
}
|
|
3679
|
+
async multiplayerTestAddPlayers(numPlayers, timeout, instance_id) {
|
|
3680
|
+
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
3681
|
+
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
3682
|
+
}
|
|
3683
|
+
const serverTarget = this._resolveSingleTarget("server", instance_id);
|
|
3684
|
+
const before = this._clientRolesForInstance(serverTarget.instanceId).length;
|
|
3685
|
+
const response = await this.client.request("/api/multiplayer-test-add-players", { numPlayers, timeout: timeout ?? 10 }, serverTarget.instanceId, serverTarget.role);
|
|
3686
|
+
if (response?.error) {
|
|
3687
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3688
|
+
}
|
|
3689
|
+
const wait = await this._waitForExactClientCount(serverTarget.instanceId, before + numPlayers, timeout ?? 30);
|
|
3690
|
+
const state = await this._buildMultiplayerState(serverTarget.instanceId);
|
|
3691
|
+
return {
|
|
3692
|
+
content: [{
|
|
3693
|
+
type: "text",
|
|
3694
|
+
text: JSON.stringify({
|
|
3695
|
+
...response,
|
|
3696
|
+
ready: wait.ok,
|
|
3697
|
+
timedOut: wait.timedOut,
|
|
3698
|
+
wait,
|
|
3699
|
+
roles: wait.roles,
|
|
3700
|
+
state
|
|
3701
|
+
})
|
|
3702
|
+
}]
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
async multiplayerTestLeaveClient(target = "client-1", timeout, instance_id) {
|
|
3706
|
+
if (!/^client-\d+$/.test(target)) {
|
|
3707
|
+
throw new Error(`multiplayer_test_leave_client requires target=client-N (got: ${target})`);
|
|
3708
|
+
}
|
|
3709
|
+
const clientTarget = this._resolveSingleTarget(target, instance_id);
|
|
3710
|
+
const response = await this.client.request("/api/multiplayer-test-leave-client", {}, clientTarget.instanceId, clientTarget.role);
|
|
3711
|
+
if (response?.error) {
|
|
3712
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3713
|
+
}
|
|
3714
|
+
const wait = await this._waitForRuntimeRoles(clientTarget.instanceId, { absentRole: clientTarget.role }, timeout ?? 30);
|
|
3715
|
+
const state = await this._buildMultiplayerState(clientTarget.instanceId);
|
|
3716
|
+
return {
|
|
3717
|
+
content: [{
|
|
3718
|
+
type: "text",
|
|
3719
|
+
text: JSON.stringify({
|
|
3720
|
+
...response,
|
|
3721
|
+
left: wait.ok,
|
|
3722
|
+
timedOut: wait.timedOut,
|
|
3723
|
+
roles: wait.roles,
|
|
3724
|
+
state
|
|
3725
|
+
})
|
|
3726
|
+
}]
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
async multiplayerTestEnd(value, timeout, instance_id) {
|
|
3730
|
+
const serverTarget = this._resolveSingleTarget("server", instance_id);
|
|
3731
|
+
const response = await this.client.request("/api/multiplayer-test-end", { value: value ?? "ended_by_mcp" }, serverTarget.instanceId, serverTarget.role);
|
|
3732
|
+
if (response?.error) {
|
|
3733
|
+
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3734
|
+
}
|
|
3735
|
+
const editDone = await this._waitForMultiplayerEditDone(serverTarget.instanceId, timeout ?? 30);
|
|
3736
|
+
const wait = await this._waitForRuntimeRoles(serverTarget.instanceId, { noRuntime: true }, timeout ?? 30);
|
|
3737
|
+
const state = await this._buildMultiplayerState(serverTarget.instanceId);
|
|
3738
|
+
return {
|
|
3739
|
+
content: [{
|
|
3740
|
+
type: "text",
|
|
3741
|
+
text: JSON.stringify({
|
|
3742
|
+
...response,
|
|
3743
|
+
ended: wait.ok,
|
|
3744
|
+
editDone,
|
|
3745
|
+
timedOut: wait.timedOut,
|
|
3746
|
+
roles: wait.roles,
|
|
3747
|
+
state
|
|
3748
|
+
})
|
|
3749
|
+
}]
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
2474
3752
|
async getConnectedInstances() {
|
|
2475
3753
|
const instances = this.bridge.getPublicInstances();
|
|
2476
3754
|
return {
|
|
@@ -2506,15 +3784,15 @@ ${code}`
|
|
|
2506
3784
|
}
|
|
2507
3785
|
static findProjectRoot(startDir) {
|
|
2508
3786
|
let dir = path.resolve(startDir);
|
|
2509
|
-
|
|
3787
|
+
let previous = "";
|
|
3788
|
+
while (dir !== previous) {
|
|
2510
3789
|
if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
|
|
2511
3790
|
return dir;
|
|
2512
3791
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
return null;
|
|
2516
|
-
dir = parent;
|
|
3792
|
+
previous = dir;
|
|
3793
|
+
dir = path.dirname(dir);
|
|
2517
3794
|
}
|
|
3795
|
+
return null;
|
|
2518
3796
|
}
|
|
2519
3797
|
static isDirectory(candidate) {
|
|
2520
3798
|
if (!candidate)
|
|
@@ -3195,13 +4473,13 @@ ${code}`
|
|
|
3195
4473
|
if (!action) {
|
|
3196
4474
|
throw new Error("action is required for simulate_mouse_input");
|
|
3197
4475
|
}
|
|
4476
|
+
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
3198
4477
|
const response = await this._callSingle("/api/simulate-mouse-input", {
|
|
3199
4478
|
action,
|
|
3200
4479
|
x,
|
|
3201
4480
|
y,
|
|
3202
|
-
button
|
|
3203
|
-
|
|
3204
|
-
}, target || "edit", instance_id);
|
|
4481
|
+
button
|
|
4482
|
+
}, target || clientRole || "edit", instanceId);
|
|
3205
4483
|
return {
|
|
3206
4484
|
content: [{
|
|
3207
4485
|
type: "text",
|
|
@@ -3209,15 +4487,17 @@ ${code}`
|
|
|
3209
4487
|
}]
|
|
3210
4488
|
};
|
|
3211
4489
|
}
|
|
3212
|
-
async simulateKeyboardInput(keyCode, action, duration, target, instance_id) {
|
|
3213
|
-
if (!keyCode) {
|
|
3214
|
-
throw new Error("keyCode is required for simulate_keyboard_input");
|
|
4490
|
+
async simulateKeyboardInput(keyCode, action, duration, text, target, instance_id) {
|
|
4491
|
+
if (!keyCode && text === void 0) {
|
|
4492
|
+
throw new Error("keyCode or text is required for simulate_keyboard_input");
|
|
3215
4493
|
}
|
|
4494
|
+
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
3216
4495
|
const response = await this._callSingle("/api/simulate-keyboard-input", {
|
|
3217
4496
|
keyCode,
|
|
3218
4497
|
action,
|
|
3219
|
-
duration
|
|
3220
|
-
|
|
4498
|
+
duration,
|
|
4499
|
+
text
|
|
4500
|
+
}, target || clientRole || "edit", instanceId);
|
|
3221
4501
|
return {
|
|
3222
4502
|
content: [{
|
|
3223
4503
|
type: "text",
|
|
@@ -3429,23 +4709,74 @@ ${code}`
|
|
|
3429
4709
|
}, tgt, instance_id);
|
|
3430
4710
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
3431
4711
|
}
|
|
3432
|
-
async captureScreenshot(instance_id) {
|
|
3433
|
-
const
|
|
4712
|
+
async captureScreenshot(instance_id, format, quality) {
|
|
4713
|
+
const { instanceId, clientRole } = this._resolveRuntime(instance_id);
|
|
4714
|
+
let response;
|
|
4715
|
+
if (clientRole) {
|
|
4716
|
+
const begin = await this._callSingle("/api/capture-begin", {}, clientRole, instanceId);
|
|
4717
|
+
if (begin.error) {
|
|
4718
|
+
return { content: [{ type: "text", text: begin.error }] };
|
|
4719
|
+
}
|
|
4720
|
+
if (!begin.contentId) {
|
|
4721
|
+
return { content: [{ type: "text", text: "Screenshot capture failed: no content id returned from client." }] };
|
|
4722
|
+
}
|
|
4723
|
+
response = await this._callSingle("/api/capture-read", { contentId: begin.contentId }, "edit", instanceId);
|
|
4724
|
+
} else {
|
|
4725
|
+
response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
|
|
4726
|
+
}
|
|
3434
4727
|
if (response.error) {
|
|
4728
|
+
let text = response.error;
|
|
4729
|
+
if (clientRole && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
|
|
4730
|
+
text = `Screenshot capture reached the multiplayer client, but Roblox returned a temporary screenshot texture that the edit peer cannot read in StudioTestService multiplayer sessions. Regular start_playtest capture works because the temporary rbxtemp:// handle is readable from the edit process; multiplayer client handles appear to be scoped to the client process. Raw error: ${response.error}`;
|
|
4731
|
+
}
|
|
3435
4732
|
return {
|
|
3436
4733
|
content: [{
|
|
3437
4734
|
type: "text",
|
|
3438
|
-
text
|
|
4735
|
+
text
|
|
3439
4736
|
}]
|
|
3440
4737
|
};
|
|
3441
4738
|
}
|
|
3442
|
-
const
|
|
4739
|
+
const w = response.width;
|
|
4740
|
+
const h = response.height;
|
|
4741
|
+
if (w === void 0 || h === void 0) {
|
|
4742
|
+
return { content: [{ type: "text", text: "Screenshot response missing dimensions." }] };
|
|
4743
|
+
}
|
|
4744
|
+
const fmt = format === "png" ? "png" : "jpeg";
|
|
4745
|
+
const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
|
|
4746
|
+
const MAX_IMAGE_BYTES = 6e6;
|
|
4747
|
+
const encoded = encodeImageFromRgbaResponse(response, fmt, q);
|
|
4748
|
+
let { buffer } = encoded;
|
|
4749
|
+
const { mimeType } = encoded;
|
|
4750
|
+
let usedQ = q;
|
|
4751
|
+
let note = "";
|
|
4752
|
+
if (buffer.length > MAX_IMAGE_BYTES) {
|
|
4753
|
+
if (fmt === "png") {
|
|
4754
|
+
const mb = (buffer.length / 1048576).toFixed(1);
|
|
4755
|
+
return {
|
|
4756
|
+
content: [{
|
|
4757
|
+
type: "text",
|
|
4758
|
+
text: `PNG screenshot is ${mb}MB, over the ~${(MAX_IMAGE_BYTES / 1048576).toFixed(0)}MB inline image limit. Use the default jpeg format (optionally with a "quality" value) or make the Studio window smaller for a lossless capture.`
|
|
4759
|
+
}]
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
while (buffer.length > MAX_IMAGE_BYTES && usedQ > 25) {
|
|
4763
|
+
usedQ = Math.max(25, usedQ - 20);
|
|
4764
|
+
buffer = encodeImageFromRgbaResponse(response, "jpeg", usedQ).buffer;
|
|
4765
|
+
}
|
|
4766
|
+
note = ` \u2014 auto-reduced to q${usedQ} to fit the inline size limit; enlarge the Studio window or capture a smaller region for finer detail`;
|
|
4767
|
+
}
|
|
3443
4768
|
return {
|
|
3444
|
-
content: [
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
4769
|
+
content: [
|
|
4770
|
+
{
|
|
4771
|
+
type: "text",
|
|
4772
|
+
text: `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`
|
|
4773
|
+
},
|
|
4774
|
+
{
|
|
4775
|
+
type: "image",
|
|
4776
|
+
data: buffer.toString("base64"),
|
|
4777
|
+
mimeType
|
|
4778
|
+
}
|
|
4779
|
+
]
|
|
3449
4780
|
};
|
|
3450
4781
|
}
|
|
3451
4782
|
};
|
|
@@ -4582,7 +5913,7 @@ var init_definitions = __esm({
|
|
|
4582
5913
|
{
|
|
4583
5914
|
name: "execute_luau",
|
|
4584
5915
|
category: "write",
|
|
4585
|
-
description:
|
|
5916
|
+
description: 'Execute Luau code in plugin context. target="server" and target="client-N" run against live runtime DataModels with PluginSecurity permissions; use eval_*_runtime instead when you need the game Script/LocalScript VM require cache. Use print()/warn() for output. Return value is captured.',
|
|
4586
5917
|
inputSchema: {
|
|
4587
5918
|
type: "object",
|
|
4588
5919
|
properties: {
|
|
@@ -4605,7 +5936,7 @@ var init_definitions = __esm({
|
|
|
4605
5936
|
{
|
|
4606
5937
|
name: "eval_server_runtime",
|
|
4607
5938
|
category: "write",
|
|
4608
|
-
description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts
|
|
5939
|
+
description: "Execute Luau on the server peer in the running game's Script VM (shares require cache with user game scripts, unlike execute_luau target=server which runs in plugin context). Requires a running playtest; the bridge is installed automatically (including for playtests started manually via the Studio Play button).",
|
|
4609
5940
|
inputSchema: {
|
|
4610
5941
|
type: "object",
|
|
4611
5942
|
properties: {
|
|
@@ -4624,7 +5955,7 @@ var init_definitions = __esm({
|
|
|
4624
5955
|
{
|
|
4625
5956
|
name: "eval_client_runtime",
|
|
4626
5957
|
category: "write",
|
|
4627
|
-
description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts
|
|
5958
|
+
description: "Execute Luau on a client peer in the running game's LocalScript VM (shares require cache with user game scripts, unlike execute_luau target=client-N which runs in plugin context). Requires a running playtest; the bridge is installed automatically (including for playtests started manually via the Studio Play button).",
|
|
4628
5959
|
inputSchema: {
|
|
4629
5960
|
type: "object",
|
|
4630
5961
|
properties: {
|
|
@@ -4701,7 +6032,7 @@ var init_definitions = __esm({
|
|
|
4701
6032
|
{
|
|
4702
6033
|
name: "start_playtest",
|
|
4703
6034
|
category: "write",
|
|
4704
|
-
description: "Start playtest. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest.
|
|
6035
|
+
description: "Start a simple single-player Studio playtest in play or run mode. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
|
|
4705
6036
|
inputSchema: {
|
|
4706
6037
|
type: "object",
|
|
4707
6038
|
properties: {
|
|
@@ -4712,7 +6043,7 @@ var init_definitions = __esm({
|
|
|
4712
6043
|
},
|
|
4713
6044
|
numPlayers: {
|
|
4714
6045
|
type: "number",
|
|
4715
|
-
description: "
|
|
6046
|
+
description: "Deprecated and rejected. Use multiplayer_test_start for multi-client testing."
|
|
4716
6047
|
},
|
|
4717
6048
|
instance_id: {
|
|
4718
6049
|
type: "string",
|
|
@@ -4754,6 +6085,112 @@ var init_definitions = __esm({
|
|
|
4754
6085
|
}
|
|
4755
6086
|
}
|
|
4756
6087
|
},
|
|
6088
|
+
{
|
|
6089
|
+
name: "multiplayer_test_start",
|
|
6090
|
+
category: "write",
|
|
6091
|
+
description: "Start a StudioTestService multiplayer test and wait for the server plus requested client peers to connect. Use this for multi-client runtime testing.",
|
|
6092
|
+
inputSchema: {
|
|
6093
|
+
type: "object",
|
|
6094
|
+
properties: {
|
|
6095
|
+
numPlayers: {
|
|
6096
|
+
type: "number",
|
|
6097
|
+
description: "Number of client players to start (1-8)."
|
|
6098
|
+
},
|
|
6099
|
+
testArgs: {
|
|
6100
|
+
description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
|
|
6101
|
+
},
|
|
6102
|
+
timeout: {
|
|
6103
|
+
type: "number",
|
|
6104
|
+
description: "Max seconds to wait for server + clients to register (default 30)."
|
|
6105
|
+
},
|
|
6106
|
+
instance_id: {
|
|
6107
|
+
type: "string",
|
|
6108
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6109
|
+
}
|
|
6110
|
+
},
|
|
6111
|
+
required: ["numPlayers"]
|
|
6112
|
+
}
|
|
6113
|
+
},
|
|
6114
|
+
{
|
|
6115
|
+
name: "multiplayer_test_state",
|
|
6116
|
+
category: "read",
|
|
6117
|
+
description: "Get the active multiplayer StudioTestService state for a place: phase, peers, players, original testArgs, result/error, and connected client roles.",
|
|
6118
|
+
inputSchema: {
|
|
6119
|
+
type: "object",
|
|
6120
|
+
properties: {
|
|
6121
|
+
instance_id: {
|
|
6122
|
+
type: "string",
|
|
6123
|
+
description: "Which connected Studio place to inspect. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
},
|
|
6128
|
+
{
|
|
6129
|
+
name: "multiplayer_test_add_players",
|
|
6130
|
+
category: "write",
|
|
6131
|
+
description: "Add client players to a running StudioTestService multiplayer test and wait for the new clients to connect.",
|
|
6132
|
+
inputSchema: {
|
|
6133
|
+
type: "object",
|
|
6134
|
+
properties: {
|
|
6135
|
+
numPlayers: {
|
|
6136
|
+
type: "number",
|
|
6137
|
+
description: "Number of additional client players to add (1-8)."
|
|
6138
|
+
},
|
|
6139
|
+
timeout: {
|
|
6140
|
+
type: "number",
|
|
6141
|
+
description: "Max seconds to wait for new clients to register (default 30)."
|
|
6142
|
+
},
|
|
6143
|
+
instance_id: {
|
|
6144
|
+
type: "string",
|
|
6145
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6146
|
+
}
|
|
6147
|
+
},
|
|
6148
|
+
required: ["numPlayers"]
|
|
6149
|
+
}
|
|
6150
|
+
},
|
|
6151
|
+
{
|
|
6152
|
+
name: "multiplayer_test_leave_client",
|
|
6153
|
+
category: "write",
|
|
6154
|
+
description: "Disconnect a specific client from a running StudioTestService multiplayer test, then wait for that client peer to leave.",
|
|
6155
|
+
inputSchema: {
|
|
6156
|
+
type: "object",
|
|
6157
|
+
properties: {
|
|
6158
|
+
target: {
|
|
6159
|
+
type: "string",
|
|
6160
|
+
description: 'Client target to leave: "client-1" (default), "client-2", etc.'
|
|
6161
|
+
},
|
|
6162
|
+
timeout: {
|
|
6163
|
+
type: "number",
|
|
6164
|
+
description: "Max seconds to wait for the client peer to disconnect (default 30)."
|
|
6165
|
+
},
|
|
6166
|
+
instance_id: {
|
|
6167
|
+
type: "string",
|
|
6168
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
},
|
|
6173
|
+
{
|
|
6174
|
+
name: "multiplayer_test_end",
|
|
6175
|
+
category: "write",
|
|
6176
|
+
description: "End a running StudioTestService multiplayer test with an optional return value, then wait for all runtime peers to disconnect.",
|
|
6177
|
+
inputSchema: {
|
|
6178
|
+
type: "object",
|
|
6179
|
+
properties: {
|
|
6180
|
+
value: {
|
|
6181
|
+
description: "JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call."
|
|
6182
|
+
},
|
|
6183
|
+
timeout: {
|
|
6184
|
+
type: "number",
|
|
6185
|
+
description: "Max seconds to wait for runtime peers to disconnect (default 30)."
|
|
6186
|
+
},
|
|
6187
|
+
instance_id: {
|
|
6188
|
+
type: "string",
|
|
6189
|
+
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
},
|
|
4757
6194
|
{
|
|
4758
6195
|
name: "get_runtime_logs",
|
|
4759
6196
|
category: "read",
|
|
@@ -5298,10 +6735,19 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
5298
6735
|
{
|
|
5299
6736
|
name: "capture_screenshot",
|
|
5300
6737
|
category: "read",
|
|
5301
|
-
description: 'Capture
|
|
6738
|
+
description: 'Capture the Roblox Studio viewport at native resolution and return it as an image, plus a text line stating the exact pixel dimensions. Works in Edit mode and regular playtests (auto-detects a running client and captures the live play viewport). StudioTestService multiplayer client screenshots are currently blocked by Roblox temporary-texture process scoping; the tool returns a clear error in that case. The returned image is never downscaled, so its pixel grid is exactly the coordinate space simulate_mouse_input uses \u2014 read click positions straight off this image. For reading fine text/UI, use format="png" (lossless) or a higher quality; enlarging the Studio window raises resolution. Requires EditableImage API enabled (Game Settings > Security > "Allow Mesh / Image APIs") and the window to be visible.',
|
|
5302
6739
|
inputSchema: {
|
|
5303
6740
|
type: "object",
|
|
5304
6741
|
properties: {
|
|
6742
|
+
format: {
|
|
6743
|
+
type: "string",
|
|
6744
|
+
enum: ["jpeg", "png"],
|
|
6745
|
+
description: 'Image format. "jpeg" (default) is compact and crisp at high quality. "png" is lossless \u2014 best for reading dense text/UI, but larger (a busy 3D scene may be big).'
|
|
6746
|
+
},
|
|
6747
|
+
quality: {
|
|
6748
|
+
type: "number",
|
|
6749
|
+
description: "JPEG quality 1-100 (default 92). Higher = sharper text, larger size. Ignored for png."
|
|
6750
|
+
},
|
|
5305
6751
|
instance_id: {
|
|
5306
6752
|
type: "string",
|
|
5307
6753
|
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
@@ -5313,36 +6759,31 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
5313
6759
|
{
|
|
5314
6760
|
name: "simulate_mouse_input",
|
|
5315
6761
|
category: "write",
|
|
5316
|
-
description: "Simulate mouse
|
|
6762
|
+
description: "Simulate a mouse click in the running game via UserInputService:CreateVirtualInput. Use during a playtest to click UI buttons, interact with objects, or aim. Fires real UserInputService input and activates GUI buttons. Coordinates are viewport pixels matching capture_screenshot (top-left is 0,0) \u2014 take a screenshot first to find positions. Auto-targets the running client; only works during a playtest. Note: only click/mouseDown/mouseUp are supported (the API has no mouse-move or scroll).",
|
|
5317
6763
|
inputSchema: {
|
|
5318
6764
|
type: "object",
|
|
5319
6765
|
properties: {
|
|
5320
6766
|
action: {
|
|
5321
6767
|
type: "string",
|
|
5322
|
-
enum: ["click", "mouseDown", "mouseUp"
|
|
5323
|
-
description: 'Mouse action
|
|
6768
|
+
enum: ["click", "mouseDown", "mouseUp"],
|
|
6769
|
+
description: 'Mouse action. "click" does mouseDown + short delay + mouseUp.'
|
|
5324
6770
|
},
|
|
5325
6771
|
x: {
|
|
5326
6772
|
type: "number",
|
|
5327
|
-
description: "Viewport pixel X coordinate"
|
|
6773
|
+
description: "Viewport pixel X coordinate (as seen in capture_screenshot)"
|
|
5328
6774
|
},
|
|
5329
6775
|
y: {
|
|
5330
6776
|
type: "number",
|
|
5331
|
-
description: "Viewport pixel Y coordinate"
|
|
6777
|
+
description: "Viewport pixel Y coordinate (as seen in capture_screenshot)"
|
|
5332
6778
|
},
|
|
5333
6779
|
button: {
|
|
5334
6780
|
type: "string",
|
|
5335
6781
|
enum: ["Left", "Right", "Middle"],
|
|
5336
6782
|
description: "Mouse button (default: Left)"
|
|
5337
6783
|
},
|
|
5338
|
-
scrollDirection: {
|
|
5339
|
-
type: "string",
|
|
5340
|
-
enum: ["up", "down"],
|
|
5341
|
-
description: 'Scroll direction (only for "scroll" action)'
|
|
5342
|
-
},
|
|
5343
6784
|
target: {
|
|
5344
6785
|
type: "string",
|
|
5345
|
-
description: 'Instance target
|
|
6786
|
+
description: 'Instance target. Defaults to the running playtest client (client-1) when present, else "edit". Override with "server", "client-2", etc.'
|
|
5346
6787
|
},
|
|
5347
6788
|
instance_id: {
|
|
5348
6789
|
type: "string",
|
|
@@ -5355,13 +6796,13 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
5355
6796
|
{
|
|
5356
6797
|
name: "simulate_keyboard_input",
|
|
5357
6798
|
category: "write",
|
|
5358
|
-
description: 'Simulate keyboard input via
|
|
6799
|
+
description: 'Simulate keyboard input in the running game via UserInputService:CreateVirtualInput. Use during a playtest for character movement (W/A/S/D walks at full WalkSpeed with player controls intact), jumping (Space), interactions (E), or any key-driven action. Drives the real input pipeline so game scripts and control modules respond. For sustained movement use action="press" to hold and "release" to let go. Pass "text" instead of keyCode to type a string into the focused TextBox. Auto-targets the running client; only works during a playtest.',
|
|
5359
6800
|
inputSchema: {
|
|
5360
6801
|
type: "object",
|
|
5361
6802
|
properties: {
|
|
5362
6803
|
keyCode: {
|
|
5363
6804
|
type: "string",
|
|
5364
|
-
description: 'Enum.KeyCode name: "W", "A", "S", "D", "Space", "E", "F", "LeftShift", "LeftControl", "Return", "Tab", "Escape", "One", "Two", etc.'
|
|
6805
|
+
description: 'Enum.KeyCode name: "W", "A", "S", "D", "Space", "E", "F", "LeftShift", "LeftControl", "Return", "Tab", "Escape", "One", "Two", etc. Omit if using "text".'
|
|
5365
6806
|
},
|
|
5366
6807
|
action: {
|
|
5367
6808
|
type: "string",
|
|
@@ -5372,16 +6813,19 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
5372
6813
|
type: "number",
|
|
5373
6814
|
description: 'Hold duration in seconds for "tap" action (default: 0.1). Use longer values for sustained input like walking.'
|
|
5374
6815
|
},
|
|
6816
|
+
text: {
|
|
6817
|
+
type: "string",
|
|
6818
|
+
description: "Type this string into the currently focused TextBox (uses SendTextInput). When provided, keyCode/action are ignored."
|
|
6819
|
+
},
|
|
5375
6820
|
target: {
|
|
5376
6821
|
type: "string",
|
|
5377
|
-
description: 'Instance target
|
|
6822
|
+
description: 'Instance target. Defaults to the running playtest client (client-1) when present, else "edit". Override with "server", "client-2", etc.'
|
|
5378
6823
|
},
|
|
5379
6824
|
instance_id: {
|
|
5380
6825
|
type: "string",
|
|
5381
6826
|
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
5382
6827
|
}
|
|
5383
|
-
}
|
|
5384
|
-
required: ["keyCode"]
|
|
6828
|
+
}
|
|
5385
6829
|
}
|
|
5386
6830
|
},
|
|
5387
6831
|
// === Character Navigation ===
|
|
@@ -5749,7 +7193,7 @@ function handleVariantConflict({ pluginsFolder, otherAssetName, replace }) {
|
|
|
5749
7193
|
}
|
|
5750
7194
|
console.warn(`
|
|
5751
7195
|
[install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
|
|
5752
|
-
|
|
7196
|
+
Only one MCP plugin variant should be present. If both variants are in the Studio Plugins folder, Studio loads both and runtime routing can become unpredictable.
|
|
5753
7197
|
Re-run with --replace-variant to remove ${otherAssetName}, or delete it manually.
|
|
5754
7198
|
`);
|
|
5755
7199
|
}
|