@chrrxs/robloxstudio-mcp 2.16.0 → 2.16.1
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 +56 -15
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +446 -107
- package/studio-plugin/MCPPlugin.rbxmx +446 -107
- package/studio-plugin/src/modules/ClientBroker.ts +32 -6
- package/studio-plugin/src/modules/Communication.ts +40 -22
- package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
- package/studio-plugin/src/modules/ServerUrlSettings.ts +48 -0
- package/studio-plugin/src/modules/StopPlayMonitor.ts +152 -35
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +18 -13
- package/studio-plugin/src/server/index.server.ts +15 -4
|
@@ -10,6 +10,7 @@ local State = TS.import(script, script, "modules", "State")
|
|
|
10
10
|
local UI = TS.import(script, script, "modules", "UI")
|
|
11
11
|
local Communication = TS.import(script, script, "modules", "Communication")
|
|
12
12
|
local ClientBroker = TS.import(script, script, "modules", "ClientBroker")
|
|
13
|
+
local ServerUrlSettings = TS.import(script, script, "modules", "ServerUrlSettings")
|
|
13
14
|
local _EvalBridges = TS.import(script, script, "modules", "EvalBridges")
|
|
14
15
|
local cleanupLegacyEditBridges = _EvalBridges.cleanupLegacyEditBridges
|
|
15
16
|
local ensureRuntimeBridgeInstalled = _EvalBridges.ensureRuntimeBridgeInstalled
|
|
@@ -28,6 +29,7 @@ RuntimeLogBuffer.install()
|
|
|
28
29
|
-- edit DM (write the flag) and the play-server DM (read+act on the flag) can
|
|
29
30
|
-- access plugin:SetSetting/GetSetting.
|
|
30
31
|
StopPlayMonitor.init(plugin)
|
|
32
|
+
ServerUrlSettings.init(plugin)
|
|
31
33
|
UI.init(plugin)
|
|
32
34
|
local elements = UI.getElements()
|
|
33
35
|
local ICON_DISCONNECTED = "rbxassetid://75876056391496"
|
|
@@ -76,7 +78,7 @@ task.delay(2, function()
|
|
|
76
78
|
else
|
|
77
79
|
local result = ensureRuntimeBridgeInstalled()
|
|
78
80
|
if not result.installed then
|
|
79
|
-
warn(`[
|
|
81
|
+
warn(`[robloxstudio-mcp] Runtime eval bridge install failed: {result.error}`)
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
84
|
if role == "edit" or role == "server" then
|
|
@@ -84,10 +86,29 @@ task.delay(2, function()
|
|
|
84
86
|
local idx = State.getActiveTabIndex()
|
|
85
87
|
local conn = State.getConnection(idx)
|
|
86
88
|
if conn and not conn.isActive then
|
|
89
|
+
if role == "server" then
|
|
90
|
+
local _condition = ServerUrlSettings.readServerUrl()
|
|
91
|
+
if _condition == nil then
|
|
92
|
+
_condition = ClientBroker.DEFAULT_MCP_URL
|
|
93
|
+
end
|
|
94
|
+
local inheritedServerUrl = _condition
|
|
95
|
+
conn.serverUrl = inheritedServerUrl
|
|
96
|
+
elements.urlInput.Text = inheritedServerUrl
|
|
97
|
+
local portStr = string.match(conn.serverUrl, ":(%d+)$")
|
|
98
|
+
if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
|
|
99
|
+
local _condition_1 = tonumber(portStr)
|
|
100
|
+
if _condition_1 == nil then
|
|
101
|
+
_condition_1 = conn.port
|
|
102
|
+
end
|
|
103
|
+
conn.port = _condition_1
|
|
104
|
+
end
|
|
105
|
+
ClientBroker.setServerUrl(inheritedServerUrl)
|
|
106
|
+
end
|
|
87
107
|
-- Defensive default: in invisible play-DM UIs, the input field
|
|
88
108
|
-- may not be populated by the time we activate.
|
|
89
109
|
if conn.serverUrl == nil or conn.serverUrl == "" then
|
|
90
|
-
conn.serverUrl = ClientBroker.
|
|
110
|
+
conn.serverUrl = ClientBroker.DEFAULT_MCP_URL
|
|
111
|
+
elements.urlInput.Text = conn.serverUrl
|
|
91
112
|
end
|
|
92
113
|
Communication.activatePlugin(idx)
|
|
93
114
|
end
|
|
@@ -96,8 +117,8 @@ task.delay(2, function()
|
|
|
96
117
|
if role == "server" then
|
|
97
118
|
ClientBroker.setupServerBroker()
|
|
98
119
|
-- The play-server DM is the only one where StudioTestService:EndTest is
|
|
99
|
-
-- legal, so the stop-play monitor lives here.
|
|
100
|
-
--
|
|
120
|
+
-- legal, so the stop-play monitor lives here. It consumes tokenized
|
|
121
|
+
-- stop requests from plugin settings and acknowledges EndTest results.
|
|
101
122
|
StopPlayMonitor.startMonitor()
|
|
102
123
|
elseif role == "client" then
|
|
103
124
|
ClientBroker.setupClientBroker()
|
|
@@ -128,6 +149,7 @@ local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandler
|
|
|
128
149
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
129
150
|
local LuauExec = TS.import(script, script.Parent, "LuauExec")
|
|
130
151
|
local State = TS.import(script, script.Parent, "State")
|
|
152
|
+
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
131
153
|
local StudioTestService = game:GetService("StudioTestService")
|
|
132
154
|
-- Mirror of Communication.computeInstanceId() — duplicated here because the
|
|
133
155
|
-- client broker runs in the play-server DM where it can't easily import from
|
|
@@ -180,7 +202,8 @@ end
|
|
|
180
202
|
-- intercept /api/stop-playtest and call StudioTestService:EndTest. That hack
|
|
181
203
|
-- is gone: stop now uses StopPlayMonitor with plugin:SetSetting cross-DM
|
|
182
204
|
-- signaling, which works regardless of MCP server state.)
|
|
183
|
-
local
|
|
205
|
+
local DEFAULT_MCP_URL = "http://localhost:58741"
|
|
206
|
+
local mcpUrl = DEFAULT_MCP_URL
|
|
184
207
|
local BROKER_NAME = "__MCPClientBroker"
|
|
185
208
|
local BROKER_OWNER_ATTRIBUTE = "__MCPBrokerOwner"
|
|
186
209
|
-- Endpoints the server-peer broker is allowed to forward to the client peer.
|
|
@@ -241,7 +264,7 @@ end
|
|
|
241
264
|
function postJson(endpoint, body)
|
|
242
265
|
return pcall(function()
|
|
243
266
|
return HttpService:RequestAsync({
|
|
244
|
-
Url = `{
|
|
267
|
+
Url = `{mcpUrl}{endpoint}`,
|
|
245
268
|
Method = "POST",
|
|
246
269
|
Headers = {
|
|
247
270
|
["Content-Type"] = "application/json",
|
|
@@ -250,6 +273,17 @@ function postJson(endpoint, body)
|
|
|
250
273
|
})
|
|
251
274
|
end)
|
|
252
275
|
end
|
|
276
|
+
local function formatPostJsonFailure(endpoint, ok, res)
|
|
277
|
+
return HttpDiagnostics.formatRequestFailure(`{mcpUrl}{endpoint}`, ok, res)
|
|
278
|
+
end
|
|
279
|
+
local function setServerUrl(serverUrl)
|
|
280
|
+
if serverUrl ~= nil and serverUrl ~= "" then
|
|
281
|
+
mcpUrl = serverUrl
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
local function getServerUrl()
|
|
285
|
+
return mcpUrl
|
|
286
|
+
end
|
|
253
287
|
local function handleExecuteLuau(data)
|
|
254
288
|
local code = data and (data.code)
|
|
255
289
|
if type(code) == "string" == false or code == "" then
|
|
@@ -396,6 +430,7 @@ local function setupClientBroker()
|
|
|
396
430
|
end
|
|
397
431
|
end
|
|
398
432
|
local proxyByPlayer = {}
|
|
433
|
+
local proxyRegisterFailuresByPlayer = {}
|
|
399
434
|
local serverBrokerStarted = false
|
|
400
435
|
local function pollProxy(proxyId, player, rf)
|
|
401
436
|
while true do
|
|
@@ -409,7 +444,7 @@ local function pollProxy(proxyId, player, rf)
|
|
|
409
444
|
end
|
|
410
445
|
local ok, res = pcall(function()
|
|
411
446
|
return HttpService:RequestAsync({
|
|
412
|
-
Url = `{
|
|
447
|
+
Url = `{mcpUrl}/poll?pluginSessionId={proxyId}`,
|
|
413
448
|
Method = "GET",
|
|
414
449
|
Headers = {
|
|
415
450
|
["Content-Type"] = "application/json",
|
|
@@ -488,7 +523,9 @@ local function registerProxy(player, rf)
|
|
|
488
523
|
pluginVariant = State.PLUGIN_VARIANT,
|
|
489
524
|
})
|
|
490
525
|
if not ok or not res or not res.Success then
|
|
491
|
-
|
|
526
|
+
local _player_1 = player
|
|
527
|
+
proxyRegisterFailuresByPlayer[_player_1] = true
|
|
528
|
+
warn(`[robloxstudio-mcp] proxy register failed for {player.Name}: {formatPostJsonFailure("/ready", ok, res)}`)
|
|
492
529
|
return nil
|
|
493
530
|
end
|
|
494
531
|
local body = HttpService:JSONDecode(res.Body)
|
|
@@ -503,11 +540,17 @@ local function registerProxy(player, rf)
|
|
|
503
540
|
role = assigned,
|
|
504
541
|
}
|
|
505
542
|
proxyByPlayer[_player_1] = _arg1
|
|
543
|
+
local _player_2 = player
|
|
544
|
+
if proxyRegisterFailuresByPlayer[_player_2] ~= nil then
|
|
545
|
+
local _player_3 = player
|
|
546
|
+
proxyRegisterFailuresByPlayer[_player_3] = nil
|
|
547
|
+
print(`[robloxstudio-mcp] proxy registered for {player.Name} as {assigned} via {mcpUrl}`)
|
|
548
|
+
end
|
|
506
549
|
task.spawn(pollProxy, proxyId, player, rf)
|
|
507
550
|
end
|
|
508
551
|
-- (Removed: startEditProxyLoop. The play-server DM no longer registers an
|
|
509
552
|
-- "edit-proxy" peer with the MCP server. stop_playtest now uses a cross-DM
|
|
510
|
-
-- plugin:SetSetting
|
|
553
|
+
-- plugin:SetSetting request consumed by StopPlayMonitor in the play-server DM,
|
|
511
554
|
-- which doesn't depend on MCP server state or peer registration at all.)
|
|
512
555
|
local function setupServerBroker()
|
|
513
556
|
if serverBrokerStarted then
|
|
@@ -537,6 +580,8 @@ local function setupServerBroker()
|
|
|
537
580
|
if entry then
|
|
538
581
|
local _p_1 = p
|
|
539
582
|
proxyByPlayer[_p_1] = nil
|
|
583
|
+
local _p_2 = p
|
|
584
|
+
proxyRegisterFailuresByPlayer[_p_2] = nil
|
|
540
585
|
postJson("/disconnect", {
|
|
541
586
|
pluginSessionId = entry.pluginSessionId,
|
|
542
587
|
})
|
|
@@ -552,7 +597,10 @@ local function setupServerBroker()
|
|
|
552
597
|
end)
|
|
553
598
|
end
|
|
554
599
|
return {
|
|
555
|
-
MCP_URL =
|
|
600
|
+
MCP_URL = DEFAULT_MCP_URL,
|
|
601
|
+
DEFAULT_MCP_URL = DEFAULT_MCP_URL,
|
|
602
|
+
getServerUrl = getServerUrl,
|
|
603
|
+
setServerUrl = setServerUrl,
|
|
556
604
|
forkRole = forkRole,
|
|
557
605
|
setupClientBroker = setupClientBroker,
|
|
558
606
|
setupServerBroker = setupServerBroker,
|
|
@@ -588,6 +636,8 @@ local SerializationHandlers = TS.import(script, script.Parent, "handlers", "Seri
|
|
|
588
636
|
local MemoryHandlers = TS.import(script, script.Parent, "handlers", "MemoryHandlers")
|
|
589
637
|
local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "SceneAnalysisHandlers")
|
|
590
638
|
local EvalRuntimeHandlers = TS.import(script, script.Parent, "handlers", "EvalRuntimeHandlers")
|
|
639
|
+
local ServerUrlSettings = TS.import(script, script.Parent, "ServerUrlSettings")
|
|
640
|
+
local HttpDiagnostics = TS.import(script, script.Parent, "HttpDiagnostics")
|
|
591
641
|
-- Per-plugin-load random GUID. Used as the /poll URL param so the server
|
|
592
642
|
-- can tell our polls apart from any other plugin's polls. Not user-facing —
|
|
593
643
|
-- MCP tools and the LLM operate on instanceId (the place identifier).
|
|
@@ -617,6 +667,7 @@ local assignedRole
|
|
|
617
667
|
local duplicateInstanceRole = false
|
|
618
668
|
local hasVersionMismatch = false
|
|
619
669
|
local lastVersionMismatchWarningKey
|
|
670
|
+
local readyFailureLogKeys = {}
|
|
620
671
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
621
672
|
-- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
622
673
|
-- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
@@ -824,31 +875,49 @@ function sendReady(conn)
|
|
|
824
875
|
}),
|
|
825
876
|
})
|
|
826
877
|
end)
|
|
878
|
+
local readyUrl = `{conn.serverUrl}/ready`
|
|
879
|
+
local readyRole = detectRole()
|
|
880
|
+
local readyLogKey = `{conn.serverUrl}|{instanceId}|{readyRole}`
|
|
827
881
|
if not readyOk then
|
|
882
|
+
readyFailureLogKeys[readyLogKey] = true
|
|
883
|
+
warn(`[robloxstudio-mcp] /ready failed for {instanceId}/{readyRole}: {HttpDiagnostics.formatRequestFailure(readyUrl, readyOk, readyResult)}`)
|
|
828
884
|
return nil
|
|
829
885
|
end
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
ui
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
886
|
+
if not readyResult.Success then
|
|
887
|
+
local reason = HttpDiagnostics.formatRequestFailure(readyUrl, true, readyResult)
|
|
888
|
+
readyFailureLogKeys[readyLogKey] = true
|
|
889
|
+
-- 409 = duplicate_instance_role. Surface in UI and stop polling.
|
|
890
|
+
if readyResult.StatusCode == 409 then
|
|
891
|
+
duplicateInstanceRole = true
|
|
892
|
+
conn.isActive = false
|
|
893
|
+
local ui = UI.getElements()
|
|
894
|
+
if State.getActiveTabIndex() == 0 then
|
|
895
|
+
ui.statusLabel.Text = "Duplicate instance"
|
|
896
|
+
ui.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
897
|
+
ui.detailStatusLabel.Text = reason
|
|
898
|
+
ui.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
|
|
899
|
+
end
|
|
900
|
+
warn(`[robloxstudio-mcp] /ready rejected for {instanceId}/{readyRole}: {reason}`)
|
|
901
|
+
return nil
|
|
902
|
+
end
|
|
903
|
+
warn(`[robloxstudio-mcp] /ready rejected for {instanceId}/{readyRole}: {reason}`)
|
|
842
904
|
return nil
|
|
843
905
|
end
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
906
|
+
local parseOk, readyData = pcall(function()
|
|
907
|
+
return HttpService:JSONDecode(readyResult.Body)
|
|
908
|
+
end)
|
|
909
|
+
local _value = parseOk and readyData.assignedRole
|
|
910
|
+
if _value ~= "" and _value then
|
|
911
|
+
assignedRole = readyData.assignedRole
|
|
912
|
+
end
|
|
913
|
+
local _condition = assignedRole
|
|
914
|
+
if _condition == nil then
|
|
915
|
+
_condition = detectRole()
|
|
916
|
+
end
|
|
917
|
+
local connectedRole = _condition
|
|
918
|
+
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
919
|
+
readyFailureLogKeys[readyLogKey] = nil
|
|
920
|
+
print(`[robloxstudio-mcp] /ready connected for {instanceId}/{connectedRole} via {conn.serverUrl}`)
|
|
852
921
|
end
|
|
853
922
|
end)
|
|
854
923
|
end
|
|
@@ -894,7 +963,7 @@ local function pollForRequests(connIndex)
|
|
|
894
963
|
local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
|
|
895
964
|
if lastVersionMismatchWarningKey ~= warningKey then
|
|
896
965
|
lastVersionMismatchWarningKey = warningKey
|
|
897
|
-
warn(`[
|
|
966
|
+
warn(`[robloxstudio-mcp] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin and restart Studio.`)
|
|
898
967
|
end
|
|
899
968
|
UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
|
|
900
969
|
elseif hasVersionMismatch then
|
|
@@ -1062,6 +1131,7 @@ local function activatePlugin(connIndex)
|
|
|
1062
1131
|
UI.updateTabLabel(idx)
|
|
1063
1132
|
UI.updateUIState()
|
|
1064
1133
|
end
|
|
1134
|
+
ServerUrlSettings.rememberServerUrl(conn.serverUrl)
|
|
1065
1135
|
UI.updateTabDot(idx)
|
|
1066
1136
|
if not conn.heartbeatConnection then
|
|
1067
1137
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
@@ -1292,9 +1362,9 @@ local function computeBridgeStamp()
|
|
|
1292
1362
|
for i = 1, #combined do
|
|
1293
1363
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1294
1364
|
end
|
|
1295
|
-
-- "2.16.
|
|
1365
|
+
-- "2.16.1" is replaced with the package version at package time
|
|
1296
1366
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1297
|
-
return `{tostring(h)}-2.16.
|
|
1367
|
+
return `{tostring(h)}-2.16.1`
|
|
1298
1368
|
end
|
|
1299
1369
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1300
1370
|
local function setSource(scriptInst, source)
|
|
@@ -6674,7 +6744,7 @@ local function startPlaytest(requestData)
|
|
|
6674
6744
|
return injectStopListener()
|
|
6675
6745
|
end)
|
|
6676
6746
|
if not injected then
|
|
6677
|
-
warn(`[
|
|
6747
|
+
warn(`[robloxstudio-mcp] Failed to inject stop listener: {injErr}`)
|
|
6678
6748
|
end
|
|
6679
6749
|
task.spawn(function()
|
|
6680
6750
|
local ok, result = pcall(function()
|
|
@@ -6702,17 +6772,17 @@ local function startPlaytest(requestData)
|
|
|
6702
6772
|
return response
|
|
6703
6773
|
end
|
|
6704
6774
|
local function stopPlaytest(_requestData)
|
|
6705
|
-
-- Signal the play-server DM's StopPlayMonitor via plugin:SetSetting
|
|
6706
|
-
--
|
|
6707
|
-
--
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
if not StopPlayMonitor.requestStop() then
|
|
6775
|
+
-- Signal the play-server DM's StopPlayMonitor via plugin:SetSetting.
|
|
6776
|
+
-- The monitor acknowledges with the matching request id only after its
|
|
6777
|
+
-- StudioTestService:EndTest call returns from pcall.
|
|
6778
|
+
local stopRequest = StopPlayMonitor.requestStop()
|
|
6779
|
+
if not stopRequest.ok or stopRequest.requestId == nil then
|
|
6711
6780
|
return {
|
|
6712
6781
|
error = "Plugin not ready. Try again in a moment.",
|
|
6713
6782
|
}
|
|
6714
6783
|
end
|
|
6715
|
-
|
|
6784
|
+
local consumption = StopPlayMonitor.waitForConsumption(stopRequest.requestId)
|
|
6785
|
+
if not consumption.ok then
|
|
6716
6786
|
-- Two distinct failure modes collapse here, distinguished by whether
|
|
6717
6787
|
-- THIS edit DM has a playtest tracked:
|
|
6718
6788
|
--
|
|
@@ -6724,19 +6794,28 @@ local function stopPlaytest(_requestData)
|
|
|
6724
6794
|
-- from the caller's perspective — playtest may actually have ended).
|
|
6725
6795
|
-- Tell the caller it's a timing issue and they can retry.
|
|
6726
6796
|
--
|
|
6727
|
-
-- Either way clean up the pending
|
|
6797
|
+
-- Either way clean up the pending request so a future playtest's monitor
|
|
6728
6798
|
-- doesn't fire EndTest on startup against a stale signal.
|
|
6729
|
-
StopPlayMonitor.clearPending()
|
|
6799
|
+
StopPlayMonitor.clearPending(stopRequest.requestId)
|
|
6730
6800
|
if testRunning then
|
|
6731
6801
|
return {
|
|
6732
|
-
error = "Playtest stop signal
|
|
6802
|
+
error = "Playtest stop signal failed or was not acknowledged. " .. "The playtest may have ended anyway; check get_connected_instances.",
|
|
6803
|
+
detail = consumption.error,
|
|
6804
|
+
}
|
|
6805
|
+
end
|
|
6806
|
+
if consumption.consumed then
|
|
6807
|
+
return {
|
|
6808
|
+
error = "Playtest stop request reached the play server, but EndTest failed.",
|
|
6809
|
+
detail = consumption.error,
|
|
6733
6810
|
}
|
|
6734
6811
|
end
|
|
6735
6812
|
return {
|
|
6736
6813
|
error = "No active playtest to stop.",
|
|
6814
|
+
detail = consumption.error,
|
|
6737
6815
|
}
|
|
6738
6816
|
end
|
|
6739
|
-
|
|
6817
|
+
StopPlayMonitor.clearPending(stopRequest.requestId)
|
|
6818
|
+
-- Request was consumed (EndTest called). ExecutePlayModeAsync in our
|
|
6740
6819
|
-- startPlaytest task.spawn is still unwinding though — testRunning stays
|
|
6741
6820
|
-- true until that yield completes and the post-block runs. Wait so
|
|
6742
6821
|
-- back-to-back stop -> start sequences don't race against the prior
|
|
@@ -7040,6 +7119,87 @@ return {
|
|
|
7040
7119
|
</Item>
|
|
7041
7120
|
</Item>
|
|
7042
7121
|
<Item class="ModuleScript" referent="21">
|
|
7122
|
+
<Properties>
|
|
7123
|
+
<string name="Name">HttpDiagnostics</string>
|
|
7124
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7125
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
7126
|
+
local HttpService = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services").HttpService
|
|
7127
|
+
local function encodeForLog(value)
|
|
7128
|
+
local ok, encoded = pcall(function()
|
|
7129
|
+
return HttpService:JSONEncode(value)
|
|
7130
|
+
end)
|
|
7131
|
+
return if ok then encoded else tostring(value)
|
|
7132
|
+
end
|
|
7133
|
+
local function formatBody(body)
|
|
7134
|
+
if body == "" then
|
|
7135
|
+
return ""
|
|
7136
|
+
end
|
|
7137
|
+
local ok, decoded = pcall(function()
|
|
7138
|
+
return HttpService:JSONDecode(body)
|
|
7139
|
+
end)
|
|
7140
|
+
if ok and type(decoded) == "table" then
|
|
7141
|
+
local data = decoded
|
|
7142
|
+
local parts = {}
|
|
7143
|
+
local _error = data.error
|
|
7144
|
+
local _condition = type(_error) == "string"
|
|
7145
|
+
if _condition then
|
|
7146
|
+
_condition = data.error ~= ""
|
|
7147
|
+
end
|
|
7148
|
+
if _condition then
|
|
7149
|
+
local _arg0 = `error={data.error}`
|
|
7150
|
+
table.insert(parts, _arg0)
|
|
7151
|
+
end
|
|
7152
|
+
local _message = data.message
|
|
7153
|
+
local _condition_1 = type(_message) == "string"
|
|
7154
|
+
if _condition_1 then
|
|
7155
|
+
_condition_1 = data.message ~= ""
|
|
7156
|
+
end
|
|
7157
|
+
if _condition_1 then
|
|
7158
|
+
local _arg0 = `message={data.message}`
|
|
7159
|
+
table.insert(parts, _arg0)
|
|
7160
|
+
end
|
|
7161
|
+
if data.missingFields ~= nil then
|
|
7162
|
+
local _arg0 = `missingFields={encodeForLog(data.missingFields)}`
|
|
7163
|
+
table.insert(parts, _arg0)
|
|
7164
|
+
end
|
|
7165
|
+
if data.request ~= nil then
|
|
7166
|
+
local _arg0 = `request={encodeForLog(data.request)}`
|
|
7167
|
+
table.insert(parts, _arg0)
|
|
7168
|
+
end
|
|
7169
|
+
if data.existing ~= nil then
|
|
7170
|
+
local _arg0 = `existing={encodeForLog(data.existing)}`
|
|
7171
|
+
table.insert(parts, _arg0)
|
|
7172
|
+
end
|
|
7173
|
+
if data.details ~= nil then
|
|
7174
|
+
local _arg0 = `details={encodeForLog(data.details)}`
|
|
7175
|
+
table.insert(parts, _arg0)
|
|
7176
|
+
end
|
|
7177
|
+
if #parts > 0 then
|
|
7178
|
+
return table.concat(parts, " ")
|
|
7179
|
+
end
|
|
7180
|
+
end
|
|
7181
|
+
return `body={body}`
|
|
7182
|
+
end
|
|
7183
|
+
local function formatRequestFailure(url, ok, res)
|
|
7184
|
+
if not ok then
|
|
7185
|
+
return `RequestAsync threw for {url}: {tostring(res)}`
|
|
7186
|
+
end
|
|
7187
|
+
if res == nil then
|
|
7188
|
+
return `RequestAsync returned no response for {url}`
|
|
7189
|
+
end
|
|
7190
|
+
local response = res
|
|
7191
|
+
local statusMessage = if response.StatusMessage ~= "" then ` {response.StatusMessage}` else ""
|
|
7192
|
+
local body = formatBody(response.Body)
|
|
7193
|
+
local suffix = if body ~= "" then `: {body}` else ""
|
|
7194
|
+
return `HTTP {response.StatusCode}{statusMessage} from {url}{suffix}`
|
|
7195
|
+
end
|
|
7196
|
+
return {
|
|
7197
|
+
formatRequestFailure = formatRequestFailure,
|
|
7198
|
+
}
|
|
7199
|
+
]]></string>
|
|
7200
|
+
</Properties>
|
|
7201
|
+
</Item>
|
|
7202
|
+
<Item class="ModuleScript" referent="22">
|
|
7043
7203
|
<Properties>
|
|
7044
7204
|
<string name="Name">LuauExec</string>
|
|
7045
7205
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7473,7 +7633,7 @@ return {
|
|
|
7473
7633
|
]]></string>
|
|
7474
7634
|
</Properties>
|
|
7475
7635
|
</Item>
|
|
7476
|
-
<Item class="ModuleScript" referent="
|
|
7636
|
+
<Item class="ModuleScript" referent="23">
|
|
7477
7637
|
<Properties>
|
|
7478
7638
|
<string name="Name">Recording</string>
|
|
7479
7639
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7503,7 +7663,7 @@ return {
|
|
|
7503
7663
|
]]></string>
|
|
7504
7664
|
</Properties>
|
|
7505
7665
|
</Item>
|
|
7506
|
-
<Item class="ModuleScript" referent="
|
|
7666
|
+
<Item class="ModuleScript" referent="24">
|
|
7507
7667
|
<Properties>
|
|
7508
7668
|
<string name="Name">RenderMonitor</string>
|
|
7509
7669
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7571,7 +7731,7 @@ return {
|
|
|
7571
7731
|
]]></string>
|
|
7572
7732
|
</Properties>
|
|
7573
7733
|
</Item>
|
|
7574
|
-
<Item class="ModuleScript" referent="
|
|
7734
|
+
<Item class="ModuleScript" referent="25">
|
|
7575
7735
|
<Properties>
|
|
7576
7736
|
<string name="Name">RuntimeLogBuffer</string>
|
|
7577
7737
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7752,11 +7912,71 @@ return {
|
|
|
7752
7912
|
]]></string>
|
|
7753
7913
|
</Properties>
|
|
7754
7914
|
</Item>
|
|
7755
|
-
<Item class="ModuleScript" referent="
|
|
7915
|
+
<Item class="ModuleScript" referent="26">
|
|
7916
|
+
<Properties>
|
|
7917
|
+
<string name="Name">ServerUrlSettings</string>
|
|
7918
|
+
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7919
|
+
local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
7920
|
+
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
7921
|
+
local HttpService = _services.HttpService
|
|
7922
|
+
local ServerStorage = _services.ServerStorage
|
|
7923
|
+
local SETTING_KEY_PREFIX = "MCP_SERVER_URL_"
|
|
7924
|
+
local pluginRef
|
|
7925
|
+
local function init(p)
|
|
7926
|
+
pluginRef = p
|
|
7927
|
+
end
|
|
7928
|
+
local function computeInstanceId()
|
|
7929
|
+
if game.PlaceId ~= 0 then
|
|
7930
|
+
return `place:{tostring(game.PlaceId)}`
|
|
7931
|
+
end
|
|
7932
|
+
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
7933
|
+
if type(existing) == "string" and existing ~= "" then
|
|
7934
|
+
return `anon:{existing}`
|
|
7935
|
+
end
|
|
7936
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
7937
|
+
pcall(function()
|
|
7938
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
7939
|
+
end)
|
|
7940
|
+
return `anon:{fresh}`
|
|
7941
|
+
end
|
|
7942
|
+
local function settingKey(instanceId)
|
|
7943
|
+
return SETTING_KEY_PREFIX .. instanceId
|
|
7944
|
+
end
|
|
7945
|
+
local function rememberServerUrl(serverUrl)
|
|
7946
|
+
if not pluginRef or serverUrl == "" then
|
|
7947
|
+
return nil
|
|
7948
|
+
end
|
|
7949
|
+
local key = settingKey(computeInstanceId())
|
|
7950
|
+
pcall(function()
|
|
7951
|
+
return pluginRef:SetSetting(key, serverUrl)
|
|
7952
|
+
end)
|
|
7953
|
+
end
|
|
7954
|
+
local function readServerUrl()
|
|
7955
|
+
if not pluginRef then
|
|
7956
|
+
return nil
|
|
7957
|
+
end
|
|
7958
|
+
local key = settingKey(computeInstanceId())
|
|
7959
|
+
local ok, value = pcall(function()
|
|
7960
|
+
return pluginRef:GetSetting(key)
|
|
7961
|
+
end)
|
|
7962
|
+
if ok and type(value) == "string" and value ~= "" then
|
|
7963
|
+
return value
|
|
7964
|
+
end
|
|
7965
|
+
return nil
|
|
7966
|
+
end
|
|
7967
|
+
return {
|
|
7968
|
+
init = init,
|
|
7969
|
+
rememberServerUrl = rememberServerUrl,
|
|
7970
|
+
readServerUrl = readServerUrl,
|
|
7971
|
+
}
|
|
7972
|
+
]]></string>
|
|
7973
|
+
</Properties>
|
|
7974
|
+
</Item>
|
|
7975
|
+
<Item class="ModuleScript" referent="27">
|
|
7756
7976
|
<Properties>
|
|
7757
7977
|
<string name="Name">State</string>
|
|
7758
7978
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7759
|
-
local CURRENT_VERSION = "2.16.
|
|
7979
|
+
local CURRENT_VERSION = "2.16.1"
|
|
7760
7980
|
local PLUGIN_VARIANT = "main"
|
|
7761
7981
|
local MAX_CONNECTIONS = 5
|
|
7762
7982
|
local BASE_PORT = 58741
|
|
@@ -7850,7 +8070,7 @@ return {
|
|
|
7850
8070
|
]]></string>
|
|
7851
8071
|
</Properties>
|
|
7852
8072
|
</Item>
|
|
7853
|
-
<Item class="ModuleScript" referent="
|
|
8073
|
+
<Item class="ModuleScript" referent="28">
|
|
7854
8074
|
<Properties>
|
|
7855
8075
|
<string name="Name">StopPlayMonitor</string>
|
|
7856
8076
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -7862,16 +8082,16 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
7862
8082
|
-- `plugin:SetSetting` / `plugin:GetSetting` is a per-plugin persistent store
|
|
7863
8083
|
-- shared across every DataModel the plugin runs in (edit DMs, play-server
|
|
7864
8084
|
-- DMs, play-client DMs). For each connected place we use a dedicated key
|
|
7865
|
-
-- "MCP_STOP_PLAY_<instanceId>" as a
|
|
8085
|
+
-- "MCP_STOP_PLAY_<instanceId>" as a tiny request/result mailbox:
|
|
7866
8086
|
--
|
|
7867
|
-
-- * The edit DM's
|
|
8087
|
+
-- * The edit DM's handler writes a tokenized stop request into its own key
|
|
7868
8088
|
-- (computed from its placeId / ServerStorage anon UUID).
|
|
7869
8089
|
-- * Each play-server DM's monitor loop polls the key matching its own
|
|
7870
|
-
-- instanceId at
|
|
7871
|
-
--
|
|
7872
|
-
-- touch this key.
|
|
7873
|
-
-- * The edit DM waits up to ~8s for its
|
|
7874
|
-
--
|
|
8090
|
+
-- instanceId at 1Hz. On a fresh token, it calls StudioTestService:EndTest
|
|
8091
|
+
-- and writes a matching result token. Play-server DMs for other places
|
|
8092
|
+
-- never touch this key.
|
|
8093
|
+
-- * The edit DM waits up to ~8s for its result token, confirming a matching
|
|
8094
|
+
-- play-server actually consumed the request.
|
|
7875
8095
|
--
|
|
7876
8096
|
-- Earlier versions used a single shared boolean flag, which let any
|
|
7877
8097
|
-- play-server DM in the same Studio process consume any place's stop
|
|
@@ -7879,20 +8099,24 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
7879
8099
|
-- below is the fix.
|
|
7880
8100
|
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
|
|
7881
8101
|
local HttpService = _services.HttpService
|
|
8102
|
+
local RunService = _services.RunService
|
|
7882
8103
|
local ServerStorage = _services.ServerStorage
|
|
7883
8104
|
local StudioTestService = game:GetService("StudioTestService")
|
|
7884
8105
|
local SETTING_KEY_PREFIX = "MCP_STOP_PLAY_"
|
|
7885
|
-
--
|
|
7886
|
-
--
|
|
7887
|
-
--
|
|
7888
|
-
local POLL_INTERVAL_SEC =
|
|
8106
|
+
-- Keep this conservative. plugin:GetSetting is backed by Studio's plugin
|
|
8107
|
+
-- settings store, and this monitor runs during every play session, including
|
|
8108
|
+
-- manually-started Play. The official reference implementation polls at 1s.
|
|
8109
|
+
local POLL_INTERVAL_SEC = 1
|
|
7889
8110
|
-- Total time we wait for the matching play-server DM to consume the
|
|
7890
8111
|
-- signal. Must cover: monitor detection (<= POLL_INTERVAL_SEC) +
|
|
7891
8112
|
-- StudioTestService:EndTest teardown (several seconds on heavier places).
|
|
7892
|
-
-- 8s is
|
|
8113
|
+
-- 8s is intentionally shorter than the MCP request timeout but long enough
|
|
8114
|
+
-- for the 1s monitor cadence plus ordinary Studio teardown latency.
|
|
7893
8115
|
local WAIT_FOR_CONSUMPTION_TIMEOUT_SEC = 8.0
|
|
7894
8116
|
local WAIT_POLL_SEC = 0.1
|
|
8117
|
+
local REQUEST_TTL_SEC = 12.0
|
|
7895
8118
|
local pluginRef
|
|
8119
|
+
local endTestIssued = false
|
|
7896
8120
|
local function init(p)
|
|
7897
8121
|
pluginRef = p
|
|
7898
8122
|
end
|
|
@@ -7918,32 +8142,125 @@ end
|
|
|
7918
8142
|
local function settingKey(instanceId)
|
|
7919
8143
|
return SETTING_KEY_PREFIX .. instanceId
|
|
7920
8144
|
end
|
|
8145
|
+
local function readSetting(key)
|
|
8146
|
+
if not pluginRef then
|
|
8147
|
+
return nil
|
|
8148
|
+
end
|
|
8149
|
+
local ok, value = pcall(function()
|
|
8150
|
+
return pluginRef:GetSetting(key)
|
|
8151
|
+
end)
|
|
8152
|
+
return if ok then value else nil
|
|
8153
|
+
end
|
|
8154
|
+
local function writeSetting(key, value)
|
|
8155
|
+
if not pluginRef then
|
|
8156
|
+
return false
|
|
8157
|
+
end
|
|
8158
|
+
local ok = pcall(function()
|
|
8159
|
+
return pluginRef:SetSetting(key, value)
|
|
8160
|
+
end)
|
|
8161
|
+
return ok
|
|
8162
|
+
end
|
|
8163
|
+
local function decodePayload(value)
|
|
8164
|
+
local decoded = value
|
|
8165
|
+
local _value = value
|
|
8166
|
+
if type(_value) == "string" then
|
|
8167
|
+
local ok, result = pcall(function()
|
|
8168
|
+
return HttpService:JSONDecode(value)
|
|
8169
|
+
end)
|
|
8170
|
+
if not ok then
|
|
8171
|
+
return nil
|
|
8172
|
+
end
|
|
8173
|
+
decoded = result
|
|
8174
|
+
end
|
|
8175
|
+
local _decoded = decoded
|
|
8176
|
+
if not (type(_decoded) == "table") then
|
|
8177
|
+
return nil
|
|
8178
|
+
end
|
|
8179
|
+
local payload = decoded
|
|
8180
|
+
local _kind = payload.kind
|
|
8181
|
+
local _condition = not (type(_kind) == "string")
|
|
8182
|
+
if not _condition then
|
|
8183
|
+
local _id = payload.id
|
|
8184
|
+
_condition = not (type(_id) == "string")
|
|
8185
|
+
end
|
|
8186
|
+
if _condition then
|
|
8187
|
+
return nil
|
|
8188
|
+
end
|
|
8189
|
+
return payload
|
|
8190
|
+
end
|
|
8191
|
+
local function writePayload(key, payload)
|
|
8192
|
+
local encodedOk, encoded = pcall(function()
|
|
8193
|
+
return HttpService:JSONEncode(payload)
|
|
8194
|
+
end)
|
|
8195
|
+
if not encodedOk or not (type(encoded) == "string") then
|
|
8196
|
+
return false
|
|
8197
|
+
end
|
|
8198
|
+
return writeSetting(key, encoded)
|
|
8199
|
+
end
|
|
8200
|
+
local function writeResult(key, request, ok, errText)
|
|
8201
|
+
writePayload(key, {
|
|
8202
|
+
kind = "result",
|
|
8203
|
+
id = request.id,
|
|
8204
|
+
requestedAt = request.requestedAt,
|
|
8205
|
+
consumedAt = tick(),
|
|
8206
|
+
ok = ok,
|
|
8207
|
+
error = errText,
|
|
8208
|
+
})
|
|
8209
|
+
end
|
|
8210
|
+
local function handleStopRequest(key, request)
|
|
8211
|
+
local _condition = request.kind ~= "request"
|
|
8212
|
+
if not _condition then
|
|
8213
|
+
local _id = request.id
|
|
8214
|
+
_condition = not (type(_id) == "string")
|
|
8215
|
+
end
|
|
8216
|
+
if _condition then
|
|
8217
|
+
return nil
|
|
8218
|
+
end
|
|
8219
|
+
local _requestedAt = request.requestedAt
|
|
8220
|
+
if not (type(_requestedAt) == "number") then
|
|
8221
|
+
writeSetting(key, false)
|
|
8222
|
+
return nil
|
|
8223
|
+
end
|
|
8224
|
+
local age = tick() - request.requestedAt
|
|
8225
|
+
if age < -5 or age > REQUEST_TTL_SEC then
|
|
8226
|
+
writeSetting(key, false)
|
|
8227
|
+
return nil
|
|
8228
|
+
end
|
|
8229
|
+
if endTestIssued then
|
|
8230
|
+
writeResult(key, request, true)
|
|
8231
|
+
return nil
|
|
8232
|
+
end
|
|
8233
|
+
if not RunService:IsRunning() or not RunService:IsServer() then
|
|
8234
|
+
writeResult(key, request, false, "StopPlayMonitor is not running in the server DataModel.")
|
|
8235
|
+
return nil
|
|
8236
|
+
end
|
|
8237
|
+
endTestIssued = true
|
|
8238
|
+
local endOk, endErr = pcall(function()
|
|
8239
|
+
return StudioTestService:EndTest("stopped_by_mcp")
|
|
8240
|
+
end)
|
|
8241
|
+
writeResult(key, request, endOk, if endOk then nil else tostring(endErr))
|
|
8242
|
+
if not endOk then
|
|
8243
|
+
endTestIssued = false
|
|
8244
|
+
end
|
|
8245
|
+
end
|
|
7921
8246
|
local function startMonitor()
|
|
7922
8247
|
if not pluginRef then
|
|
7923
|
-
warn("[
|
|
8248
|
+
warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
|
|
7924
8249
|
return nil
|
|
7925
8250
|
end
|
|
7926
8251
|
local myKey = settingKey(computeInstanceId())
|
|
7927
|
-
-- Clear any stale value left from a prior session. If a real stop
|
|
7928
|
-
-- request is in-flight when this runs, the requesting edit DM will
|
|
7929
|
-
-- write again within its consumption-confirmation window.
|
|
7930
|
-
pcall(function()
|
|
7931
|
-
return pluginRef:SetSetting(myKey, false)
|
|
7932
|
-
end)
|
|
7933
8252
|
task.spawn(function()
|
|
7934
8253
|
while true do
|
|
7935
|
-
local
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
return StudioTestService:EndTest("stopped_by_mcp")
|
|
7946
|
-
end)
|
|
8254
|
+
local value = readSetting(myKey)
|
|
8255
|
+
if value == true then
|
|
8256
|
+
-- Legacy boolean requests are ambiguous and may be stale from
|
|
8257
|
+
-- a prior crashed session. New stop requests use token payloads.
|
|
8258
|
+
writeSetting(myKey, false)
|
|
8259
|
+
else
|
|
8260
|
+
local payload = decodePayload(value)
|
|
8261
|
+
if payload then
|
|
8262
|
+
handleStopRequest(myKey, payload)
|
|
8263
|
+
end
|
|
7947
8264
|
end
|
|
7948
8265
|
task.wait(POLL_INTERVAL_SEC)
|
|
7949
8266
|
end
|
|
@@ -7951,39 +8268,61 @@ local function startMonitor()
|
|
|
7951
8268
|
end
|
|
7952
8269
|
local function requestStop()
|
|
7953
8270
|
if not pluginRef then
|
|
7954
|
-
return
|
|
8271
|
+
return {
|
|
8272
|
+
ok = false,
|
|
8273
|
+
}
|
|
7955
8274
|
end
|
|
7956
8275
|
local myKey = settingKey(computeInstanceId())
|
|
7957
|
-
local
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
8276
|
+
local requestId = HttpService:GenerateGUID(false)
|
|
8277
|
+
local ok = writePayload(myKey, {
|
|
8278
|
+
kind = "request",
|
|
8279
|
+
id = requestId,
|
|
8280
|
+
requestedAt = tick(),
|
|
8281
|
+
})
|
|
8282
|
+
return {
|
|
8283
|
+
ok = ok,
|
|
8284
|
+
requestId = if ok then requestId else nil,
|
|
8285
|
+
}
|
|
7961
8286
|
end
|
|
7962
|
-
local function waitForConsumption()
|
|
8287
|
+
local function waitForConsumption(requestId)
|
|
7963
8288
|
if not pluginRef then
|
|
7964
|
-
return
|
|
8289
|
+
return {
|
|
8290
|
+
ok = false,
|
|
8291
|
+
consumed = false,
|
|
8292
|
+
error = "Plugin reference is not initialized.",
|
|
8293
|
+
}
|
|
7965
8294
|
end
|
|
7966
8295
|
local myKey = settingKey(computeInstanceId())
|
|
7967
8296
|
local start = tick()
|
|
7968
8297
|
while tick() - start < WAIT_FOR_CONSUMPTION_TIMEOUT_SEC do
|
|
7969
|
-
local
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
8298
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8299
|
+
if payload and payload.kind == "result" and payload.id == requestId then
|
|
8300
|
+
return {
|
|
8301
|
+
ok = payload.ok == true,
|
|
8302
|
+
consumed = true,
|
|
8303
|
+
error = payload.error,
|
|
8304
|
+
}
|
|
7974
8305
|
end
|
|
7975
8306
|
task.wait(WAIT_POLL_SEC)
|
|
7976
8307
|
end
|
|
7977
|
-
return
|
|
8308
|
+
return {
|
|
8309
|
+
ok = false,
|
|
8310
|
+
consumed = false,
|
|
8311
|
+
error = "Timed out waiting for the play-server DataModel to acknowledge stop_playtest.",
|
|
8312
|
+
}
|
|
7978
8313
|
end
|
|
7979
|
-
local function clearPending()
|
|
8314
|
+
local function clearPending(requestId)
|
|
7980
8315
|
if not pluginRef then
|
|
7981
8316
|
return nil
|
|
7982
8317
|
end
|
|
7983
8318
|
local myKey = settingKey(computeInstanceId())
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
8319
|
+
if requestId ~= nil then
|
|
8320
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8321
|
+
if payload and payload.id ~= requestId then
|
|
8322
|
+
return nil
|
|
8323
|
+
end
|
|
8324
|
+
end
|
|
8325
|
+
writeSetting(myKey, false)
|
|
7987
8326
|
end
|
|
7988
8327
|
return {
|
|
7989
8328
|
init = init,
|
|
@@ -7995,7 +8334,7 @@ return {
|
|
|
7995
8334
|
]]></string>
|
|
7996
8335
|
</Properties>
|
|
7997
8336
|
</Item>
|
|
7998
|
-
<Item class="ModuleScript" referent="
|
|
8337
|
+
<Item class="ModuleScript" referent="29">
|
|
7999
8338
|
<Properties>
|
|
8000
8339
|
<string name="Name">UI</string>
|
|
8001
8340
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -8766,7 +9105,7 @@ return {
|
|
|
8766
9105
|
]]></string>
|
|
8767
9106
|
</Properties>
|
|
8768
9107
|
</Item>
|
|
8769
|
-
<Item class="ModuleScript" referent="
|
|
9108
|
+
<Item class="ModuleScript" referent="30">
|
|
8770
9109
|
<Properties>
|
|
8771
9110
|
<string name="Name">Utils</string>
|
|
8772
9111
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
@@ -9296,11 +9635,11 @@ return {
|
|
|
9296
9635
|
</Properties>
|
|
9297
9636
|
</Item>
|
|
9298
9637
|
</Item>
|
|
9299
|
-
<Item class="Folder" referent="
|
|
9638
|
+
<Item class="Folder" referent="34">
|
|
9300
9639
|
<Properties>
|
|
9301
9640
|
<string name="Name">include</string>
|
|
9302
9641
|
</Properties>
|
|
9303
|
-
<Item class="ModuleScript" referent="
|
|
9642
|
+
<Item class="ModuleScript" referent="31">
|
|
9304
9643
|
<Properties>
|
|
9305
9644
|
<string name="Name">Promise</string>
|
|
9306
9645
|
<string name="Source"><![CDATA[--[[
|
|
@@ -11374,7 +11713,7 @@ return Promise
|
|
|
11374
11713
|
]]></string>
|
|
11375
11714
|
</Properties>
|
|
11376
11715
|
</Item>
|
|
11377
|
-
<Item class="ModuleScript" referent="
|
|
11716
|
+
<Item class="ModuleScript" referent="32">
|
|
11378
11717
|
<Properties>
|
|
11379
11718
|
<string name="Name">RuntimeLib</string>
|
|
11380
11719
|
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
|
|
@@ -11641,15 +11980,15 @@ return TS
|
|
|
11641
11980
|
</Properties>
|
|
11642
11981
|
</Item>
|
|
11643
11982
|
</Item>
|
|
11644
|
-
<Item class="Folder" referent="
|
|
11983
|
+
<Item class="Folder" referent="35">
|
|
11645
11984
|
<Properties>
|
|
11646
11985
|
<string name="Name">node_modules</string>
|
|
11647
11986
|
</Properties>
|
|
11648
|
-
<Item class="Folder" referent="
|
|
11987
|
+
<Item class="Folder" referent="36">
|
|
11649
11988
|
<Properties>
|
|
11650
11989
|
<string name="Name">@rbxts</string>
|
|
11651
11990
|
</Properties>
|
|
11652
|
-
<Item class="ModuleScript" referent="
|
|
11991
|
+
<Item class="ModuleScript" referent="33">
|
|
11653
11992
|
<Properties>
|
|
11654
11993
|
<string name="Name">services</string>
|
|
11655
11994
|
<string name="Source"><![CDATA[return setmetatable({}, {
|