@chrrxs/robloxstudio-mcp-inspector 2.16.2 → 2.16.4
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 +72 -5
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +45 -22
- package/studio-plugin/MCPPlugin.rbxmx +45 -22
- package/studio-plugin/src/modules/ClientBroker.ts +23 -10
- package/studio-plugin/src/modules/Communication.ts +8 -0
- package/studio-plugin/src/modules/StopPlayMonitor.ts +6 -1
package/dist/index.js
CHANGED
|
@@ -3221,6 +3221,20 @@ var init_tools = __esm({
|
|
|
3221
3221
|
_clientRolesForInstance(instanceId) {
|
|
3222
3222
|
return this._rolesForInstance(instanceId).filter((role) => /^client-\d+$/.test(role)).sort((a, b) => Number(a.slice("client-".length)) - Number(b.slice("client-".length)));
|
|
3223
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
|
+
}
|
|
3228
|
+
_compactSimulationResetResult(result) {
|
|
3229
|
+
const compact = {};
|
|
3230
|
+
if ("network" in result)
|
|
3231
|
+
compact.network = true;
|
|
3232
|
+
if ("deviceSimulator" in result)
|
|
3233
|
+
compact.deviceSimulator = true;
|
|
3234
|
+
if (result.errors !== void 0)
|
|
3235
|
+
compact.errors = result.errors;
|
|
3236
|
+
return compact;
|
|
3237
|
+
}
|
|
3224
3238
|
_resolveDeviceSimulatorSingleTarget(target, instance_id, toolName) {
|
|
3225
3239
|
const selectedTarget = target ?? "edit";
|
|
3226
3240
|
if (selectedTarget === "server" || selectedTarget === "all" || selectedTarget === "all-clients" || selectedTarget === "edit-proxy") {
|
|
@@ -4133,10 +4147,12 @@ ${code}`
|
|
|
4133
4147
|
}
|
|
4134
4148
|
return { role, result };
|
|
4135
4149
|
}));
|
|
4150
|
+
const rawRoles = {};
|
|
4136
4151
|
const roles = {};
|
|
4137
4152
|
const failures = [];
|
|
4138
4153
|
for (const entry of roleEntries) {
|
|
4139
|
-
|
|
4154
|
+
rawRoles[entry.role] = entry.result;
|
|
4155
|
+
roles[entry.role] = this._compactSimulationResetResult(entry.result);
|
|
4140
4156
|
const errors = entry.result.errors;
|
|
4141
4157
|
if (errors) {
|
|
4142
4158
|
for (const [kind, message] of Object.entries(errors)) {
|
|
@@ -4145,15 +4161,15 @@ ${code}`
|
|
|
4145
4161
|
}
|
|
4146
4162
|
}
|
|
4147
4163
|
const body = {
|
|
4164
|
+
success: true,
|
|
4148
4165
|
target: resolved.selectedTarget,
|
|
4149
4166
|
network: resetNetwork,
|
|
4150
4167
|
deviceSimulator: resetDeviceSimulator,
|
|
4151
4168
|
roles,
|
|
4152
|
-
warnings: resolved.warnings
|
|
4153
|
-
persistenceNotes: SIMULATION_PERSISTENCE_NOTES
|
|
4169
|
+
warnings: resolved.warnings
|
|
4154
4170
|
};
|
|
4155
4171
|
if (failures.length > 0) {
|
|
4156
|
-
throw new Error(`reset_simulation_state failed for ${failures.join("; ")}. Partial result: ${JSON.stringify(body)}`);
|
|
4172
|
+
throw new Error(`reset_simulation_state failed for ${failures.join("; ")}. Partial result: ${JSON.stringify({ ...body, roles: rawRoles })}`);
|
|
4157
4173
|
}
|
|
4158
4174
|
return {
|
|
4159
4175
|
content: [{
|
|
@@ -4464,6 +4480,24 @@ ${code}`
|
|
|
4464
4480
|
}
|
|
4465
4481
|
});
|
|
4466
4482
|
}
|
|
4483
|
+
const existingRuntime = this._runtimeTargetsForEquivalentInstances(resolved.targetInstanceId);
|
|
4484
|
+
if (existingRuntime.length > 0) {
|
|
4485
|
+
const roles = this._rolesForEquivalentInstances(resolved.targetInstanceId);
|
|
4486
|
+
return {
|
|
4487
|
+
content: [{
|
|
4488
|
+
type: "text",
|
|
4489
|
+
text: JSON.stringify({
|
|
4490
|
+
success: false,
|
|
4491
|
+
error: "Playtest already running.",
|
|
4492
|
+
message: "A playtest is already running for this Studio place. Stop the current playtest before starting another.",
|
|
4493
|
+
runtimeReady: true,
|
|
4494
|
+
timedOut: false,
|
|
4495
|
+
roles,
|
|
4496
|
+
runtimeRoles: existingRuntime.map((target) => target.role)
|
|
4497
|
+
})
|
|
4498
|
+
}]
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4467
4501
|
const response = await this.client.request("/api/start-playtest", data, resolved.targetInstanceId, resolved.targetRole);
|
|
4468
4502
|
let wait;
|
|
4469
4503
|
if (response?.success === true) {
|
|
@@ -4487,10 +4521,27 @@ ${code}`
|
|
|
4487
4521
|
}
|
|
4488
4522
|
async stopPlaytest(instance_id) {
|
|
4489
4523
|
const { instanceId } = this._resolveSingleTarget("edit", instance_id);
|
|
4490
|
-
|
|
4524
|
+
let response;
|
|
4525
|
+
let stopRequestError;
|
|
4526
|
+
try {
|
|
4527
|
+
response = await this.client.request("/api/stop-playtest", {}, instanceId, "edit");
|
|
4528
|
+
} catch (error) {
|
|
4529
|
+
stopRequestError = errorMessage(error);
|
|
4530
|
+
response = {
|
|
4531
|
+
success: false,
|
|
4532
|
+
error: "Edit stop request failed.",
|
|
4533
|
+
detail: stopRequestError
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4491
4536
|
let wait;
|
|
4492
4537
|
if (response?.success === true) {
|
|
4493
4538
|
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, 15, true);
|
|
4539
|
+
} else if (this._runtimeTargetsForEquivalentInstances(instanceId).length > 0) {
|
|
4540
|
+
wait = {
|
|
4541
|
+
ok: false,
|
|
4542
|
+
roles: this._rolesForEquivalentInstances(instanceId),
|
|
4543
|
+
timedOut: false
|
|
4544
|
+
};
|
|
4494
4545
|
}
|
|
4495
4546
|
const body = wait ? {
|
|
4496
4547
|
...response,
|
|
@@ -4498,6 +4549,22 @@ ${code}`
|
|
|
4498
4549
|
timedOut: wait.timedOut,
|
|
4499
4550
|
roles: wait.roles
|
|
4500
4551
|
} : response;
|
|
4552
|
+
if (wait && !wait.ok) {
|
|
4553
|
+
const runtimeRoles = wait.roles.filter((role) => role === "server" || /^client-\d+$/.test(role));
|
|
4554
|
+
const failureBody = {
|
|
4555
|
+
...body,
|
|
4556
|
+
success: false,
|
|
4557
|
+
error: "Playtest teardown did not complete.",
|
|
4558
|
+
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.",
|
|
4559
|
+
stopSignalAccepted: response?.success === true,
|
|
4560
|
+
stopRequestError,
|
|
4561
|
+
runtimeRoles,
|
|
4562
|
+
possibleCause: "A game shutdown hook such as BindToClose may be blocking Studio teardown. No runtime hard-stop or synthetic keyboard fallback was attempted."
|
|
4563
|
+
};
|
|
4564
|
+
return {
|
|
4565
|
+
content: [{ type: "text", text: JSON.stringify(failureBody) }]
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4501
4568
|
return {
|
|
4502
4569
|
content: [{ type: "text", text: JSON.stringify(body) }]
|
|
4503
4570
|
};
|
package/package.json
CHANGED
|
@@ -432,6 +432,31 @@ end
|
|
|
432
432
|
local proxyByPlayer = {}
|
|
433
433
|
local proxyRegisterFailuresByPlayer = {}
|
|
434
434
|
local serverBrokerStarted = false
|
|
435
|
+
local function unregisterProxy(player, entry)
|
|
436
|
+
local _condition = entry
|
|
437
|
+
if _condition == nil then
|
|
438
|
+
local _player = player
|
|
439
|
+
_condition = proxyByPlayer[_player]
|
|
440
|
+
end
|
|
441
|
+
local proxy = _condition
|
|
442
|
+
if not proxy then
|
|
443
|
+
return nil
|
|
444
|
+
end
|
|
445
|
+
local _player = player
|
|
446
|
+
proxyByPlayer[_player] = nil
|
|
447
|
+
local _player_1 = player
|
|
448
|
+
proxyRegisterFailuresByPlayer[_player_1] = nil
|
|
449
|
+
postJson("/disconnect", {
|
|
450
|
+
pluginSessionId = proxy.pluginSessionId,
|
|
451
|
+
})
|
|
452
|
+
end
|
|
453
|
+
local function disconnectAllProxies()
|
|
454
|
+
for player, entry in proxyByPlayer do
|
|
455
|
+
unregisterProxy(player, entry)
|
|
456
|
+
end
|
|
457
|
+
table.clear(proxyByPlayer)
|
|
458
|
+
table.clear(proxyRegisterFailuresByPlayer)
|
|
459
|
+
end
|
|
435
460
|
local function pollProxy(proxyId, player, rf)
|
|
436
461
|
while true do
|
|
437
462
|
local _condition = player.Parent ~= nil
|
|
@@ -442,6 +467,10 @@ local function pollProxy(proxyId, player, rf)
|
|
|
442
467
|
if not _condition then
|
|
443
468
|
break
|
|
444
469
|
end
|
|
470
|
+
if not RunService:IsRunning() then
|
|
471
|
+
unregisterProxy(player)
|
|
472
|
+
break
|
|
473
|
+
end
|
|
445
474
|
local ok, res = pcall(function()
|
|
446
475
|
return HttpService:RequestAsync({
|
|
447
476
|
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
@@ -575,25 +604,10 @@ local function setupServerBroker()
|
|
|
575
604
|
task.spawn(registerProxy, p, broker)
|
|
576
605
|
end
|
|
577
606
|
Players.PlayerRemoving:Connect(function(p)
|
|
578
|
-
|
|
579
|
-
local entry = proxyByPlayer[_p]
|
|
580
|
-
if entry then
|
|
581
|
-
local _p_1 = p
|
|
582
|
-
proxyByPlayer[_p_1] = nil
|
|
583
|
-
local _p_2 = p
|
|
584
|
-
proxyRegisterFailuresByPlayer[_p_2] = nil
|
|
585
|
-
postJson("/disconnect", {
|
|
586
|
-
pluginSessionId = entry.pluginSessionId,
|
|
587
|
-
})
|
|
588
|
-
end
|
|
607
|
+
unregisterProxy(p)
|
|
589
608
|
end)
|
|
590
609
|
game:BindToClose(function()
|
|
591
|
-
|
|
592
|
-
postJson("/disconnect", {
|
|
593
|
-
pluginSessionId = entry.pluginSessionId,
|
|
594
|
-
})
|
|
595
|
-
end
|
|
596
|
-
table.clear(proxyByPlayer)
|
|
610
|
+
disconnectAllProxies()
|
|
597
611
|
end)
|
|
598
612
|
end
|
|
599
613
|
return {
|
|
@@ -601,6 +615,7 @@ return {
|
|
|
601
615
|
DEFAULT_MCP_URL = DEFAULT_MCP_URL,
|
|
602
616
|
getServerUrl = getServerUrl,
|
|
603
617
|
setServerUrl = setServerUrl,
|
|
618
|
+
disconnectAllProxies = disconnectAllProxies,
|
|
604
619
|
forkRole = forkRole,
|
|
605
620
|
setupClientBroker = setupClientBroker,
|
|
606
621
|
setupServerBroker = setupServerBroker,
|
|
@@ -636,6 +651,7 @@ local SerializationHandlers = TS.import(script, script.Parent, "handlers", "Seri
|
|
|
636
651
|
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
637
652
|
local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
|
|
638
653
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
654
|
+
local ClientBroker = TS.import(script, script.Parent, "ClientBroker")
|
|
639
655
|
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
640
656
|
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
641
657
|
-- Per-plugin-load random GUID. Used as the /poll URL param so the server
|
|
@@ -708,6 +724,7 @@ local function detectRole()
|
|
|
708
724
|
end
|
|
709
725
|
return "client"
|
|
710
726
|
end
|
|
727
|
+
local initialRole = detectRole()
|
|
711
728
|
local routeMap = {
|
|
712
729
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
713
730
|
["/api/search-files"] = QueryHandlers.searchFiles,
|
|
@@ -1125,6 +1142,7 @@ local function pollForRequests(connIndex)
|
|
|
1125
1142
|
end
|
|
1126
1143
|
end
|
|
1127
1144
|
end
|
|
1145
|
+
local deactivatePlugin
|
|
1128
1146
|
local function activatePlugin(connIndex)
|
|
1129
1147
|
local _condition = connIndex
|
|
1130
1148
|
if _condition == nil then
|
|
@@ -1157,6 +1175,11 @@ local function activatePlugin(connIndex)
|
|
|
1157
1175
|
if not conn.heartbeatConnection then
|
|
1158
1176
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
1159
1177
|
local now = tick()
|
|
1178
|
+
if initialRole == "server" and not RunService:IsRunning() then
|
|
1179
|
+
ClientBroker.disconnectAllProxies()
|
|
1180
|
+
deactivatePlugin(idx)
|
|
1181
|
+
return nil
|
|
1182
|
+
end
|
|
1160
1183
|
local currentInstanceId = computeInstanceId()
|
|
1161
1184
|
if lastReadyInstanceId ~= nil and currentInstanceId ~= lastReadyInstanceId then
|
|
1162
1185
|
cachedPlaceName = nil
|
|
@@ -1181,7 +1204,7 @@ local function activatePlugin(connIndex)
|
|
|
1181
1204
|
-- Watch identity fields so stale name or anon instance ids are refreshed.
|
|
1182
1205
|
ensureIdentityWatcher(conn)
|
|
1183
1206
|
end
|
|
1184
|
-
|
|
1207
|
+
function deactivatePlugin(connIndex)
|
|
1185
1208
|
local _condition = connIndex
|
|
1186
1209
|
if _condition == nil then
|
|
1187
1210
|
_condition = State.getActiveTabIndex()
|
|
@@ -1388,9 +1411,9 @@ local function computeBridgeStamp()
|
|
|
1388
1411
|
for i = 1, #combined do
|
|
1389
1412
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1390
1413
|
end
|
|
1391
|
-
-- "2.16.
|
|
1414
|
+
-- "2.16.4" is replaced with the package version at package time
|
|
1392
1415
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1393
|
-
return `{tostring(h)}-2.16.
|
|
1416
|
+
return `{tostring(h)}-2.16.4`
|
|
1394
1417
|
end
|
|
1395
1418
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1396
1419
|
local function setSource(scriptInst, source)
|
|
@@ -8018,7 +8041,7 @@ return {
|
|
|
8018
8041
|
<Properties>
|
|
8019
8042
|
<string name="Name">State</string>
|
|
8020
8043
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8021
|
-
local CURRENT_VERSION = "2.16.
|
|
8044
|
+
local CURRENT_VERSION = "2.16.4"
|
|
8022
8045
|
local PLUGIN_VARIANT = "inspector"
|
|
8023
8046
|
local MAX_CONNECTIONS = 5
|
|
8024
8047
|
local BASE_PORT = 58741
|
|
@@ -8297,7 +8320,7 @@ local function handleStopRequest(key, request)
|
|
|
8297
8320
|
return nil
|
|
8298
8321
|
end
|
|
8299
8322
|
if endTestIssued then
|
|
8300
|
-
writeResult(key, request,
|
|
8323
|
+
writeResult(key, request, false, "StudioTestService:EndTest was already issued for this play session, but the runtime DataModel is still alive.")
|
|
8301
8324
|
return nil
|
|
8302
8325
|
end
|
|
8303
8326
|
if not RunService:IsRunning() or not RunService:IsServer() then
|
|
@@ -432,6 +432,31 @@ end
|
|
|
432
432
|
local proxyByPlayer = {}
|
|
433
433
|
local proxyRegisterFailuresByPlayer = {}
|
|
434
434
|
local serverBrokerStarted = false
|
|
435
|
+
local function unregisterProxy(player, entry)
|
|
436
|
+
local _condition = entry
|
|
437
|
+
if _condition == nil then
|
|
438
|
+
local _player = player
|
|
439
|
+
_condition = proxyByPlayer[_player]
|
|
440
|
+
end
|
|
441
|
+
local proxy = _condition
|
|
442
|
+
if not proxy then
|
|
443
|
+
return nil
|
|
444
|
+
end
|
|
445
|
+
local _player = player
|
|
446
|
+
proxyByPlayer[_player] = nil
|
|
447
|
+
local _player_1 = player
|
|
448
|
+
proxyRegisterFailuresByPlayer[_player_1] = nil
|
|
449
|
+
postJson("/disconnect", {
|
|
450
|
+
pluginSessionId = proxy.pluginSessionId,
|
|
451
|
+
})
|
|
452
|
+
end
|
|
453
|
+
local function disconnectAllProxies()
|
|
454
|
+
for player, entry in proxyByPlayer do
|
|
455
|
+
unregisterProxy(player, entry)
|
|
456
|
+
end
|
|
457
|
+
table.clear(proxyByPlayer)
|
|
458
|
+
table.clear(proxyRegisterFailuresByPlayer)
|
|
459
|
+
end
|
|
435
460
|
local function pollProxy(proxyId, player, rf)
|
|
436
461
|
while true do
|
|
437
462
|
local _condition = player.Parent ~= nil
|
|
@@ -442,6 +467,10 @@ local function pollProxy(proxyId, player, rf)
|
|
|
442
467
|
if not _condition then
|
|
443
468
|
break
|
|
444
469
|
end
|
|
470
|
+
if not RunService:IsRunning() then
|
|
471
|
+
unregisterProxy(player)
|
|
472
|
+
break
|
|
473
|
+
end
|
|
445
474
|
local ok, res = pcall(function()
|
|
446
475
|
return HttpService:RequestAsync({
|
|
447
476
|
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
@@ -575,25 +604,10 @@ local function setupServerBroker()
|
|
|
575
604
|
task.spawn(registerProxy, p, broker)
|
|
576
605
|
end
|
|
577
606
|
Players.PlayerRemoving:Connect(function(p)
|
|
578
|
-
|
|
579
|
-
local entry = proxyByPlayer[_p]
|
|
580
|
-
if entry then
|
|
581
|
-
local _p_1 = p
|
|
582
|
-
proxyByPlayer[_p_1] = nil
|
|
583
|
-
local _p_2 = p
|
|
584
|
-
proxyRegisterFailuresByPlayer[_p_2] = nil
|
|
585
|
-
postJson("/disconnect", {
|
|
586
|
-
pluginSessionId = entry.pluginSessionId,
|
|
587
|
-
})
|
|
588
|
-
end
|
|
607
|
+
unregisterProxy(p)
|
|
589
608
|
end)
|
|
590
609
|
game:BindToClose(function()
|
|
591
|
-
|
|
592
|
-
postJson("/disconnect", {
|
|
593
|
-
pluginSessionId = entry.pluginSessionId,
|
|
594
|
-
})
|
|
595
|
-
end
|
|
596
|
-
table.clear(proxyByPlayer)
|
|
610
|
+
disconnectAllProxies()
|
|
597
611
|
end)
|
|
598
612
|
end
|
|
599
613
|
return {
|
|
@@ -601,6 +615,7 @@ return {
|
|
|
601
615
|
DEFAULT_MCP_URL = DEFAULT_MCP_URL,
|
|
602
616
|
getServerUrl = getServerUrl,
|
|
603
617
|
setServerUrl = setServerUrl,
|
|
618
|
+
disconnectAllProxies = disconnectAllProxies,
|
|
604
619
|
forkRole = forkRole,
|
|
605
620
|
setupClientBroker = setupClientBroker,
|
|
606
621
|
setupServerBroker = setupServerBroker,
|
|
@@ -636,6 +651,7 @@ local SerializationHandlers = TS.import(script, script.Parent, "handlers", "Seri
|
|
|
636
651
|
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
637
652
|
local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
|
|
638
653
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
654
|
+
local ClientBroker = TS.import(script, script.Parent, "ClientBroker")
|
|
639
655
|
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
640
656
|
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
641
657
|
-- Per-plugin-load random GUID. Used as the /poll URL param so the server
|
|
@@ -708,6 +724,7 @@ local function detectRole()
|
|
|
708
724
|
end
|
|
709
725
|
return "client"
|
|
710
726
|
end
|
|
727
|
+
local initialRole = detectRole()
|
|
711
728
|
local routeMap = {
|
|
712
729
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
713
730
|
["/api/search-files"] = QueryHandlers.searchFiles,
|
|
@@ -1125,6 +1142,7 @@ local function pollForRequests(connIndex)
|
|
|
1125
1142
|
end
|
|
1126
1143
|
end
|
|
1127
1144
|
end
|
|
1145
|
+
local deactivatePlugin
|
|
1128
1146
|
local function activatePlugin(connIndex)
|
|
1129
1147
|
local _condition = connIndex
|
|
1130
1148
|
if _condition == nil then
|
|
@@ -1157,6 +1175,11 @@ local function activatePlugin(connIndex)
|
|
|
1157
1175
|
if not conn.heartbeatConnection then
|
|
1158
1176
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
1159
1177
|
local now = tick()
|
|
1178
|
+
if initialRole == "server" and not RunService:IsRunning() then
|
|
1179
|
+
ClientBroker.disconnectAllProxies()
|
|
1180
|
+
deactivatePlugin(idx)
|
|
1181
|
+
return nil
|
|
1182
|
+
end
|
|
1160
1183
|
local currentInstanceId = computeInstanceId()
|
|
1161
1184
|
if lastReadyInstanceId ~= nil and currentInstanceId ~= lastReadyInstanceId then
|
|
1162
1185
|
cachedPlaceName = nil
|
|
@@ -1181,7 +1204,7 @@ local function activatePlugin(connIndex)
|
|
|
1181
1204
|
-- Watch identity fields so stale name or anon instance ids are refreshed.
|
|
1182
1205
|
ensureIdentityWatcher(conn)
|
|
1183
1206
|
end
|
|
1184
|
-
|
|
1207
|
+
function deactivatePlugin(connIndex)
|
|
1185
1208
|
local _condition = connIndex
|
|
1186
1209
|
if _condition == nil then
|
|
1187
1210
|
_condition = State.getActiveTabIndex()
|
|
@@ -1388,9 +1411,9 @@ local function computeBridgeStamp()
|
|
|
1388
1411
|
for i = 1, #combined do
|
|
1389
1412
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1390
1413
|
end
|
|
1391
|
-
-- "2.16.
|
|
1414
|
+
-- "2.16.4" is replaced with the package version at package time
|
|
1392
1415
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1393
|
-
return `{tostring(h)}-2.16.
|
|
1416
|
+
return `{tostring(h)}-2.16.4`
|
|
1394
1417
|
end
|
|
1395
1418
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1396
1419
|
local function setSource(scriptInst, source)
|
|
@@ -8018,7 +8041,7 @@ return {
|
|
|
8018
8041
|
<Properties>
|
|
8019
8042
|
<string name="Name">State</string>
|
|
8020
8043
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
8021
|
-
local CURRENT_VERSION = "2.16.
|
|
8044
|
+
local CURRENT_VERSION = "2.16.4"
|
|
8022
8045
|
local PLUGIN_VARIANT = "main"
|
|
8023
8046
|
local MAX_CONNECTIONS = 5
|
|
8024
8047
|
local BASE_PORT = 58741
|
|
@@ -8297,7 +8320,7 @@ local function handleStopRequest(key, request)
|
|
|
8297
8320
|
return nil
|
|
8298
8321
|
end
|
|
8299
8322
|
if endTestIssued then
|
|
8300
|
-
writeResult(key, request,
|
|
8323
|
+
writeResult(key, request, false, "StudioTestService:EndTest was already issued for this play session, but the runtime DataModel is still alive.")
|
|
8301
8324
|
return nil
|
|
8302
8325
|
end
|
|
8303
8326
|
if not RunService:IsRunning() or not RunService:IsServer() then
|
|
@@ -296,8 +296,28 @@ const proxyByPlayer = new Map<Player, ProxyEntry>();
|
|
|
296
296
|
const proxyRegisterFailuresByPlayer = new Set<Player>();
|
|
297
297
|
let serverBrokerStarted = false;
|
|
298
298
|
|
|
299
|
+
function unregisterProxy(player: Player, entry?: ProxyEntry): void {
|
|
300
|
+
const proxy = entry ?? proxyByPlayer.get(player);
|
|
301
|
+
if (!proxy) return;
|
|
302
|
+
proxyByPlayer.delete(player);
|
|
303
|
+
proxyRegisterFailuresByPlayer.delete(player);
|
|
304
|
+
postJson("/disconnect", { pluginSessionId: proxy.pluginSessionId });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function disconnectAllProxies(): void {
|
|
308
|
+
for (const [player, entry] of proxyByPlayer) {
|
|
309
|
+
unregisterProxy(player, entry);
|
|
310
|
+
}
|
|
311
|
+
proxyByPlayer.clear();
|
|
312
|
+
proxyRegisterFailuresByPlayer.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
299
315
|
function pollProxy(proxyId: string, player: Player, rf: RemoteFunction) {
|
|
300
316
|
while (player.Parent !== undefined && proxyByPlayer.has(player)) {
|
|
317
|
+
if (!RunService.IsRunning()) {
|
|
318
|
+
unregisterProxy(player);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
301
321
|
const [ok, res] = pcall(() =>
|
|
302
322
|
HttpService.RequestAsync({
|
|
303
323
|
Url: `${mcpUrl}/poll?pluginSessionId=${proxyId}`,
|
|
@@ -396,18 +416,10 @@ function setupServerBroker() {
|
|
|
396
416
|
task.spawn(registerProxy, p, broker);
|
|
397
417
|
}
|
|
398
418
|
Players.PlayerRemoving.Connect((p) => {
|
|
399
|
-
|
|
400
|
-
if (entry) {
|
|
401
|
-
proxyByPlayer.delete(p);
|
|
402
|
-
proxyRegisterFailuresByPlayer.delete(p);
|
|
403
|
-
postJson("/disconnect", { pluginSessionId: entry.pluginSessionId });
|
|
404
|
-
}
|
|
419
|
+
unregisterProxy(p);
|
|
405
420
|
});
|
|
406
421
|
game.BindToClose(() => {
|
|
407
|
-
|
|
408
|
-
postJson("/disconnect", { pluginSessionId: entry.pluginSessionId });
|
|
409
|
-
}
|
|
410
|
-
proxyByPlayer.clear();
|
|
422
|
+
disconnectAllProxies();
|
|
411
423
|
});
|
|
412
424
|
}
|
|
413
425
|
|
|
@@ -416,6 +428,7 @@ export = {
|
|
|
416
428
|
DEFAULT_MCP_URL,
|
|
417
429
|
getServerUrl,
|
|
418
430
|
setServerUrl,
|
|
431
|
+
disconnectAllProxies,
|
|
419
432
|
forkRole,
|
|
420
433
|
setupClientBroker,
|
|
421
434
|
setupServerBroker,
|
|
@@ -18,6 +18,7 @@ import SerializationHandlers from "./handlers/SerializationHandlers";
|
|
|
18
18
|
import MemoryHandlers from "./handlers/MemoryHandlers";
|
|
19
19
|
import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
|
|
20
20
|
import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
|
|
21
|
+
import ClientBroker from "./ClientBroker";
|
|
21
22
|
import ServerUrlSettings from "./ServerUrlSettings";
|
|
22
23
|
import HttpDiagnostics from "./HttpDiagnostics";
|
|
23
24
|
import { Connection, RequestPayload, PollResponse, ReadyResponse } from "../types";
|
|
@@ -89,6 +90,8 @@ function detectRole(): string {
|
|
|
89
90
|
return "client";
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
const initialRole = detectRole();
|
|
94
|
+
|
|
92
95
|
type Handler = (data: Record<string, unknown>) => unknown;
|
|
93
96
|
|
|
94
97
|
const routeMap: Record<string, Handler> = {
|
|
@@ -510,6 +513,11 @@ function activatePlugin(connIndex?: number) {
|
|
|
510
513
|
if (!conn.heartbeatConnection) {
|
|
511
514
|
conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
|
|
512
515
|
const now = tick();
|
|
516
|
+
if (initialRole === "server" && !RunService.IsRunning()) {
|
|
517
|
+
ClientBroker.disconnectAllProxies();
|
|
518
|
+
deactivatePlugin(idx);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
513
521
|
const currentInstanceId = computeInstanceId();
|
|
514
522
|
if (lastReadyInstanceId !== undefined && currentInstanceId !== lastReadyInstanceId) {
|
|
515
523
|
cachedPlaceName = undefined;
|
|
@@ -162,7 +162,12 @@ function handleStopRequest(key: string, request: StopPayload): void {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
if (endTestIssued) {
|
|
165
|
-
writeResult(
|
|
165
|
+
writeResult(
|
|
166
|
+
key,
|
|
167
|
+
request,
|
|
168
|
+
false,
|
|
169
|
+
"StudioTestService:EndTest was already issued for this play session, but the runtime DataModel is still alive.",
|
|
170
|
+
);
|
|
166
171
|
return;
|
|
167
172
|
}
|
|
168
173
|
|