@quenty/studio-bridge 0.23.0 → 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 +21 -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 +2 -2
- 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
|
@@ -5,20 +5,23 @@ import {
|
|
|
5
5
|
removePlaytestPlayersAsync,
|
|
6
6
|
} from './playtest-players.js';
|
|
7
7
|
|
|
8
|
+
type Reply = { players?: unknown[]; remaining?: string[] };
|
|
9
|
+
|
|
8
10
|
function createSession(
|
|
9
11
|
sessionId: string,
|
|
10
12
|
context: string,
|
|
11
|
-
|
|
13
|
+
replies: Reply[],
|
|
12
14
|
playerName?: string
|
|
13
15
|
) {
|
|
14
|
-
const
|
|
16
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
15
17
|
return {
|
|
16
|
-
|
|
18
|
+
requests,
|
|
17
19
|
info: { sessionId, instanceId: 'place-1a2b', context, playerName },
|
|
18
20
|
context,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
playtestPlayersAsync: vi.fn(async (request: Record<string, unknown>) => {
|
|
22
|
+
requests.push(request);
|
|
23
|
+
const reply = replies.shift() ?? {};
|
|
24
|
+
return { players: reply.players ?? [], remaining: reply.remaining ?? [] };
|
|
22
25
|
}),
|
|
23
26
|
};
|
|
24
27
|
}
|
|
@@ -39,7 +42,7 @@ const player = (name: string, userId: number) => ({
|
|
|
39
42
|
describe('listPlaytestPlayersAsync', () => {
|
|
40
43
|
it('pairs each player with its client session', async () => {
|
|
41
44
|
const server = createSession('place-1a2b-server', 'server', [
|
|
42
|
-
[
|
|
45
|
+
{ players: [player('Player1', -1), player('Player2', -2)] },
|
|
43
46
|
]);
|
|
44
47
|
const client = createSession(
|
|
45
48
|
'place-1a2b-client-player1',
|
|
@@ -53,6 +56,7 @@ describe('listPlaytestPlayersAsync', () => {
|
|
|
53
56
|
server as any
|
|
54
57
|
);
|
|
55
58
|
|
|
59
|
+
expect(server.requests).toEqual([{ op: 'list' }]);
|
|
56
60
|
expect(players).toEqual([
|
|
57
61
|
{ ...player('Player1', -1), sessionId: 'place-1a2b-client-player1' },
|
|
58
62
|
player('Player2', -2),
|
|
@@ -63,7 +67,7 @@ describe('listPlaytestPlayersAsync', () => {
|
|
|
63
67
|
describe('addPlaytestPlayersAsync', () => {
|
|
64
68
|
it('adds players from the server and reports the ones that joined', async () => {
|
|
65
69
|
const server = createSession('place-1a2b-server', 'server', [
|
|
66
|
-
[
|
|
70
|
+
{ players: [player('Player3', -3)] },
|
|
67
71
|
]);
|
|
68
72
|
const client = createSession(
|
|
69
73
|
'place-1a2b-client-player3',
|
|
@@ -78,7 +82,7 @@ describe('addPlaytestPlayersAsync', () => {
|
|
|
78
82
|
1
|
|
79
83
|
);
|
|
80
84
|
|
|
81
|
-
expect(server.
|
|
85
|
+
expect(server.requests).toEqual([{ op: 'add', count: 1, timeoutSec: 60 }]);
|
|
82
86
|
expect(players).toEqual([
|
|
83
87
|
{ ...player('Player3', -3), sessionId: 'place-1a2b-client-player3' },
|
|
84
88
|
]);
|
|
@@ -88,14 +92,13 @@ describe('addPlaytestPlayersAsync', () => {
|
|
|
88
92
|
describe('removePlaytestPlayersAsync', () => {
|
|
89
93
|
it('has a connected client leave and kicks the rest', async () => {
|
|
90
94
|
const server = createSession('place-1a2b-server', 'server', [
|
|
91
|
-
[
|
|
92
|
-
[],
|
|
93
|
-
[[]],
|
|
95
|
+
{ players: [player('Player1', -1), player('Player2', -2)] },
|
|
96
|
+
{ remaining: [] },
|
|
94
97
|
]);
|
|
95
98
|
const client = createSession(
|
|
96
99
|
'place-1a2b-client-player1',
|
|
97
100
|
'client',
|
|
98
|
-
[
|
|
101
|
+
[{}],
|
|
99
102
|
'Player1'
|
|
100
103
|
);
|
|
101
104
|
|
|
@@ -109,16 +112,19 @@ describe('removePlaytestPlayersAsync', () => {
|
|
|
109
112
|
{ name: 'Player1', how: 'left' },
|
|
110
113
|
{ name: 'Player2', how: 'kicked' },
|
|
111
114
|
]);
|
|
112
|
-
expect(client.
|
|
113
|
-
expect(server.
|
|
114
|
-
|
|
115
|
+
expect(client.requests).toEqual([{ op: 'leave' }]);
|
|
116
|
+
expect(server.requests[1]).toEqual({
|
|
117
|
+
op: 'remove',
|
|
118
|
+
kick: ['Player2'],
|
|
119
|
+
names: ['Player1', 'Player2'],
|
|
120
|
+
timeoutSec: 60,
|
|
121
|
+
});
|
|
115
122
|
});
|
|
116
123
|
|
|
117
124
|
it('removes a player named twice only once', async () => {
|
|
118
125
|
const server = createSession('place-1a2b-server', 'server', [
|
|
119
|
-
[
|
|
120
|
-
[],
|
|
121
|
-
[[]],
|
|
126
|
+
{ players: [player('Player2', -2)] },
|
|
127
|
+
{ remaining: [] },
|
|
122
128
|
]);
|
|
123
129
|
|
|
124
130
|
const removed = await removePlaytestPlayersAsync(
|
|
@@ -130,9 +136,22 @@ describe('removePlaytestPlayersAsync', () => {
|
|
|
130
136
|
expect(removed).toEqual([{ name: 'Player2', how: 'kicked' }]);
|
|
131
137
|
});
|
|
132
138
|
|
|
139
|
+
it('reports players still there at the timeout', async () => {
|
|
140
|
+
const server = createSession('place-1a2b-server', 'server', [
|
|
141
|
+
{ players: [player('Player2', -2)] },
|
|
142
|
+
{ remaining: ['Player2'] },
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
await expect(
|
|
146
|
+
removePlaytestPlayersAsync(createConnection([server]), server as any, [
|
|
147
|
+
'Player2',
|
|
148
|
+
])
|
|
149
|
+
).rejects.toThrow('Players still in the playtest after 60s: Player2');
|
|
150
|
+
});
|
|
151
|
+
|
|
133
152
|
it('refuses an unknown name before removing anyone', async () => {
|
|
134
153
|
const server = createSession('place-1a2b-server', 'server', [
|
|
135
|
-
[
|
|
154
|
+
{ players: [player('Player1', -1)] },
|
|
136
155
|
]);
|
|
137
156
|
|
|
138
157
|
await expect(
|
|
@@ -141,6 +160,6 @@ describe('removePlaytestPlayersAsync', () => {
|
|
|
141
160
|
'Nobody',
|
|
142
161
|
])
|
|
143
162
|
).rejects.toThrow("No player named 'Nobody' (players: Player1)");
|
|
144
|
-
expect(server.
|
|
163
|
+
expect(server.requests).toHaveLength(1);
|
|
145
164
|
});
|
|
146
165
|
});
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
const PLAYER_CHANGE_TIMEOUT_SEC = 60;
|
|
19
19
|
const CLIENT_SESSION_TIMEOUT_MS = 30_000;
|
|
20
|
-
const
|
|
20
|
+
const REPLY_GRACE_MS = 10_000;
|
|
21
21
|
|
|
22
22
|
export interface PlaytestPlayer {
|
|
23
23
|
name: string;
|
|
@@ -27,103 +27,12 @@ export interface PlaytestPlayer {
|
|
|
27
27
|
sessionId?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
'local function playerInfo(player)',
|
|
32
|
-
'\treturn { name = player.Name, userId = player.UserId, displayName = player.DisplayName }',
|
|
33
|
-
'end',
|
|
34
|
-
].join('\n');
|
|
30
|
+
const REQUEST_TIMEOUT_MS = PLAYER_CHANGE_TIMEOUT_SEC * 1000 + REPLY_GRACE_MS;
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'\ttable.insert(players, playerInfo(player))',
|
|
41
|
-
'end',
|
|
42
|
-
'return players',
|
|
43
|
-
].join('\n');
|
|
44
|
-
|
|
45
|
-
function addPlayersScript(count: number): string {
|
|
46
|
-
return [
|
|
47
|
-
PLAYER_INFO_FUNCTION,
|
|
48
|
-
'local Players = game:GetService("Players")',
|
|
49
|
-
'local before = {}',
|
|
50
|
-
'for _, player in Players:GetPlayers() do',
|
|
51
|
-
'\tbefore[player] = true',
|
|
52
|
-
'end',
|
|
53
|
-
`game:GetService("StudioTestService"):AddPlayers(${count})`,
|
|
54
|
-
`local deadline = os.clock() + ${PLAYER_CHANGE_TIMEOUT_SEC}`,
|
|
55
|
-
'local added = {}',
|
|
56
|
-
'repeat',
|
|
57
|
-
'\ttask.wait(0.25)',
|
|
58
|
-
'\tadded = {}',
|
|
59
|
-
'\tfor _, player in Players:GetPlayers() do',
|
|
60
|
-
'\t\tif not before[player] then',
|
|
61
|
-
'\t\t\ttable.insert(added, playerInfo(player))',
|
|
62
|
-
'\t\tend',
|
|
63
|
-
'\tend',
|
|
64
|
-
`until #added >= ${count} or os.clock() > deadline`,
|
|
65
|
-
'return added',
|
|
66
|
-
].join('\n');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function kickPlayersScript(names: string[]): string {
|
|
70
|
-
return [
|
|
71
|
-
`local names = ${luaStringList(names)}`,
|
|
72
|
-
'local Players = game:GetService("Players")',
|
|
73
|
-
'for _, name in names do',
|
|
74
|
-
'\tlocal player = Players:FindFirstChild(name)',
|
|
75
|
-
'\tif player then',
|
|
76
|
-
'\t\tplayer:Kick("Removed by studio-bridge")',
|
|
77
|
-
'\tend',
|
|
78
|
-
'end',
|
|
79
|
-
].join('\n');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function waitForPlayersGoneScript(names: string[]): string {
|
|
83
|
-
return [
|
|
84
|
-
`local names = ${luaStringList(names)}`,
|
|
85
|
-
'local Players = game:GetService("Players")',
|
|
86
|
-
`local deadline = os.clock() + ${PLAYER_CHANGE_TIMEOUT_SEC}`,
|
|
87
|
-
'local remaining',
|
|
88
|
-
'repeat',
|
|
89
|
-
'\tremaining = {}',
|
|
90
|
-
'\tfor _, name in names do',
|
|
91
|
-
'\t\tif Players:FindFirstChild(name) then',
|
|
92
|
-
'\t\t\ttable.insert(remaining, name)',
|
|
93
|
-
'\t\tend',
|
|
94
|
-
'\tend',
|
|
95
|
-
'\tif #remaining > 0 then',
|
|
96
|
-
'\t\ttask.wait(0.25)',
|
|
97
|
-
'\tend',
|
|
98
|
-
'until #remaining == 0 or os.clock() > deadline',
|
|
99
|
-
'return remaining',
|
|
100
|
-
].join('\n');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const LEAVE_TEST_SCRIPT =
|
|
104
|
-
'task.defer(function() game:GetService("StudioTestService"):LeaveTest() end)';
|
|
105
|
-
|
|
106
|
-
function luaStringList(values: string[]): string {
|
|
107
|
-
return `{ ${values.map((value) => JSON.stringify(value)).join(', ')} }`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function runScriptAsync(
|
|
111
|
-
session: BridgeSession,
|
|
112
|
-
script: string
|
|
113
|
-
): Promise<unknown> {
|
|
114
|
-
const result = await session.execAsync(
|
|
115
|
-
script,
|
|
116
|
-
PLAYER_CHANGE_TIMEOUT_SEC * 1000 + SCRIPT_GRACE_MS
|
|
117
|
-
);
|
|
118
|
-
if (!result.success) {
|
|
119
|
-
throw new Error(result.error ?? 'Player script failed');
|
|
120
|
-
}
|
|
121
|
-
return result.returnValues?.[0];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function toPlayers(value: unknown): PlaytestPlayer[] {
|
|
125
|
-
if (!Array.isArray(value)) return [];
|
|
126
|
-
return value.map((entry) => ({
|
|
32
|
+
function toPlayers(
|
|
33
|
+
players: Array<{ name: string; userId: number; displayName: string }>
|
|
34
|
+
): PlaytestPlayer[] {
|
|
35
|
+
return players.map((entry) => ({
|
|
127
36
|
name: String(entry.name),
|
|
128
37
|
userId: Number(entry.userId),
|
|
129
38
|
displayName: String(entry.displayName),
|
|
@@ -162,10 +71,15 @@ export async function listPlaytestPlayersAsync(
|
|
|
162
71
|
connection: BridgeConnection,
|
|
163
72
|
serverSession: BridgeSession
|
|
164
73
|
): Promise<PlaytestPlayer[]> {
|
|
165
|
-
const players =
|
|
166
|
-
|
|
74
|
+
const { players } = await serverSession.playtestPlayersAsync(
|
|
75
|
+
{ op: 'list' },
|
|
76
|
+
REQUEST_TIMEOUT_MS
|
|
77
|
+
);
|
|
78
|
+
return withSessions(
|
|
79
|
+
connection,
|
|
80
|
+
serverSession.info.instanceId,
|
|
81
|
+
toPlayers(players)
|
|
167
82
|
);
|
|
168
|
-
return withSessions(connection, serverSession.info.instanceId, players);
|
|
169
83
|
}
|
|
170
84
|
|
|
171
85
|
/**
|
|
@@ -178,7 +92,12 @@ export async function addPlaytestPlayersAsync(
|
|
|
178
92
|
count: number
|
|
179
93
|
): Promise<PlaytestPlayer[]> {
|
|
180
94
|
const added = toPlayers(
|
|
181
|
-
|
|
95
|
+
(
|
|
96
|
+
await serverSession.playtestPlayersAsync(
|
|
97
|
+
{ op: 'add', count, timeoutSec: PLAYER_CHANGE_TIMEOUT_SEC },
|
|
98
|
+
REQUEST_TIMEOUT_MS
|
|
99
|
+
)
|
|
100
|
+
).players
|
|
182
101
|
);
|
|
183
102
|
const { instanceId } = serverSession.info;
|
|
184
103
|
|
|
@@ -230,22 +149,23 @@ export async function removePlaytestPlayersAsync(
|
|
|
230
149
|
? connection.getSession(player.sessionId)
|
|
231
150
|
: undefined;
|
|
232
151
|
if (client) {
|
|
233
|
-
await
|
|
152
|
+
await client.playtestPlayersAsync({ op: 'leave' }, REQUEST_TIMEOUT_MS);
|
|
234
153
|
removed.push({ name: player.name, how: 'left' });
|
|
235
154
|
} else {
|
|
236
155
|
toKick.push(player.name);
|
|
237
156
|
removed.push({ name: player.name, how: 'kicked' });
|
|
238
157
|
}
|
|
239
158
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
159
|
+
const { remaining } = await serverSession.playtestPlayersAsync(
|
|
160
|
+
{
|
|
161
|
+
op: 'remove',
|
|
162
|
+
kick: toKick,
|
|
163
|
+
names: targets.map((p) => p.name),
|
|
164
|
+
timeoutSec: PLAYER_CHANGE_TIMEOUT_SEC,
|
|
165
|
+
},
|
|
166
|
+
REQUEST_TIMEOUT_MS
|
|
247
167
|
);
|
|
248
|
-
if (
|
|
168
|
+
if (remaining.length > 0) {
|
|
249
169
|
throw new Error(
|
|
250
170
|
`Players still in the playtest after ${PLAYER_CHANGE_TIMEOUT_SEC}s: ${remaining.join(
|
|
251
171
|
', '
|
|
@@ -12,6 +12,18 @@ vi.mock('../../process/studio-settings.js', () => ({
|
|
|
12
12
|
settings.loadUserPluginsOnPlaytest,
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
+
vi.mock('../../plugin/playtest-bridge.js', () => ({
|
|
16
|
+
buildPlaytestBridgeAsync: async (
|
|
17
|
+
scripts?: string[],
|
|
18
|
+
options?: { withClient?: boolean }
|
|
19
|
+
) => ({
|
|
20
|
+
entrySource: 'entry',
|
|
21
|
+
...(options?.withClient ? { clientEntrySource: 'client entry' } : {}),
|
|
22
|
+
modules: {},
|
|
23
|
+
precompiled: scripts ?? [],
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
15
27
|
import { ContextNotFoundError } from '../../bridge/index.js';
|
|
16
28
|
import {
|
|
17
29
|
explainMissingPlaytestContextAsync,
|
|
@@ -63,24 +75,12 @@ describe('startPlaytestAsync multiplayer checks', () => {
|
|
|
63
75
|
});
|
|
64
76
|
|
|
65
77
|
describe('with "Load user plugins on playtest" off', () => {
|
|
66
|
-
it('
|
|
67
|
-
settings.loadUserPluginsOnPlaytest = false;
|
|
68
|
-
const editSession = createEditSession('current');
|
|
69
|
-
|
|
70
|
-
await expect(
|
|
71
|
-
startPlaytestAsync({} as any, editSession, 'play')
|
|
72
|
-
).rejects.toThrow(
|
|
73
|
-
`Studio's "Load user plugins on playtest" setting is off`
|
|
74
|
-
);
|
|
75
|
-
expect(editSession.runPlaytestAsync).not.toHaveBeenCalled();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('names the setting when no server session is connected', async () => {
|
|
78
|
+
it('does not blame the setting for a missing server session, which is planted', async () => {
|
|
79
79
|
settings.loadUserPluginsOnPlaytest = false;
|
|
80
80
|
const connection = { listSessions: () => [] } as any;
|
|
81
81
|
|
|
82
82
|
await expect(resolvePlaytestServerSessionAsync(connection)).rejects.toThrow(
|
|
83
|
-
|
|
83
|
+
"No playtest is running. Start one with 'studio-bridge playtest start'."
|
|
84
84
|
);
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -141,3 +141,105 @@ describe('explainMissingPlaytestContextAsync', () => {
|
|
|
141
141
|
);
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
describe('planting the server session', () => {
|
|
146
|
+
function createConnection() {
|
|
147
|
+
return {
|
|
148
|
+
port: 38741,
|
|
149
|
+
isRemote: false,
|
|
150
|
+
listSessions: () => [],
|
|
151
|
+
getSession: () => undefined,
|
|
152
|
+
on: vi.fn(),
|
|
153
|
+
off: vi.fn(),
|
|
154
|
+
} as any;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The start fails at once, so the test sees what it was asked to start
|
|
158
|
+
function createFailingEditSession(pluginVersion: string) {
|
|
159
|
+
const editSession = createEditSession(pluginVersion);
|
|
160
|
+
editSession.runPlaytestAsync.mockResolvedValue({
|
|
161
|
+
playtestId: 'p',
|
|
162
|
+
success: false,
|
|
163
|
+
error: 'refused',
|
|
164
|
+
});
|
|
165
|
+
return editSession;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
it('plants a bridge with the scripts to run when the setting is off', async () => {
|
|
169
|
+
settings.loadUserPluginsOnPlaytest = false;
|
|
170
|
+
const editSession = createFailingEditSession('old');
|
|
171
|
+
|
|
172
|
+
await expect(
|
|
173
|
+
startPlaytestAsync(createConnection(), editSession, 'run', undefined, {
|
|
174
|
+
scripts: ['return 1'],
|
|
175
|
+
})
|
|
176
|
+
).rejects.toThrow('Playtest failed to start: refused');
|
|
177
|
+
|
|
178
|
+
const bridge = editSession.runPlaytestAsync.mock.calls[0][5];
|
|
179
|
+
expect(bridge.precompiled).toEqual(['return 1']);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('plants a client bridge too for a play mode playtest when the setting is off', async () => {
|
|
183
|
+
settings.loadUserPluginsOnPlaytest = false;
|
|
184
|
+
const editSession = createFailingEditSession('old');
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
startPlaytestAsync(createConnection(), editSession, 'play')
|
|
188
|
+
).rejects.toThrow('Playtest failed to start: refused');
|
|
189
|
+
|
|
190
|
+
expect(
|
|
191
|
+
editSession.runPlaytestAsync.mock.calls[0][5].clientEntrySource
|
|
192
|
+
).toContain('client entry');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('plants no client bridge in run mode, or when the setting is unknown', async () => {
|
|
196
|
+
settings.loadUserPluginsOnPlaytest = false;
|
|
197
|
+
const runMode = createFailingEditSession('old');
|
|
198
|
+
await expect(
|
|
199
|
+
startPlaytestAsync(createConnection(), runMode, 'run')
|
|
200
|
+
).rejects.toThrow('refused');
|
|
201
|
+
expect(
|
|
202
|
+
runMode.runPlaytestAsync.mock.calls[0][5].clientEntrySource
|
|
203
|
+
).toBeUndefined();
|
|
204
|
+
|
|
205
|
+
settings.loadUserPluginsOnPlaytest = undefined;
|
|
206
|
+
const unknown = createFailingEditSession('current');
|
|
207
|
+
await expect(
|
|
208
|
+
startPlaytestAsync(createConnection(), unknown, 'play')
|
|
209
|
+
).rejects.toThrow('refused');
|
|
210
|
+
expect(
|
|
211
|
+
unknown.runPlaytestAsync.mock.calls[0][5].clientEntrySource
|
|
212
|
+
).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('plants a bridge when the setting is unknown', async () => {
|
|
216
|
+
const editSession = createFailingEditSession('current');
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
startPlaytestAsync(createConnection(), editSession, 'run')
|
|
220
|
+
).rejects.toThrow('refused');
|
|
221
|
+
|
|
222
|
+
expect(editSession.runPlaytestAsync.mock.calls[0][5]).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('leaves the server to the plugin when the setting is on', async () => {
|
|
226
|
+
settings.loadUserPluginsOnPlaytest = true;
|
|
227
|
+
const editSession = createFailingEditSession('current');
|
|
228
|
+
|
|
229
|
+
await expect(
|
|
230
|
+
startPlaytestAsync(createConnection(), editSession, 'run')
|
|
231
|
+
).rejects.toThrow('refused');
|
|
232
|
+
|
|
233
|
+
expect(editSession.runPlaytestAsync.mock.calls[0][5]).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('needs the current plugin when plugin sessions will connect', async () => {
|
|
237
|
+
settings.loadUserPluginsOnPlaytest = true;
|
|
238
|
+
const editSession = createFailingEditSession('old');
|
|
239
|
+
|
|
240
|
+
await expect(
|
|
241
|
+
startPlaytestAsync(createConnection(), editSession, 'run')
|
|
242
|
+
).rejects.toThrow("Run 'studio-bridge plugin install' first");
|
|
243
|
+
expect(editSession.runPlaytestAsync).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|