@quenty/studio-bridge 0.22.1 → 0.24.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/CHANGELOG.md +25 -0
- package/README.md +36 -8
- package/dist/src/bridge/bridge-connection.d.ts.map +1 -1
- package/dist/src/bridge/bridge-connection.js +1 -0
- package/dist/src/bridge/bridge-connection.js.map +1 -1
- package/dist/src/bridge/bridge-session.d.ts +11 -16
- package/dist/src/bridge/bridge-session.d.ts.map +1 -1
- package/dist/src/bridge/bridge-session.js +37 -29
- package/dist/src/bridge/bridge-session.js.map +1 -1
- package/dist/src/bridge/bridge-session.test.js +35 -0
- package/dist/src/bridge/bridge-session.test.js.map +1 -1
- package/dist/src/bridge/internal/bridge-host.d.ts +2 -0
- package/dist/src/bridge/internal/bridge-host.d.ts.map +1 -1
- package/dist/src/bridge/internal/bridge-host.js +1 -0
- package/dist/src/bridge/internal/bridge-host.js.map +1 -1
- package/dist/src/bridge/internal/host-protocol.d.ts.map +1 -1
- package/dist/src/bridge/internal/host-protocol.js +23 -10
- package/dist/src/bridge/internal/host-protocol.js.map +1 -1
- package/dist/src/bridge/types.d.ts +5 -2
- package/dist/src/bridge/types.d.ts.map +1 -1
- package/dist/src/bridge/types.js.map +1 -1
- package/dist/src/commands/console/exec/exec.d.ts +13 -3
- package/dist/src/commands/console/exec/exec.d.ts.map +1 -1
- package/dist/src/commands/console/exec/exec.js +82 -7
- package/dist/src/commands/console/exec/exec.js.map +1 -1
- package/dist/src/commands/console/exec/exec.test.js +63 -1
- package/dist/src/commands/console/exec/exec.test.js.map +1 -1
- package/dist/src/commands/framework/action-loader.test.js +1 -1
- package/dist/src/commands/framework/action-loader.test.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.d.ts.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.js +15 -93
- package/dist/src/commands/playtest/player/playtest-players.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.test.js +34 -21
- package/dist/src/commands/playtest/player/playtest-players.test.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.d.ts +19 -3
- package/dist/src/commands/playtest/playtest-sessions.d.ts.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.js +54 -74
- package/dist/src/commands/playtest/playtest-sessions.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.test.js +74 -8
- package/dist/src/commands/playtest/playtest-sessions.test.js.map +1 -1
- package/dist/src/commands/playtest/run/run.d.ts +9 -7
- package/dist/src/commands/playtest/run/run.d.ts.map +1 -1
- package/dist/src/commands/playtest/run/run.js +33 -38
- package/dist/src/commands/playtest/run/run.js.map +1 -1
- package/dist/src/commands/process/close/close.d.ts +4 -0
- package/dist/src/commands/process/close/close.d.ts.map +1 -1
- package/dist/src/commands/process/close/close.js +54 -4
- package/dist/src/commands/process/close/close.js.map +1 -1
- package/dist/src/commands/process/close/close.test.js +36 -3
- package/dist/src/commands/process/close/close.test.js.map +1 -1
- package/dist/src/commands/process/info/info.d.ts +15 -2
- package/dist/src/commands/process/info/info.d.ts.map +1 -1
- package/dist/src/commands/process/info/info.js +44 -4
- package/dist/src/commands/process/info/info.js.map +1 -1
- package/dist/src/commands/process/info/info.test.js +48 -0
- package/dist/src/commands/process/info/info.test.js.map +1 -1
- package/dist/src/commands/process/launch/launch.d.ts.map +1 -1
- package/dist/src/commands/process/launch/launch.js +22 -3
- package/dist/src/commands/process/launch/launch.js.map +1 -1
- package/dist/src/commands/process/launch/launch.test.js +27 -5
- package/dist/src/commands/process/launch/launch.test.js.map +1 -1
- package/dist/src/commands/process/list/list.d.ts +49 -6
- package/dist/src/commands/process/list/list.d.ts.map +1 -1
- package/dist/src/commands/process/list/list.js +185 -18
- package/dist/src/commands/process/list/list.js.map +1 -1
- package/dist/src/commands/process/list/list.test.js +92 -1
- package/dist/src/commands/process/list/list.test.js.map +1 -1
- package/dist/src/commands/process/run/run.d.ts +1 -1
- package/dist/src/commands/process/run/run.d.ts.map +1 -1
- package/dist/src/commands/process/run/run.js +3 -3
- package/dist/src/commands/process/run/run.js.map +1 -1
- package/dist/src/docker/docker-delegator.js +1 -1
- package/dist/src/docker/docker-delegator.js.map +1 -1
- package/dist/src/docker/docker-delegator.test.js +1 -1
- package/dist/src/docker/docker-delegator.test.js.map +1 -1
- package/dist/src/plugin/playtest-bridge.d.ts +36 -0
- package/dist/src/plugin/playtest-bridge.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.js +65 -0
- package/dist/src/plugin/playtest-bridge.js.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts +2 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.js +35 -0
- package/dist/src/plugin/playtest-bridge.test.js.map +1 -0
- package/dist/src/process/studio-launch-registry.d.ts +14 -0
- package/dist/src/process/studio-launch-registry.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.js +37 -0
- package/dist/src/process/studio-launch-registry.js.map +1 -0
- package/dist/src/process/studio-launch-registry.test.d.ts +2 -0
- package/dist/src/process/studio-launch-registry.test.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.test.js +38 -0
- package/dist/src/process/studio-launch-registry.test.js.map +1 -0
- package/dist/src/process/studio-process-closer.d.ts +3 -1
- package/dist/src/process/studio-process-closer.d.ts.map +1 -1
- package/dist/src/process/studio-process-closer.js +29 -11
- package/dist/src/process/studio-process-closer.js.map +1 -1
- package/dist/src/process/studio-process-identity.d.ts +10 -0
- package/dist/src/process/studio-process-identity.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.js +63 -0
- package/dist/src/process/studio-process-identity.js.map +1 -0
- package/dist/src/process/studio-process-identity.test.d.ts +2 -0
- package/dist/src/process/studio-process-identity.test.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.test.js +30 -0
- package/dist/src/process/studio-process-identity.test.js.map +1 -0
- package/dist/src/roblox-mcp/index.d.ts +3 -0
- package/dist/src/roblox-mcp/index.d.ts.map +1 -0
- package/dist/src/roblox-mcp/index.js +3 -0
- package/dist/src/roblox-mcp/index.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts +60 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js +171 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js +104 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js +22 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js +42 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts +34 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js +190 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts +2 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js +123 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts +39 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.js +175 -0
- package/dist/src/roblox-mcp/studio/studio-session.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js +213 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts +19 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js +121 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js +107 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts +23 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js +65 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js +81 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts +27 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js +55 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js.map +1 -0
- package/dist/src/server/web-socket-protocol.d.ts +50 -15
- package/dist/src/server/web-socket-protocol.d.ts.map +1 -1
- package/dist/src/server/web-socket-protocol.js +63 -13
- package/dist/src/server/web-socket-protocol.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/playtest-bridge.md +21 -0
- package/package.json +5 -5
- package/src/bridge/bridge-connection.ts +1 -0
- package/src/bridge/bridge-session.test.ts +51 -0
- package/src/bridge/bridge-session.ts +66 -47
- package/src/bridge/internal/bridge-host.ts +3 -0
- package/src/bridge/internal/host-protocol.ts +22 -9
- package/src/bridge/types.ts +5 -0
- package/src/commands/console/exec/exec.test.ts +94 -0
- package/src/commands/console/exec/exec.ts +130 -16
- package/src/commands/console/exec/execute.lua +147 -5
- package/src/commands/console/exec/execute.spec.lua +22 -2
- package/src/commands/framework/action-loader.test.ts +1 -1
- package/src/commands/playtest/player/playtest-players.lua +127 -0
- package/src/commands/playtest/player/playtest-players.spec.lua +48 -0
- package/src/commands/playtest/player/playtest-players.test.ts +40 -21
- package/src/commands/playtest/player/playtest-players.ts +30 -110
- package/src/commands/playtest/playtest-sessions.test.ts +116 -14
- package/src/commands/playtest/playtest-sessions.ts +77 -88
- package/src/commands/playtest/run/run.ts +44 -58
- package/src/commands/playtest/start/start-playtest.lua +105 -6
- package/src/commands/process/close/close.test.ts +52 -3
- package/src/commands/process/close/close.ts +78 -4
- package/src/commands/process/info/info.test.ts +76 -0
- package/src/commands/process/info/info.ts +74 -6
- package/src/commands/process/launch/launch.test.ts +33 -5
- package/src/commands/process/launch/launch.ts +41 -3
- package/src/commands/process/list/list.test.ts +130 -1
- package/src/commands/process/list/list.ts +290 -22
- package/src/commands/process/run/run.ts +4 -4
- package/src/docker/docker-delegator.test.ts +1 -1
- package/src/docker/docker-delegator.ts +1 -1
- package/src/plugin/playtest-bridge.test.ts +53 -0
- package/src/plugin/playtest-bridge.ts +98 -0
- package/src/process/studio-launch-registry.test.ts +56 -0
- package/src/process/studio-launch-registry.ts +63 -0
- package/src/process/studio-process-closer.ts +36 -11
- package/src/process/studio-process-identity.test.ts +45 -0
- package/src/process/studio-process-identity.ts +90 -0
- package/src/roblox-mcp/index.ts +17 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.test.ts +148 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.ts +270 -0
- package/src/roblox-mcp/mcp/repair-list-result.test.ts +52 -0
- package/src/roblox-mcp/mcp/repair-list-result.ts +23 -0
- package/src/roblox-mcp/roblox-studio-mcp.test.ts +168 -0
- package/src/roblox-mcp/roblox-studio-mcp.ts +330 -0
- package/src/roblox-mcp/studio/studio-session.test.ts +284 -0
- package/src/roblox-mcp/studio/studio-session.ts +279 -0
- package/src/roblox-mcp/studio-mcp-locator.test.ts +186 -0
- package/src/roblox-mcp/studio-mcp-locator.ts +180 -0
- package/src/roblox-mcp/studio-mcp-snapshot.test.ts +113 -0
- package/src/roblox-mcp/studio-mcp-snapshot.ts +104 -0
- package/src/roblox-mcp/test/fake-mcp-server.ts +74 -0
- package/src/server/web-socket-protocol.ts +119 -29
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.client.lua +32 -0
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.server.lua +40 -0
- package/templates/studio-bridge-plugin/src/Shared/ActionRouter.lua +25 -3
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.lua +47 -0
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.spec.lua +57 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeClient.lua +357 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.lua +29 -3
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.spec.lua +35 -2
- package/templates/studio-bridge-plugin/src/StudioBridgePlugin.server.lua +16 -292
- package/src/commands/playtest/run/playtest-runner.luau +0 -264
- package/src/commands/playtest/run/playtest-runner.spec.lua +0 -71
- package/src/commands/playtest/run/run-playtest-script.lua +0 -139
- package/src/commands/playtest/run/run-playtest-script.spec.lua +0 -43
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getStudioMcpSnapshotAsync,
|
|
4
|
+
resetStudioMcpSnapshot,
|
|
5
|
+
} from './studio-mcp-snapshot.js';
|
|
6
|
+
import type { StudioMcpResult, StudioMcpTarget } from './roblox-studio-mcp.js';
|
|
7
|
+
|
|
8
|
+
type ListResult = StudioMcpResult<StudioMcpTarget[]>;
|
|
9
|
+
|
|
10
|
+
const OK: ListResult = {
|
|
11
|
+
success: true,
|
|
12
|
+
value: [{ studioId: 'mcp-1', placeName: 'Place1' }],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('getStudioMcpSnapshotAsync', () => {
|
|
16
|
+
beforeEach(() => resetStudioMcpSnapshot());
|
|
17
|
+
afterEach(() => vi.useRealTimers());
|
|
18
|
+
|
|
19
|
+
it('waits for the first check and reports the Studios it found', async () => {
|
|
20
|
+
const listAsync = vi.fn(async () => OK);
|
|
21
|
+
|
|
22
|
+
const snapshot = await getStudioMcpSnapshotAsync({ listAsync });
|
|
23
|
+
|
|
24
|
+
expect(snapshot).toMatchObject({
|
|
25
|
+
status: 'ok',
|
|
26
|
+
studios: [{ studioId: 'mcp-1' }],
|
|
27
|
+
});
|
|
28
|
+
expect(listAsync).toHaveBeenCalledWith(
|
|
29
|
+
expect.objectContaining({ readPlaceIds: false, timeoutMs: 3000 })
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('reuses a fresh snapshot without checking again', async () => {
|
|
34
|
+
const listAsync = vi.fn(async () => OK);
|
|
35
|
+
|
|
36
|
+
await getStudioMcpSnapshotAsync({ listAsync });
|
|
37
|
+
await getStudioMcpSnapshotAsync({ listAsync });
|
|
38
|
+
|
|
39
|
+
expect(listAsync).toHaveBeenCalledTimes(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('answers with the stale snapshot at once and refreshes in the background', async () => {
|
|
43
|
+
let resolveSecond: (result: ListResult) => void = () => {};
|
|
44
|
+
const listAsync = vi
|
|
45
|
+
.fn<() => Promise<ListResult>>()
|
|
46
|
+
.mockResolvedValueOnce(OK)
|
|
47
|
+
.mockImplementationOnce(
|
|
48
|
+
() => new Promise((resolve) => (resolveSecond = resolve))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const first = await getStudioMcpSnapshotAsync({ listAsync, maxAgeMs: 0 });
|
|
52
|
+
const second = await getStudioMcpSnapshotAsync({ listAsync, maxAgeMs: 0 });
|
|
53
|
+
|
|
54
|
+
expect(second).toBe(first);
|
|
55
|
+
expect(listAsync).toHaveBeenCalledTimes(2);
|
|
56
|
+
resolveSecond({ success: false, reason: 'timeout', message: 'slow' });
|
|
57
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
58
|
+
const third = await getStudioMcpSnapshotAsync({ listAsync });
|
|
59
|
+
expect(third).toMatchObject({
|
|
60
|
+
status: 'timeout',
|
|
61
|
+
message: 'slow',
|
|
62
|
+
lastOkAt: first?.checkedAt,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('runs one check at a time', async () => {
|
|
67
|
+
let finish: (result: ListResult) => void = () => {};
|
|
68
|
+
const listAsync = vi.fn(
|
|
69
|
+
() => new Promise<ListResult>((resolve) => (finish = resolve))
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const both = Promise.all([
|
|
73
|
+
getStudioMcpSnapshotAsync({ listAsync }),
|
|
74
|
+
getStudioMcpSnapshotAsync({ listAsync }),
|
|
75
|
+
]);
|
|
76
|
+
finish(OK);
|
|
77
|
+
await both;
|
|
78
|
+
|
|
79
|
+
expect(listAsync).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('gives up waiting rather than hang when the first check does not finish', async () => {
|
|
83
|
+
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
|
84
|
+
const listAsync = vi.fn(() => new Promise<ListResult>(() => {}));
|
|
85
|
+
let settled = false;
|
|
86
|
+
|
|
87
|
+
const pending = getStudioMcpSnapshotAsync({ listAsync }).then((s) => {
|
|
88
|
+
settled = true;
|
|
89
|
+
return s;
|
|
90
|
+
});
|
|
91
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
92
|
+
expect(settled).toBe(false);
|
|
93
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
94
|
+
|
|
95
|
+
expect(await pending).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('records a failure with its reason', async () => {
|
|
99
|
+
const listAsync = vi.fn(
|
|
100
|
+
async (): Promise<ListResult> => ({
|
|
101
|
+
success: false,
|
|
102
|
+
reason: 'unavailable',
|
|
103
|
+
message: 'not installed',
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(await getStudioMcpSnapshotAsync({ listAsync })).toMatchObject({
|
|
108
|
+
status: 'unavailable',
|
|
109
|
+
message: 'not installed',
|
|
110
|
+
studios: [],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { SessionInfo } from '../bridge/types.js';
|
|
2
|
+
import type { StudioWindow } from './studio/studio-session.js';
|
|
3
|
+
import {
|
|
4
|
+
tryListStudiosAsync,
|
|
5
|
+
type StudioMcpFailure,
|
|
6
|
+
type StudioMcpOptions,
|
|
7
|
+
type StudioMcpResult,
|
|
8
|
+
type StudioMcpTarget,
|
|
9
|
+
} from './roblox-studio-mcp.js';
|
|
10
|
+
|
|
11
|
+
export interface StudioMcpSnapshot {
|
|
12
|
+
status: 'ok' | StudioMcpFailure;
|
|
13
|
+
studios: StudioMcpTarget[];
|
|
14
|
+
message?: string;
|
|
15
|
+
checkedAt: number;
|
|
16
|
+
latencyMs: number;
|
|
17
|
+
lastOkAt?: number;
|
|
18
|
+
settled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StudioMcpSnapshotOptions {
|
|
22
|
+
sessions?: SessionInfo[];
|
|
23
|
+
sessionPids?: Map<string, number>;
|
|
24
|
+
windows?: StudioWindow[];
|
|
25
|
+
maxWaitMs?: number;
|
|
26
|
+
maxAgeMs?: number;
|
|
27
|
+
listAsync?: (
|
|
28
|
+
options: StudioMcpOptions
|
|
29
|
+
) => Promise<StudioMcpResult<StudioMcpTarget[]>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_MAX_WAIT_MS = 3000;
|
|
33
|
+
const DEFAULT_MAX_AGE_MS = 5000;
|
|
34
|
+
|
|
35
|
+
let _cached: StudioMcpSnapshot | undefined;
|
|
36
|
+
let _inFlight: Promise<StudioMcpSnapshot> | undefined;
|
|
37
|
+
|
|
38
|
+
export async function getStudioMcpSnapshotAsync(
|
|
39
|
+
options: StudioMcpSnapshotOptions = {}
|
|
40
|
+
): Promise<StudioMcpSnapshot | undefined> {
|
|
41
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
42
|
+
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
43
|
+
if (_cached && Date.now() - _cached.checkedAt < maxAgeMs) {
|
|
44
|
+
return _cached;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_inFlight ??= _refreshAsync(options, maxWaitMs).finally(() => {
|
|
48
|
+
_inFlight = undefined;
|
|
49
|
+
});
|
|
50
|
+
if (_cached) {
|
|
51
|
+
return _cached;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let timer: NodeJS.Timeout | undefined;
|
|
55
|
+
const gaveUp = new Promise<undefined>((resolve) => {
|
|
56
|
+
timer = setTimeout(() => resolve(undefined), maxWaitMs + 250);
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
return await Promise.race([_inFlight, gaveUp]);
|
|
60
|
+
} finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resetStudioMcpSnapshot(): void {
|
|
66
|
+
_cached = undefined;
|
|
67
|
+
_inFlight = undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function _refreshAsync(
|
|
71
|
+
options: StudioMcpSnapshotOptions,
|
|
72
|
+
timeoutMs: number
|
|
73
|
+
): Promise<StudioMcpSnapshot> {
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
const result = await (options.listAsync ?? tryListStudiosAsync)({
|
|
76
|
+
sessions: options.sessions,
|
|
77
|
+
sessionPids: options.sessionPids,
|
|
78
|
+
...(options.windows
|
|
79
|
+
? { listWindowsAsync: async () => options.windows! }
|
|
80
|
+
: {}),
|
|
81
|
+
timeoutMs,
|
|
82
|
+
readPlaceIds: false,
|
|
83
|
+
});
|
|
84
|
+
const checkedAt = Date.now();
|
|
85
|
+
const snapshot: StudioMcpSnapshot = result.success
|
|
86
|
+
? {
|
|
87
|
+
status: 'ok',
|
|
88
|
+
studios: result.value,
|
|
89
|
+
settled: result.settled !== false,
|
|
90
|
+
checkedAt,
|
|
91
|
+
latencyMs: checkedAt - startedAt,
|
|
92
|
+
lastOkAt: checkedAt,
|
|
93
|
+
}
|
|
94
|
+
: {
|
|
95
|
+
status: result.reason,
|
|
96
|
+
studios: [],
|
|
97
|
+
message: result.message,
|
|
98
|
+
checkedAt,
|
|
99
|
+
latencyMs: checkedAt - startedAt,
|
|
100
|
+
lastOkAt: _cached?.lastOkAt,
|
|
101
|
+
};
|
|
102
|
+
_cached = snapshot;
|
|
103
|
+
return snapshot;
|
|
104
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { PassThrough } from 'stream';
|
|
3
|
+
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export type FakeHandler = (
|
|
6
|
+
method: string,
|
|
7
|
+
params: any
|
|
8
|
+
) =>
|
|
9
|
+
| { result?: unknown; error?: { code: number; message: string } }
|
|
10
|
+
| undefined;
|
|
11
|
+
|
|
12
|
+
export interface FakeMcpServer {
|
|
13
|
+
child: ChildProcessWithoutNullStreams;
|
|
14
|
+
requests: Array<{ method: string; params: any }>;
|
|
15
|
+
exit(code: number, stderr?: string): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createFakeMcpServer(handler: FakeHandler): FakeMcpServer {
|
|
19
|
+
const stdin = new PassThrough();
|
|
20
|
+
const stdout = new PassThrough();
|
|
21
|
+
const stderr = new PassThrough();
|
|
22
|
+
const child = Object.assign(new EventEmitter(), {
|
|
23
|
+
stdin,
|
|
24
|
+
stdout,
|
|
25
|
+
stderr,
|
|
26
|
+
exitCode: null as number | null,
|
|
27
|
+
killed: false,
|
|
28
|
+
kill() {
|
|
29
|
+
child.killed = true;
|
|
30
|
+
child.emit('close', null);
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const requests: FakeMcpServer['requests'] = [];
|
|
35
|
+
|
|
36
|
+
let buffer = '';
|
|
37
|
+
stdin.setEncoding('utf8');
|
|
38
|
+
stdin.on('data', (chunk: string) => {
|
|
39
|
+
buffer += chunk;
|
|
40
|
+
let newline: number;
|
|
41
|
+
while ((newline = buffer.indexOf('\n')) >= 0) {
|
|
42
|
+
const message = JSON.parse(buffer.slice(0, newline));
|
|
43
|
+
buffer = buffer.slice(newline + 1);
|
|
44
|
+
requests.push({ method: message.method, params: message.params });
|
|
45
|
+
if (message.id === undefined) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const reply = handler(message.method, message.params);
|
|
49
|
+
if (reply) {
|
|
50
|
+
stdout.write(
|
|
51
|
+
JSON.stringify({ jsonrpc: '2.0', id: message.id, ...reply }) + '\n'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
child: child as unknown as ChildProcessWithoutNullStreams,
|
|
59
|
+
requests,
|
|
60
|
+
exit(code, text) {
|
|
61
|
+
if (text) {
|
|
62
|
+
stderr.write(text);
|
|
63
|
+
}
|
|
64
|
+
setImmediate(() => {
|
|
65
|
+
child.exitCode = code;
|
|
66
|
+
child.emit('close', code);
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function toolText(text: string, isError = false) {
|
|
73
|
+
return { result: { content: [{ type: 'text', text }], isError } };
|
|
74
|
+
}
|
|
@@ -23,6 +23,8 @@ export type PluginContext = 'edit' | 'client' | 'server';
|
|
|
23
23
|
*/
|
|
24
24
|
export const MAX_MESSAGE_BYTES = 512 * 1024 * 1024;
|
|
25
25
|
|
|
26
|
+
export const MAX_EXECUTE_INPUT_BYTES = 192 * 1024 * 1024;
|
|
27
|
+
|
|
26
28
|
export type Capability =
|
|
27
29
|
| 'execute'
|
|
28
30
|
| 'queryState'
|
|
@@ -35,6 +37,7 @@ export type Capability =
|
|
|
35
37
|
| 'grepScripts'
|
|
36
38
|
| 'startPlaytest'
|
|
37
39
|
| 'endPlaytest'
|
|
40
|
+
| 'playtestPlayers'
|
|
38
41
|
| 'heartbeat'
|
|
39
42
|
| 'registerAction'
|
|
40
43
|
| 'syncActions';
|
|
@@ -142,6 +145,7 @@ export interface ScriptCompleteMessage extends BaseMessage {
|
|
|
142
145
|
* which means the script ran and returned nothing.
|
|
143
146
|
*/
|
|
144
147
|
returnValues?: SerializedReturnValue[];
|
|
148
|
+
ranAs?: RunAs;
|
|
145
149
|
};
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -160,6 +164,8 @@ export interface RegisterMessage extends PushMessage {
|
|
|
160
164
|
launchId?: string;
|
|
161
165
|
/** The player of a multiplayer test client. */
|
|
162
166
|
playerName?: string;
|
|
167
|
+
/** The playtest studio-bridge started this session's DataModel for. */
|
|
168
|
+
playtestId?: string;
|
|
163
169
|
capabilities: Capability[];
|
|
164
170
|
};
|
|
165
171
|
}
|
|
@@ -320,6 +326,22 @@ export interface EndPlaytestResultMessage extends RequestMessage {
|
|
|
320
326
|
};
|
|
321
327
|
}
|
|
322
328
|
|
|
329
|
+
export interface PlaytestPlayerPayload {
|
|
330
|
+
name: string;
|
|
331
|
+
userId: number;
|
|
332
|
+
displayName: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface PlaytestPlayersResultMessage extends RequestMessage {
|
|
336
|
+
type: 'playtestPlayersResult';
|
|
337
|
+
payload: {
|
|
338
|
+
/** The players `list` found or `add` added. */
|
|
339
|
+
players?: PlaytestPlayerPayload[];
|
|
340
|
+
/** The players `remove` waited for that had not left by its timeout. */
|
|
341
|
+
remaining?: string[];
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
323
345
|
export interface PluginErrorMessage extends BaseMessage {
|
|
324
346
|
type: 'error';
|
|
325
347
|
requestId?: string;
|
|
@@ -347,6 +369,7 @@ export type PluginMessage =
|
|
|
347
369
|
| SyncActionsResultMessage
|
|
348
370
|
| PlaytestEndedMessage
|
|
349
371
|
| EndPlaytestResultMessage
|
|
372
|
+
| PlaytestPlayersResultMessage
|
|
350
373
|
| PluginErrorMessage;
|
|
351
374
|
|
|
352
375
|
export interface ExecuteMessage extends BaseMessage {
|
|
@@ -358,9 +381,14 @@ export interface ExecuteMessage extends BaseMessage {
|
|
|
358
381
|
timeoutMs?: number;
|
|
359
382
|
/** Passed to the script as `...`. */
|
|
360
383
|
args?: string[];
|
|
384
|
+
/** Base64 files, passed to the script as buffers in `...` before `args`. */
|
|
385
|
+
inputs?: string[];
|
|
386
|
+
runAs?: RunAs;
|
|
361
387
|
};
|
|
362
388
|
}
|
|
363
389
|
|
|
390
|
+
export type RunAs = 'plugin' | 'game' | 'mcp';
|
|
391
|
+
|
|
364
392
|
export interface ShutdownMessage extends PushMessage {
|
|
365
393
|
type: 'shutdown';
|
|
366
394
|
payload: Record<string, never>;
|
|
@@ -478,26 +506,32 @@ export interface StartPlaytestMessage extends RequestMessage {
|
|
|
478
506
|
instanceId: string;
|
|
479
507
|
/** Starts a multiplayer test, a server and a client process per player. */
|
|
480
508
|
players?: number;
|
|
509
|
+
/** Plants a studio-bridge client as the playtest's server session. */
|
|
510
|
+
bridge?: PlaytestBridgePayload;
|
|
481
511
|
};
|
|
482
512
|
}
|
|
483
513
|
|
|
514
|
+
/** The studio-bridge client a `startPlaytest` plants, see playtest-bridge.ts. */
|
|
515
|
+
export interface PlaytestBridgePayload {
|
|
516
|
+
entrySource: string;
|
|
517
|
+
modules: Record<string, string>;
|
|
518
|
+
precompiled: string[];
|
|
519
|
+
}
|
|
520
|
+
|
|
484
521
|
export interface EndPlaytestMessage extends RequestMessage {
|
|
485
522
|
type: 'endPlaytest';
|
|
486
523
|
payload: Record<string, never>;
|
|
487
524
|
}
|
|
488
525
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
/** Passed to the script as `...`. */
|
|
499
|
-
args?: string[];
|
|
500
|
-
};
|
|
526
|
+
export type PlaytestPlayersRequest =
|
|
527
|
+
| { op: 'list' }
|
|
528
|
+
| { op: 'add'; count: number; timeoutSec: number }
|
|
529
|
+
| { op: 'remove'; kick: string[]; names: string[]; timeoutSec: number }
|
|
530
|
+
| { op: 'leave' };
|
|
531
|
+
|
|
532
|
+
export interface PlaytestPlayersMessage extends RequestMessage {
|
|
533
|
+
type: 'playtestPlayers';
|
|
534
|
+
payload: PlaytestPlayersRequest;
|
|
501
535
|
}
|
|
502
536
|
|
|
503
537
|
export interface ServerErrorMessage extends BaseMessage {
|
|
@@ -525,7 +559,7 @@ export type ServerMessage =
|
|
|
525
559
|
| SyncActionsMessage
|
|
526
560
|
| StartPlaytestMessage
|
|
527
561
|
| EndPlaytestMessage
|
|
528
|
-
|
|
|
562
|
+
| PlaytestPlayersMessage
|
|
529
563
|
| ServerErrorMessage;
|
|
530
564
|
|
|
531
565
|
export function encodeMessage(msg: ServerMessage): string {
|
|
@@ -616,6 +650,11 @@ export function decodePluginMessage(raw: string): PluginMessage | null {
|
|
|
616
650
|
: undefined,
|
|
617
651
|
output,
|
|
618
652
|
returnValues,
|
|
653
|
+
...(payload.ranAs === 'plugin' ||
|
|
654
|
+
payload.ranAs === 'game' ||
|
|
655
|
+
payload.ranAs === 'mcp'
|
|
656
|
+
? { ranAs: payload.ranAs }
|
|
657
|
+
: {}),
|
|
619
658
|
},
|
|
620
659
|
};
|
|
621
660
|
}
|
|
@@ -657,6 +696,10 @@ export function decodePluginMessage(raw: string): PluginMessage | null {
|
|
|
657
696
|
typeof payload.playerName === 'string'
|
|
658
697
|
? payload.playerName
|
|
659
698
|
: undefined,
|
|
699
|
+
playtestId:
|
|
700
|
+
typeof payload.playtestId === 'string'
|
|
701
|
+
? payload.playtestId
|
|
702
|
+
: undefined,
|
|
660
703
|
capabilities: payload.capabilities as Capability[],
|
|
661
704
|
},
|
|
662
705
|
};
|
|
@@ -906,6 +949,22 @@ export function decodePluginMessage(raw: string): PluginMessage | null {
|
|
|
906
949
|
},
|
|
907
950
|
};
|
|
908
951
|
|
|
952
|
+
case 'playtestPlayersResult':
|
|
953
|
+
if (requestId === undefined) return null;
|
|
954
|
+
return {
|
|
955
|
+
type: 'playtestPlayersResult',
|
|
956
|
+
sessionId,
|
|
957
|
+
requestId,
|
|
958
|
+
payload: {
|
|
959
|
+
players: Array.isArray(payload.players)
|
|
960
|
+
? (payload.players as PlaytestPlayerPayload[])
|
|
961
|
+
: undefined,
|
|
962
|
+
remaining: isStringArray(payload.remaining)
|
|
963
|
+
? payload.remaining
|
|
964
|
+
: undefined,
|
|
965
|
+
},
|
|
966
|
+
};
|
|
967
|
+
|
|
909
968
|
case 'error':
|
|
910
969
|
if (
|
|
911
970
|
typeof payload.code !== 'string' ||
|
|
@@ -974,6 +1033,12 @@ export function decodeServerMessage(raw: string): ServerMessage | null {
|
|
|
974
1033
|
? { timeoutMs: payload.timeoutMs }
|
|
975
1034
|
: {}),
|
|
976
1035
|
...(isStringArray(payload.args) ? { args: payload.args } : {}),
|
|
1036
|
+
...(isStringArray(payload.inputs)
|
|
1037
|
+
? { inputs: payload.inputs }
|
|
1038
|
+
: {}),
|
|
1039
|
+
...(payload.runAs === 'plugin' || payload.runAs === 'game'
|
|
1040
|
+
? { runAs: payload.runAs }
|
|
1041
|
+
: {}),
|
|
977
1042
|
},
|
|
978
1043
|
};
|
|
979
1044
|
}
|
|
@@ -1185,6 +1250,9 @@ export function decodeServerMessage(raw: string): ServerMessage | null {
|
|
|
1185
1250
|
...(typeof payload.players === 'number'
|
|
1186
1251
|
? { players: payload.players }
|
|
1187
1252
|
: {}),
|
|
1253
|
+
...(isPlaytestBridge(payload.bridge)
|
|
1254
|
+
? { bridge: payload.bridge }
|
|
1255
|
+
: {}),
|
|
1188
1256
|
},
|
|
1189
1257
|
};
|
|
1190
1258
|
|
|
@@ -1197,26 +1265,14 @@ export function decodeServerMessage(raw: string): ServerMessage | null {
|
|
|
1197
1265
|
payload: {} as Record<string, never>,
|
|
1198
1266
|
};
|
|
1199
1267
|
|
|
1200
|
-
case '
|
|
1268
|
+
case 'playtestPlayers':
|
|
1201
1269
|
if (requestId === undefined) return null;
|
|
1202
|
-
if (
|
|
1203
|
-
(payload.mode !== 'play' && payload.mode !== 'run') ||
|
|
1204
|
-
typeof payload.script !== 'string' ||
|
|
1205
|
-
typeof payload.runnerSource !== 'string' ||
|
|
1206
|
-
typeof payload.timeoutSec !== 'number'
|
|
1207
|
-
)
|
|
1208
|
-
return null;
|
|
1270
|
+
if (!isPlaytestPlayersRequest(payload)) return null;
|
|
1209
1271
|
return {
|
|
1210
|
-
type: '
|
|
1272
|
+
type: 'playtestPlayers',
|
|
1211
1273
|
sessionId,
|
|
1212
1274
|
requestId,
|
|
1213
|
-
payload
|
|
1214
|
-
mode: payload.mode,
|
|
1215
|
-
script: payload.script,
|
|
1216
|
-
runnerSource: payload.runnerSource,
|
|
1217
|
-
timeoutSec: payload.timeoutSec,
|
|
1218
|
-
...(isStringArray(payload.args) ? { args: payload.args } : {}),
|
|
1219
|
-
},
|
|
1275
|
+
payload,
|
|
1220
1276
|
};
|
|
1221
1277
|
|
|
1222
1278
|
case 'error':
|
|
@@ -1240,3 +1296,37 @@ export function decodeServerMessage(raw: string): ServerMessage | null {
|
|
|
1240
1296
|
return null;
|
|
1241
1297
|
}
|
|
1242
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
function isPlaytestBridge(value: unknown): value is PlaytestBridgePayload {
|
|
1301
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
1302
|
+
const bridge = value as Record<string, unknown>;
|
|
1303
|
+
return (
|
|
1304
|
+
typeof bridge.entrySource === 'string' &&
|
|
1305
|
+
typeof bridge.modules === 'object' &&
|
|
1306
|
+
bridge.modules !== null &&
|
|
1307
|
+
Object.values(bridge.modules).every((v) => typeof v === 'string') &&
|
|
1308
|
+
isStringArray(bridge.precompiled)
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function isPlaytestPlayersRequest(
|
|
1313
|
+
value: Record<string, unknown>
|
|
1314
|
+
): value is PlaytestPlayersRequest {
|
|
1315
|
+
switch (value.op) {
|
|
1316
|
+
case 'list':
|
|
1317
|
+
case 'leave':
|
|
1318
|
+
return true;
|
|
1319
|
+
case 'add':
|
|
1320
|
+
return (
|
|
1321
|
+
typeof value.count === 'number' && typeof value.timeoutSec === 'number'
|
|
1322
|
+
);
|
|
1323
|
+
case 'remove':
|
|
1324
|
+
return (
|
|
1325
|
+
isStringArray(value.kick) &&
|
|
1326
|
+
isStringArray(value.names) &&
|
|
1327
|
+
typeof value.timeoutSec === 'number'
|
|
1328
|
+
);
|
|
1329
|
+
default:
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Entry point for the bridge planted into a playtest's client, the client twin
|
|
4
|
+
of StudioBridgePlaytest.server.lua: an ordinary LocalScript, so a play mode
|
|
5
|
+
playtest has a client session without Studio loading user plugins into it.
|
|
6
|
+
It only starts in the playtest it was planted for.
|
|
7
|
+
]]
|
|
8
|
+
|
|
9
|
+
local RunService = game:GetService("RunService")
|
|
10
|
+
|
|
11
|
+
local PrecompiledSources = require(script.Shared.PrecompiledSources)
|
|
12
|
+
local StudioBridgeClient = require(script.Shared.StudioBridgeClient)
|
|
13
|
+
local StudioBridgeSessionUtils = require(script.Shared.StudioBridgeSessionUtils)
|
|
14
|
+
|
|
15
|
+
if not (RunService:IsRunning() and RunService:IsClient()) then
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
local ok, testArgs = pcall(function()
|
|
20
|
+
return (game:GetService("StudioTestService") :: any):GetTestArgs()
|
|
21
|
+
end)
|
|
22
|
+
local playtestArgs = if ok then StudioBridgeSessionUtils.getPlaytestArgs(testArgs) else nil
|
|
23
|
+
if not playtestArgs or script:GetAttribute("PlaytestId") ~= playtestArgs.playtestId then
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
local stop = StudioBridgeClient.start({
|
|
28
|
+
playtestArgs = playtestArgs,
|
|
29
|
+
compile = PrecompiledSources.createCompiler(script.Precompiled),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
script.Destroying:Connect(stop)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Entry point for the bridge planted into a playtest's server, so the playtest
|
|
4
|
+
has a server session without Studio loading user plugins into playtests.
|
|
5
|
+
|
|
6
|
+
The `startPlaytest` action plants this as a Script in the edit DataModel's
|
|
7
|
+
ServerScriptService, with the plugin's Shared modules and the sources
|
|
8
|
+
compiled for it ahead of time under it. The playtest copies the edit
|
|
9
|
+
DataModel, so it starts in the playtest's server as an ordinary Script. It
|
|
10
|
+
only starts in the playtest it was planted for: anywhere else, such as a
|
|
11
|
+
place saved while it was planted, it does nothing.
|
|
12
|
+
|
|
13
|
+
Built from the plugin template, so like the plugin it cannot use the
|
|
14
|
+
Nevermore loader.
|
|
15
|
+
]]
|
|
16
|
+
|
|
17
|
+
local RunService = game:GetService("RunService")
|
|
18
|
+
|
|
19
|
+
local PrecompiledSources = require(script.Shared.PrecompiledSources)
|
|
20
|
+
local StudioBridgeClient = require(script.Shared.StudioBridgeClient)
|
|
21
|
+
local StudioBridgeSessionUtils = require(script.Shared.StudioBridgeSessionUtils)
|
|
22
|
+
|
|
23
|
+
if not (RunService:IsRunning() and RunService:IsServer()) then
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
local ok, testArgs = pcall(function()
|
|
28
|
+
return (game:GetService("StudioTestService") :: any):GetTestArgs()
|
|
29
|
+
end)
|
|
30
|
+
local playtestArgs = if ok then StudioBridgeSessionUtils.getPlaytestArgs(testArgs) else nil
|
|
31
|
+
if not playtestArgs or StudioBridgeSessionUtils.findPlantedBridge(script.Parent, playtestArgs.playtestId) ~= script then
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
local stop = StudioBridgeClient.start({
|
|
36
|
+
playtestArgs = playtestArgs,
|
|
37
|
+
compile = PrecompiledSources.createCompiler(script.Precompiled),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
script.Destroying:Connect(stop)
|
|
@@ -30,6 +30,13 @@ export type Handler = (payload: { [string]: any }, requestId: string, sessionId:
|
|
|
30
30
|
|
|
31
31
|
export type SendMessage = (message: { [string]: any }) -> ()
|
|
32
32
|
|
|
33
|
+
--[=[
|
|
34
|
+
Compiles Luau source into a function, or returns nil and why it could not.
|
|
35
|
+
@type Compiler (source: string, chunkName: string?) -> (any, string?)
|
|
36
|
+
@within ActionRouter
|
|
37
|
+
]=]
|
|
38
|
+
export type Compiler = (source: string, chunkName: string?) -> (any, string?)
|
|
39
|
+
|
|
33
40
|
export type ActionInfo = {
|
|
34
41
|
module: any,
|
|
35
42
|
hash: string,
|
|
@@ -42,6 +49,7 @@ export type ActionRouter = typeof(setmetatable(
|
|
|
42
49
|
_responseTypes: { [string]: string },
|
|
43
50
|
_actions: { [string]: ActionInfo },
|
|
44
51
|
_vendorPng: any?,
|
|
52
|
+
_compiler: Compiler?,
|
|
45
53
|
},
|
|
46
54
|
{} :: typeof({ __index = ActionRouter })
|
|
47
55
|
))
|
|
@@ -58,6 +66,7 @@ function ActionRouter.new(): ActionRouter
|
|
|
58
66
|
|
|
59
67
|
-- The screenshot action reads its PNG encoder off this field
|
|
60
68
|
self._vendorPng = nil
|
|
69
|
+
self._compiler = nil
|
|
61
70
|
|
|
62
71
|
return self
|
|
63
72
|
end
|
|
@@ -83,6 +92,15 @@ function ActionRouter.SetVendorPng(self: ActionRouter, png: any)
|
|
|
83
92
|
self._vendorPng = png
|
|
84
93
|
end
|
|
85
94
|
|
|
95
|
+
--[=[
|
|
96
|
+
Sets how source compiles where `loadstring` is off, in place of compiling
|
|
97
|
+
through a ModuleScript. The bridge planted into a playtest cannot set a
|
|
98
|
+
ModuleScript's source, so it looks up sources compiled for it ahead of time.
|
|
99
|
+
]=]
|
|
100
|
+
function ActionRouter.SetCompiler(self: ActionRouter, compiler: Compiler)
|
|
101
|
+
self._compiler = compiler
|
|
102
|
+
end
|
|
103
|
+
|
|
86
104
|
--[=[
|
|
87
105
|
Dispatches a message to its handler and returns the response message, if any.
|
|
88
106
|
]=]
|
|
@@ -130,15 +148,19 @@ end
|
|
|
130
148
|
|
|
131
149
|
--[=[
|
|
132
150
|
Compiles Luau source into a function, like `loadstring`. A playtest's client
|
|
133
|
-
has no `loadstring`, so there the source compiles through
|
|
134
|
-
|
|
151
|
+
has no `loadstring`, so there the source compiles through the compiler set
|
|
152
|
+
with `SetCompiler`, or else a ModuleScript. Returns nil and the compile error
|
|
153
|
+
when the source does not compile.
|
|
135
154
|
]=]
|
|
136
|
-
function ActionRouter.Compile(
|
|
155
|
+
function ActionRouter.Compile(self: ActionRouter, source: string, chunkName: string?): (any, string?)
|
|
137
156
|
local loadstringOk, fn, compileErr = pcall(loadstring :: any, source, chunkName)
|
|
138
157
|
if loadstringOk then
|
|
139
158
|
return fn, compileErr
|
|
140
159
|
end
|
|
141
160
|
|
|
161
|
+
if self._compiler then
|
|
162
|
+
return self._compiler(source, chunkName)
|
|
163
|
+
end
|
|
142
164
|
return ActionRouter._compileWithModuleScript(source, chunkName)
|
|
143
165
|
end
|
|
144
166
|
|