@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,8 +5,14 @@
|
|
|
5
5
|
* each connects to the bridge as its own session. Those sessions can't be
|
|
6
6
|
* matched to the Studio that started them by instance id, since unpublished
|
|
7
7
|
* places give every DataModel its own. Instead, `startPlaytestAsync` passes a
|
|
8
|
-
* playtest id as the StudioTestService test args, and each
|
|
9
|
-
*
|
|
8
|
+
* playtest id as the StudioTestService test args, and each session registers
|
|
9
|
+
* with it.
|
|
10
|
+
*
|
|
11
|
+
* The playtest's server session is the plugin when Studio loads user plugins
|
|
12
|
+
* into playtests. Otherwise the edit session plants the same client into the
|
|
13
|
+
* playtest as an ordinary Script (see plugin/playtest-bridge.ts). That one can
|
|
14
|
+
* only run code compiled when the playtest started: the actions, and the
|
|
15
|
+
* scripts `startPlaytestAsync` is given.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import { randomUUID } from 'crypto';
|
|
@@ -20,6 +26,7 @@ import type {
|
|
|
20
26
|
} from '../../bridge/index.js';
|
|
21
27
|
import { ContextNotFoundError } from '../../bridge/index.js';
|
|
22
28
|
import { getPluginBuildVersionAsync } from '../../plugin/plugin-build-version.js';
|
|
29
|
+
import { buildPlaytestBridgeAsync } from '../../plugin/playtest-bridge.js';
|
|
23
30
|
import { tryReadLoadUserPluginsOnPlaytestAsync } from '../../process/studio-settings.js';
|
|
24
31
|
|
|
25
32
|
/** How long a started playtest may run before its pending start request is dropped. */
|
|
@@ -29,16 +36,19 @@ const SESSION_WAIT_TIMEOUT_MS = 30_000;
|
|
|
29
36
|
/** Each player of a multiplayer test boots its own Studio process. */
|
|
30
37
|
const MULTIPLAYER_SESSION_WAIT_TIMEOUT_MS = 90_000;
|
|
31
38
|
const SESSION_CLOSE_TIMEOUT_MS = 10_000;
|
|
32
|
-
const READ_RETRY_DELAY_MS = 1_000;
|
|
33
|
-
|
|
34
|
-
const PLAYTEST_PLUGINS_OFF_MESSAGE = `Studio's "Load user plugins on playtest" setting is off, so the studio-bridge plugin doesn't load into playtests and their sessions can't connect. Turn it on in Studio Settings, or run a script in a playtest's server with 'studio-bridge playtest run', which works without it.`;
|
|
35
39
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const PLAYTEST_PLUGINS_OFF_MESSAGE = `Studio's "Load user plugins on playtest" setting is off, so the studio-bridge plugin doesn't load into a playtest's client. A playtest started with 'studio-bridge playtest start' gets a planted client session instead, which runs only code compiled when it started; turn the setting on to run any code.`;
|
|
41
|
+
|
|
42
|
+
export interface StartPlaytestOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Code the caller will run in the playtest's server session. A server
|
|
45
|
+
* session planted without plugin permissions can only run code compiled
|
|
46
|
+
* when the playtest starts.
|
|
47
|
+
*/
|
|
48
|
+
scripts?: string[];
|
|
49
|
+
/** Wait for the server session only, so play mode needs no client plugin. */
|
|
50
|
+
serverOnly?: boolean;
|
|
51
|
+
}
|
|
42
52
|
|
|
43
53
|
export interface StartedPlaytest {
|
|
44
54
|
playtestId: string;
|
|
@@ -68,19 +78,33 @@ export async function startPlaytestAsync(
|
|
|
68
78
|
connection: BridgeConnection,
|
|
69
79
|
editSession: BridgeSession,
|
|
70
80
|
mode: PlaytestMode,
|
|
71
|
-
players?: number
|
|
81
|
+
players?: number,
|
|
82
|
+
options: StartPlaytestOptions = {}
|
|
72
83
|
): Promise<StartedPlaytest> {
|
|
73
84
|
if (editSession.context !== 'edit') {
|
|
74
85
|
throw new Error(
|
|
75
86
|
`Playtests start from an edit session, but '${editSession.info.sessionId}' is a ${editSession.context} session`
|
|
76
87
|
);
|
|
77
88
|
}
|
|
78
|
-
|
|
79
|
-
throw new Error(PLAYTEST_PLUGINS_OFF_MESSAGE);
|
|
80
|
-
}
|
|
89
|
+
const pluginsOnPlaytest = await tryReadPlaytestPluginsAsync(connection);
|
|
81
90
|
if (players !== undefined) {
|
|
82
|
-
|
|
91
|
+
assertValidMultiplayer(mode, players);
|
|
83
92
|
}
|
|
93
|
+
// Plugin sessions in the playtest register with its id, which older plugins leave out
|
|
94
|
+
if (pluginsOnPlaytest !== false) {
|
|
95
|
+
await assertCurrentPluginAsync(editSession);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A server plugin, when Studio does load one, stands down for a planted bridge
|
|
99
|
+
const bridge =
|
|
100
|
+
pluginsOnPlaytest === true
|
|
101
|
+
? undefined
|
|
102
|
+
: await buildPlaytestBridgeAsync(options.scripts, {
|
|
103
|
+
withClient:
|
|
104
|
+
pluginsOnPlaytest === false &&
|
|
105
|
+
mode === 'play' &&
|
|
106
|
+
!options.serverOnly,
|
|
107
|
+
});
|
|
84
108
|
|
|
85
109
|
const playtestId = randomUUID();
|
|
86
110
|
const ended = editSession
|
|
@@ -89,7 +113,8 @@ export async function startPlaytestAsync(
|
|
|
89
113
|
playtestId,
|
|
90
114
|
connection.port,
|
|
91
115
|
PLAYTEST_MAX_DURATION_MS,
|
|
92
|
-
players
|
|
116
|
+
players,
|
|
117
|
+
bridge
|
|
93
118
|
)
|
|
94
119
|
.catch(
|
|
95
120
|
(err): PlaytestEndResult => ({
|
|
@@ -99,7 +124,9 @@ export async function startPlaytestAsync(
|
|
|
99
124
|
})
|
|
100
125
|
);
|
|
101
126
|
|
|
102
|
-
const clientCount =
|
|
127
|
+
const clientCount = options.serverOnly
|
|
128
|
+
? 0
|
|
129
|
+
: players ?? (mode === 'play' ? 1 : 0);
|
|
103
130
|
const waitTimeoutMs =
|
|
104
131
|
players !== undefined
|
|
105
132
|
? MULTIPLAYER_SESSION_WAIT_TIMEOUT_MS
|
|
@@ -153,20 +180,14 @@ export async function startPlaytestAsync(
|
|
|
153
180
|
|
|
154
181
|
// Without its server session the playtest can't be ended from here
|
|
155
182
|
if (!server?.isConnected) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Playtest started, but ${waited}, so stop it in Studio. Play sessions only connect with Studio's "Load user plugins on playtest" setting on.`
|
|
158
|
-
);
|
|
183
|
+
throw new Error(`Playtest started, but ${waited}, so stop it in Studio.`);
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
await endPlaytestAsync(connection, server);
|
|
162
187
|
throw new Error(`Playtest started, but ${waited}, so it was stopped.`);
|
|
163
188
|
}
|
|
164
189
|
|
|
165
|
-
|
|
166
|
-
editSession: BridgeSession,
|
|
167
|
-
mode: PlaytestMode,
|
|
168
|
-
players: number
|
|
169
|
-
): Promise<void> {
|
|
190
|
+
function assertValidMultiplayer(mode: PlaytestMode, players: number): void {
|
|
170
191
|
if (!Number.isInteger(players) || players < 1) {
|
|
171
192
|
throw new Error(
|
|
172
193
|
`--players must be a whole number of at least 1, not ${players}`
|
|
@@ -175,12 +196,15 @@ async function assertCanStartMultiplayerAsync(
|
|
|
175
196
|
if (mode !== 'play') {
|
|
176
197
|
throw new Error('A multiplayer test runs in play mode');
|
|
177
198
|
}
|
|
199
|
+
}
|
|
178
200
|
|
|
179
|
-
|
|
201
|
+
async function assertCurrentPluginAsync(
|
|
202
|
+
editSession: BridgeSession
|
|
203
|
+
): Promise<void> {
|
|
180
204
|
const expected = await getPluginBuildVersionAsync();
|
|
181
205
|
if (editSession.info.pluginVersion !== expected) {
|
|
182
206
|
throw new Error(
|
|
183
|
-
`
|
|
207
|
+
`Playtests with plugin sessions need the current studio-bridge plugin, but '${editSession.info.placeName}' has an older one. Run 'studio-bridge plugin install' first.`
|
|
184
208
|
);
|
|
185
209
|
}
|
|
186
210
|
}
|
|
@@ -255,16 +279,15 @@ export async function explainMissingPlaytestContextAsync(
|
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
/**
|
|
258
|
-
* Whether this machine's Studio has "Load user plugins on playtest"
|
|
259
|
-
* remote Studio's settings aren't on this machine, so they
|
|
282
|
+
* Whether this machine's Studio has "Load user plugins on playtest" on. A
|
|
283
|
+
* remote Studio's settings aren't on this machine, so they are unknown.
|
|
260
284
|
*/
|
|
261
|
-
async function
|
|
285
|
+
async function tryReadPlaytestPluginsAsync(
|
|
262
286
|
connection: BridgeConnection
|
|
263
|
-
): Promise<boolean> {
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
);
|
|
287
|
+
): Promise<boolean | undefined> {
|
|
288
|
+
return connection.isRemote
|
|
289
|
+
? undefined
|
|
290
|
+
: await tryReadLoadUserPluginsOnPlaytestAsync();
|
|
268
291
|
}
|
|
269
292
|
|
|
270
293
|
async function explainNoPlaytestAsync(
|
|
@@ -273,7 +296,10 @@ async function explainNoPlaytestAsync(
|
|
|
273
296
|
context?: SessionContext
|
|
274
297
|
): Promise<string> {
|
|
275
298
|
const of = studio ? ` of ${studio}` : '';
|
|
276
|
-
if (
|
|
299
|
+
if (
|
|
300
|
+
context === 'client' &&
|
|
301
|
+
(await tryReadPlaytestPluginsAsync(connection)) === false
|
|
302
|
+
) {
|
|
277
303
|
return `No playtest${of} is connected. ${PLAYTEST_PLUGINS_OFF_MESSAGE}`;
|
|
278
304
|
}
|
|
279
305
|
const missing = context ? `, so there is no ${context} session` : '';
|
|
@@ -307,7 +333,6 @@ async function waitForPlaytestSessionsAsync(
|
|
|
307
333
|
): Promise<PlaytestSessions> {
|
|
308
334
|
return new Promise((resolve) => {
|
|
309
335
|
const found: PlaytestSessions = { clients: [] };
|
|
310
|
-
const checked = new Set<string>();
|
|
311
336
|
|
|
312
337
|
const cleanup = () => {
|
|
313
338
|
clearTimeout(timer);
|
|
@@ -316,30 +341,22 @@ async function waitForPlaytestSessionsAsync(
|
|
|
316
341
|
};
|
|
317
342
|
|
|
318
343
|
const onSession = (session: BridgeSession) => {
|
|
319
|
-
|
|
320
|
-
if (session.context === 'edit' || checked.has(sessionId)) {
|
|
344
|
+
if (session.info.playtestId !== playtestId) {
|
|
321
345
|
return;
|
|
322
346
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
found.clients.push(read.session);
|
|
337
|
-
}
|
|
338
|
-
if (found.server && found.clients.length >= clientCount) {
|
|
339
|
-
cleanup();
|
|
340
|
-
resolve(found);
|
|
341
|
-
}
|
|
342
|
-
});
|
|
347
|
+
|
|
348
|
+
if (session.context === 'server') {
|
|
349
|
+
found.server = session;
|
|
350
|
+
} else if (
|
|
351
|
+
session.context === 'client' &&
|
|
352
|
+
!found.clients.some((c) => c.info.sessionId === session.info.sessionId)
|
|
353
|
+
) {
|
|
354
|
+
found.clients.push(session);
|
|
355
|
+
}
|
|
356
|
+
if (found.server && found.clients.length >= clientCount) {
|
|
357
|
+
cleanup();
|
|
358
|
+
resolve(found);
|
|
359
|
+
}
|
|
343
360
|
};
|
|
344
361
|
|
|
345
362
|
// Resolves with whichever sessions connected; the caller reports the rest
|
|
@@ -358,34 +375,6 @@ async function waitForPlaytestSessionsAsync(
|
|
|
358
375
|
});
|
|
359
376
|
}
|
|
360
377
|
|
|
361
|
-
/**
|
|
362
|
-
* Reads the playtest id a session was started under, retrying while the read
|
|
363
|
-
* fails: a playtest's DataModels are slow to answer while their Studio boots,
|
|
364
|
-
* and a session that reconnects is read again under the same id.
|
|
365
|
-
*/
|
|
366
|
-
async function readPlaytestIdAsync(
|
|
367
|
-
connection: BridgeConnection,
|
|
368
|
-
sessionId: string,
|
|
369
|
-
signal: AbortSignal
|
|
370
|
-
): Promise<{ session: BridgeSession; id: string | undefined } | undefined> {
|
|
371
|
-
while (!signal.aborted) {
|
|
372
|
-
const session = connection.getSession(sessionId);
|
|
373
|
-
if (session) {
|
|
374
|
-
try {
|
|
375
|
-
const result = await session.execAsync(READ_PLAYTEST_ID_SCRIPT, 5_000);
|
|
376
|
-
if (result.success) {
|
|
377
|
-
const value = result.returnValues?.[0];
|
|
378
|
-
return { session, id: typeof value === 'string' ? value : undefined };
|
|
379
|
-
}
|
|
380
|
-
} catch {
|
|
381
|
-
// Retried below
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
await new Promise((resolve) => setTimeout(resolve, READ_RETRY_DELAY_MS));
|
|
385
|
-
}
|
|
386
|
-
return undefined;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
378
|
async function waitForSessionCloseAsync(
|
|
390
379
|
connection: BridgeConnection,
|
|
391
380
|
sessionId: string
|
|
@@ -5,26 +5,24 @@
|
|
|
5
5
|
* `--context client`. Its output and return values come back the same way as
|
|
6
6
|
* `console exec`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* playtest
|
|
8
|
+
* Both run through the playtest's sessions. The server session works even
|
|
9
|
+
* when Studio does not load user plugins into playtests, since the edit
|
|
10
|
+
* session then plants it, with this code compiled ahead of time (see
|
|
11
|
+
* playtest-sessions.ts). The client session needs that Studio setting on.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import * as fs from 'fs/promises';
|
|
15
|
-
import { resolvePackagePath } from '@quenty/nevermore-template-helpers';
|
|
16
14
|
import { defineCommand } from '../../framework/define-command.js';
|
|
17
15
|
import { arg } from '../../framework/arg-builder.js';
|
|
18
16
|
import { resolveScriptContentAsync } from '../../../cli/resolve-script-content.js';
|
|
19
17
|
import {
|
|
20
18
|
execHandlerAsync,
|
|
21
19
|
formatExecText,
|
|
22
|
-
|
|
20
|
+
INPUT_ARG_DESCRIPTION,
|
|
21
|
+
readInputFilesAsync,
|
|
23
22
|
} from '../../console/exec/exec.js';
|
|
24
23
|
import type {
|
|
25
24
|
BridgeConnection,
|
|
26
25
|
BridgeSession,
|
|
27
|
-
ExecResult,
|
|
28
26
|
PlaytestMode,
|
|
29
27
|
SessionContext,
|
|
30
28
|
} from '../../../bridge/index.js';
|
|
@@ -45,15 +43,15 @@ export interface PlaytestRunOptions {
|
|
|
45
43
|
timeout?: number;
|
|
46
44
|
/** Passed to the script as `...`. */
|
|
47
45
|
args?: string[];
|
|
46
|
+
/** Passed to the script as buffers in `...`, before `args`. */
|
|
47
|
+
inputs?: Buffer[];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
50
|
+
const PLAYER_JOIN_TIMEOUT_MS = 30_000;
|
|
52
51
|
|
|
53
52
|
export interface PlaytestRunResult {
|
|
54
53
|
success: boolean;
|
|
55
|
-
|
|
56
|
-
playtestId?: string;
|
|
54
|
+
playtestId: string;
|
|
57
55
|
output: string[];
|
|
58
56
|
error?: string;
|
|
59
57
|
returnValues?: SerializedReturnValue[];
|
|
@@ -66,8 +64,9 @@ interface PlaytestRunArgs {
|
|
|
66
64
|
code?: string;
|
|
67
65
|
file?: string;
|
|
68
66
|
mode?: PlaytestMode;
|
|
69
|
-
|
|
67
|
+
scriptTimeout?: number;
|
|
70
68
|
scriptArgs?: string[];
|
|
69
|
+
input?: string[];
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
export async function playtestRunHandlerAsync(
|
|
@@ -85,14 +84,15 @@ export async function playtestRunHandlerAsync(
|
|
|
85
84
|
'edit'
|
|
86
85
|
);
|
|
87
86
|
|
|
88
|
-
if (options.context === 'server') {
|
|
89
|
-
return runWithPlantedRunnerAsync(editSession, options);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
87
|
const playtest = await startPlaytestAsync(
|
|
93
88
|
connection,
|
|
94
89
|
editSession,
|
|
95
|
-
options.mode
|
|
90
|
+
options.mode,
|
|
91
|
+
undefined,
|
|
92
|
+
{
|
|
93
|
+
scripts: [options.scriptContent],
|
|
94
|
+
serverOnly: options.context === 'server',
|
|
95
|
+
}
|
|
96
96
|
);
|
|
97
97
|
|
|
98
98
|
let result: PlaytestRunResult;
|
|
@@ -101,12 +101,17 @@ export async function playtestRunHandlerAsync(
|
|
|
101
101
|
if (!session) {
|
|
102
102
|
throw new Error(`Playtest has no ${options.context} session`);
|
|
103
103
|
}
|
|
104
|
+
// In play mode the server's code expects the player, who joins a moment later
|
|
105
|
+
if (options.mode === 'play' && options.context === 'server') {
|
|
106
|
+
await waitForPlayerAsync(session);
|
|
107
|
+
}
|
|
104
108
|
|
|
105
109
|
result = {
|
|
106
110
|
...(await execHandlerAsync(session, {
|
|
107
111
|
scriptContent: options.scriptContent,
|
|
108
112
|
timeout: options.timeout,
|
|
109
113
|
args: options.args,
|
|
114
|
+
inputs: options.inputs,
|
|
110
115
|
})),
|
|
111
116
|
playtestId: playtest.playtestId,
|
|
112
117
|
};
|
|
@@ -123,44 +128,19 @@ export async function playtestRunHandlerAsync(
|
|
|
123
128
|
return stopError ? { ...result, stopError } : result;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
async function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
),
|
|
139
|
-
'utf-8'
|
|
131
|
+
async function waitForPlayerAsync(serverSession: BridgeSession): Promise<void> {
|
|
132
|
+
const deadline = Date.now() + PLAYER_JOIN_TIMEOUT_MS;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
const { players } = await serverSession.playtestPlayersAsync(
|
|
135
|
+
{ op: 'list' },
|
|
136
|
+
PLAYER_JOIN_TIMEOUT_MS
|
|
137
|
+
);
|
|
138
|
+
if (players.length > 0) return;
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
140
|
+
}
|
|
141
|
+
throw new Error(
|
|
142
|
+
`No player joined the playtest within ${PLAYER_JOIN_TIMEOUT_MS / 1000}s`
|
|
140
143
|
);
|
|
141
|
-
|
|
142
|
-
const result = await editSession.runPlaytestScriptAsync({
|
|
143
|
-
mode: options.mode,
|
|
144
|
-
script: options.scriptContent,
|
|
145
|
-
runnerSource,
|
|
146
|
-
timeoutSec: (options.timeout ?? DEFAULT_TIMEOUT_MS) / 1000,
|
|
147
|
-
args: options.args,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return toCommandResult(result);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Shapes a bridge ExecResult the way `console exec` reports it. */
|
|
154
|
-
function toCommandResult(result: ExecResult): CommandExecResult {
|
|
155
|
-
return {
|
|
156
|
-
success: result.success,
|
|
157
|
-
output: result.output.map((entry) => entry.body),
|
|
158
|
-
error: result.error,
|
|
159
|
-
returnValues: result.returnValues,
|
|
160
|
-
summary: result.success
|
|
161
|
-
? 'Script executed successfully'
|
|
162
|
-
: `Script failed: ${result.error}`,
|
|
163
|
-
};
|
|
164
144
|
}
|
|
165
145
|
|
|
166
146
|
/** Ends the playtest, returning why it could not instead of throwing. */
|
|
@@ -207,10 +187,14 @@ export const playtestRunCommand = defineCommand<
|
|
|
207
187
|
choices: ['play', 'run'],
|
|
208
188
|
default: 'play',
|
|
209
189
|
}),
|
|
210
|
-
|
|
211
|
-
description: '
|
|
190
|
+
scriptTimeout: arg.option({
|
|
191
|
+
description: 'Cancel the script after this many milliseconds',
|
|
212
192
|
type: 'number',
|
|
213
193
|
}),
|
|
194
|
+
input: arg.option({
|
|
195
|
+
description: INPUT_ARG_DESCRIPTION,
|
|
196
|
+
array: true,
|
|
197
|
+
}),
|
|
214
198
|
scriptArgs: arg.passthrough({
|
|
215
199
|
description:
|
|
216
200
|
'Arguments after --, passed to the script as ... (all strings)',
|
|
@@ -226,14 +210,16 @@ export const playtestRunCommand = defineCommand<
|
|
|
226
210
|
},
|
|
227
211
|
handler: async (connection, args, targeting) => {
|
|
228
212
|
const { scriptContent } = await resolveScriptContentAsync(args);
|
|
213
|
+
const inputs = await readInputFilesAsync(args.input);
|
|
229
214
|
|
|
230
215
|
return playtestRunHandlerAsync(connection, {
|
|
231
216
|
scriptContent,
|
|
232
217
|
mode: args.mode ?? 'play',
|
|
233
218
|
context: targeting.context ?? 'server',
|
|
234
219
|
target: targeting.target,
|
|
235
|
-
timeout: args.
|
|
220
|
+
timeout: args.scriptTimeout,
|
|
236
221
|
args: args.scriptArgs,
|
|
222
|
+
inputs,
|
|
237
223
|
});
|
|
238
224
|
},
|
|
239
225
|
});
|
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
client per player, each in its own Studio process.
|
|
13
13
|
- The playtest id, bridge port and edit instance id ride along as the test
|
|
14
14
|
args. The playtest's DataModels cannot make the HTTP requests port
|
|
15
|
-
discovery needs, so the plugin in each connects straight to that port
|
|
16
|
-
registers under the edit session's instance
|
|
17
|
-
|
|
15
|
+
discovery needs, so the plugin in each connects straight to that port and
|
|
16
|
+
registers under the edit session's instance and the playtest id.
|
|
17
|
+
- With `bridge`, it first plants the studio-bridge client as a Script in
|
|
18
|
+
ServerScriptService, for when Studio does not load user plugins into
|
|
19
|
+
playtests. The playtest copies the edit DataModel, so the client starts in
|
|
20
|
+
its server and connects as its server session. With
|
|
21
|
+
`bridge.clientEntrySource` it also plants a LocalScript in
|
|
22
|
+
StarterPlayerScripts, which every player's client runs and connects as its
|
|
23
|
+
client session. The plants are removed once the playtest ends.
|
|
18
24
|
|
|
19
|
-
Request: `{ type: "startPlaytest", payload: { mode: "play" | "run", playtestId, bridgePort, instanceId, players? } }`
|
|
25
|
+
Request: `{ type: "startPlaytest", payload: { mode: "play" | "run", playtestId, bridgePort, instanceId, players?, bridge? } }`
|
|
26
|
+
`bridge`: `{ entrySource, clientEntrySource?, modules: { [name]: source }, precompiled: { source } }`
|
|
20
27
|
Response: `{ type: "playtestEnded", payload: { playtestId, success, error? } }`
|
|
21
28
|
|
|
22
29
|
@class StartPlaytestAction
|
|
@@ -62,7 +69,10 @@ function StartPlaytestAction.register(router: any, sendMessage: SendMessage?)
|
|
|
62
69
|
studioBridgeInstanceId = instanceId,
|
|
63
70
|
studioBridgePlayers = players,
|
|
64
71
|
}
|
|
65
|
-
|
|
72
|
+
local planted = if payload.bridge ~= nil
|
|
73
|
+
then StartPlaytestAction._plant(router, payload.bridge, playtestId)
|
|
74
|
+
else {}
|
|
75
|
+
task.spawn(StartPlaytestAction._runAsync, sendMessage, mode, testArgs, requestId, sessionId, planted)
|
|
66
76
|
|
|
67
77
|
-- Replied through sendMessage, so the router must not reply as well
|
|
68
78
|
return nil
|
|
@@ -80,7 +90,8 @@ function StartPlaytestAction._runAsync(
|
|
|
80
90
|
studioBridgePlayers: number?,
|
|
81
91
|
},
|
|
82
92
|
requestId: string,
|
|
83
|
-
sessionId: string
|
|
93
|
+
sessionId: string,
|
|
94
|
+
planted: { Instance }
|
|
84
95
|
)
|
|
85
96
|
local ok, err = pcall(function()
|
|
86
97
|
local studioTestService = game:GetService("StudioTestService") :: any
|
|
@@ -93,6 +104,10 @@ function StartPlaytestAction._runAsync(
|
|
|
93
104
|
end
|
|
94
105
|
end)
|
|
95
106
|
|
|
107
|
+
for _, instance in planted do
|
|
108
|
+
instance:Destroy()
|
|
109
|
+
end
|
|
110
|
+
|
|
96
111
|
sendMessage({
|
|
97
112
|
type = "playtestEnded",
|
|
98
113
|
sessionId = sessionId,
|
|
@@ -105,4 +120,88 @@ function StartPlaytestAction._runAsync(
|
|
|
105
120
|
})
|
|
106
121
|
end
|
|
107
122
|
|
|
123
|
+
--[[
|
|
124
|
+
Plants the studio-bridge client for the playtest `playtestId` and returns it.
|
|
125
|
+
Errors on a malformed `bridge`, or a precompiled source that does not
|
|
126
|
+
compile, before anything is planted: the planted client skips a source that
|
|
127
|
+
fails to load, which would hide the compile error.
|
|
128
|
+
]]
|
|
129
|
+
function StartPlaytestAction._plant(router: any, bridge: any, playtestId: string): { Instance }
|
|
130
|
+
if
|
|
131
|
+
type(bridge) ~= "table"
|
|
132
|
+
or type(bridge.entrySource) ~= "string"
|
|
133
|
+
or type(bridge.modules) ~= "table"
|
|
134
|
+
or type(bridge.precompiled) ~= "table"
|
|
135
|
+
then
|
|
136
|
+
error("Invalid bridge in startPlaytest request")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
for _, source in bridge.precompiled do
|
|
140
|
+
local fn, compileErr
|
|
141
|
+
if type(router.Compile) == "function" then
|
|
142
|
+
fn, compileErr = router:Compile(source, "=exec")
|
|
143
|
+
else
|
|
144
|
+
fn, compileErr = (loadstring :: any)(source, "=exec")
|
|
145
|
+
end
|
|
146
|
+
if not fn then
|
|
147
|
+
error(`Script does not compile: {compileErr or "unknown error"}`, 0)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
local entry = Instance.new("Script")
|
|
152
|
+
entry.Name = "StudioBridgePlaytest"
|
|
153
|
+
entry.Source = bridge.entrySource
|
|
154
|
+
entry:SetAttribute("PlaytestId", playtestId)
|
|
155
|
+
|
|
156
|
+
local shared = Instance.new("Folder")
|
|
157
|
+
shared.Name = "Shared"
|
|
158
|
+
shared.Parent = entry
|
|
159
|
+
for name, source in bridge.modules do
|
|
160
|
+
local moduleScript = Instance.new("ModuleScript")
|
|
161
|
+
moduleScript.Name = name
|
|
162
|
+
moduleScript.Source = source
|
|
163
|
+
moduleScript.Parent = shared
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
local precompiled = Instance.new("Folder")
|
|
167
|
+
precompiled.Name = "Precompiled"
|
|
168
|
+
precompiled.Parent = entry
|
|
169
|
+
for index, source in bridge.precompiled do
|
|
170
|
+
local moduleScript = Instance.new("ModuleScript")
|
|
171
|
+
moduleScript.Name = tostring(index)
|
|
172
|
+
moduleScript.Source = source
|
|
173
|
+
moduleScript.Parent = precompiled
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
local planted = { entry }
|
|
177
|
+
local clientEntry: LocalScript? = nil
|
|
178
|
+
if type(bridge.clientEntrySource) == "string" then
|
|
179
|
+
clientEntry = Instance.new("LocalScript")
|
|
180
|
+
local client = clientEntry :: LocalScript
|
|
181
|
+
client.Name = "StudioBridgePlaytestClient"
|
|
182
|
+
client.Source = bridge.clientEntrySource
|
|
183
|
+
client:SetAttribute("PlaytestId", playtestId)
|
|
184
|
+
shared:Clone().Parent = client
|
|
185
|
+
precompiled:Clone().Parent = client
|
|
186
|
+
table.insert(planted, client)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
-- A bridge left from an earlier playtest would stand in for this one's
|
|
190
|
+
local serverScriptService = game:GetService("ServerScriptService")
|
|
191
|
+
local starterPlayerScripts = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts")
|
|
192
|
+
for _, container in { serverScriptService, starterPlayerScripts } do
|
|
193
|
+
for _, child in container:GetChildren() do
|
|
194
|
+
if child.Name == entry.Name or child.Name == "StudioBridgePlaytestClient" then
|
|
195
|
+
child:Destroy()
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
entry.Parent = serverScriptService
|
|
201
|
+
if clientEntry then
|
|
202
|
+
clientEntry.Parent = starterPlayerScripts
|
|
203
|
+
end
|
|
204
|
+
return planted
|
|
205
|
+
end
|
|
206
|
+
|
|
108
207
|
return StartPlaytestAction
|