@chrrxs/robloxstudio-mcp-inspector 2.16.1 → 2.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +193 -24
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +201 -102
- package/studio-plugin/MCPPlugin.rbxmx +201 -102
- package/studio-plugin/src/modules/ClientBroker.ts +23 -10
- package/studio-plugin/src/modules/Communication.ts +49 -19
- package/studio-plugin/src/modules/ServerUrlSettings.ts +25 -12
- package/studio-plugin/src/modules/StopPlayMonitor.ts +60 -33
package/dist/index.js
CHANGED
|
@@ -27,7 +27,12 @@ function toPublic(inst) {
|
|
|
27
27
|
connectedAt: inst.connectedAt
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
function publishedInstanceId(placeId) {
|
|
31
|
+
if (placeId === void 0 || !Number.isFinite(placeId) || placeId <= 0)
|
|
32
|
+
return void 0;
|
|
33
|
+
return `place:${Math.trunc(placeId)}`;
|
|
34
|
+
}
|
|
35
|
+
var RoutingFailure, STALE_INSTANCE_MS, INSTANCE_ALIAS_TTL_MS, BridgeService;
|
|
31
36
|
var init_bridge_service = __esm({
|
|
32
37
|
"../core/dist/bridge-service.js"() {
|
|
33
38
|
"use strict";
|
|
@@ -40,21 +45,91 @@ var init_bridge_service = __esm({
|
|
|
40
45
|
}
|
|
41
46
|
};
|
|
42
47
|
STALE_INSTANCE_MS = 3e4;
|
|
48
|
+
INSTANCE_ALIAS_TTL_MS = 5 * 60 * 1e3;
|
|
43
49
|
BridgeService = class {
|
|
44
50
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
45
51
|
// Keyed by pluginSessionId (the per-plugin GUID).
|
|
46
52
|
instances = /* @__PURE__ */ new Map();
|
|
53
|
+
instanceAliases = /* @__PURE__ */ new Map();
|
|
47
54
|
requestTimeout = 3e4;
|
|
55
|
+
canonicalInstanceId(instanceId, placeId) {
|
|
56
|
+
return publishedInstanceId(placeId) ?? instanceId;
|
|
57
|
+
}
|
|
58
|
+
rememberInstanceAlias(aliasInstanceId, targetInstanceId) {
|
|
59
|
+
if (aliasInstanceId === targetInstanceId)
|
|
60
|
+
return;
|
|
61
|
+
this.instanceAliases.set(aliasInstanceId, {
|
|
62
|
+
targetInstanceId,
|
|
63
|
+
lastSeen: Date.now()
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
resolveInstanceAlias(instanceId) {
|
|
67
|
+
const alias = this.instanceAliases.get(instanceId);
|
|
68
|
+
if (!alias)
|
|
69
|
+
return instanceId;
|
|
70
|
+
alias.lastSeen = Date.now();
|
|
71
|
+
return alias.targetInstanceId;
|
|
72
|
+
}
|
|
73
|
+
migratePendingRequests(fromInstanceId, toInstanceId) {
|
|
74
|
+
if (fromInstanceId === toInstanceId)
|
|
75
|
+
return;
|
|
76
|
+
for (const request of this.pendingRequests.values()) {
|
|
77
|
+
if (request.targetInstanceId === fromInstanceId) {
|
|
78
|
+
request.targetInstanceId = toInstanceId;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
cleanupStaleAliases(now = Date.now()) {
|
|
83
|
+
for (const [alias, entry] of this.instanceAliases.entries()) {
|
|
84
|
+
const targetIsLive = this.getInstances().some((inst) => inst.instanceId === entry.targetInstanceId);
|
|
85
|
+
if (!targetIsLive && now - entry.lastSeen > INSTANCE_ALIAS_TTL_MS) {
|
|
86
|
+
this.instanceAliases.delete(alias);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
routingKeyForInstance(inst) {
|
|
91
|
+
return publishedInstanceId(inst.placeId) ?? this.resolveInstanceAlias(inst.instanceId);
|
|
92
|
+
}
|
|
93
|
+
matchingInstancesForInstanceId(instanceId) {
|
|
94
|
+
const resolvedInstanceId = this.resolveInstanceAlias(instanceId);
|
|
95
|
+
const ids = /* @__PURE__ */ new Set([instanceId, resolvedInstanceId]);
|
|
96
|
+
const placeIds = /* @__PURE__ */ new Set();
|
|
97
|
+
const addPlaceId = (placeId) => {
|
|
98
|
+
const published = publishedInstanceId(placeId);
|
|
99
|
+
if (!published || placeId === void 0)
|
|
100
|
+
return;
|
|
101
|
+
ids.add(published);
|
|
102
|
+
placeIds.add(Math.trunc(placeId));
|
|
103
|
+
};
|
|
104
|
+
const placeMatch = resolvedInstanceId.match(/^place:(\d+)$/) ?? instanceId.match(/^place:(\d+)$/);
|
|
105
|
+
if (placeMatch)
|
|
106
|
+
addPlaceId(Number(placeMatch[1]));
|
|
107
|
+
for (const inst of this.getInstances()) {
|
|
108
|
+
if (ids.has(inst.instanceId))
|
|
109
|
+
addPlaceId(inst.placeId);
|
|
110
|
+
}
|
|
111
|
+
return this.getInstances().filter((inst) => ids.has(inst.instanceId) || inst.placeId > 0 && placeIds.has(Math.trunc(inst.placeId)));
|
|
112
|
+
}
|
|
113
|
+
resolveInstanceId(instanceId) {
|
|
114
|
+
return this.resolveInstanceAlias(instanceId);
|
|
115
|
+
}
|
|
48
116
|
registerInstance(input) {
|
|
49
|
-
const { pluginSessionId,
|
|
117
|
+
const { pluginSessionId, role } = input;
|
|
118
|
+
const rawInstanceId = input.instanceId;
|
|
119
|
+
const instanceId = this.canonicalInstanceId(rawInstanceId, input.placeId);
|
|
50
120
|
const prior = this.instances.get(pluginSessionId);
|
|
51
121
|
let assignedRole = role;
|
|
52
122
|
const pluginVersion = input.pluginVersion ?? "";
|
|
53
123
|
const pluginVariant = input.pluginVariant ?? "unknown";
|
|
54
124
|
const serverVersion = input.serverVersion ?? "";
|
|
55
125
|
const versionMismatch = pluginVersion !== "" && serverVersion !== "" && pluginVersion !== serverVersion;
|
|
126
|
+
this.rememberInstanceAlias(rawInstanceId, instanceId);
|
|
127
|
+
if (prior && prior.instanceId !== instanceId) {
|
|
128
|
+
this.rememberInstanceAlias(prior.instanceId, instanceId);
|
|
129
|
+
this.migratePendingRequests(prior.instanceId, instanceId);
|
|
130
|
+
}
|
|
56
131
|
if (role === "client") {
|
|
57
|
-
if (prior && prior.
|
|
132
|
+
if (prior && prior.role.match(/^client-\d+$/)) {
|
|
58
133
|
assignedRole = prior.role;
|
|
59
134
|
} else {
|
|
60
135
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -135,6 +210,7 @@ var init_bridge_service = __esm({
|
|
|
135
210
|
const inst = this.instances.get(pluginSessionId);
|
|
136
211
|
if (!inst)
|
|
137
212
|
return;
|
|
213
|
+
const priorInstanceId = inst.instanceId;
|
|
138
214
|
if (metadata.placeId !== void 0)
|
|
139
215
|
inst.placeId = metadata.placeId;
|
|
140
216
|
if (metadata.placeName !== void 0)
|
|
@@ -143,6 +219,15 @@ var init_bridge_service = __esm({
|
|
|
143
219
|
inst.dataModelName = metadata.dataModelName;
|
|
144
220
|
if (metadata.isRunning !== void 0)
|
|
145
221
|
inst.isRunning = metadata.isRunning;
|
|
222
|
+
const canonicalInstanceId = this.canonicalInstanceId(inst.instanceId, inst.placeId);
|
|
223
|
+
if (canonicalInstanceId !== inst.instanceId) {
|
|
224
|
+
const duplicate = Array.from(this.instances.values()).find((other) => other.pluginSessionId !== pluginSessionId && other.instanceId === canonicalInstanceId && other.role === inst.role);
|
|
225
|
+
if (!duplicate) {
|
|
226
|
+
this.rememberInstanceAlias(priorInstanceId, canonicalInstanceId);
|
|
227
|
+
this.migratePendingRequests(priorInstanceId, canonicalInstanceId);
|
|
228
|
+
inst.instanceId = canonicalInstanceId;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
146
231
|
}
|
|
147
232
|
cleanupStaleInstances() {
|
|
148
233
|
const now = Date.now();
|
|
@@ -151,6 +236,37 @@ var init_bridge_service = __esm({
|
|
|
151
236
|
this.unregisterInstance(id);
|
|
152
237
|
}
|
|
153
238
|
}
|
|
239
|
+
this.cleanupStaleAliases(now);
|
|
240
|
+
}
|
|
241
|
+
getEquivalentInstanceIds(instanceId) {
|
|
242
|
+
const resolvedInstanceId = this.resolveInstanceAlias(instanceId);
|
|
243
|
+
const ids = /* @__PURE__ */ new Set([instanceId, resolvedInstanceId]);
|
|
244
|
+
const placeIds = /* @__PURE__ */ new Set();
|
|
245
|
+
const addPlaceId = (placeId) => {
|
|
246
|
+
const published = publishedInstanceId(placeId);
|
|
247
|
+
if (!published || placeId === void 0)
|
|
248
|
+
return;
|
|
249
|
+
ids.add(published);
|
|
250
|
+
placeIds.add(Math.trunc(placeId));
|
|
251
|
+
};
|
|
252
|
+
const placeMatch = resolvedInstanceId.match(/^place:(\d+)$/) ?? instanceId.match(/^place:(\d+)$/);
|
|
253
|
+
if (placeMatch)
|
|
254
|
+
addPlaceId(Number(placeMatch[1]));
|
|
255
|
+
for (const inst of this.getInstances()) {
|
|
256
|
+
if (ids.has(inst.instanceId)) {
|
|
257
|
+
addPlaceId(inst.placeId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const inst of this.getInstances()) {
|
|
261
|
+
if (inst.placeId > 0 && placeIds.has(Math.trunc(inst.placeId))) {
|
|
262
|
+
ids.add(inst.instanceId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [alias, entry] of this.instanceAliases.entries()) {
|
|
266
|
+
if (ids.has(entry.targetInstanceId))
|
|
267
|
+
ids.add(alias);
|
|
268
|
+
}
|
|
269
|
+
return Array.from(ids);
|
|
154
270
|
}
|
|
155
271
|
// Resolves (instance_id, target-role) MCP arguments to a concrete
|
|
156
272
|
// routing decision: either a single (instanceId, role) tuple or a fanout
|
|
@@ -164,7 +280,7 @@ var init_bridge_service = __esm({
|
|
|
164
280
|
const isFanout = target === "all";
|
|
165
281
|
const role = target && target !== "all" ? target : void 0;
|
|
166
282
|
if (instance_id !== void 0) {
|
|
167
|
-
const matchingInstances =
|
|
283
|
+
const matchingInstances = this.matchingInstancesForInstanceId(instance_id);
|
|
168
284
|
if (matchingInstances.length === 0) {
|
|
169
285
|
return {
|
|
170
286
|
ok: false,
|
|
@@ -197,19 +313,19 @@ var init_bridge_service = __esm({
|
|
|
197
313
|
}
|
|
198
314
|
};
|
|
199
315
|
}
|
|
200
|
-
return { ok: true, mode: "single", targetInstanceId:
|
|
316
|
+
return { ok: true, mode: "single", targetInstanceId: exact.instanceId, targetRole: role };
|
|
201
317
|
}
|
|
202
318
|
if (matchingInstances.length === 1) {
|
|
203
319
|
return {
|
|
204
320
|
ok: true,
|
|
205
321
|
mode: "single",
|
|
206
|
-
targetInstanceId:
|
|
322
|
+
targetInstanceId: matchingInstances[0].instanceId,
|
|
207
323
|
targetRole: matchingInstances[0].role
|
|
208
324
|
};
|
|
209
325
|
}
|
|
210
326
|
const edit = matchingInstances.find((i) => i.role === "edit");
|
|
211
327
|
if (edit) {
|
|
212
|
-
return { ok: true, mode: "single", targetInstanceId:
|
|
328
|
+
return { ok: true, mode: "single", targetInstanceId: edit.instanceId, targetRole: "edit" };
|
|
213
329
|
}
|
|
214
330
|
return {
|
|
215
331
|
ok: false,
|
|
@@ -220,7 +336,7 @@ var init_bridge_service = __esm({
|
|
|
220
336
|
}
|
|
221
337
|
};
|
|
222
338
|
}
|
|
223
|
-
const distinctInstanceIds = new Set(instances.map((i) => i
|
|
339
|
+
const distinctInstanceIds = new Set(instances.map((i) => this.routingKeyForInstance(i)));
|
|
224
340
|
if (distinctInstanceIds.size === 0) {
|
|
225
341
|
return {
|
|
226
342
|
ok: false,
|
|
@@ -236,7 +352,7 @@ var init_bridge_service = __esm({
|
|
|
236
352
|
const msg = role ? `target=${role} is ambiguous because multiple Studio places are connected. Pass instance_id to choose a place.` : "Multiple Studio places are connected. Pass instance_id to disambiguate.";
|
|
237
353
|
return { ok: false, error: { code: errorCode, message: msg, data: errorData } };
|
|
238
354
|
}
|
|
239
|
-
const onlyInstanceId =
|
|
355
|
+
const onlyInstanceId = distinctInstanceIds.values().next().value;
|
|
240
356
|
return this.resolveTarget({ instance_id: onlyInstanceId, target });
|
|
241
357
|
}
|
|
242
358
|
async sendRequest(endpoint, data, targetInstanceId, targetRole) {
|
|
@@ -3042,23 +3158,25 @@ var init_tools = __esm({
|
|
|
3042
3158
|
if (!r.ok)
|
|
3043
3159
|
throw new RoutingFailure(r.error);
|
|
3044
3160
|
const resolvedId = r.targetInstanceId;
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3161
|
+
const equivalentIds = new Set(this.bridge.getEquivalentInstanceIds(resolvedId));
|
|
3162
|
+
const instances = this.bridge.getInstances().filter((i) => equivalentIds.has(i.instanceId));
|
|
3163
|
+
const client = instances.filter((inst) => inst.role.startsWith("client")).sort((a, b) => a.role.localeCompare(b.role))[0];
|
|
3164
|
+
return { instanceId: client?.instanceId ?? resolvedId, clientRole: client?.role };
|
|
3048
3165
|
}
|
|
3049
3166
|
_resolveInstanceIdOnly(instance_id) {
|
|
3050
3167
|
const instances = this.bridge.getInstances();
|
|
3051
3168
|
const publicList = this.bridge.getPublicInstances();
|
|
3052
3169
|
const errorData = { instances: publicList, count: publicList.length };
|
|
3053
3170
|
if (instance_id !== void 0) {
|
|
3054
|
-
|
|
3171
|
+
const resolvedInstanceId = this.bridge.resolveInstanceId(instance_id);
|
|
3172
|
+
if (!instances.some((i) => i.instanceId === resolvedInstanceId)) {
|
|
3055
3173
|
throw new RoutingFailure({
|
|
3056
3174
|
code: "unrecognized_instance_id",
|
|
3057
3175
|
message: `instance_id "${instance_id}" is not connected. Pass one from data.instances.`,
|
|
3058
3176
|
data: errorData
|
|
3059
3177
|
});
|
|
3060
3178
|
}
|
|
3061
|
-
return
|
|
3179
|
+
return resolvedInstanceId;
|
|
3062
3180
|
}
|
|
3063
3181
|
const distinct = Array.from(new Set(instances.map((i) => i.instanceId)));
|
|
3064
3182
|
if (distinct.length === 0) {
|
|
@@ -3096,9 +3214,17 @@ var init_tools = __esm({
|
|
|
3096
3214
|
_rolesForInstance(instanceId) {
|
|
3097
3215
|
return this.bridge.getInstances().filter((i) => i.instanceId === instanceId).map((i) => i.role);
|
|
3098
3216
|
}
|
|
3217
|
+
_rolesForEquivalentInstances(instanceId) {
|
|
3218
|
+
const instanceIds = new Set(this.bridge.getEquivalentInstanceIds(instanceId));
|
|
3219
|
+
return this.bridge.getInstances().filter((i) => instanceIds.has(i.instanceId)).map((i) => i.role);
|
|
3220
|
+
}
|
|
3099
3221
|
_clientRolesForInstance(instanceId) {
|
|
3100
3222
|
return this._rolesForInstance(instanceId).filter((role) => /^client-\d+$/.test(role)).sort((a, b) => Number(a.slice("client-".length)) - Number(b.slice("client-".length)));
|
|
3101
3223
|
}
|
|
3224
|
+
_runtimeTargetsForEquivalentInstances(instanceId) {
|
|
3225
|
+
const instanceIds = new Set(this.bridge.getEquivalentInstanceIds(instanceId));
|
|
3226
|
+
return this.bridge.getInstances().filter((i) => instanceIds.has(i.instanceId) && (i.role === "server" || /^client-\d+$/.test(i.role))).map((i) => ({ instanceId: i.instanceId, role: i.role }));
|
|
3227
|
+
}
|
|
3102
3228
|
_resolveDeviceSimulatorSingleTarget(target, instance_id, toolName) {
|
|
3103
3229
|
const selectedTarget = target ?? "edit";
|
|
3104
3230
|
if (selectedTarget === "server" || selectedTarget === "all" || selectedTarget === "all-clients" || selectedTarget === "edit-proxy") {
|
|
@@ -3253,12 +3379,13 @@ var init_tools = __esm({
|
|
|
3253
3379
|
throw new Error(`capture_device_matrix cannot safely restore active custom device "${s.activeDeviceId}". Switch the simulator to default or a built-in preset first, or pass restoreAfter=false only if you intentionally accept changing the simulator state.`);
|
|
3254
3380
|
}
|
|
3255
3381
|
}
|
|
3256
|
-
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30) {
|
|
3382
|
+
async _waitForRuntimeRoles(instanceId, opts, timeoutSec = 30, equivalentInstances = false) {
|
|
3257
3383
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3258
3384
|
while (Date.now() < deadline) {
|
|
3259
|
-
const roles = this._rolesForInstance(instanceId);
|
|
3385
|
+
const roles = equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId);
|
|
3386
|
+
const clientRoles = equivalentInstances ? roles.filter((role) => /^client-\d+$/.test(role)) : this._clientRolesForInstance(instanceId);
|
|
3260
3387
|
const hasServer = !opts.server || roles.includes("server");
|
|
3261
|
-
const hasClients = opts.clientCount === void 0 ||
|
|
3388
|
+
const hasClients = opts.clientCount === void 0 || clientRoles.length >= opts.clientCount;
|
|
3262
3389
|
const absent = opts.absentRole === void 0 || !roles.includes(opts.absentRole);
|
|
3263
3390
|
const runtimeAbsent = !opts.noRuntime || !roles.some((role) => role === "server" || /^client-\d+$/.test(role));
|
|
3264
3391
|
if (hasServer && hasClients && absent && runtimeAbsent) {
|
|
@@ -3266,7 +3393,11 @@ var init_tools = __esm({
|
|
|
3266
3393
|
}
|
|
3267
3394
|
await sleep(250);
|
|
3268
3395
|
}
|
|
3269
|
-
return {
|
|
3396
|
+
return {
|
|
3397
|
+
ok: false,
|
|
3398
|
+
roles: equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId),
|
|
3399
|
+
timedOut: true
|
|
3400
|
+
};
|
|
3270
3401
|
}
|
|
3271
3402
|
async _waitForExactClientCount(instanceId, expectedClientCount, timeoutSec = 30, stableMs = 3e3) {
|
|
3272
3403
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
@@ -3291,10 +3422,11 @@ var init_tools = __esm({
|
|
|
3291
3422
|
const clientCount = this._clientRolesForInstance(instanceId).length;
|
|
3292
3423
|
return { ok: false, roles, timedOut: true, extraClients: clientCount > expectedClientCount, clientCount };
|
|
3293
3424
|
}
|
|
3294
|
-
async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60) {
|
|
3425
|
+
async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60, equivalentInstances = false) {
|
|
3295
3426
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
3296
3427
|
while (Date.now() < deadline) {
|
|
3297
|
-
const
|
|
3428
|
+
const instanceIds = equivalentInstances ? new Set(this.bridge.getEquivalentInstanceIds(instanceId)) : /* @__PURE__ */ new Set([instanceId]);
|
|
3429
|
+
const instances = this.bridge.getInstances().filter((i) => instanceIds.has(i.instanceId));
|
|
3298
3430
|
const roles = instances.map((i) => i.role);
|
|
3299
3431
|
const freshRoles = new Set(instances.filter((i) => i.connectedAt >= connectedAfter).map((i) => i.role));
|
|
3300
3432
|
if (requiredRoles.every((role) => freshRoles.has(role))) {
|
|
@@ -3302,7 +3434,11 @@ var init_tools = __esm({
|
|
|
3302
3434
|
}
|
|
3303
3435
|
await sleep(250);
|
|
3304
3436
|
}
|
|
3305
|
-
return {
|
|
3437
|
+
return {
|
|
3438
|
+
ok: false,
|
|
3439
|
+
roles: equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId),
|
|
3440
|
+
timedOut: true
|
|
3441
|
+
};
|
|
3306
3442
|
}
|
|
3307
3443
|
async getFileTree(path2 = "", instance_id) {
|
|
3308
3444
|
const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
|
|
@@ -4336,7 +4472,7 @@ ${code}`
|
|
|
4336
4472
|
let wait;
|
|
4337
4473
|
if (response?.success === true) {
|
|
4338
4474
|
const requiredRoles = mode === "play" ? ["server", "client-1"] : ["server"];
|
|
4339
|
-
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles);
|
|
4475
|
+
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles, 60, true);
|
|
4340
4476
|
}
|
|
4341
4477
|
const body = wait ? {
|
|
4342
4478
|
...response,
|
|
@@ -4355,10 +4491,27 @@ ${code}`
|
|
|
4355
4491
|
}
|
|
4356
4492
|
async stopPlaytest(instance_id) {
|
|
4357
4493
|
const { instanceId } = this._resolveSingleTarget("edit", instance_id);
|
|
4358
|
-
|
|
4494
|
+
let response;
|
|
4495
|
+
let stopRequestError;
|
|
4496
|
+
try {
|
|
4497
|
+
response = await this.client.request("/api/stop-playtest", {}, instanceId, "edit");
|
|
4498
|
+
} catch (error) {
|
|
4499
|
+
stopRequestError = errorMessage(error);
|
|
4500
|
+
response = {
|
|
4501
|
+
success: false,
|
|
4502
|
+
error: "Edit stop request failed.",
|
|
4503
|
+
detail: stopRequestError
|
|
4504
|
+
};
|
|
4505
|
+
}
|
|
4359
4506
|
let wait;
|
|
4360
4507
|
if (response?.success === true) {
|
|
4361
|
-
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, 15);
|
|
4508
|
+
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, 15, true);
|
|
4509
|
+
} else if (this._runtimeTargetsForEquivalentInstances(instanceId).length > 0) {
|
|
4510
|
+
wait = {
|
|
4511
|
+
ok: false,
|
|
4512
|
+
roles: this._rolesForEquivalentInstances(instanceId),
|
|
4513
|
+
timedOut: false
|
|
4514
|
+
};
|
|
4362
4515
|
}
|
|
4363
4516
|
const body = wait ? {
|
|
4364
4517
|
...response,
|
|
@@ -4366,6 +4519,22 @@ ${code}`
|
|
|
4366
4519
|
timedOut: wait.timedOut,
|
|
4367
4520
|
roles: wait.roles
|
|
4368
4521
|
} : response;
|
|
4522
|
+
if (wait && !wait.ok) {
|
|
4523
|
+
const runtimeRoles = wait.roles.filter((role) => role === "server" || /^client-\d+$/.test(role));
|
|
4524
|
+
const failureBody = {
|
|
4525
|
+
...body,
|
|
4526
|
+
success: false,
|
|
4527
|
+
error: "Playtest teardown did not complete.",
|
|
4528
|
+
message: response?.success === true ? wait.timedOut ? "Stop signal was accepted, but runtime peers did not disconnect before timeout." : "Stop signal was accepted, but runtime peers are still connected." : "Edit stop request failed, and runtime peers are still connected.",
|
|
4529
|
+
stopSignalAccepted: response?.success === true,
|
|
4530
|
+
stopRequestError,
|
|
4531
|
+
runtimeRoles,
|
|
4532
|
+
possibleCause: "A game shutdown hook such as BindToClose may be blocking Studio teardown. No runtime hard-stop or synthetic keyboard fallback was attempted."
|
|
4533
|
+
};
|
|
4534
|
+
return {
|
|
4535
|
+
content: [{ type: "text", text: JSON.stringify(failureBody) }]
|
|
4536
|
+
};
|
|
4537
|
+
}
|
|
4369
4538
|
return {
|
|
4370
4539
|
content: [{ type: "text", text: JSON.stringify(body) }]
|
|
4371
4540
|
};
|
package/package.json
CHANGED