@chrrxs/robloxstudio-mcp 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
|
@@ -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
|
|
@@ -662,21 +678,24 @@ local function computeInstanceId()
|
|
|
662
678
|
end)
|
|
663
679
|
return `anon:{fresh}`
|
|
664
680
|
end
|
|
665
|
-
local instanceId = computeInstanceId()
|
|
666
681
|
local assignedRole
|
|
667
682
|
local duplicateInstanceRole = false
|
|
668
683
|
local hasVersionMismatch = false
|
|
669
684
|
local lastVersionMismatchWarningKey
|
|
685
|
+
local lastReadyInstanceId
|
|
670
686
|
local readyFailureLogKeys = {}
|
|
671
687
|
-- Cache the published place name from MarketplaceService:GetProductInfo so
|
|
672
688
|
-- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
673
689
|
-- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
674
690
|
-- once per plugin load; the published name doesn't change mid-session.
|
|
675
691
|
local cachedPlaceName
|
|
692
|
+
local cachedPlaceNamePlaceId
|
|
676
693
|
local function resolvePlaceName()
|
|
677
|
-
if cachedPlaceName ~= nil then
|
|
694
|
+
if cachedPlaceName ~= nil and cachedPlaceNamePlaceId == game.PlaceId then
|
|
678
695
|
return cachedPlaceName
|
|
679
696
|
end
|
|
697
|
+
cachedPlaceName = nil
|
|
698
|
+
cachedPlaceNamePlaceId = game.PlaceId
|
|
680
699
|
if game.PlaceId == 0 then
|
|
681
700
|
cachedPlaceName = game.Name
|
|
682
701
|
return cachedPlaceName
|
|
@@ -705,6 +724,7 @@ local function detectRole()
|
|
|
705
724
|
end
|
|
706
725
|
return "client"
|
|
707
726
|
end
|
|
727
|
+
local initialRole = detectRole()
|
|
708
728
|
local routeMap = {
|
|
709
729
|
["/api/file-tree"] = QueryHandlers.getFileTree,
|
|
710
730
|
["/api/search-files"] = QueryHandlers.searchFiles,
|
|
@@ -820,28 +840,36 @@ end
|
|
|
820
840
|
-- Without this, every poll during the brief window where the server has just
|
|
821
841
|
-- restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
|
|
822
842
|
local lastReadyPostAt = 0
|
|
823
|
-
-- game.Name
|
|
824
|
-
--
|
|
825
|
-
--
|
|
826
|
-
-- get_connected_instances doesn't show a stale dataModelName forever. Set
|
|
827
|
-
-- up once per plugin load — the connection passed in is whichever was
|
|
828
|
-
-- active when activatePlugin was first called.
|
|
843
|
+
-- game.Name and game.PlaceId can both settle after plugin load. PlaceId also
|
|
844
|
+
-- changes when an unpublished file is published while MCP is already active.
|
|
845
|
+
-- Re-fire /ready so the bridge can migrate anon:<uuid> to place:<PlaceId>.
|
|
829
846
|
local nameChangeConn
|
|
847
|
+
local placeIdChangeConn
|
|
830
848
|
local sendReady
|
|
831
|
-
local function
|
|
832
|
-
if nameChangeConn then
|
|
833
|
-
|
|
849
|
+
local function ensureIdentityWatcher(conn)
|
|
850
|
+
if not nameChangeConn then
|
|
851
|
+
local okSig, signal = pcall(function()
|
|
852
|
+
return game:GetPropertyChangedSignal("Name")
|
|
853
|
+
end)
|
|
854
|
+
if okSig and signal then
|
|
855
|
+
nameChangeConn = signal:Connect(function()
|
|
856
|
+
-- sendReady has its own 2s throttle, so rapid burst changes coalesce.
|
|
857
|
+
sendReady(conn)
|
|
858
|
+
end)
|
|
859
|
+
end
|
|
834
860
|
end
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
861
|
+
if not placeIdChangeConn then
|
|
862
|
+
local okSig, signal = pcall(function()
|
|
863
|
+
return game:GetPropertyChangedSignal("PlaceId")
|
|
864
|
+
end)
|
|
865
|
+
if okSig and signal then
|
|
866
|
+
placeIdChangeConn = signal:Connect(function()
|
|
867
|
+
cachedPlaceName = nil
|
|
868
|
+
cachedPlaceNamePlaceId = nil
|
|
869
|
+
sendReady(conn)
|
|
870
|
+
end)
|
|
871
|
+
end
|
|
840
872
|
end
|
|
841
|
-
nameChangeConn = signal:Connect(function()
|
|
842
|
-
-- sendReady has its own 2s throttle, so rapid burst changes coalesce.
|
|
843
|
-
sendReady(conn)
|
|
844
|
-
end)
|
|
845
873
|
end
|
|
846
874
|
function sendReady(conn)
|
|
847
875
|
if duplicateInstanceRole then
|
|
@@ -852,6 +880,7 @@ function sendReady(conn)
|
|
|
852
880
|
return nil
|
|
853
881
|
end
|
|
854
882
|
lastReadyPostAt = now
|
|
883
|
+
local instanceId = computeInstanceId()
|
|
855
884
|
task.spawn(function()
|
|
856
885
|
local readyOk, readyResult = pcall(function()
|
|
857
886
|
return HttpService:RequestAsync({
|
|
@@ -910,11 +939,20 @@ function sendReady(conn)
|
|
|
910
939
|
if _value ~= "" and _value then
|
|
911
940
|
assignedRole = readyData.assignedRole
|
|
912
941
|
end
|
|
913
|
-
local _condition =
|
|
914
|
-
if _condition
|
|
915
|
-
|
|
942
|
+
local _condition = parseOk
|
|
943
|
+
if _condition then
|
|
944
|
+
local _instanceId = readyData.instanceId
|
|
945
|
+
_condition = type(_instanceId) == "string"
|
|
946
|
+
if _condition then
|
|
947
|
+
_condition = readyData.instanceId ~= ""
|
|
948
|
+
end
|
|
916
949
|
end
|
|
917
|
-
|
|
950
|
+
lastReadyInstanceId = if _condition then readyData.instanceId else instanceId
|
|
951
|
+
local _condition_1 = assignedRole
|
|
952
|
+
if _condition_1 == nil then
|
|
953
|
+
_condition_1 = detectRole()
|
|
954
|
+
end
|
|
955
|
+
local connectedRole = _condition_1
|
|
918
956
|
if readyFailureLogKeys[readyLogKey] ~= nil then
|
|
919
957
|
readyFailureLogKeys[readyLogKey] = nil
|
|
920
958
|
print(`[robloxstudio-mcp] /ready connected for {instanceId}/{connectedRole} via {conn.serverUrl}`)
|
|
@@ -1104,6 +1142,7 @@ local function pollForRequests(connIndex)
|
|
|
1104
1142
|
end
|
|
1105
1143
|
end
|
|
1106
1144
|
end
|
|
1145
|
+
local deactivatePlugin
|
|
1107
1146
|
local function activatePlugin(connIndex)
|
|
1108
1147
|
local _condition = connIndex
|
|
1109
1148
|
if _condition == nil then
|
|
@@ -1136,6 +1175,17 @@ local function activatePlugin(connIndex)
|
|
|
1136
1175
|
if not conn.heartbeatConnection then
|
|
1137
1176
|
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
|
|
1138
1177
|
local now = tick()
|
|
1178
|
+
if initialRole == "server" and not RunService:IsRunning() then
|
|
1179
|
+
ClientBroker.disconnectAllProxies()
|
|
1180
|
+
deactivatePlugin(idx)
|
|
1181
|
+
return nil
|
|
1182
|
+
end
|
|
1183
|
+
local currentInstanceId = computeInstanceId()
|
|
1184
|
+
if lastReadyInstanceId ~= nil and currentInstanceId ~= lastReadyInstanceId then
|
|
1185
|
+
cachedPlaceName = nil
|
|
1186
|
+
cachedPlaceNamePlaceId = nil
|
|
1187
|
+
sendReady(conn)
|
|
1188
|
+
end
|
|
1139
1189
|
local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
|
|
1140
1190
|
if now - conn.lastPoll > currentInterval then
|
|
1141
1191
|
conn.lastPoll = now
|
|
@@ -1151,11 +1201,10 @@ local function activatePlugin(connIndex)
|
|
|
1151
1201
|
if not RunService:IsRunning() then
|
|
1152
1202
|
task.spawn(cleanupLegacyEditBridges)
|
|
1153
1203
|
end
|
|
1154
|
-
-- Watch
|
|
1155
|
-
|
|
1156
|
-
ensureNameChangeWatcher(conn)
|
|
1204
|
+
-- Watch identity fields so stale name or anon instance ids are refreshed.
|
|
1205
|
+
ensureIdentityWatcher(conn)
|
|
1157
1206
|
end
|
|
1158
|
-
|
|
1207
|
+
function deactivatePlugin(connIndex)
|
|
1159
1208
|
local _condition = connIndex
|
|
1160
1209
|
if _condition == nil then
|
|
1161
1210
|
_condition = State.getActiveTabIndex()
|
|
@@ -1362,9 +1411,9 @@ local function computeBridgeStamp()
|
|
|
1362
1411
|
for i = 1, #combined do
|
|
1363
1412
|
h = (h * 33 + (string.byte(combined, i))) % 2147483647
|
|
1364
1413
|
end
|
|
1365
|
-
-- "2.16.
|
|
1414
|
+
-- "2.16.3" is replaced with the package version at package time
|
|
1366
1415
|
-- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
1367
|
-
return `{tostring(h)}-2.16.
|
|
1416
|
+
return `{tostring(h)}-2.16.3`
|
|
1368
1417
|
end
|
|
1369
1418
|
local BRIDGE_STAMP = computeBridgeStamp()
|
|
1370
1419
|
local function setSource(scriptInst, source)
|
|
@@ -7925,19 +7974,31 @@ local pluginRef
|
|
|
7925
7974
|
local function init(p)
|
|
7926
7975
|
pluginRef = p
|
|
7927
7976
|
end
|
|
7928
|
-
local function
|
|
7977
|
+
local function addUnique(values, value)
|
|
7978
|
+
local _values = values
|
|
7979
|
+
local _value = value
|
|
7980
|
+
if not (table.find(_values, _value) ~= nil) then
|
|
7981
|
+
local _values_1 = values
|
|
7982
|
+
local _value_1 = value
|
|
7983
|
+
table.insert(_values_1, _value_1)
|
|
7984
|
+
end
|
|
7985
|
+
end
|
|
7986
|
+
local function computeInstanceIds()
|
|
7987
|
+
local ids = {}
|
|
7929
7988
|
if game.PlaceId ~= 0 then
|
|
7930
|
-
|
|
7989
|
+
addUnique(ids, `place:{tostring(game.PlaceId)}`)
|
|
7931
7990
|
end
|
|
7932
7991
|
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
7933
7992
|
if type(existing) == "string" and existing ~= "" then
|
|
7934
|
-
|
|
7993
|
+
addUnique(ids, `anon:{existing}`)
|
|
7994
|
+
elseif game.PlaceId == 0 then
|
|
7995
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
7996
|
+
pcall(function()
|
|
7997
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
7998
|
+
end)
|
|
7999
|
+
addUnique(ids, `anon:{fresh}`)
|
|
7935
8000
|
end
|
|
7936
|
-
|
|
7937
|
-
pcall(function()
|
|
7938
|
-
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
7939
|
-
end)
|
|
7940
|
-
return `anon:{fresh}`
|
|
8001
|
+
return ids
|
|
7941
8002
|
end
|
|
7942
8003
|
local function settingKey(instanceId)
|
|
7943
8004
|
return SETTING_KEY_PREFIX .. instanceId
|
|
@@ -7946,21 +8007,25 @@ local function rememberServerUrl(serverUrl)
|
|
|
7946
8007
|
if not pluginRef or serverUrl == "" then
|
|
7947
8008
|
return nil
|
|
7948
8009
|
end
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
8010
|
+
for _, instanceId in computeInstanceIds() do
|
|
8011
|
+
local key = settingKey(instanceId)
|
|
8012
|
+
pcall(function()
|
|
8013
|
+
return pluginRef:SetSetting(key, serverUrl)
|
|
8014
|
+
end)
|
|
8015
|
+
end
|
|
7953
8016
|
end
|
|
7954
8017
|
local function readServerUrl()
|
|
7955
8018
|
if not pluginRef then
|
|
7956
8019
|
return nil
|
|
7957
8020
|
end
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
8021
|
+
for _, instanceId in computeInstanceIds() do
|
|
8022
|
+
local key = settingKey(instanceId)
|
|
8023
|
+
local ok, value = pcall(function()
|
|
8024
|
+
return pluginRef:GetSetting(key)
|
|
8025
|
+
end)
|
|
8026
|
+
if ok and type(value) == "string" and value ~= "" then
|
|
8027
|
+
return value
|
|
8028
|
+
end
|
|
7964
8029
|
end
|
|
7965
8030
|
return nil
|
|
7966
8031
|
end
|
|
@@ -7976,7 +8041,7 @@ return {
|
|
|
7976
8041
|
<Properties>
|
|
7977
8042
|
<string name="Name">State</string>
|
|
7978
8043
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
7979
|
-
local CURRENT_VERSION = "2.16.
|
|
8044
|
+
local CURRENT_VERSION = "2.16.3"
|
|
7980
8045
|
local PLUGIN_VARIANT = "inspector"
|
|
7981
8046
|
local MAX_CONNECTIONS = 5
|
|
7982
8047
|
local BASE_PORT = 58741
|
|
@@ -8078,6 +8143,9 @@ local TS = require(script.Parent.Parent.include.RuntimeLib)
|
|
|
8078
8143
|
-- Cross-DM stop_playtest signaling via plugin:SetSetting, scoped by
|
|
8079
8144
|
-- per-instance setting key so the same Studio process can host playtests
|
|
8080
8145
|
-- for multiple places without one place's stop_playtest yanking another's.
|
|
8146
|
+
-- During publish-after-connect, both "anon:<uuid>" and "place:<PlaceId>"
|
|
8147
|
+
-- can refer to the same Studio place, so stop requests are mirrored across
|
|
8148
|
+
-- both keys while the monitor waits for a matching result on either key.
|
|
8081
8149
|
--
|
|
8082
8150
|
-- `plugin:SetSetting` / `plugin:GetSetting` is a per-plugin persistent store
|
|
8083
8151
|
-- shared across every DataModel the plugin runs in (edit DMs, play-server
|
|
@@ -8125,23 +8193,48 @@ end
|
|
|
8125
8193
|
-- agree on the place identifier (published places: placeId; unpublished:
|
|
8126
8194
|
-- UUID on ServerStorage's __MCPPlaceId attribute, travels with the .rbxl
|
|
8127
8195
|
-- into the play DM).
|
|
8128
|
-
local function
|
|
8196
|
+
local function addUnique(values, value)
|
|
8197
|
+
local _values = values
|
|
8198
|
+
local _value = value
|
|
8199
|
+
if not (table.find(_values, _value) ~= nil) then
|
|
8200
|
+
local _values_1 = values
|
|
8201
|
+
local _value_1 = value
|
|
8202
|
+
table.insert(_values_1, _value_1)
|
|
8203
|
+
end
|
|
8204
|
+
end
|
|
8205
|
+
local function computeInstanceIds()
|
|
8206
|
+
local ids = {}
|
|
8129
8207
|
if game.PlaceId ~= 0 then
|
|
8130
|
-
|
|
8208
|
+
addUnique(ids, `place:{tostring(game.PlaceId)}`)
|
|
8131
8209
|
end
|
|
8132
8210
|
local existing = ServerStorage:GetAttribute("__MCPPlaceId")
|
|
8133
8211
|
if type(existing) == "string" and existing ~= "" then
|
|
8134
|
-
|
|
8212
|
+
addUnique(ids, `anon:{existing}`)
|
|
8213
|
+
elseif game.PlaceId == 0 then
|
|
8214
|
+
local fresh = HttpService:GenerateGUID(false)
|
|
8215
|
+
pcall(function()
|
|
8216
|
+
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
8217
|
+
end)
|
|
8218
|
+
addUnique(ids, `anon:{fresh}`)
|
|
8135
8219
|
end
|
|
8136
|
-
|
|
8137
|
-
pcall(function()
|
|
8138
|
-
return ServerStorage:SetAttribute("__MCPPlaceId", fresh)
|
|
8139
|
-
end)
|
|
8140
|
-
return `anon:{fresh}`
|
|
8220
|
+
return ids
|
|
8141
8221
|
end
|
|
8142
8222
|
local function settingKey(instanceId)
|
|
8143
8223
|
return SETTING_KEY_PREFIX .. instanceId
|
|
8144
8224
|
end
|
|
8225
|
+
local function settingKeys()
|
|
8226
|
+
local _exp = computeInstanceIds()
|
|
8227
|
+
-- ▼ ReadonlyArray.map ▼
|
|
8228
|
+
local _newValue = table.create(#_exp)
|
|
8229
|
+
local _callback = function(instanceId)
|
|
8230
|
+
return settingKey(instanceId)
|
|
8231
|
+
end
|
|
8232
|
+
for _k, _v in _exp do
|
|
8233
|
+
_newValue[_k] = _callback(_v, _k - 1, _exp)
|
|
8234
|
+
end
|
|
8235
|
+
-- ▲ ReadonlyArray.map ▲
|
|
8236
|
+
return _newValue
|
|
8237
|
+
end
|
|
8145
8238
|
local function readSetting(key)
|
|
8146
8239
|
if not pluginRef then
|
|
8147
8240
|
return nil
|
|
@@ -8227,7 +8320,7 @@ local function handleStopRequest(key, request)
|
|
|
8227
8320
|
return nil
|
|
8228
8321
|
end
|
|
8229
8322
|
if endTestIssued then
|
|
8230
|
-
writeResult(key, request,
|
|
8323
|
+
writeResult(key, request, false, "StudioTestService:EndTest was already issued for this play session, but the runtime DataModel is still alive.")
|
|
8231
8324
|
return nil
|
|
8232
8325
|
end
|
|
8233
8326
|
if not RunService:IsRunning() or not RunService:IsServer() then
|
|
@@ -8248,18 +8341,19 @@ local function startMonitor()
|
|
|
8248
8341
|
warn("[robloxstudio-mcp] StopPlayMonitor.startMonitor called before init; skipping")
|
|
8249
8342
|
return nil
|
|
8250
8343
|
end
|
|
8251
|
-
local myKey = settingKey(computeInstanceId())
|
|
8252
8344
|
task.spawn(function()
|
|
8253
8345
|
while true do
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8346
|
+
for _, myKey in settingKeys() do
|
|
8347
|
+
local value = readSetting(myKey)
|
|
8348
|
+
if value == true then
|
|
8349
|
+
-- Legacy boolean requests are ambiguous and may be stale from
|
|
8350
|
+
-- a prior crashed session. New stop requests use token payloads.
|
|
8351
|
+
writeSetting(myKey, false)
|
|
8352
|
+
else
|
|
8353
|
+
local payload = decodePayload(value)
|
|
8354
|
+
if payload then
|
|
8355
|
+
handleStopRequest(myKey, payload)
|
|
8356
|
+
end
|
|
8263
8357
|
end
|
|
8264
8358
|
end
|
|
8265
8359
|
task.wait(POLL_INTERVAL_SEC)
|
|
@@ -8272,13 +8366,16 @@ local function requestStop()
|
|
|
8272
8366
|
ok = false,
|
|
8273
8367
|
}
|
|
8274
8368
|
end
|
|
8275
|
-
local myKey = settingKey(computeInstanceId())
|
|
8276
8369
|
local requestId = HttpService:GenerateGUID(false)
|
|
8277
|
-
local
|
|
8370
|
+
local payload = {
|
|
8278
8371
|
kind = "request",
|
|
8279
8372
|
id = requestId,
|
|
8280
8373
|
requestedAt = tick(),
|
|
8281
|
-
}
|
|
8374
|
+
}
|
|
8375
|
+
local ok = false
|
|
8376
|
+
for _, myKey in settingKeys() do
|
|
8377
|
+
ok = writePayload(myKey, payload) or ok
|
|
8378
|
+
end
|
|
8282
8379
|
return {
|
|
8283
8380
|
ok = ok,
|
|
8284
8381
|
requestId = if ok then requestId else nil,
|
|
@@ -8292,16 +8389,17 @@ local function waitForConsumption(requestId)
|
|
|
8292
8389
|
error = "Plugin reference is not initialized.",
|
|
8293
8390
|
}
|
|
8294
8391
|
end
|
|
8295
|
-
local myKey = settingKey(computeInstanceId())
|
|
8296
8392
|
local start = tick()
|
|
8297
8393
|
while tick() - start < WAIT_FOR_CONSUMPTION_TIMEOUT_SEC do
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8394
|
+
for _, myKey in settingKeys() do
|
|
8395
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8396
|
+
if payload and payload.kind == "result" and payload.id == requestId then
|
|
8397
|
+
return {
|
|
8398
|
+
ok = payload.ok == true,
|
|
8399
|
+
consumed = true,
|
|
8400
|
+
error = payload.error,
|
|
8401
|
+
}
|
|
8402
|
+
end
|
|
8305
8403
|
end
|
|
8306
8404
|
task.wait(WAIT_POLL_SEC)
|
|
8307
8405
|
end
|
|
@@ -8315,14 +8413,15 @@ local function clearPending(requestId)
|
|
|
8315
8413
|
if not pluginRef then
|
|
8316
8414
|
return nil
|
|
8317
8415
|
end
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8416
|
+
for _, myKey in settingKeys() do
|
|
8417
|
+
if requestId ~= nil then
|
|
8418
|
+
local payload = decodePayload(readSetting(myKey))
|
|
8419
|
+
if payload and payload.id ~= requestId then
|
|
8420
|
+
continue
|
|
8421
|
+
end
|
|
8323
8422
|
end
|
|
8423
|
+
writeSetting(myKey, false)
|
|
8324
8424
|
end
|
|
8325
|
-
writeSetting(myKey, false)
|
|
8326
8425
|
end
|
|
8327
8426
|
return {
|
|
8328
8427
|
init = init,
|